Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
d956b0e
feat(fs): Enhance API for incremental build, add tracking readers/wri…
RandomByte Nov 18, 2025
76a6162
feat(server): Use incremental build in server
RandomByte Nov 18, 2025
455aab9
feat(builder): Adapt tasks for incremental build
RandomByte Nov 18, 2025
910488f
refactor(project): Align getReader API internals with ComponentProjects
RandomByte Nov 18, 2025
8745fbd
refactor(project): Refactor specification-internal workspace handling
RandomByte Nov 18, 2025
bfbc936
refactor(project): Implement basic incremental build functionality
RandomByte Nov 18, 2025
82df49f
refactor(cli): Use cache in ui5 build
RandomByte Nov 18, 2025
92ade64
refactor(project): Use cacache
RandomByte Nov 24, 2025
5da3a94
refactor(project): Add cache manager
RandomByte Nov 28, 2025
a95343e
refactor(fs): Refactor Resource internals
RandomByte Nov 27, 2025
4f3ec4a
refactor(fs): Provide createBuffer factory in FileSystem adapter
RandomByte Dec 1, 2025
19db106
refactor(project): Refactor cache classes
RandomByte Dec 1, 2025
021b5db
refactor(fs): Add Proxy reader
RandomByte Dec 4, 2025
863c616
refactor(project): API refactoring
RandomByte Dec 8, 2025
71288a5
refactor(builder): Rename task param 'buildCache' to 'cacheUtil'
RandomByte Dec 10, 2025
2a3e2ae
refactor(project): Cleanup
RandomByte Dec 10, 2025
5f4d9b4
refactor(project): Move resource comparison to util
RandomByte Dec 12, 2025
93e77d1
refactor(project): Refactor stage handling
RandomByte Dec 16, 2025
3ff5d80
refactor(project): Fix cache handling
RandomByte Dec 16, 2025
547dad4
refactor(fs): Remove contentAccess mutex timeout from Resource
RandomByte Dec 16, 2025
02fe6bb
refactor(project): Cleanup obsolete code/comments
RandomByte Dec 16, 2025
2f0c30d
refactor(server): Cleanup obsolete code
RandomByte Dec 16, 2025
c60a9e8
refactor(project): Rename watch handler events
RandomByte Dec 16, 2025
0c861bc
refactor: Fix linting issues
matz3 Dec 17, 2025
57a3654
test(fs): Adjust getIntegrity tests
matz3 Dec 17, 2025
5cea296
refactor: Integrity handling
matz3 Dec 17, 2025
4c95929
test(fs): Adjust getIntegrity tests again
matz3 Dec 17, 2025
7789f04
refactor: Consider npm-shrinkwrap.json
matz3 Dec 17, 2025
4f4ea1d
refactor: Rename Tracker => MonitoredReader
RandomByte Dec 17, 2025
94a54ac
refactor(project): Use workspace version in stage name
RandomByte Dec 17, 2025
11c46b1
refactor(project): Fix stage writer order
RandomByte Dec 17, 2025
1db48f9
refactor(fs): Add Switch reader
RandomByte Dec 17, 2025
f16ebe5
refactor(project): Cleanup WatchHandler debounce
RandomByte Dec 17, 2025
b872739
refactor(project): Fix outdated API call
RandomByte Dec 17, 2025
709124e
refactor(project): Fix build signature calculation
RandomByte Dec 17, 2025
d62cc41
refactor(fs): Pass integrity to cloned resource
RandomByte Dec 17, 2025
581ecc3
refactor(project): Fix pattern matching and resource comparison
RandomByte Dec 17, 2025
8515d2d
refactor(project): Import/overwrite stages from cache after saving
RandomByte Dec 18, 2025
383af1f
test(builder): Sort files/folders
matz3 Dec 19, 2025
292e0d4
refactor(builder): Prevent duplicate entries on app build from cache
matz3 Dec 19, 2025
0adb1bc
refactor(fs): Refactor MonitorReader API
RandomByte Dec 23, 2025
80c88fb
refactor(fs): Always calculate integrity on clone
RandomByte Dec 24, 2025
4fefd02
refactor(fs): Add getter to WriterCollection
RandomByte Dec 29, 2025
9c6889f
refactor(builder): Remove cache handling from tasks
RandomByte Dec 24, 2025
e11b648
refactor(builder): Add env variable for disabling workers
RandomByte Dec 30, 2025
5a37c6c
refactor(project): Track resource changes using hash trees
RandomByte Dec 23, 2025
9546fe8
refactor(project): Compress cache using gzip
RandomByte Jan 2, 2026
8ff3227
refactor(fs): Ensure writer collection uses unique readers
RandomByte Jan 2, 2026
9ab328b
refactor(fs): Remove write tracking from MonitoredReaderWriter
RandomByte Jan 2, 2026
f1d8e73
refactor(project): Identify written resources using stage writer
RandomByte Jan 2, 2026
e00d138
refactor(project): Add basic differential update functionality
RandomByte Jan 2, 2026
88678d5
refactor(builder): Re-add cache handling in tasks
RandomByte Jan 7, 2026
6023484
refactor(project): Cleanup HashTree implementation
RandomByte Jan 7, 2026
394b361
refactor(project): Make WatchHandler wait for build to finish before …
RandomByte Jan 7, 2026
e339bc5
refactor(project): Use cleanup hooks in update builds
RandomByte Jan 7, 2026
84d8496
refactor(logger): Log skipped projects/tasks info in grey color
RandomByte Jan 7, 2026
d41a2cc
refactor(project): Fix cache update mechanism
RandomByte Jan 7, 2026
d168d16
refactor(project): WatchHandler emit error event
RandomByte Jan 7, 2026
4b7be69
refactor(server): Exit process on rebuild error
RandomByte Jan 7, 2026
36f8250
refactor(project): Fix delta indices
RandomByte Jan 7, 2026
1d23478
refactor(logger): Support differential update task logging
RandomByte Jan 8, 2026
c5f77ae
refactor(project): Provide differential update flag to logger
RandomByte Jan 8, 2026
a284f72
refactor(server): Log error stack on build error
RandomByte Jan 8, 2026
a1734c1
refactor(project): Add chokidar
RandomByte Jan 8, 2026
99d0131
refactor(project): Limit build signature to project name and config
RandomByte Jan 8, 2026
bc41f38
refactor(project): Improve ResourceRequestGraph handling
RandomByte Jan 8, 2026
e121b80
refactor(server): Remove obsolete code from serveResources middleware
matz3 Jan 8, 2026
a6d1a60
refactor(project): Add env variable to skip cache update
RandomByte Jan 9, 2026
d896ad0
refactor(project): Fix hash tree updates
RandomByte Jan 9, 2026
253b4fa
refactor(project): Remove unused 'cacheDir' param
matz3 Jan 9, 2026
9bad9ea
fix(project): Prevent projects from being always invalidated
matz3 Jan 9, 2026
23f823c
fix(project): Prevent exception when not building in watch mode
matz3 Jan 12, 2026
ed0f9c9
refactor(project): Only store new or modified cache entries
RandomByte Jan 9, 2026
f1177e7
refactor(project): Refactor project cache validation
RandomByte Jan 11, 2026
b143507
refactor(project): Extract project build into own method
matz3 Jan 13, 2026
2fdaa1a
fix(project): Clear cleanup task queue
matz3 Jan 13, 2026
d44a533
test(project): Add ProjectBuilder integration test
matz3 Jan 13, 2026
fa94176
test(project): Add failing ProjectBuilder test case
matz3 Jan 13, 2026
c9e2144
test(project): Enhance ProjectBuilder test assertions
matz3 Jan 13, 2026
7840cf1
test(project): Add library test case for ProjectBuilder
matz3 Jan 13, 2026
1d35e15
test(project): Build dependencies in application test of ProjectBuilder
matz3 Jan 13, 2026
2a1aeaa
test(project): Refactor ProjectBuilder test code
matz3 Jan 14, 2026
41eed91
refactor(project): Refactor task resource request tracking
RandomByte Jan 14, 2026
adf0c36
test(project): Add theme-library test and update assertions for fixed…
matz3 Jan 14, 2026
0837817
test(project): Use graph.build for ProjectBuilder test
matz3 Jan 15, 2026
84c74fb
test(project): Add custom task to ProjectBuilder test
matz3 Jan 15, 2026
d3ac850
fix(project): Fix custom task execution
matz3 Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,147 changes: 231 additions & 3,916 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/builder/lib/processors/nonAsciiEscaper.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ async function nonAsciiEscaper({resources, options: {encoding}}) {
// only modify the resource's string if it was changed
if (escaped.modified) {
resource.setString(escaped.string);
return resource;
}
return resource;
}

