Skip to content

Commit a516158

Browse files
committed
refactor(project): Add basic handling for resource tags
* Add new class 'ProjectResources' * Add new ProjectBuildCache lifecycle hook "buildFinished" * This is used to clear build-tags which must not be visible to dependant projects (maybe we can find a better solution here) * ProjectBuilder must read the tags before triggering the hook Still missing: Integrate resource tags into hash tree for correct invalidation
1 parent 790f561 commit a516158

File tree

10 files changed

+666
-283
lines changed

10 files changed

+666
-283
lines changed

packages/project/lib/build/ProjectBuilder.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,15 @@ class ProjectBuilder {
208208
});
209209
}
210210
const pWrites = [];
211-
await this.#build(requestedProjects, (projectName, project, projectBuildContext) => {
211+
await this.#build(requestedProjects, async (projectName, project, projectBuildContext) => {
212212
if (!fsTarget) {
213213
// Nothing to write to
214214
return;
215215
}
216216
// Only write requested projects to target
217217
// (excluding dependencies that were required to be built, but not requested)
218218
this.#log.verbose(`Writing out files for project ${projectName}...`);
219-
pWrites.push(this._writeResults(projectBuildContext, fsTarget));
219+
await this._writeResults(projectBuildContext, fsTarget, pWrites);
220220
});
221221
await Promise.all(pWrites);
222222
}
@@ -329,13 +329,15 @@ class ProjectBuilder {
329329
signal?.throwIfAborted();
330330

331331
if (projectBuiltCallback && requestedProjects.includes(projectName)) {
332-
projectBuiltCallback(projectName, project, projectBuildContext);
332+
await projectBuiltCallback(projectName, project, projectBuildContext);
333333
}
334334

335335
if (!alreadyBuilt.includes(projectName) && !process.env.UI5_BUILD_NO_WRITE_CACHE) {
336336
this.#log.verbose(`Triggering cache update for project ${projectName}...`);
337337
pCacheWrites.push(projectBuildContext.writeBuildCache());
338338
}
339+
340+
projectBuildContext.buildFinished();
339341
}
340342
this.#log.info(`Build succeeded in ${this._getElapsedTime(startTime)}`);
341343
} catch (err) {
@@ -434,9 +436,10 @@ class ProjectBuilder {
434436
*
435437
* @param {object} projectBuildContext Build context for the project
436438
* @param {@ui5/fs/adapters/FileSystem} target Target adapter to write to
439+
* @param {Array<Promise>} deferredWork
437440
* @returns {Promise<void>} Promise resolving when write is complete
438441
*/
439-
async _writeResults(projectBuildContext, target) {
442+
async _writeResults(projectBuildContext, target, deferredWork) {
440443
const project = projectBuildContext.getProject();
441444
const taskUtil = projectBuildContext.getTaskUtil();
442445
const buildConfig = this._buildContext.getBuildConfig();
@@ -472,7 +475,21 @@ class ProjectBuilder {
472475
}));
473476
}
474477

475-
await Promise.all(resources.map((resource) => {
478+
const resourcesToWrite = resources.filter((resource) => {
479+
if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) {
480+
this.#log.silly(`Skipping resource tagged as "OmitFromBuildResult": ` +
481+
resource.getPath());
482+
return false; // Skip this resource
483+
}
484+
return true;
485+
});
486+
487+
deferredWork.push(
488+
this._writeToDisk(resourcesToWrite, target, resources, taskUtil, project, isRootProject, outputStyle));
489+
}
490+
491+
async _writeToDisk(resourcesToWrite, target, resources, taskUtil, project, isRootProject, outputStyle) {
492+
await Promise.all(resourcesToWrite.map((resource) => {
476493
if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) {
477494
this.#log.silly(`Skipping write of resource tagged as "OmitFromBuildResult": ` +
478495
resource.getPath());
@@ -483,12 +500,14 @@ class ProjectBuilder {
483500

484501
if (isRootProject &&
485502
outputStyle === OutputStyleEnum.Flat &&
486-
project.getType() !== "application" /* application type is with a default flat build output structure */) {
503+
/* application type is with a default flat build output structure */
504+
project.getType() !== "application") {
487505
const namespace = project.getNamespace();
488506
const libraryResourcesPrefix = `/resources/${namespace}/`;
489507
const testResourcesPrefix = "/test-resources/";
490508
const namespacedRegex = new RegExp(`/(resources|test-resources)/${namespace}`);
491-
const processedResourcesSet = resources.reduce((acc, resource) => acc.add(resource.getPath()), new Set());
509+
const processedResourcesSet = resources.reduce(
510+
(acc, resource) => acc.add(resource.getPath()), new Set());
492511

493512
// If outputStyle === "Flat", then the FlatReader would have filtered
494513
// some resources. We now need to get all of the available resources and
@@ -507,7 +526,8 @@ class ProjectBuilder {
507526
skippedResources.forEach((resource) => {
508527
if (resource.originalPath.startsWith(testResourcesPrefix)) {
509528
this.#log.verbose(
510-
`Omitting ${resource.originalPath} from build result. File is part of ${testResourcesPrefix}.`
529+
`Omitting ${resource.originalPath} from build result. ` +
530+
`File is part of ${testResourcesPrefix}.`
511531
);
512532
} else if (!resource.originalPath.startsWith(libraryResourcesPrefix)) {
513533
this.#log.warn(

packages/project/lib/build/cache/CacheManager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const chacheManagerInstances = new Map();
2020
const CACACHE_OPTIONS = {algorithms: ["sha256"]};
2121

2222
// Cache version for compatibility management
23-
const CACHE_VERSION = "v0_1";
23+
const CACHE_VERSION = "v0_2";
2424

2525
/**
2626
* Manages persistence for the build cache using file-based storage and cacache

packages/project/lib/build/cache/ProjectBuildCache.js

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export const RESULT_CACHE_STATES = Object.freeze({
3636
* @property {string} signature Signature of the cached stage
3737
* @property {@ui5/fs/AbstractReader} stage Reader for the cached stage
3838
* @property {string[]} writtenResourcePaths Array of resource paths written by the task
39+
* @property {Map<string, Map<string, {string|number|boolean|undefined}>>} projectTagOperations
40+
* Map of resource paths to their tags that were set or cleared during this stage's execution, for project tags
41+
* @property {Map<string, Map<string, {string|number|boolean|undefined}>>} buildTagOperations
42+
* Map of resource paths to their tags that were set or cleared during this stage's execution, for build tags
3943
*/
4044

4145
export default class ProjectBuildCache {
@@ -116,6 +120,7 @@ export default class ProjectBuildCache {
116120
*/
117121
async prepareProjectBuildAndValidateCache(dependencyReader) {
118122
this.#currentProjectReader = this.#project.getReader();
123+
119124
this.#currentDependencyReader = dependencyReader;
120125

121126
if (this.#combinedIndexState === INDEX_STATES.INITIAL) {
@@ -295,9 +300,9 @@ export default class ProjectBuildCache {
295300
*/
296301
async #importStages(stageSignatures) {
297302
const stageNames = Object.keys(stageSignatures);
298-
if (this.#project.getStage()?.getId() === "initial") {
303+
if (this.#project.getProjectResources().getStage()?.getId() === "initial") {
299304
// Only initialize stages once
300-
this.#project.initStages(stageNames);
305+
this.#project.getProjectResources().initStages(stageNames);
301306
}
302307
const importedStages = await Promise.all(stageNames.map(async (stageName) => {
303308
const stageSignature = stageSignatures[stageName];
@@ -308,13 +313,14 @@ export default class ProjectBuildCache {
308313
}
309314
return [stageName, stageCache];
310315
}));
311-
this.#project.useResultStage();
316+
this.#project.getProjectResources().useResultStage();
312317
const writtenResourcePaths = new Set();
313318
for (const [stageName, stageCache] of importedStages) {
314319
// Check whether the stage differs form the one currently in use
315320
if (this.#currentStageSignatures.get(stageName)?.join("-") !== stageCache.signature) {
316321
// Set stage
317-
this.#project.setStage(stageName, stageCache.stage);
322+
this.#project.getProjectResources().setStage(stageName, stageCache.stage,
323+
stageCache.projectTagOperations, stageCache.buildTagOperations);
318324

319325
// Store signature for later use in result stage signature calculation
320326
this.#currentStageSignatures.set(stageName, stageCache.signature.split("-"));
@@ -387,7 +393,7 @@ export default class ProjectBuildCache {
387393
// Store current project reader (= state of the previous stage) for later use (e.g. in recordTaskResult)
388394
this.#currentProjectReader = this.#project.getReader();
389395
// Switch project to new stage
390-
this.#project.useStage(stageName);
396+
this.#project.getProjectResources().useStage(stageName);
391397
log.verbose(`Preparing task execution for task ${taskName} in project ${this.#project.getName()}...`);
392398
if (!taskCache) {
393399
log.verbose(`No task cache found`);
@@ -420,7 +426,8 @@ export default class ProjectBuildCache {
420426
const stageCache = await this.#findStageCache(stageName, stageSignatures);
421427
const oldStageSig = this.#currentStageSignatures.get(stageName)?.join("-");
422428
if (stageCache) {
423-
this.#project.setStage(stageName, stageCache.stage);
429+
this.#project.getProjectResources().setStage(stageName, stageCache.stage,
430+
stageCache.projectTagOperations, stageCache.buildTagOperations);
424431

425432
// Check whether the stage actually changed
426433
if (stageCache.signature !== oldStageSig) {
@@ -541,7 +548,7 @@ export default class ProjectBuildCache {
541548
return;
542549
}
543550
log.verbose(`Found cached stage with signature ${stageSignature}`);
544-
const {resourceMapping, resourceMetadata} = stageMetadata;
551+
const {resourceMapping, resourceMetadata, projectTagOperations, buildTagOperations} = stageMetadata;
545552
let writtenResourcePaths;
546553
let stageReader;
547554
if (resourceMapping) {
@@ -570,10 +577,13 @@ export default class ProjectBuildCache {
570577
writtenResourcePaths = Object.keys(resourceMetadata);
571578
stageReader = this.#createReaderForStageCache(stageName, stageSignature, resourceMetadata);
572579
}
580+
573581
return {
574582
signature: stageSignature,
575583
stage: stageReader,
576584
writtenResourcePaths,
585+
projectTagOperations: tagOpsToMap(projectTagOperations),
586+
buildTagOperations: tagOpsToMap(buildTagOperations),
577587
};
578588
}));
579589
return stageCache;
@@ -610,10 +620,12 @@ export default class ProjectBuildCache {
610620
const taskCache = this.#taskCache.get(taskName);
611621

612622
// Identify resources written by task
613-
const stage = this.#project.getStage();
623+
const stage = this.#project.getProjectResources().getStage();
614624
const stageWriter = stage.getWriter();
615625
const writtenResources = await stageWriter.byGlob("/**/*");
616626
const writtenResourcePaths = writtenResources.map((res) => res.getOriginalPath());
627+
const {projectTagOperations, buildTagOperations} =
628+
this.#project.getProjectResources().getResourceTagOperations();
617629

618630
let stageSignature;
619631
if (cacheInfo) {
@@ -654,8 +666,8 @@ export default class ProjectBuildCache {
654666

655667
// Store resulting stage in stage cache
656668
this.#stageCache.addSignature(
657-
this.#getStageNameForTask(taskName), stageSignature, this.#project.getStage(),
658-
writtenResourcePaths);
669+
this.#getStageNameForTask(taskName), stageSignature, this.#project.getProjectResources().getStage(),
670+
writtenResourcePaths, projectTagOperations, buildTagOperations);
659671

660672
// Update task cache with new metadata
661673
log.verbose(`Task ${taskName} produced ${writtenResourcePaths.length} resources`);
@@ -733,7 +745,7 @@ export default class ProjectBuildCache {
733745
*/
734746
async setTasks(taskNames) {
735747
const stageNames = taskNames.map((taskName) => this.#getStageNameForTask(taskName));
736-
this.#project.initStages(stageNames);
748+
this.#project.getProjectResources().initStages(stageNames);
737749

738750
// TODO: Rename function? We simply use it to have a point in time right before the project is built
739751
}
@@ -749,7 +761,7 @@ export default class ProjectBuildCache {
749761
* @returns {Promise<string[]>} Array of changed resource paths since the last build
750762
*/
751763
async allTasksCompleted() {
752-
this.#project.useResultStage();
764+
this.#project.getProjectResources().useResultStage();
753765
if (this.#combinedIndexState === INDEX_STATES.INITIAL) {
754766
this.#combinedIndexState = INDEX_STATES.FRESH;
755767
}
@@ -763,6 +775,10 @@ export default class ProjectBuildCache {
763775
return changedPaths;
764776
}
765777

778+
buildFinished() {
779+
this.#project.getProjectResources().buildFinished();
780+
}
781+
766782
/**
767783
* Generates the stage name for a given task
768784
*
@@ -947,7 +963,8 @@ export default class ProjectBuildCache {
947963
`with build signature ${this.#buildSignature}`);
948964
const stageQueue = this.#stageCache.flushCacheQueue();
949965
await Promise.all(stageQueue.map(async ([stageId, stageSignature]) => {
950-
const {stage} = this.#stageCache.getCacheForSignature(stageId, stageSignature);
966+
const {stage, projectTagOperations, buildTagOperations} =
967+
this.#stageCache.getCacheForSignature(stageId, stageSignature);
951968
const writer = stage.getWriter();
952969

953970
let metadata;
@@ -974,7 +991,8 @@ export default class ProjectBuildCache {
974991
const resourceMetadata = await this.#writeStageResources(resources, stageId, stageSignature);
975992
metadata = {resourceMetadata};
976993
}
977-
994+
metadata.projectTagOperations = tagOpsToObject(projectTagOperations);
995+
metadata.buildTagOperations = tagOpsToObject(buildTagOperations);
978996
await this.#cacheManager.writeStageCache(
979997
this.#project.getId(), this.#buildSignature, stageId, stageSignature, metadata);
980998
}));
@@ -1183,3 +1201,23 @@ function createStageSignature(projectSignature, dependencySignature) {
11831201
function createDependencySignature(stageDependencySignatures) {
11841202
return crypto.createHash("sha256").update(stageDependencySignatures.join("")).digest("hex");
11851203
}
1204+
1205+
function tagOpsToMap(tagOps) {
1206+
const map = new Map();
1207+
for (const [resourcePath, tags] of Object.entries(tagOps)) {
1208+
map.set(resourcePath, new Map(Object.entries(tags)));
1209+
}
1210+
return map;
1211+
}
1212+
1213+
/**
1214+
* @param {Map<string, Map<string, {string|number|boolean|undefined}>>} tagOps
1215+
* Map of resource paths to their tag operations
1216+
*/
1217+
function tagOpsToObject(tagOps) {
1218+
const obj = Object.create(null);
1219+
for (const [resourcePath, tags] of tagOps.entries()) {
1220+
obj[resourcePath] = Object.fromEntries(tags.entries());
1221+
}
1222+
return obj;
1223+
}

packages/project/lib/build/cache/StageCache.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* @typedef {object} StageCacheEntry
33
* @property {object} stage The cached stage instance (typically a reader or writer)
44
* @property {string[]} writtenResourcePaths Array of resource paths written during stage execution
5+
* @property {Map<string, Map<string, {string|number|boolean|undefined}>>} resourceTagOperations
6+
* Map of resource paths to their tags that were set or cleared during this stage's execution
57
*/
68

79
/**
@@ -40,8 +42,11 @@ export default class StageCache {
4042
* @param {string} signature Content hash signature of the stage's input resources
4143
* @param {object} stageInstance The stage instance to cache (typically a reader or writer)
4244
* @param {string[]} writtenResourcePaths Array of resource paths written during this stage
45+
* @param {Map<string, Map<string, {string|number|boolean|undefined}>>} projectTagOperations
46+
* @param {Map<string, Map<string, {string|number|boolean|undefined}>>} buildTagOperations
47+
* Map of resource paths to their tags that were set or cleared during this stage's execution
4348
*/
44-
addSignature(stageId, signature, stageInstance, writtenResourcePaths) {
49+
addSignature(stageId, signature, stageInstance, writtenResourcePaths, projectTagOperations, buildTagOperations) {
4550
if (!this.#stageIdToSignatures.has(stageId)) {
4651
this.#stageIdToSignatures.set(stageId, new Map());
4752
}
@@ -50,6 +55,8 @@ export default class StageCache {
5055
signature,
5156
stage: stageInstance,
5257
writtenResourcePaths,
58+
projectTagOperations,
59+
buildTagOperations,
5360
});
5461
this.#cacheQueue.push([stageId, signature]);
5562
}

packages/project/lib/build/helpers/ProjectBuildContext.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection";
21
import ProjectBuildLogger from "@ui5/logger/internal/loggers/ProjectBuild";
32
import TaskUtil from "./TaskUtil.js";
43
import TaskRunner from "../TaskRunner.js";
@@ -40,11 +39,6 @@ class ProjectBuildContext {
4039
this._queues = {
4140
cleanup: []
4241
};
43-
44-
this._resourceTagCollection = new ResourceTagCollection({
45-
allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"],
46-
allowedNamespaces: ["build"]
47-
});
4842
}
4943

5044
/**
@@ -178,18 +172,8 @@ class ProjectBuildContext {
178172
if (!resource.hasProject()) {
179173
this._log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`);
180174
resource.setProject(this._project);
181-
// throw new Error(
182-
// `Unable to get tag collection for resource ${resource.getPath()}: ` +
183-
// `Resource must be associated to a project`);
184-
}
185-
const projectCollection = resource.getProject().getResourceTagCollection();
186-
if (projectCollection.acceptsTag(tag)) {
187-
return projectCollection;
188-
}
189-
if (this._resourceTagCollection.acceptsTag(tag)) {
190-
return this._resourceTagCollection;
191175
}
192-
throw new Error(`Could not find collection for resource ${resource.getPath()} and tag ${tag}`);
176+
return resource.getProject().getResourceTagCollection(resource, tag);
193177
}
194178

195179
/**
@@ -288,6 +272,11 @@ class ProjectBuildContext {
288272
// Propagate changed paths to dependents
289273
this.propagateResourceChanges(changedPaths);
290274
}
275+
276+
buildFinished() {
277+
this.getBuildCache().buildFinished();
278+
}
279+
291280
/**
292281
* Informs the build cache about changed project source resources
293282
*

packages/project/lib/build/helpers/TaskUtil.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ class TaskUtil {
3535
* This tag identifies resources that contain (i.e. bundle) multiple other resources
3636
* @property {string} IsDebugVariant
3737
* This tag identifies resources that are a debug variant (typically named with a "-dbg" suffix)
38-
* of another resource. This tag is part of the build manifest.
38+
* of another resource. This tag is visible to other projects
3939
* @property {string} HasDebugVariant
4040
* This tag identifies resources for which a debug variant has been created.
41-
* This tag is part of the build manifest.
41+
* This tag is visible to other projects
4242
*/
4343

4444
/**

0 commit comments

Comments
 (0)