return Promise.all(resources.map(processResource));
Expand Down
3 changes: 2 additions & 1 deletion packages/builder/lib/processors/stringReplacer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export default function({resources, options: {pattern, replacement}}) {
const newContent = content.replaceAll(pattern, replacement);
if (content !== newContent) {
resource.setString(newContent);
return resource;
}
return resource;
// return resource;
}));
}
2 changes: 1 addition & 1 deletion packages/builder/lib/tasks/buildThemes.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export default async function({
}

let processedResources;
const useWorkers = !!taskUtil;
const useWorkers = !process.env.UI5_CLI_NO_WORKERS && !!taskUtil;
if (useWorkers) {
const threadMessageHandler = new FsMainThreadInterface(fsInterface(combo));

Expand Down
13 changes: 10 additions & 3 deletions packages/builder/lib/tasks/escapeNonAsciiCharacters.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js";
*
* @param {object} parameters Parameters
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project.
* This is only set if a cache is used and changes have been detected.
* @param {object} parameters.options Options
* @param {string} parameters.options.pattern Glob pattern to locate the files to be processed
* @param {string} parameters.options.encoding source file encoding either "UTF-8" or "ISO-8859-1"
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
export default async function({workspace, options: {pattern, encoding}}) {
export default async function({workspace, changedProjectResourcePaths, options: {pattern, encoding}}) {
if (!encoding) {
throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided");
}

const allResources = await workspace.byGlob(pattern);
let allResources;
if (changedProjectResourcePaths) {
allResources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource)));
} else {
allResources = await workspace.byGlob(pattern);
}

const processedResources = await nonAsciiEscaper({
resources: allResources,
Expand All @@ -33,5 +40,5 @@ export default async function({workspace, options: {pattern, encoding}}) {
}
});

await Promise.all(processedResources.map((resource) => workspace.write(resource)));
await Promise.all(processedResources.map((resource) => resource && workspace.write(resource)));
}
19 changes: 15 additions & 4 deletions packages/builder/lib/tasks/minify.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import fsInterface from "@ui5/fs/fsInterface";
* @param {object} parameters Parameters
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil
* @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project.
* This is only set if a cache is used and changes have been detected.
* @param {object} parameters.options Options
* @param {string} parameters.options.pattern Pattern to locate the files to be processed
* @param {boolean} [parameters.options.omitSourceMapResources=false] Whether source map resources shall
Expand All @@ -26,17 +28,26 @@ import fsInterface from "@ui5/fs/fsInterface";
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
export default async function({
workspace, taskUtil, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true
}}) {
const resources = await workspace.byGlob(pattern);
workspace, taskUtil, changedProjectResourcePaths,
options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true}
}) {
let resources;
if (changedProjectResourcePaths) {
resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource)));
} else {
resources = await workspace.byGlob(pattern);
}
if (resources.length === 0) {
return;
}
const processedResources = await minifier({
resources,
fs: fsInterface(workspace),
taskUtil,
options: {
addSourceMappingUrl: !omitSourceMapResources,
readSourceMappingUrl: !!useInputSourceMaps,
useWorkers: !!taskUtil,
useWorkers: !process.env.UI5_CLI_NO_WORKERS && !!taskUtil,
}
});

Expand Down
38 changes: 21 additions & 17 deletions packages/builder/lib/tasks/replaceBuildtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,30 @@ function getTimestamp() {
*
* @param {object} parameters Parameters
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project.
* This is only set if a cache is used and changes have been detected.
* @param {object} parameters.options Options
* @param {string} parameters.options.pattern Pattern to locate the files to be processed
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
export default function({workspace, options: {pattern}}) {
export default async function({workspace, changedProjectResourcePaths, options: {pattern}}) {
let resources;
if (changedProjectResourcePaths) {
resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource)));
} else {
resources = await workspace.byGlob(pattern);
}
const timestamp = getTimestamp();

return workspace.byGlob(pattern)
.then((processedResources) => {
return stringReplacer({
resources: processedResources,
options: {
pattern: "${buildtime}",
replacement: timestamp
}
});
})
.then((processedResources) => {
return Promise.all(processedResources.map((resource) => {
return workspace.write(resource);
}));
});
const processedResources = await stringReplacer({
resources,
options: {
pattern: "${buildtime}",
replacement: timestamp
}
});
return Promise.all(processedResources.map((resource) => {
if (resource) {
return workspace.write(resource);
}
}));
}
40 changes: 23 additions & 17 deletions packages/builder/lib/tasks/replaceCopyright.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,38 @@ import stringReplacer from "../processors/stringReplacer.js";
*
* @param {object} parameters Parameters
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project.
* This is only set if a cache is used and changes have been detected.
* @param {object} parameters.options Options
* @param {string} parameters.options.copyright Replacement copyright
* @param {string} parameters.options.pattern Pattern to locate the files to be processed
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
export default function({workspace, options: {copyright, pattern}}) {
export default async function({workspace, changedProjectResourcePaths, options: {copyright, pattern}}) {
if (!copyright) {
return Promise.resolve();
return;
}

// Replace optional placeholder ${currentYear} with the current year
copyright = copyright.replace(/(?:\$\{currentYear\})/, new Date().getFullYear());

return workspace.byGlob(pattern)
.then((processedResources) => {
return stringReplacer({
resources: processedResources,
options: {
pattern: /(?:\$\{copyright\}|@copyright@)/g,
replacement: copyright
}
});
})
.then((processedResources) => {
return Promise.all(processedResources.map((resource) => {
return workspace.write(resource);
}));
});
let resources;
if (changedProjectResourcePaths) {
resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource)));
} else {
resources = await workspace.byGlob(pattern);
}

const processedResources = await stringReplacer({
resources,
options: {
pattern: /(?:\$\{copyright\}|@copyright@)/g,
replacement: copyright
}
});
return Promise.all(processedResources.map((resource) => {
if (resource) {
return workspace.write(resource);
}
}));
}
37 changes: 21 additions & 16 deletions packages/builder/lib/tasks/replaceVersion.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,30 @@ import stringReplacer from "../processors/stringReplacer.js";
*
* @param {object} parameters Parameters
* @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {string[]} [parameters.changedProjectResourcePaths] Set of changed resource paths within the project.
* This is only set if a cache is used and changes have been detected.
* @param {object} parameters.options Options
* @param {string} parameters.options.pattern Pattern to locate the files to be processed
* @param {string} parameters.options.version Replacement version
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
export default function({workspace, options: {pattern, version}}) {
return workspace.byGlob(pattern)
.then((allResources) => {
return stringReplacer({
resources: allResources,
options: {
pattern: /\$\{(?:project\.)?version\}/g,
replacement: version
}
});
})
.then((processedResources) => {
return Promise.all(processedResources.map((resource) => {
return workspace.write(resource);
}));
});
export default async function({workspace, changedProjectResourcePaths, options: {pattern, version}}) {
let resources;
if (changedProjectResourcePaths) {
resources = await Promise.all(changedProjectResourcePaths.map((resource) => workspace.byPath(resource)));
} else {
resources = await workspace.byGlob(pattern);
}
const processedResources = await stringReplacer({
resources,
options: {
pattern: /\$\{(?:project\.)?version\}/g,
replacement: version
}
});
await Promise.all(processedResources.map((resource) => {
if (resource) {
return workspace.write(resource);
}
}));
}
4 changes: 2 additions & 2 deletions packages/builder/test/utils/fshelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export async function readFileContent(filePath) {
}

export async function directoryDeepEqual(t, destPath, expectedPath) {
const dest = await readdir(destPath, {recursive: true});
const expected = await readdir(expectedPath, {recursive: true});
const dest = (await readdir(destPath, {recursive: true})).sort();
const expected = (await readdir(expectedPath, {recursive: true})).sort();
t.deepEqual(dest, expected);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lib/cli/commands/build.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import baseMiddleware from "../middlewares/base.js";
import path from "node:path";

const build = {
command: "build",
Expand Down Expand Up @@ -173,6 +174,7 @@ async function handleBuild(argv) {
const buildSettings = graph.getRoot().getBuilderSettings() || {};
await graph.build({
graph,
cacheDir: path.join(graph.getRoot().getRootPath(), ".ui5-cache"),
destPath: argv.dest,
cleanDest: argv["clean-dest"],
createBuildManifest: argv["create-build-manifest"],
Expand Down
49 changes: 49 additions & 0 deletions packages/fs/lib/MonitoredReader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import AbstractReader from "./AbstractReader.js";

export default class MonitoredReader extends AbstractReader {
#reader;
#sealed = false;
#paths = [];
#patterns = [];

constructor(reader) {
super(reader.getName());
this.#reader = reader;
}

getResourceRequests() {
this.#sealed = true;
return {
paths: this.#paths,
patterns: this.#patterns,
};
}

async _byGlob(virPattern, options, trace) {
if (this.#sealed) {
throw new Error(`Unexpected read operation after reader has been sealed`);
}
if (this.#reader.resolvePattern) {
const resolvedPattern = this.#reader.resolvePattern(virPattern);
this.#patterns.push(resolvedPattern);
} else {
this.#patterns.push(virPattern);
}
return await this.#reader._byGlob(virPattern, options, trace);
}

async _byPath(virPath, options, trace) {
if (this.#sealed) {
throw new Error(`Unexpected read operation after reader has been sealed`);
}
if (this.#reader.resolvePath) {
const resolvedPath = this.#reader.resolvePath(virPath);
if (resolvedPath) {
this.#paths.push(resolvedPath);
}
} else {
this.#paths.push(virPath);
}
return await this.#reader._byPath(virPath, options, trace);
}
}
53 changes: 53 additions & 0 deletions packages/fs/lib/MonitoredReaderWriter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import AbstractReaderWriter from "./AbstractReaderWriter.js";

export default class MonitoredReaderWriter extends AbstractReaderWriter {
#readerWriter;
#sealed = false;
#paths = new Set();
#patterns = new Set();

constructor(readerWriter) {
super(readerWriter.getName());
this.#readerWriter = readerWriter;
}

getResourceRequests() {
this.#sealed = true;
return {
paths: this.#paths,
patterns: this.#patterns,
};
}

async _byGlob(virPattern, options, trace) {
if (this.#sealed) {
throw new Error(`Unexpected read operation after reader has been sealed`);
}
if (this.#readerWriter.resolvePattern) {
const resolvedPattern = this.#readerWriter.resolvePattern(virPattern);
this.#patterns.add(resolvedPattern);
} else {
this.#patterns.add(virPattern);
}
return await this.#readerWriter._byGlob(virPattern, options, trace);
}

async _byPath(virPath, options, trace) {
if (this.#sealed) {
throw new Error(`Unexpected read operation after reader has been sealed`);
}
if (this.#readerWriter.resolvePath) {
const resolvedPath = this.#readerWriter.resolvePath(virPath);
if (resolvedPath) {
this.#paths.add(resolvedPath);
}
} else {
this.#paths.add(virPath);
}
return await this.#readerWriter._byPath(virPath, options, trace);
}

async _write(resource, options) {
return this.#readerWriter.write(resource, options);
}
}
2 changes: 1 addition & 1 deletion packages/fs/lib/ReaderCollectionPrioritized.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class ReaderCollectionPrioritized extends AbstractReader {
* @returns {Promise<@ui5/fs/Resource|null>}
* Promise resolving to a single resource or <code>null</code> if no resource is found
*/
_byPath(virPath, options, trace) {
async _byPath(virPath, options, trace) {
const that = this;
const byPath = (i) => {
if (i > this._readers.length - 1) {
Expand Down
Loading