From ba035db204713e149b6529a7016c0c059cb7f82d Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Mon, 12 Oct 2026 16:47:56 +0200 Subject: [PATCH 1/8] feat(project): Replace stepBased opt-in with a task descriptor factory API From Specification Version 5.0, a task module's default export is a factory (options) => descriptor. The descriptor carries the whole task contract in one object: exactly one of `steps` (step-based) or `task` (legacy single stage), plus the optional `determineRequiredDependencies`, `determineBuildSignature`, and `specVersion` properties. This replaces the former static `stepBased` export and the top-level named-export callbacks, and resembles established plugin APIs such as that of Vite. All built-in tasks adopt the same shape: `determineBuildSignature`, `requiresDependencies`, and the step-vs-task decision move out of the build definitions into the task modules. The definition files shrink to per-project options and skip decisions (`skip: true` replaces `taskFunction: null`). The `generateBundle` wrapper stays in the definitions, because its `enhanceBundlesWithDefaults` helper lives in @ui5/project and cannot move into @ui5/builder. A custom task's `specVersion` property, if declared, must equal the task extension's configured Specification Version. Below 5.0 the default export stays the legacy task body, invoked directly during the build; any object it returns is ignored. The cache-internal `stepBased` flag (whether a stage ran the step runner) is unrelated and is retained. The descriptor is resolved once per build and memoized on `TaskDefinitions`, shared by the build-signature phase and task execution. `BUILD_SIG_VERSION` and `CACHE_VERSION` are bumped to invalidate existing caches; no backward-compat shim is added, as the v5 line is still in alpha. --- .../skills/incremental-build/architecture.md | 16 +- .../docs/pages/extensibility/CustomTasks.md | 96 ++- packages/builder/lib/tasks/buildThemes.js | 6 +- .../lib/tasks/bundlers/generateBundle.js | 175 +++--- .../bundlers/generateComponentPreload.js | 278 ++++----- .../bundlers/generateFlexChangesBundle.js | 237 ++++---- .../tasks/bundlers/generateLibraryPreload.js | 549 +++++++++--------- .../bundlers/generateStandaloneAppBundle.js | 230 ++++---- packages/builder/lib/tasks/enhanceManifest.js | 6 +- .../lib/tasks/escapeNonAsciiCharacters.js | 6 +- .../lib/tasks/generateCachebusterInfo.js | 54 +- .../lib/tasks/generateLibraryManifest.js | 99 ++-- .../lib/tasks/generateResourcesJson.js | 49 +- .../tasks/generateThemeDesignerResources.js | 6 +- .../builder/lib/tasks/generateVersionInfo.js | 61 +- .../jsdoc/executeJsdocSdkTransformation.js | 103 ++-- .../lib/tasks/jsdoc/generateApiIndex.js | 62 +- .../builder/lib/tasks/jsdoc/generateJsdoc.js | 121 ++-- packages/builder/lib/tasks/minify.js | 6 +- .../builder/lib/tasks/replaceBuildtime.js | 6 +- .../builder/lib/tasks/replaceCopyright.js | 8 +- packages/builder/lib/tasks/replaceVersion.js | 6 +- packages/builder/lib/tasks/runSteps.js | 15 +- packages/builder/lib/tasks/taskRepository.js | 4 +- .../lib/tasks/transformBootstrapHtml.js | 46 +- .../test/lib/tasks/bundlers/generateBundle.js | 16 +- .../bundlers/generateComponentPreload.js | 16 +- .../bundlers/generateFlexChangesBundle.js | 2 +- .../generateLibraryPreload.integration.js | 4 +- .../tasks/bundlers/generateLibraryPreload.js | 20 +- .../bundlers/generateStandaloneAppBundle.js | 12 +- .../test/lib/tasks/generateLibraryManifest.js | 2 +- .../test/lib/tasks/generateResourcesJson.js | 10 +- .../test/lib/tasks/generateVersionInfo.js | 2 +- .../jsdoc/executeJsdocSdkTransformation.js | 12 +- .../test/lib/tasks/jsdoc/generateApiIndex.js | 4 +- .../test/lib/tasks/jsdoc/generateJsdoc.js | 31 +- packages/builder/test/lib/tasks/runSteps.js | 46 +- .../test/lib/tasks/transformBootstrapHtml.js | 6 +- packages/project/lib/build/TaskDefinitions.js | 89 ++- packages/project/lib/build/TaskRunner.js | 178 +++--- .../project/lib/build/cache/CacheManager.js | 2 +- .../lib/build/definitions/application.js | 24 +- .../lib/build/definitions/component.js | 19 +- .../project/lib/build/definitions/library.js | 64 +- .../lib/build/definitions/themeLibrary.js | 10 +- .../project/lib/build/helpers/StepRunner.js | 52 ++ .../lib/build/helpers/getBuildSignature.js | 2 +- .../lib/specifications/extensions/Task.js | 114 +++- .../cross-project-tag-tasks/dep-tag-reader.js | 62 +- .../cross-project-tag-tasks/dep-tag-setter.js | 35 +- .../custom-tasks-2/custom-task-0.js | 32 +- .../custom-tasks-2/custom-task-1.js | 50 +- .../custom-tasks/custom-task-0.js | 32 +- .../custom-tasks/custom-task-1.js | 20 +- .../custom-tasks/custom-task-2.js | 32 +- .../dependency-race-condition-task.js | 78 +-- .../race-condition-add-file-task.js | 16 +- .../race-condition-delete-file-task.js | 16 +- .../application.a/race-condition-task.js | 20 +- .../application.a/task.build-signature.js | 43 +- .../application.a/task.dependency-change.js | 73 +-- .../fixtures/application.a/task.example.js | 20 +- .../application.a/task.root-conditional.js | 31 +- .../application.a/task.root-config.js | 23 +- .../fixtures/application.a/task.root-glob.js | 23 +- .../fixtures/application.a/task.step-based.js | 59 +- .../project/test/lib/build/TaskDefinitions.js | 74 ++- packages/project/test/lib/build/TaskRunner.js | 364 ++++++------ .../test/lib/build/definitions/application.js | 70 +-- .../test/lib/build/definitions/component.js | 28 +- .../test/lib/build/definitions/library.js | 247 ++------ .../lib/build/definitions/themeLibrary.js | 33 +- .../lib/build/helpers/getBuildSignature.js | 2 +- 74 files changed, 2308 insertions(+), 2157 deletions(-) diff --git a/.agents/skills/incremental-build/architecture.md b/.agents/skills/incremental-build/architecture.md index cc8305d9ef3..92ee5e68c4c 100644 --- a/.agents/skills/incremental-build/architecture.md +++ b/.agents/skills/incremental-build/architecture.md @@ -47,8 +47,8 @@ Use this table to locate source files. ALWAYS read the relevant source file befo | `drainSubscriptions` | `lib/build/helpers/watchSubscriptions.js` | Unsubscribes a list of subscriptions in parallel (`Promise.allSettled`), returns the failures. Used by both watchers' `destroy()` and BuildServer's recovery re-subscribe | | `fileWatcher` | `lib/build/helpers/fileWatcher.js` | Watcher-backend facade. Exposes a `subscribe()` matching `@parcel/watcher`'s contract and picks a backend once per process: `UI5_WATCH_MODE=polling\|native` forces the choice, otherwise it auto-detects containers (`/.dockerenv`, `/run/.containerenv`, PID 1 cgroup) and uses polling there. Also falls back to polling if the native `@parcel/watcher` binding cannot load. `UI5_WATCH_MODE=off` disables watching entirely: `subscribe()` returns an inert subscription (callback never invoked, `unsubscribe` a no-op) so no backend loads, for CI and other environments where sources do not change while the server runs. The memoized decision is a mode string exposed via `shouldUsePolling()` and `isWatchingDisabled()`. All three watchers subscribe through this facade rather than importing `@parcel/watcher` directly | | `pollingWatcher` | `lib/build/helpers/pollingWatcher.js` | Pure-JS polling backend. Walks the tree and diffs an `{mtimeMs, size}` snapshot every 250 ms (`DEFAULT_POLL_INTERVAL_MS`), emitting the same `{type, path}` events as the native backend. Needed on bind-mounted container volumes where inotify misses writes made from outside the container | -| `TaskRunner` | `lib/build/TaskRunner.js` | Task composition, execution loop, abort handling. Hands every task a plain `MonitoredTaskUtil`; for a step-based task it calls the task's step factory once per build at plan time to discover step names for `setTasks`, keeps the returned step array on the task, and reuses it to drive the `StepRunner` with per-stage hooks (`#createStepStageHooks`: `prepareStage`/`getPreviousInvocationData`/`createStageContext`/`recordStage`), and reports the task skipped/executed from the runner's `anyStepExecuted`. A legacy task runs a plain body with no step runner | -| `StepRunner` | `lib/build/helpers/StepRunner.js` | Per-task driver behind the step-factory build API, driving **one pipeline stage per step** (Phase B). Runs scalar and map steps in array order; per step it calls `prepareStage` (switch + cache verdict), runs/restores the step's invocations against a fresh per-stage recording context and per-step `MonitoredTaskUtil`, threads `needs` returns between steps, folds the stage's own keys' reads/inputs (`#foldStageKeys`, including cached keys) and derives the stage's stale outputs (`#computeStaleOutputs`), then calls `recordStage`. No cross-step fold. See "Step-Based Build Tasks" | +| `TaskRunner` | `lib/build/TaskRunner.js` | Task composition, execution loop, abort handling. Hands every task a plain `MonitoredTaskUtil`; for a step-based task it reads the memoized `descriptor.steps` (resolved by `TaskDefinitions`) at plan time to discover step names for `setTasks`, keeps the frozen step array on the task, and reuses it to drive the `StepRunner` with per-stage hooks (`#createStepStageHooks`: `prepareStage`/`getPreviousInvocationData`/`createStageContext`/`recordStage`), and reports the task skipped/executed from the runner's `anyStepExecuted`. A task with a `descriptor.task` (or a `taskFunction` definition override) runs a plain body with no step runner | +| `StepRunner` | `lib/build/helpers/StepRunner.js` | Per-task driver behind the descriptor's `steps`, driving **one pipeline stage per step** (Phase B). Also exports `validateSteps` and `validateDescriptor` (the task-descriptor validator). Runs scalar and map steps in array order; per step it calls `prepareStage` (switch + cache verdict), runs/restores the step's invocations against a fresh per-stage recording context and per-step `MonitoredTaskUtil`, threads `needs` returns between steps, folds the stage's own keys' reads/inputs (`#foldStageKeys`, including cached keys) and derives the stage's stale outputs (`#computeStaleOutputs`), then calls `recordStage`. No cross-step fold. See "Step-Based Build Tasks" | | `Cache` enum | `lib/build/cache/Cache.js` | Cache mode constants: `Default`, `Force`, `ReadOnly`, `Off` (CLI `--cache` option) | | `ProjectBuildCache` | `lib/build/cache/ProjectBuildCache.js` | Cache orchestration per project: index management, stage lookup, result recording. The unit of caching is a **stage**: `#stageCaches` is keyed by stage id (a legacy task's `task/{taskName}`, or a step-based task's `task/{taskName}::step/{stepName}` per step); `prepareStageExecutionAndValidateCache(taskName, stepName?)` and `recordStageResult({taskName, …, stepName?})` operate per stage; `setTasks([{taskName, stepNames?}])` creates one stage per step | | `BuildStageCache` | `lib/build/cache/BuildStageCache.js` | Per-stage resource request tracking and index management (one instance per stage, keyed in `ProjectBuildCache` by stage id). Also holds the stage's `TaskInputSet` (non-resource inputs) and exposes `getInputSignature(resolveValue)` | @@ -268,7 +268,7 @@ The cache uses content-based signatures at multiple levels: | Level | What it captures | Where computed | |-------|-----------------|----------------| -| **Build signature** | `getBaseSignature()` (`BUILD_SIG_VERSION` + build config), combined by `getProjectSignature()` with the aggregated task signatures, `project.getId()`, project config, the `@ui5/project` version (`getPackageVersion("@ui5/project")`), and the effective `@ui5/builder` / `@ui5/fs` versions (`taskRepository.getVersions()`). Task signatures come from `TaskDefinitions.getBuildSignatures()`, which calls each task's `determineBuildSignature()` (falling back to a hash of the task options/configuration). | `getBuildSignature.js`, `TaskDefinitions.getBuildSignatures()`, `ProjectBuildContext.create()` | +| **Build signature** | `getBaseSignature()` (`BUILD_SIG_VERSION` + build config), combined by `getProjectSignature()` with the aggregated task signatures, `project.getId()`, project config, the `@ui5/project` version (`getPackageVersion("@ui5/project")`), and the effective `@ui5/builder` / `@ui5/fs` versions (`taskRepository.getVersions()`). Task signatures come from `TaskDefinitions.getBuildSignatures()`, which resolves each task's descriptor (once per build, memoized) and calls its `determineBuildSignature()` (falling back to a hash of the task options/configuration). | `getBuildSignature.js`, `TaskDefinitions.getBuildSignatures()`, `ProjectBuildContext.create()` | | **Source signature** | Merkle root of all source resources | `ResourceIndex` (source index) | | **Stage signature** | An explicit four-component tuple `projectIndexSignature-dependencyIndexSignature-inputSignature-rootSignature`, joined by `-` (each component a SHA-256 hex digest, so the separator never occurs inside a component and the split is lossless). The four dimensions (project resources, dependency resources, non-resource **input signature**, root resources) are independent slots rather than folded into two, so a delta pairs a changed project or dependency signature with the current input and root signatures with no reverse mapping. The join/split primitives live in `cache/stageSignature.js`; `BuildStageCache.getStageSignatures()` composes the exact-match candidates. One stage per step for a step-based task, one per legacy task | `ProjectBuildCache.prepareStageExecutionAndValidateCache()`, `BuildStageCache.getStageSignatures()` | | **Result signature** | A four-component tuple `sourceSignature-combinedDependencySignature-aggregatedInputSignature-aggregatedRootSignature`. The dependency component is a cartesian product over per-stage candidate dependency signatures; the aggregated input and root signatures are single current values (constants across the product). Both the candidate list and the stored signature derive the per-stage dependency list from the single stage order (`#stageOrder`), so the store and lookup sides cover the same stages in the same order (a declared stage missing a signature throws rather than silently never matching). Grows by one factor per stage, bounded because only dependency-reading stages with a delta contribute a factor >1 | `ProjectBuildCache.#getResultStageSignature()` / `#getPossibleResultStageSignatures()` | @@ -367,11 +367,13 @@ Two DSL rules make the per-step recording honest, and both are visible here. The **Returns.** A step may return resources (stored in the CAS by integrity, as before) or a JSON-serializable value (persisted inline with the invocation's invocation data). Either is injected into a consumer via `needs.`, and the return's signature (resource integrities, or the serialized value) folds into the consumer's per-invocation selection so a changed producer return re-runs the consumer. A map producer's return signature is order-independent: it hashes a `keyId -> signature` map sorted by `keyId`, not a positional list in `keys()` order, so a key reordering that changes nothing semantically (an adapter change, filesystem ordering, a reader-collection reshuffle) does not move the signature and re-run every consumer for nothing. A step returning nothing has a `null` return descriptor. A returned value that is neither resources nor JSON-serializable throws. -**The `stepBased` opt-in.** A task is step-based only when it declares it; absent, the default export is a legacy task body and runs unchanged. A standard task sets `stepBased: true` in its build-definition entry (`definitions/*.js`), read by `_addTask`. A custom task declares a static `stepBased` export on its task module, surfaced by `Task#getStepBased` and honored from Specification Version 5.0 (`_addCustomTask` computes `specVersion.gte("5.0") && (await getStepBased()) === true`); a `stepBased` export below 5.0 is ignored and the task runs as a legacy body. There is no shape-sniffing: both legacy and factory are default-export functions, disambiguated by the flag. +**The task descriptor (step-based vs legacy).** From Specification Version 5.0 (and for every built-in task), a task module's default export is a factory `(options) => descriptor`. The descriptor carries the whole task contract in one object: exactly one of `steps` (the step-based path) or `task` (a legacy single-stage body), plus the optional `determineRequiredDependencies` / `determineBuildSignature` callbacks and an optional declared `specVersion`. A task is step-based when its descriptor has a `steps` array. This replaced the former scattered opt-in (a static `stepBased` export on a custom task module, or a `stepBased: true` entry in `definitions/*.js`) and the former top-level named-export callbacks. The descriptor is validated by `validateDescriptor` (exported from `StepRunner.js`, next to `validateSteps`): exactly one of `steps`/`task`, callbacks must be functions, a declared `specVersion` must equal the task extension's configured Specification Version. For a **custom** task extension below 5.0 the gate is a hard version boundary with no shape-sniffing: the default export is the legacy task body invoked directly (any value it returns is ignored), so there is no descriptor and none of its callbacks apply. **This `specVersion` is the custom task extension's own (from its `ui5.yaml`), unrelated to the Specification Version of the project being built; built-in tasks carry the descriptor shape unconditionally and their project-specVersion gating stays in `definitions/*.js`.** Note the cache-internal `stepBased` flag (the persisted `stages` array `[stageId, stepBased?1:0]`, `BuildStageCache`), which means "this stage ran the step runner", is a different thing and is **retained** — only the task-API opt-in was removed. -**Driving one stage per step.** Before the build, `TaskRunner.runTasks` calls each step-based task's factory once (pure over `options`) to discover its step names and hands `ProjectBuildCache.setTasks` a `[{taskName, stepNames?}]` list, which creates one stage per step in step order. The returned step array is kept on the task (frozen, so a custom task cannot mutate the shared value) and reused when the step runs, so the factory is called once per build rather than once to discover and once to execute. One call is the single source of truth: discovery and execution cannot drive different stages, so no runtime check is needed to reconcile two factory calls. The `options` object is completed at registration time (`_addTask` fills in `projectName`, `projectNamespace`) and is fixed for a `TaskRunner`'s lifetime: a changed `ui5.yaml` task configuration rebuilds the whole serving stack through `Supervisor.reinitialize`, giving a fresh `TaskRunner`, while a plain source-change rebuild reuses the `TaskRunner` and re-derives the step array. Re-deriving per build keeps the steps in step with the options without any explicit invalidation. A factory body that reads the environment or the clock untracked is still impure, but a single call means its one output drives both the stage list and execution consistently. For each step, the `StepRunner` drives that step's own stage via hooks the `TaskRunner` supplies (`#createStepStageHooks`): `prepareStage(stepName)` switches the project to the step's stage and returns its cache verdict (`prepareStageExecutionAndValidateCache(taskName, stepName)`); `createStageContext()` builds fresh monitored `workspace`/`dependencies` readers and a `MonitoredTaskUtil` bound to that stage (so the reads reflect the cumulative output of all earlier stages with this stage's writer on top); `recordStage(stepName, …)` records the step's stage (`recordStageResult({taskName, …, stepName})`). A fully-cached stage (`prepareStage` returns `true`) does not run: its return is rebuilt from the persisted per-key data (in key order) so later steps' `needs` resolve, and its tag operations are replayed. A `needs` return is excluded from the stage signature, so a full-hit consumer whose producer re-ran with a changed return would otherwise serve stale output. The `StepRunner` checks this on the full-hit path (`#needsReturnChanged`) and, when it fires, reopens the stage with a fresh writer and re-runs it as a full execution (`reopenStage` returns a falsy verdict, not a delta). Reopen installs a fresh empty writable stage, so pruning the re-run to only the affected invocations would need re-seeding it with the cached outputs first, so the whole stage re-runs instead. No shipped multi-step task pays for this: only `generateThemeDesignerResources` wires `needs`, and a changed `scan` return regenerates its themes anyway. The verdict (`true` / delta object / falsy) is read three ways in `runSteps`. An explicit verdict shape would read more clearly but is deferred, since that shape is shared with the stage-signature code. A task is reported to the `ProjectBuildLogger` as skipped (`skipTask`) when every step was served from cache, else as executed. Its skip verdict is only known once every stage has been driven, so it cannot be announced up front like a legacy task: the `StepRunner` instead calls `notifyStepExecution` from the first stage that stops being a pure cache hit, before that stage runs anything, and `TaskRunner.#createTaskExecutionReport` turns that into `startTask`. `task-start` therefore precedes the work it announces, and the matching `endTask` (with the union of the stages' written paths) follows it, so a `project-build-status` consumer sees the task running while it runs. A step-based task that returns **no** steps still has one stage (`task/{taskName}`) that is prepared and recorded (empty), so its empty result caches and a later build reports it skipped. +**One factory call, memoized on `TaskDefinitions`.** The descriptor is resolved once per build and shared by the two phases that need it. `TaskDefinitions.getBuildSignatures()` (which needs `determineBuildSignature`) runs inside `ProjectBuildContext.create()`, before the `ProjectBuildCache` and `TaskRunner` exist; `TaskRunner` (which needs `steps`/`task`/`determineRequiredDependencies`) runs later off the **same** `TaskDefinitions` instance. For a built-in, `TaskDefinitions.#resolveStandardDescriptor` calls the factory from `taskRepository.getTask(name)` and caches it on the standard-task map entry (`getStandardDescriptor` exposes it to `TaskRunner`); a skipped (`skip:true`) or `taskFunction`-overridden task has no descriptor and falls back to hashing its options. For a custom task, `Task#getDescriptor(taskName, options)` memoizes per runtime `taskName` (so a `--N`-suffixed second configuration of the same extension instance gets its own descriptor). `TaskDefinitions` completes `projectName`/`projectNamespace` into each standard task's options **before** the first factory call, so a factory that branches on `projectNamespace` at factory time (`generateThemeDesignerResources` emits its `libraryTheming` step only for a namespace) produces the same descriptor at signature time and at execution. -**Standalone (uncached) runner.** `@ui5/builder` exposes `runSteps(factory, {workspace, dependencies, taskUtil, options})` (`lib/tasks/runSteps.js`) for standalone task invocation with no build cache: it runs every step in order, fans out each `keys` set, threads `needs` in memory, and buffers map writes in key order, without delta selection, persistence, or tag replay. It is an independent implementation (it does not use the `@ui5/project` `StepRunner`); the full cached engine stays in `@ui5/project` because `@ui5/builder` cannot import it. The two runners stay separate but share one definition of the write-buffer contract: the same-path guard (with its user-visible error message) and the key-order flush live in `@ui5/fs/internal/stepWriteBuffer` (`assertDistinctWrite`, `flushWriteBuffer`), reached by both because both already depend on `@ui5/fs`. The two writers differ only in which method they override: `@ui5/project`'s `RecordingReaderWriter` overrides `_write` (it also records writes for the cache, and the base `write` has already defaulted options), while `@ui5/builder`'s `BufferedWriter` overrides the public `write` to preserve the caller's exact arguments; both buffer `{resource, args, index}` entries and flush them identically. +**Driving one stage per step.** Before the build, `TaskRunner.runTasks` reads each step-based task's `descriptor.steps` (already resolved by `TaskDefinitions`) and hands `ProjectBuildCache.setTasks` a `[{taskName, stepNames?}]` list, which creates one stage per step in step order. The step array is kept on the task (frozen, so a custom task cannot mutate the shared value) and reused when the step runs. One memoized descriptor is the single source of truth: discovery and execution cannot drive different stages, so no runtime check is needed to reconcile two factory calls. The `options` object is completed at registration time (`TaskDefinitions`/`_addTask` fill in `projectName`, `projectNamespace`) and is fixed for a `TaskRunner`'s lifetime: a changed `ui5.yaml` task configuration rebuilds the whole serving stack through `Supervisor.reinitialize`, giving a fresh `TaskRunner`, while a plain source-change rebuild reuses the `TaskRunner` and re-reads the frozen step array. For each step, the `StepRunner` drives that step's own stage via hooks the `TaskRunner` supplies (`#createStepStageHooks`): `prepareStage(stepName)` switches the project to the step's stage and returns its cache verdict (`prepareStageExecutionAndValidateCache(taskName, stepName)`); `createStageContext()` builds fresh monitored `workspace`/`dependencies` readers and a `MonitoredTaskUtil` bound to that stage (so the reads reflect the cumulative output of all earlier stages with this stage's writer on top); `recordStage(stepName, …)` records the step's stage (`recordStageResult({taskName, …, stepName})`). A fully-cached stage (`prepareStage` returns `true`) does not run: its return is rebuilt from the persisted per-key data (in key order) so later steps' `needs` resolve, and its tag operations are replayed. A `needs` return is excluded from the stage signature, so a full-hit consumer whose producer re-ran with a changed return would otherwise serve stale output. The `StepRunner` checks this on the full-hit path (`#needsReturnChanged`) and, when it fires, reopens the stage with a fresh writer and re-runs it as a full execution (`reopenStage` returns a falsy verdict, not a delta). Reopen installs a fresh empty writable stage, so pruning the re-run to only the affected units would need re-seeding it with the cached outputs first, so the whole stage re-runs instead. No shipped multi-step task pays for this: only `generateThemeDesignerResources` wires `needs`, and a changed `scan` return regenerates its themes anyway. The verdict (`true` / delta object / falsy) is read three ways in `runSteps`. An explicit verdict shape would read more clearly but is deferred, since that shape is shared with the stage-signature code. A task is reported to the `ProjectBuildLogger` as skipped (`skipTask`) when every step was served from cache, else as executed. Its skip verdict is only known once every stage has been driven, so it cannot be announced up front like a legacy task: the `StepRunner` instead calls `notifyStepExecution` from the first stage that stops being a pure cache hit, before that stage runs anything, and `TaskRunner.#createTaskExecutionReport` turns that into `startTask`. `task-start` therefore precedes the work it announces, and the matching `endTask` (with the union of the stages' written paths) follows it, so a `project-build-status` consumer sees the task running while it runs. A step-based task that returns **no** steps still has one stage (`task/{taskName}`) that is prepared and recorded (empty), so its empty result caches and a later build reports it skipped. + +**Standalone (uncached) runner.** `@ui5/builder` exposes `runSteps(factory, {workspace, dependencies, taskUtil, options})` (`lib/tasks/runSteps.js`) for standalone task invocation with no build cache: it calls the factory, reads `descriptor.steps` (throwing if the descriptor has no `steps` array), runs every step in order, fans out each `keys` set, threads `needs` in memory, and buffers map writes in key order, without delta selection, persistence, or tag replay. It is an independent implementation (it does not use the `@ui5/project` `StepRunner`); the full cached engine stays in `@ui5/project` because `@ui5/builder` cannot import it. The two runners stay separate but share one definition of the write-buffer contract: the same-path guard (with its user-visible error message) and the key-order flush live in `@ui5/fs/internal/stepWriteBuffer` (`assertDistinctWrite`, `flushWriteBuffer`), reached by both because both already depend on `@ui5/fs`. The two writers differ only in which method they override: `@ui5/project`'s `RecordingReaderWriter` overrides `_write` (it also records writes for the cache, and the base `write` has already defaulted options), while `@ui5/builder`'s `BufferedWriter` overrides the public `write` to preserve the caller's exact arguments; both buffer `{resource, args, index}` entries and flush them identically. Per invocation within a stage, the callback receives its own recording readers (`{workspace, dependencies}`) that delegate to the stage's monitored readers and additionally attribute each read to the invocation, plus a per-invocation `MonitoredTaskUtil` wrapping the stage's one. The per-invocation monitor attributes the invocation's non-resource inputs (`getEnv`, `getTime`, `getProject(name).getVersion()`, `isRootProject`, `getDependencies`) and its `getTag`/`setTag`/`clearTag` operations to the invocation, while reads still delegate through the stage monitor so a full build's stage-level recording stays the union that keys the stage. Key identity (`StepRunner.#keyId`) is content-based: a resource key by its path and a content discriminator (path distinguishes resources that share content but produce different output; the discriminator makes a content change a new key, so the invocation re-runs and its previous output is dropped rather than served stale), a string key by its value. Compound keys are the caller's responsibility to express as a stable string. The discriminator is **tiered like `isResourceUnchanged`** (see §"HashTree") rather than always the SSRI integrity: `lastModified` + `size` when both are statically available (`getLastModified()` returns a number and `hasSize()` is true, so no content read), falling back to the integrity when either is missing — a memory-backed or generated resource with no `lastModified`, or one whose size is not statically known. A resource restored from a stage cache carries its `integrity`, so the fallback `getIntegrity()` resolves without reading content and that path stays cheap. This avoids hashing every enumerated key's full content before delta selection; it is why `#resolveEntries` is cheap for a broad `workspace.byGlob` step (`replaceCopyright`, `replaceVersion`) on a stale-cache build (see performance-investigation.md §12). The residual staleness risk is identical to `isResourceUnchanged`'s and accepted for the same reason: a content change that preserves **both** `lastModified` and `size` keeps the same key and does not re-run the invocation — a real edit moves mtime, so the gap is only for mtime-preserving replacements (`cp -p`, `tar -x`, atomic rename) that also hold size constant, and the changed-path delta does not independently cover this case because it is derived through the same tiered comparison. Because the stat tier and the integrity tier use distinct prefixes (`\0m…\0s…` vs `\0i…`), a key never aliases across the two tiers. @@ -639,7 +641,7 @@ Stage metadata stored on disk includes: 3. **Abort/retry**: File changes abort running builds; projects re-queued automatically 4. **Structural sharing**: Derived hash trees share unchanged subtrees, reducing memory 5. **Content-addressed storage**: Resources deduplicated via integrity hashes in custom CAS (synchronous path resolution, gzip-compressed) -6. **Differential caching**: Stages track resource requests; delta builds only re-process changed resources. A task participates by being step-based (the `stepBased` flag, see "Step-Based Build Tasks"), which promotes each step to its own stage; this replaced the older `changedProjectResourcePaths` parameter +6. **Differential caching**: Stages track resource requests; delta builds only re-process changed resources. A task participates by being step-based (its descriptor returns a `steps` array, see "Step-Based Build Tasks"), which promotes each step to its own stage; this replaced the older `changedProjectResourcePaths` parameter 7. **Tag propagation**: Resource tags flow through stages via cached tag operations, included in hash signatures 8. **Two-tier cache**: Fast in-memory StageCache + persistent filesystem cache via CacheManager 9. **Two-phase invalidation**: Changes queued via `projectSourcesChanged()` / `dependencyResourcesChanged()` (state -> `REQUIRES_UPDATE`), applied only during `#flushPendingChanges()` at next build start. "Definitely invalidated" only after content comparison confirms actual differences. diff --git a/internal/documentation/docs/pages/extensibility/CustomTasks.md b/internal/documentation/docs/pages/extensibility/CustomTasks.md index 444a149e25b..205e2c92f37 100644 --- a/internal/documentation/docs/pages/extensibility/CustomTasks.md +++ b/internal/documentation/docs/pages/extensibility/CustomTasks.md @@ -105,11 +105,29 @@ task: ## Task Implementation -A custom task implementation needs to return a function with the following signature: +From [Specification Version](../Configuration.md#specification-versions) 5.0, a custom task module's default export is a **factory function** that receives the task `options` and returns a **descriptor** object. The descriptor declares the task's work and its optional callbacks in one place: + +* `task` — a traditional task body (see the signature below), **or** +* `steps` — an array of steps for a [cache-aware task](#cache-aware-tasks) (exactly one of `task` or `steps` must be defined) +* `determineRequiredDependencies` — optional, see [Required Dependencies](#required-dependencies) +* `determineBuildSignature` — optional callback contributing to the project's build signature +* `specVersion` — optional; if set, it must equal the task extension's configured Specification Version + +The factory receives only the `options` object (never readers or a `taskUtil`), so it must be pure: it may branch on `options` and precompute values, but it must not read or write resources. Those happen in the `task` body (or in the step callbacks). + +::: info +Below Specification Version 5.0, the default export **is** the task body itself (the signature shown below), and the callbacks are top-level named exports. The factory-and-descriptor model, cache-aware `steps`, and the `determineBuildSignature` callback are available from Specification Version 5.0. +::: ::: code-group ```js [ESM] +export default function(options) { + return { + task: myTask + }; +}; + /** * Custom task API * @@ -142,12 +160,18 @@ A custom task implementation needs to return a function with the following signa * @returns {Promise} * Promise resolving once the task has finished */ -export default async function({dependencies, log, options, taskUtil, workspace}) { +async function myTask({dependencies, log, options, taskUtil, workspace}) { // [...] }; ``` ```js [CommonJS] +module.exports = function(options) { + return { + task: myTask + }; +}; + /** * Custom task API * @@ -180,7 +204,7 @@ export default async function({dependencies, log, options, taskUtil, workspace}) * @returns {Promise} * Promise resolving once the task has finished */ -module.exports = async function({dependencies, log, options, taskUtil, workspace}) { +async function myTask({dependencies, log, options, taskUtil, workspace}) { // [...] }; ``` @@ -193,7 +217,9 @@ This functionality has been added with UI5 CLI [`v3.0.0`](https://github.com/SAP ::: -Custom tasks can export an optional callback function `determineRequiredDependencies` to control which dependency-resources are made available through the `dependencies`-reader that is provided to the task. By reducing the amount of required dependencies or by not requiring any, UI5 CLI might be able to build a project faster. +Custom tasks can declare an optional `determineRequiredDependencies` callback to control which dependency-resources are made available through the `dependencies`-reader that is provided to the task. By reducing the amount of required dependencies or by not requiring any, UI5 CLI might be able to build a project faster. + +From Specification Version 5.0 the callback is a property of the descriptor the task factory returns. Below 5.0 it is a top-level named export of the task module. Before executing a task, UI5 CLI will ensure that all required dependencies have been built. @@ -208,6 +234,13 @@ If this callback is not provided, UI5 CLI will make an assumption as to whether ::: code-group ```js [ESM] +export default function(options) { + return { + determineRequiredDependencies, + task + }; +}; + /** * Callback function to define the list of required dependencies * @@ -232,7 +265,7 @@ If this callback is not provided, UI5 CLI will make an assumption as to whether * UI5 CLI will ensure that those dependencies have been * built before executing the task. */ -export async function determineRequiredDependencies({availableDependencies, getDependencies, getProject, options}) { +async function determineRequiredDependencies({availableDependencies, getDependencies, getProject, options}) { // "availableDependencies" could look like this: Set(3) { "sap.ui.core", "sap.m", "my.lib" } // Reduce list of required dependencies: Do not require any UI5 framework projects @@ -247,6 +280,13 @@ export async function determineRequiredDependencies({availableDependencies, getD ``` ```js [CommonJS] +module.exports = function(options) { + return { + determineRequiredDependencies, + task + }; +}; + /** * Callback function to define the list of required dependencies * @@ -271,7 +311,7 @@ export async function determineRequiredDependencies({availableDependencies, getD * UI5 CLI will ensure that those dependencies have been * built before executing the task. */ -module.exports.determineRequiredDependencies = async function({availableDependencies, getDependencies, getProject, options}) { +async function determineRequiredDependencies({availableDependencies, getDependencies, getProject, options}) { // "availableDependencies" could look like this: Set(3) { "sap.ui.core", "sap.m", "my.lib" } // Reduce list of required dependencies: Do not require any UI5 framework projects @@ -288,28 +328,30 @@ module.exports.determineRequiredDependencies = async function({availableDependen ### "Cache-aware" Tasks -Due to UI5 Builder and UI5 Server supporting **build caches** of task data, custom tasks can opt into this behavior to improve performance. A cache-aware task is a **step-based task**: instead of a task body, it default-exports a factory `build(options)` and declares a static `stepBased` flag. The factory returns an array of steps that describe the work. The build cache tracks each step's inputs and, on a delta build, re-runs only the steps (and, within a step, only the keys) whose inputs changed, restoring the rest from cache. A task that is not step-based, runs on a Specification Version below 5.0, or has no cache available, processes all resources from scratch. +Due to UI5 Builder and UI5 Server supporting **build caches** of task data, custom tasks can opt into this behavior to improve performance. A cache-aware task is a **step-based task**: its descriptor returns a `steps` array instead of a `task` body. The steps describe the work. The build cache tracks each step's inputs and, on a delta build, re-runs only the steps (and, within a step, only the keys) whose inputs changed, restoring the rest from cache. A task that returns a `task` body, runs on a Specification Version below 5.0, or has no cache available, processes all resources from scratch. -Step-based custom tasks are available from Specification Version 5.0. A task opts in with a static `stepBased` export set to `true`: +Step-based custom tasks are available from Specification Version 5.0. A task opts in by returning a `steps` array (instead of `task`) from its descriptor: ::: code-group ```js [ESM] -export const stepBased = true; -export default function build(options) { - return [ /* steps */ ]; +export default function(options) { + return { + steps: [ /* steps */ ] + }; }; ``` ```js [CommonJS] -module.exports = function build(options) { - return [ /* steps */ ]; +module.exports = function(options) { + return { + steps: [ /* steps */ ] + }; }; -module.exports.stepBased = true; ``` ::: -The factory receives the task `options` only (the same `options` object the standard task function receives); it never receives readers or a `taskUtil`, so it cannot close over build state. It must be pure over `options`: it may branch on `options.projectNamespace`, precompute glob patterns, or include or omit steps, but it must not read or write resources. Every input a step reads arrives through the step's own callback arguments. +The factory receives the task `options` only (the same `options` object the task body receives); it never receives readers or a `taskUtil`, so it cannot close over build state. It must be pure over `options`: it may branch on `options.projectNamespace`, precompute glob patterns, or include or omit steps, but it must not read or write resources. Every input a step reads arrives through the step's own callback arguments. There are two kinds of steps, run in array order: @@ -322,7 +364,7 @@ A step may list earlier step names in `needs`; those steps' return values then a ::: info Best Practices for Cache-aware Tasks 1. **Keep tasks deterministic**: Given the same inputs, always produce the same outputs. -2. **Keep the factory pure**: The `build(options)` factory must only return step descriptors from `options`. Never read or write resources in the factory; do that in the step callbacks. +2. **Keep the factory pure**: The factory must only return the descriptor (with its `steps`) from `options`. Never read or write resources in the factory; do that in the step callbacks. 3. **Read and write only through the callback arguments**: Every input a step reads must arrive through its `workspace`, `dependencies`, `taskUtil`, or `needs` arguments, and every write must go through the step's own `workspace`. A read or write that bypasses these is not recorded, so the cache cannot track it and a delta build can serve stale output. 4. **Import heavy processors lazily**: UI5 CLI calls the factory on every build to discover the steps, even when every step is a cache hit. Import heavy modules inside the step callbacks (for example `const p = (await import("./processor.js")).default`), not at the top of the module, so a fully cached build does not load them. 5. **Split work carefully**: Only use a map step if each key can be processed independently. Two keys that run concurrently must not write the same path. @@ -351,9 +393,8 @@ const log = getLogger("builder:tasks:renderMarkdownFiles"); /* * Render all .md (Markdown) files in the project to HTML */ -export const stepBased = true; export default function build(options) { - return [{ + return {steps: [{ name: "render", // One cached invocation per Markdown file, so a delta build re-renders only the files that changed. keys: async ({workspace}) => workspace.byGlob("**/*.md"), @@ -375,7 +416,7 @@ export default function build(options) { string: htmlString })); } - }]; + }]}; }; ``` @@ -389,7 +430,7 @@ const log = getLogger("builder:tasks:renderMarkdownFiles"); * Render all .md (Markdown) files in the project to HTML */ module.exports = function build(options) { - return [{ + return {steps: [{ name: "render", // One cached invocation per Markdown file, so a delta build re-renders only the files that changed. keys: async ({workspace}) => workspace.byGlob("**/*.md"), @@ -411,9 +452,8 @@ module.exports = function build(options) { string: htmlString })); } - }]; + }]}; }; -module.exports.stepBased = true; ``` ::: @@ -435,11 +475,15 @@ APIs to retrieve additional information about the project currently being built ```js [ESM] import path from "node:path"; +export default function(options) { + return {task}; +}; + /* * Compile a list of all licenses of the project's dependencies * and write it to "dependency-license-summary.json */ -export default async function({dependencies, log, options, taskUtil, workspace}) { +async function task({dependencies, log, options, taskUtil, workspace}) { const {createResource} = taskUtil.resourceFactory; const licenses = new Map(); const projectsVisited = new Set(); @@ -483,11 +527,15 @@ export default async function({dependencies, log, options, taskUtil, workspace}) ```js [CommonJS] const path = require("node:path"); +module.exports = function(options) { + return {task}; +}; + /* * Compile a list of all licenses of the project's dependencies * and write it to "dependency-license-summary.json" */ -module.exports = async function({dependencies, log, options, taskUtil, workspace}) { +async function task({dependencies, log, options, taskUtil, workspace}) { const {createResource} = taskUtil.resourceFactory; const licenses = new Map(); const projectsVisited = new Set(); diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index 2305440cd0d..b282d0d3378 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -180,12 +180,12 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP * @param {string} [options.librariesPattern] Search pattern for .library files * @param {string} [options.themesPattern] Search pattern for sap.ui.core theme folders * @param {boolean} [options.compress=true] - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({projectName, inputPattern, librariesPattern, themesPattern, compress}) { compress = compress === undefined ? true : compress; - return [{ + return {requiresDependencies: true, steps: [{ name: "buildThemes", // One key per theme's library.source.less, so a delta build rebuilds only the affected theme. keys: async ({workspace}) => workspace.byGlob(inputPattern), @@ -204,5 +204,5 @@ export default function build({projectName, inputPattern, librariesPattern, them const processedResources = await buildThemeResources([themeResource], combo, compress, taskUtil); await Promise.all(processedResources.map((resource) => workspace.write(resource))); }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/bundlers/generateBundle.js b/packages/builder/lib/tasks/bundlers/generateBundle.js index 12c2841c5fb..f55f7210a3a 100644 --- a/packages/builder/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateBundle.js @@ -26,109 +26,112 @@ import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; * bundle definition * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [parameters.options.bundleOptions] Module * bundle options - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ /* eslint-enable max-len */ -export default async function({ - workspace, dependencies, taskUtil, options: {projectName, bundleDefinition, bundleOptions} -}) { - let combo = new ReaderCollectionPrioritized({ - name: `generateBundle - prioritize workspace over dependencies: ${projectName}`, - readers: [workspace, dependencies] - }); +export default function() { + return {requiresDependencies: true, task: async function({ + workspace, dependencies, taskUtil, options: {projectName, bundleDefinition, bundleOptions} + }) { + let combo = new ReaderCollectionPrioritized({ + name: `generateBundle - prioritize workspace over dependencies: ${projectName}`, + readers: [workspace, dependencies] + }); - const optimize = !bundleOptions || bundleOptions.optimize !== false; - if (taskUtil) { - /* Scenarios - 1. Optimize bundle with minification already done - Workspace: - * /resources/my/lib/Control.js [ui5:HasDebugVariant] - * /resources/my/lib/Control.js.map [ui5:HasDebugVariant] - * /resources/my/lib/Control-dbg.js [ui5:IsDebugVariant] + const optimize = !bundleOptions || bundleOptions.optimize !== false; + if (taskUtil) { + /* Scenarios + 1. Optimize bundle with minification already done + Workspace: + * /resources/my/lib/Control.js [ui5:HasDebugVariant] + * /resources/my/lib/Control.js.map [ui5:HasDebugVariant] + * /resources/my/lib/Control-dbg.js [ui5:IsDebugVariant] - Bundler input: - * /resources/my/lib/Control.js - * /resources/my/lib/Control.js.map + Bundler input: + * /resources/my/lib/Control.js + * /resources/my/lib/Control.js.map - => Filter out debug resources + => Filter out debug resources - 2. Optimize bundle with no minification - * /resources/my/lib/Control.js + 2. Optimize bundle with no minification + * /resources/my/lib/Control.js - => No action necessary + => No action necessary - 3. Debug-bundle with minification already done - Workspace: - * /resources/my/lib/Control.js [ui5:HasDebugVariant] - * /resources/my/lib/Control.js.map [ui5:HasDebugVariant] - * /resources/my/lib/Control-dbg.js [ui5:IsDebugVariant] + 3. Debug-bundle with minification already done + Workspace: + * /resources/my/lib/Control.js [ui5:HasDebugVariant] + * /resources/my/lib/Control.js.map [ui5:HasDebugVariant] + * /resources/my/lib/Control-dbg.js [ui5:IsDebugVariant] - Bundler input: - * /resources/my/lib/Control-dbg.js - * moduleNameMapping: [{"/resources/my/lib/Control-dbg.js": "my/lib/Control.js"}] + Bundler input: + * /resources/my/lib/Control-dbg.js + * moduleNameMapping: [{"/resources/my/lib/Control-dbg.js": "my/lib/Control.js"}] - => Filter out minified-resources (tagged as "HasDebugVariant", incl. source maps) and rename debug-files + => Filter out minified-resources (tagged as "HasDebugVariant", incl. source maps) and + rename debug-files - 4. Debug-bundle with no minification - * /resources/my/lib/Control.js + 4. Debug-bundle with no minification + * /resources/my/lib/Control.js - => No action necessary + => No action necessary - 5. Bundle with external input (optimize or not), e.g. TS-project - Workspace: - * /resources/my/lib/Control.ts - * /resources/my/lib/Control.js - * /resources/my/lib/Control.js.map + 5. Bundle with external input (optimize or not), e.g. TS-project + Workspace: + * /resources/my/lib/Control.ts + * /resources/my/lib/Control.js + * /resources/my/lib/Control.js.map - Bundler input: - * /resources/my/lib/Control.js - * /resources/my/lib/Control.js.map - */ + Bundler input: + * /resources/my/lib/Control.js + * /resources/my/lib/Control.js.map + */ - // Omit -dbg files for optimize bundles and vice versa - const filterTag = optimize ? - taskUtil.STANDARD_TAGS.IsDebugVariant : taskUtil.STANDARD_TAGS.HasDebugVariant; - combo = taskUtil.resourceFactory.createFilterReader({ - reader: combo, - callback: function(resource) { - return !taskUtil.getTag(resource, filterTag); - } - }); - } - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - return combo.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}").then((resources) => { - const options = { - bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), - bundleOptions, - allowStringBundling - }; - if (!optimize && taskUtil) { - options.moduleNameMapping = createModuleNameMapping({resources, taskUtil}); - } - if (coreVersion) { - options.targetUi5CoreVersion = coreVersion; - } - return moduleBundler({options, resources}).then((bundles) => { - return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { - if (!bundle) { - // Skip empty bundles - return; + // Omit -dbg files for optimize bundles and vice versa + const filterTag = optimize ? + taskUtil.STANDARD_TAGS.IsDebugVariant : taskUtil.STANDARD_TAGS.HasDebugVariant; + combo = taskUtil.resourceFactory.createFilterReader({ + reader: combo, + callback: function(resource) { + return !taskUtil.getTag(resource, filterTag); } - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + }); + } + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + return combo.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}").then((resources) => { + const options = { + bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), + bundleOptions, + allowStringBundling + }; + if (!optimize && taskUtil) { + options.moduleNameMapping = createModuleNameMapping({resources, taskUtil}); + } + if (coreVersion) { + options.targetUi5CoreVersion = coreVersion; + } + return moduleBundler({options, resources}).then((bundles) => { + return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { + if (!bundle) { + // Skip empty bundles + return; + } + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + if (sourceMap) { + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + } + const writes = [workspace.write(bundle)]; if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + writes.push(workspace.write(sourceMap)); } - } - const writes = [workspace.write(bundle)]; - if (sourceMap) { - writes.push(workspace.write(sourceMap)); - } - return Promise.all(writes); - })); + return Promise.all(writes); + })); + }); }); - }); + }}; } diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index e5eff43a35e..b050aaefaa4 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -10,6 +10,7 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @module @ui5/builder/tasks/bundlers/generateComponentPreload */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task to for application bundling. * @@ -31,154 +32,157 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @param {string[]} [parameters.options.paths] Array of paths (or glob patterns) for component files * @param {string[]} [parameters.options.namespaces] Array of component namespaces * @param {string[]} [parameters.options.skipBundles] Names of bundles that should not be created - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({ - workspace, taskUtil, options: {projectName, paths, namespaces, skipBundles = [], excludes = []} -}) { - let nonDbgWorkspace = workspace; - if (taskUtil) { - nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } - - return nonDbgWorkspace.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}") - .then(async (resources) => { - let allNamespaces = []; - if (paths) { - allNamespaces = await Promise.all(paths.map(async (componentPath) => { - const globPath = "/resources/" + componentPath; - log.verbose(`Globbing for Components directories with configured path ${globPath}...`); - const components = await nonDbgWorkspace.byGlob(globPath); - return components.map((component) => { - const compDir = path.dirname(component.getPath()).replace(/^\/resources\//i, ""); - log.verbose(`Found component namespace ${compDir}`); - return compDir; - }); - })); - } - if (namespaces) { - allNamespaces.push(...namespaces); - } - - allNamespaces = Array.prototype.concat.apply([], allNamespaces); - // As this task is often called with a single namespace, also check - // for bad calls like "namespaces: [undefined]" - if (!allNamespaces || !allNamespaces.length || !allNamespaces[0]) { - throw new Error("generateComponentPreload: No component namespace(s) " + - `found for project: ${projectName}`); - } +export default function() { + return {task: async function({ + workspace, taskUtil, options: {projectName, paths, namespaces, skipBundles = [], excludes = []} + }) { + let nonDbgWorkspace = workspace; + if (taskUtil) { + nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + } + }); + } - const allFilterExcludes = negateFilters(excludes); - const unusedFilterExcludes = new Set(allFilterExcludes); + return nonDbgWorkspace.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}") + .then(async (resources) => { + let allNamespaces = []; + if (paths) { + allNamespaces = await Promise.all(paths.map(async (componentPath) => { + const globPath = "/resources/" + componentPath; + log.verbose(`Globbing for Components directories with configured path ${globPath}...`); + const components = await nonDbgWorkspace.byGlob(globPath); + return components.map((component) => { + const compDir = path.dirname(component.getPath()).replace(/^\/resources\//i, ""); + log.verbose(`Found component namespace ${compDir}`); + return compDir; + }); + })); + } + if (namespaces) { + allNamespaces.push(...namespaces); + } - const bundleDefinitions = allNamespaces.map((namespace) => { - const bundleName = `${namespace}/Component-preload.js`; - if (skipBundles.includes(bundleName)) { - log.verbose(`Skipping generation of bundle ${bundleName}`); - return null; + allNamespaces = Array.prototype.concat.apply([], allNamespaces); + // As this task is often called with a single namespace, also check + // for bad calls like "namespaces: [undefined]" + if (!allNamespaces || !allNamespaces.length || !allNamespaces[0]) { + throw new Error("generateComponentPreload: No component namespace(s) " + + `found for project: ${projectName}`); } - const filters = [ - `${namespace}/`, - `${namespace}/**/manifest.json`, - `${namespace}/changes/changes-bundle.json`, - `${namespace}/changes/flexibility-bundle.json`, - `!${namespace}/test/` - ]; + const allFilterExcludes = negateFilters(excludes); + const unusedFilterExcludes = new Set(allFilterExcludes); - // Add configured excludes for namespace - allFilterExcludes.forEach((filterExclude) => { - // Allow all excludes (!) and limit re-includes (+) to the component namespace - if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) { - filters.push(filterExclude); - unusedFilterExcludes.delete(filterExclude); + const bundleDefinitions = allNamespaces.map((namespace) => { + const bundleName = `${namespace}/Component-preload.js`; + if (skipBundles.includes(bundleName)) { + log.verbose(`Skipping generation of bundle ${bundleName}`); + return null; } - }); - // Exclude other namespaces at the end of filter list to override potential re-includes - // from "excludes" config - allNamespaces.forEach((ns) => { - if (ns !== namespace && ns.startsWith(`${namespace}/`)) { - filters.push(`!${ns}/`); - // Explicitly exclude manifest.json files of subcomponents since the general exclude above this - // comment only applies to the configured default file types, which do not include ".json" - filters.push(`!${ns}/**/manifest.json`); - } - }); + const filters = [ + `${namespace}/`, + `${namespace}/**/manifest.json`, + `${namespace}/changes/changes-bundle.json`, + `${namespace}/changes/flexibility-bundle.json`, + `!${namespace}/test/` + ]; - return { - name: bundleName, - defaultFileTypes: [ - ".js", - ".control.xml", - ".fragment.html", - ".fragment.json", - ".fragment.xml", - ".view.html", - ".view.json", - ".view.xml", - ".properties" - ], - sections: [ - { - mode: "preload", - filters: filters, - resolve: false, - resolveConditional: false, - renderer: false + // Add configured excludes for namespace + allFilterExcludes.forEach((filterExclude) => { + // Allow all excludes (!) and limit re-includes (+) to the component namespace + if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) { + filters.push(filterExclude); + unusedFilterExcludes.delete(filterExclude); } - ] - }; - }); + }); - if (unusedFilterExcludes.size > 0) { - unusedFilterExcludes.forEach((filterExclude) => { - log.warn( - `Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` + - `Re-includes must start with a component namespace (${allNamespaces.join(" or ")})` - ); - }); - } - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => { - log.verbose(`Generating ${bundleDefinition.name}...`); - const options = { - bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), - bundleOptions: { - ignoreMissingModules: true, - optimize: true - }, - allowStringBundling - }; - if (coreVersion) { - options.targetUi5CoreVersion = coreVersion; - } - return moduleBundler({ - resources, - options + // Exclude other namespaces at the end of filter list to override potential re-includes + // from "excludes" config + allNamespaces.forEach((ns) => { + if (ns !== namespace && ns.startsWith(`${namespace}/`)) { + filters.push(`!${ns}/`); + // Explicitly exclude manifest.json files of subcomponents since the general exclude + // above this comment only applies to the configured default file types, which do + // not include ".json" + filters.push(`!${ns}/**/manifest.json`); + } + }); + + return { + name: bundleName, + defaultFileTypes: [ + ".js", + ".control.xml", + ".fragment.html", + ".fragment.json", + ".fragment.xml", + ".view.html", + ".view.json", + ".view.xml", + ".properties" + ], + sections: [ + { + mode: "preload", + filters: filters, + resolve: false, + resolveConditional: false, + renderer: false + } + ] + }; }); - })); - }) - .then((results) => { - const bundles = Array.prototype.concat.apply([], results); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + + if (unusedFilterExcludes.size > 0) { + unusedFilterExcludes.forEach((filterExclude) => { + log.warn( + `Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` + + `Re-includes must start with a component namespace (${allNamespaces.join(" or ")})` + ); + }); } - return Promise.all([ - workspace.write(bundle), - workspace.write(sourceMap) - ]); - })); - }); + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => { + log.verbose(`Generating ${bundleDefinition.name}...`); + const options = { + bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), + bundleOptions: { + ignoreMissingModules: true, + optimize: true + }, + allowStringBundling + }; + if (coreVersion) { + options.targetUi5CoreVersion = coreVersion; + } + return moduleBundler({ + resources, + options + }); + })); + }) + .then((results) => { + const bundles = Array.prototype.concat.apply([], results); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + return Promise.all([ + workspace.write(bundle), + workspace.write(sourceMap) + ]); + })); + }); + }}; } diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index 8c2f8b0feca..1efd51589ad 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -8,7 +8,7 @@ import semver from "semver"; * @module @ui5/builder/tasks/bundlers/generateFlexChangesBundle */ -/* eslint "jsdoc/check-param-names": ["error", {"disableExtraPropertyReporting":true}] */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task to create changes-bundle.json file containing all changes stored in the /changes folder for easier consumption * at runtime. @@ -26,133 +26,136 @@ import semver from "semver"; * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil * @param {object} [parameters.options] Options * @param {string} [parameters.options.projectNamespace] Project Namespace - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({workspace, taskUtil, options = {}}) { - const namespace = options.projectNamespace; - - // Use the given namespace if available, otherwise use no namespace - // (e.g. in case no manifest.json is present) - let pathPrefix = ""; - if (namespace) { - pathPrefix = `/resources/${namespace}`; - } - - /** - * Update the manifest.json JSON in-place with the flexBundle flag and, if a bundle was - * written, ensure the sap.ui.fl dependency is present (and lazy loading disabled). - * - * @param {object} data Parsed manifest.json content - * @param {boolean|undefined} flexBundleFlag Result flag from the flex changes bundler: - * - `true`: bundle written including annotation changes - * - `undefined`: bundle written but without annotation changes - * - `false`: no bundle written - */ - function updateJson(data, flexBundleFlag) { - data["sap.ui5"] = data["sap.ui5"] || {}; - // explicit set the flag to inform the runtime if a bundle is present before the preload can be interpreted - data["sap.ui5"].flexBundle = flexBundleFlag; - - // A bundle has been written whenever the flag is not explicitly `false` - this includes the `undefined`. - // In all these cases the sap.ui.fl library dependency is required. - if (flexBundleFlag !== false) { - // ensure the existence of the libs section in the dependencies - data["sap.ui5"].dependencies = data["sap.ui5"].dependencies || {}; - const mLibs = data["sap.ui5"].dependencies.libs = data["sap.ui5"].dependencies.libs || {}; - - if (mLibs["sap.ui.fl"]) { - log.verbose("sap.ui.fl found in manifest.json"); - if (mLibs["sap.ui.fl"].lazy) { - log.verbose("sap.ui.fl 'lazy' attribute found in manifest.json, setting it to false..."); - mLibs["sap.ui.fl"].lazy = false; - } - } else { - log.verbose("sap.ui.fl not found in manifest.json, inserting it..."); - mLibs["sap.ui.fl"] = {}; - } +export default function() { + return {task: async function({workspace, taskUtil, options = {}}) { + const namespace = options.projectNamespace; + + // Use the given namespace if available, otherwise use no namespace + // (e.g. in case no manifest.json is present) + let pathPrefix = ""; + if (namespace) { + pathPrefix = `/resources/${namespace}`; } - } - async function updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag) { - const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); - if (!manifestResource) { - log.verbose("No manifest.json found, skipping update of sap.ui.fl dependency and flexBundle flag"); - return; + /** + * Update the manifest.json JSON in-place with the flexBundle flag and, if a bundle was + * written, ensure the sap.ui.fl dependency is present (and lazy loading disabled). + * + * @param {object} data Parsed manifest.json content + * @param {boolean|undefined} flexBundleFlag Result flag from the flex changes bundler: + * - `true`: bundle written including annotation changes + * - `undefined`: bundle written but without annotation changes + * - `false`: no bundle written + */ + function updateJson(data, flexBundleFlag) { + data["sap.ui5"] = data["sap.ui5"] || {}; + // explicit set the flag to inform the runtime if a bundle is present before the preload can be interpreted + data["sap.ui5"].flexBundle = flexBundleFlag; + + // A bundle has been written whenever the flag is not explicitly `false` - this includes the `undefined`. + // In all these cases the sap.ui.fl library dependency is required. + if (flexBundleFlag !== false) { + // ensure the existence of the libs section in the dependencies + data["sap.ui5"].dependencies = data["sap.ui5"].dependencies || {}; + const mLibs = data["sap.ui5"].dependencies.libs = data["sap.ui5"].dependencies.libs || {}; + + if (mLibs["sap.ui.fl"]) { + log.verbose("sap.ui.fl found in manifest.json"); + if (mLibs["sap.ui.fl"].lazy) { + log.verbose("sap.ui.fl 'lazy' attribute found in manifest.json, setting it to false..."); + mLibs["sap.ui.fl"].lazy = false; + } + } else { + log.verbose("sap.ui.fl not found in manifest.json, inserting it..."); + mLibs["sap.ui.fl"] = {}; + } + } } - const manifestContent = JSON.parse(await manifestResource.getString()); - updateJson(manifestContent, flexBundleFlag); - manifestResource.setString(JSON.stringify(manifestContent, null, "\t")); + async function updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag) { + const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); + if (!manifestResource) { + log.verbose("No manifest.json found, skipping update of sap.ui.fl dependency and flexBundle flag"); + return; + } + const manifestContent = JSON.parse(await manifestResource.getString()); - await workspace.write(manifestResource); - } + updateJson(manifestContent, flexBundleFlag); + manifestResource.setString(JSON.stringify(manifestContent, null, "\t")); - async function readManifestMinUI5Version() { - const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); - if (!manifestResource) { - log.verbose("No manifest.json found, cannot read minUI5Version"); - return []; - } - const manifestContent = JSON.parse(await manifestResource.getString()); - - manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {}; - manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {}; - if (!Array.isArray(manifestContent["sap.ui5"].dependencies.minUI5Version)) { - manifestContent["sap.ui5"].dependencies.minUI5Version = - // eslint-disable-next-line no-constant-binary-expression - [manifestContent["sap.ui5"].dependencies.minUI5Version] || [""]; + await workspace.write(manifestResource); } - return manifestContent["sap.ui5"].dependencies.minUI5Version; - } - - log.verbose("Collecting flexibility changes"); - const allResources = await workspace.byGlob( - pathPrefix + "/changes/*.{annotation_change,change,variant,ctrl_variant," + - "ctrl_variant_change,ctrl_variant_management_change}" - ); - - let flexChangesBundleResult; - - if (allResources.length > 0) { - const versionArray = await readManifestMinUI5Version(); - const versions = versionArray.map((version) => semver.coerce(version)); - const versionsAllSuitableForFlexBundle = versions.every((version) => semver.compare(version, "1.73.0") >= 0); - let hasFlexBundleVersion = false; - let flexBundle = {}; - if (versionsAllSuitableForFlexBundle) { - hasFlexBundleVersion = true; - const flexBundleResource = await workspace.byPath(`${pathPrefix}/changes/flexibility-bundle.json`); - if (flexBundleResource) { - flexBundle = JSON.parse(await flexBundleResource.getString()); + + async function readManifestMinUI5Version() { + const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); + if (!manifestResource) { + log.verbose("No manifest.json found, cannot read minUI5Version"); + return []; + } + const manifestContent = JSON.parse(await manifestResource.getString()); + + manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {}; + manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {}; + if (!Array.isArray(manifestContent["sap.ui5"].dependencies.minUI5Version)) { + manifestContent["sap.ui5"].dependencies.minUI5Version = + // eslint-disable-next-line no-constant-binary-expression + [manifestContent["sap.ui5"].dependencies.minUI5Version] || [""]; } + return manifestContent["sap.ui5"].dependencies.minUI5Version; } - flexChangesBundleResult = await flexChangesBundler({ - resources: allResources, - options: { - pathPrefix, - hasFlexBundleVersion - }, - existingFlexBundle: flexBundle - }); - - const bundleResources = flexChangesBundleResult.bundleResources; - - await Promise.all(bundleResources.map((resource) => { - log.verbose("Writing flexibility changes bundle"); - return workspace.write(resource); - })); - - // Do not write bundled source files to build result - if (taskUtil) { - allResources.forEach((resource) => { - taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + + log.verbose("Collecting flexibility changes"); + const allResources = await workspace.byGlob( + pathPrefix + "/changes/*.{annotation_change,change,variant,ctrl_variant," + + "ctrl_variant_change,ctrl_variant_management_change}" + ); + + let flexChangesBundleResult; + + if (allResources.length > 0) { + const versionArray = await readManifestMinUI5Version(); + const versions = versionArray.map((version) => semver.coerce(version)); + const versionsAllSuitableForFlexBundle = + versions.every((version) => semver.compare(version, "1.73.0") >= 0); + let hasFlexBundleVersion = false; + let flexBundle = {}; + if (versionsAllSuitableForFlexBundle) { + hasFlexBundleVersion = true; + const flexBundleResource = await workspace.byPath(`${pathPrefix}/changes/flexibility-bundle.json`); + if (flexBundleResource) { + flexBundle = JSON.parse(await flexBundleResource.getString()); + } + } + flexChangesBundleResult = await flexChangesBundler({ + resources: allResources, + options: { + pathPrefix, + hasFlexBundleVersion + }, + existingFlexBundle: flexBundle }); + + const bundleResources = flexChangesBundleResult.bundleResources; + + await Promise.all(bundleResources.map((resource) => { + log.verbose("Writing flexibility changes bundle"); + return workspace.write(resource); + })); + + // Do not write bundled source files to build result + if (taskUtil) { + allResources.forEach((resource) => { + taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }); + } } - } - // Always append the flexBundle flag to the manifest.json, even if no bundle was created. - // When no flex changes have been processed at all, no bundle is created and the flag - // is set to `false`. - const flexBundleFlag = flexChangesBundleResult ? flexChangesBundleResult.flexBundle : false; - await updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag); + // Always append the flexBundle flag to the manifest.json, even if no bundle was created. + // When no flex changes have been processed at all, no bundle is created and the flag + // is set to `false`. + const flexBundleFlag = flexChangesBundleResult ? flexChangesBundleResult.flexBundle : false; + await updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag); + }}; } diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index f310a65f4a4..416b067962b 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -314,6 +314,7 @@ function getSapUiCoreBunDef(name, filters, preload) { * @module @ui5/builder/tasks/bundlers/generateLibraryPreload */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task for library bundling. * @@ -333,299 +334,303 @@ function getSapUiCoreBunDef(name, filters, preload) { * denote an arbitrary number of characters or folder names. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later * inclusion overrides an earlier exclusion, and vice versa. - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}}) { - let nonDbgWorkspace = workspace; - if (taskUtil) { - nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - // Read the experimental flag through taskUtil.getEnv so that its usage is tracked as a task - // input by the incremental build cache (a changed value then invalidates this task's cached - // result). Fall back to a direct process.env read when the task runs without a TaskUtil. - const createBundleInfoPreload = taskUtil?.getEnv ? - !!taskUtil.getEnv("UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD") : - !!process.env.UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD; - const execModuleBundlerIfNeeded = ({options, resources}) => { - if (skipBundles.includes(options.bundleDefinition.name)) { - log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`); - return null; - } - if (coreVersion) { - options.targetUi5CoreVersion = coreVersion; +export default function() { + return {task: async function( + {workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}} + ) { + let nonDbgWorkspace = workspace; + if (taskUtil) { + nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + } + }); } - options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil); - options.allowStringBundling = allowStringBundling; - return moduleBundler({options, resources}); - }; - - return nonDbgWorkspace.byGlob( - "/resources/**/*.{js,json,xml,html,properties,library,js.map}" - ).then(async (resources) => { - // Find all libraries and create a library-preload.js bundle - - let p = Promise.resolve(); - - // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml - // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0 - if (projectName === "sap.ui.core") { - // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions - // that have been defined for sap.ui.core before the bundle configuration has been introduced. - // This is mainly to have an easier check without version parsing or using semver. - // If no project/specVersion is available, the bundles should also be created to not break potential - // existing use cases without a properly formed/formatted project tree. - if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) { - const isEvo = resources.find((resource) => { - return resource.getPath() === "/resources/ui5loader.js"; - }); + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + // Read the experimental flag through taskUtil.getEnv so that its usage is tracked as a task + // input by the incremental build cache (a changed value then invalidates this task's cached + // result). Fall back to a direct process.env read when the task runs without a TaskUtil. + const createBundleInfoPreload = taskUtil?.getEnv ? + !!taskUtil.getEnv("UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD") : + !!process.env.UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD; + const execModuleBundlerIfNeeded = ({options, resources}) => { + if (skipBundles.includes(options.bundleDefinition.name)) { + log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`); + return null; + } + if (coreVersion) { + options.targetUi5CoreVersion = coreVersion; + } + options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil); + options.allowStringBundling = allowStringBundling; + return moduleBundler({options, resources}); + }; - let unoptimizedModuleNameMapping; - let unoptimizedResources = resources; - if (taskUtil) { - const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any non-debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); - } + return nonDbgWorkspace.byGlob( + "/resources/**/*.{js,json,xml,html,properties,library,js.map}" + ).then(async (resources) => { + // Find all libraries and create a library-preload.js bundle + + let p = Promise.resolve(); + + // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml + // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0 + if (projectName === "sap.ui.core") { + // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions + // that have been defined for sap.ui.core before the bundle configuration has been introduced. + // This is mainly to have an easier check without version parsing or using semver. + // If no project/specVersion is available, the bundles should also be created to not break potential + // existing use cases without a properly formed/formatted project tree. + if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) { + const isEvo = resources.find((resource) => { + return resource.getPath() === "/resources/ui5loader.js"; }); - unoptimizedResources = await unoptimizedWorkspace.byGlob( - "/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - unoptimizedModuleNameMapping = createModuleNameMapping({ - resources: unoptimizedResources, - taskUtil - }); - } + let unoptimizedModuleNameMapping; + let unoptimizedResources = resources; + if (taskUtil) { + const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any non-debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); + } + }); + unoptimizedResources = await unoptimizedWorkspace.byGlob( + "/resources/**/*.{js,json,xml,html,properties,library,js.map}"); + + unoptimizedModuleNameMapping = createModuleNameMapping({ + resources: unoptimizedResources, + taskUtil + }); + } - let filters; - if (isEvo) { - filters = ["ui5loader-autoconfig.js"]; - } else { - filters = ["jquery.sap.global.js"]; - } - p = Promise.all([ - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}), - resources - }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-dbg.js", filters, preload: false, - moduleNameMapping: unoptimizedModuleNameMapping + let filters; + if (isEvo) { + filters = ["ui5loader-autoconfig.js"]; + } else { + filters = ["jquery.sap.global.js"]; + } + p = Promise.all([ + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}), + resources }), - resources: unoptimizedResources - }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-dbg.js", filters, preload: false, + moduleNameMapping: unoptimizedModuleNameMapping + }), + resources: unoptimizedResources }), - resources - }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true, - moduleNameMapping: unoptimizedModuleNameMapping + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true + }), + resources }), - resources: unoptimizedResources - }), - ]).then((results) => { - const bundles = Array.prototype.concat.apply([], results).filter(Boolean); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true, + moduleNameMapping: unoptimizedModuleNameMapping + }), + resources: unoptimizedResources + }), + ]).then((results) => { + const bundles = Array.prototype.concat.apply([], results).filter(Boolean); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + if (sourceMap) { + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + } + const writes = [workspace.write(bundle)]; if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + writes.push(workspace.write(sourceMap)); } - } - const writes = [workspace.write(bundle)]; - if (sourceMap) { - writes.push(workspace.write(sourceMap)); - } - return Promise.all(writes); - })); - }); - } - } - - return p.then(() => { - return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length > 0) { - return libraryIndicatorResources; - } else { - // Fallback to "library.js" as library indicator - log.verbose( - `Could not find a ".library" file for project ${projectName}, ` + - `falling back to "library.js".`); - return workspace.byGlob("/resources/**/library.js"); - } - }).then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length < 1) { - // No library found - nothing to do - log.verbose( - `Could not find a ".library" or "library.js" file for project ${projectName}. ` + - `Skipping library preload bundling.`); - return; + return Promise.all(writes); + })); + }); } + } - return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => { - // Determine library namespace from library indicator file path - // ending with either ".library" or "library.js" (see fallback logic above) - // e.g. /resources/sap/foo/.library => sap/foo - // /resources/sap/bar/library.js => sap/bar - const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/; - const libraryIndicatorPath = libraryIndicatorResource.getPath(); - const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); - if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { - const libraryNamespace = libraryNamespaceMatch[1]; - let results; - if (!createBundleInfoPreload) { - // Regular bundling - results = await Promise.all([ - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getBundleDefinition(libraryNamespace, excludes), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: true, - ignoreMissingModules: true, - skipIfEmpty: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: false, - ignoreMissingModules: true, - skipIfEmpty: true - } - // Note: Although the bundle uses optimize=false, there is - // no moduleNameMapping needed, as support files are excluded from minification. - }, - resources - }) - ]); - } else { - log.info( - `Using experimental bundling with bundle info preload ` + - `for library ${libraryNamespace} in project ${projectName}`); - log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); + return p.then(() => { + return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length > 0) { + return libraryIndicatorResources; + } else { + // Fallback to "library.js" as library indicator + log.verbose( + `Could not find a ".library" file for project ${projectName}, ` + + `falling back to "library.js".`); + return workspace.byGlob("/resources/**/library.js"); + } + }).then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length < 1) { + // No library found - nothing to do + log.verbose( + `Could not find a ".library" or "library.js" file for project ${projectName}. ` + + `Skipping library preload bundling.`); + return; + } + return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => { + // Determine library namespace from library indicator file path + // ending with either ".library" or "library.js" (see fallback logic above) + // e.g. /resources/sap/foo/.library => sap/foo + // /resources/sap/bar/library.js => sap/bar + const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/; + const libraryIndicatorPath = libraryIndicatorResource.getPath(); + const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); + if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { + const libraryNamespace = libraryNamespaceMatch[1]; + let results; + if (!createBundleInfoPreload) { + // Regular bundling + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minify. + }, + resources + }) + ]); + } else { + log.info( + `Using experimental bundling with bundle info preload ` + + `for library ${libraryNamespace} in project ${projectName}`); + log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); + + + if (skipBundles.includes(`${libraryNamespace}/library-preload.js`) && + !skipBundles.includes(`${libraryNamespace}/_library-content.js`)) { + // If the standard preload bundle is skipped, also skip the content bundle, + // since they depend on each other + skipBundles.push(`${libraryNamespace}/_library-content.js`); + } - if (skipBundles.includes(`${libraryNamespace}/library-preload.js`) && - !skipBundles.includes(`${libraryNamespace}/_library-content.js`)) { - // If the standard preload bundle is skipped, ensure to also skip the content bundle, - // since they depend on each other - skipBundles.push(`${libraryNamespace}/_library-content.js`); - } + if (skipBundles.includes(`${libraryNamespace}/_library-content.js`) && + !skipBundles.includes(`${libraryNamespace}/library-preload.js`)) { + // If the content bundle is skipped, the default preload bundle must be skipped too + throw new Error( + `A custom bundle '${libraryNamespace}/_library-content.js' has been defined, ` + + `but it also requires a corresponding custom bundle definition for ` + + `'${libraryNamespace}/library-preload.js'`); + } - if (skipBundles.includes(`${libraryNamespace}/_library-content.js`) && - !skipBundles.includes(`${libraryNamespace}/library-preload.js`)) { - // If the content bundle is skipped, the default preload bundle must be skipped as well - throw new Error( - `A custom bundle '${libraryNamespace}/_library-content.js' has been defined, ` + - `but it also requires a corresponding custom bundle definition for ` + - `'${libraryNamespace}/library-preload.js'`); + // Experimental bundling with bundle info preload + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: + getBundleInfoPreloadDefinition(libraryNamespace, excludes, coreVersion), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getContentBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minify. + }, + resources + }) + ]); } - - // Experimental bundling with bundle info preload - results = await Promise.all([ - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: - getBundleInfoPreloadDefinition(libraryNamespace, excludes, coreVersion), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getContentBundleDefinition(libraryNamespace, excludes), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: true, - ignoreMissingModules: true, - skipIfEmpty: true + const bundles = Array.prototype.concat.apply([], results).filter(Boolean); + return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { + if (bundle) { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + if (sourceMap) { + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, + taskUtil.STANDARD_TAGS.OmitFromBuildResult); } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: false, - ignoreMissingModules: true, - skipIfEmpty: true - } - // Note: Although the bundle uses optimize=false, there is - // no moduleNameMapping needed, as support files are excluded from minification. - }, - resources - }) - ]); - } - const bundles = Array.prototype.concat.apply([], results).filter(Boolean); - return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { - if (bundle) { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + } + const writes = [workspace.write(bundle)]; if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, - taskUtil.STANDARD_TAGS.OmitFromBuildResult); + writes.push(workspace.write(sourceMap)); } + return Promise.all(writes); } - const writes = [workspace.write(bundle)]; - if (sourceMap) { - writes.push(workspace.write(sourceMap)); - } - return Promise.all(writes); - } - })); - } else { - log.verbose( - `Could not determine library namespace from file "${libraryIndicatorPath}" ` + - `for project ${projectName}. Skipping library preload bundling.`); - return Promise.resolve(); - } - })); + })); + } else { + log.verbose( + `Could not determine library namespace from file "${libraryIndicatorPath}" ` + + `for project ${projectName}. Skipping library preload bundling.`); + return Promise.resolve(); + } + })); + }); }); }); - }); + }}; } diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index 93f5a1f92ef..f50ae86f8ad 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -65,7 +65,7 @@ function getBundleDefinition(config) { * @module @ui5/builder/tasks/bundlers/generateStandaloneAppBundle */ -/* eslint "jsdoc/check-param-names": ["error", {"disableExtraPropertyReporting":true}] */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task for bundling standalone applications. * @@ -80,125 +80,127 @@ function getBundleDefinition(config) { * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name * @param {string} [parameters.options.projectNamespace] Project namespace - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({workspace, dependencies, taskUtil, options}) { - const {projectName} = options; - const namespace = options.projectNamespace; - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - - if (!namespace) { - log.warn(`Namespace of project ${projectName} is not known. Self contained bundling is currently ` + - `unable to generate complete bundles for such projects.`); - } - - const combo = new ReaderCollectionPrioritized({ - name: `generateStandaloneAppBundle - prioritize workspace over dependencies: ${projectName}`, - readers: [workspace, dependencies] - }); - - let resourceReader = combo; - if (taskUtil) { - // Omit -dbg files - resourceReader = await taskUtil.resourceFactory.createFilterReader({ - reader: combo, - callback: function(resource) { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } - const resources = await resourceReader.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - - const isEvo = resources.find((resource) => { - return resource.getPath() === "/resources/ui5loader.js"; - }); - let filters; - if (isEvo) { - filters = ["ui5loader-autoconfig.js"]; - } else { - filters = ["jquery.sap.global.js"]; - } - - let unoptimizedModuleNameMapping; - let unoptimizedResources = resources; - if (taskUtil) { - const unoptimizedResourceReader = await taskUtil.resourceFactory.createFilterReader({ - reader: combo, - callback: function(resource) { - // Remove any non-debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); - } +export default function() { + return {requiresDependencies: true, task: async function({workspace, dependencies, taskUtil, options}) { + const {projectName} = options; + const namespace = options.projectNamespace; + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + + if (!namespace) { + log.warn(`Namespace of project ${projectName} is not known. Self contained bundling is currently ` + + `unable to generate complete bundles for such projects.`); + } + + const combo = new ReaderCollectionPrioritized({ + name: `generateStandaloneAppBundle - prioritize workspace over dependencies: ${projectName}`, + readers: [workspace, dependencies] }); - unoptimizedResources = await unoptimizedResourceReader - .byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); + let resourceReader = combo; + if (taskUtil) { + // Omit -dbg files + resourceReader = await taskUtil.resourceFactory.createFilterReader({ + reader: combo, + callback: function(resource) { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + } + }); + } + const resources = await resourceReader.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - unoptimizedModuleNameMapping = createModuleNameMapping({ - resources: unoptimizedResources, - taskUtil + const isEvo = resources.find((resource) => { + return resource.getPath() === "/resources/ui5loader.js"; }); - } - - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - const bundleOptions = { - bundleDefinition: applyDefaultsToBundleDefinition( - getBundleDefinition({ - name: "sap-ui-custom.js", - filters, - namespace, - preloadSection: true, - }), - taskUtil - ), - allowStringBundling - }; - - const bundleDbgOptions = { - bundleDefinition: applyDefaultsToBundleDefinition( - getBundleDefinition({ - name: "sap-ui-custom-dbg.js", - filters, - namespace, + let filters; + if (isEvo) { + filters = ["ui5loader-autoconfig.js"]; + } else { + filters = ["jquery.sap.global.js"]; + } + + let unoptimizedModuleNameMapping; + let unoptimizedResources = resources; + if (taskUtil) { + const unoptimizedResourceReader = await taskUtil.resourceFactory.createFilterReader({ + reader: combo, + callback: function(resource) { + // Remove any non-debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); + } + }); + + unoptimizedResources = await unoptimizedResourceReader + .byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); + + unoptimizedModuleNameMapping = createModuleNameMapping({ + resources: unoptimizedResources, + taskUtil + }); + } + + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + const bundleOptions = { + bundleDefinition: applyDefaultsToBundleDefinition( + getBundleDefinition({ + name: "sap-ui-custom.js", + filters, + namespace, + preloadSection: true, + }), + taskUtil + ), + allowStringBundling + }; + + const bundleDbgOptions = { + bundleDefinition: applyDefaultsToBundleDefinition( + getBundleDefinition({ + name: "sap-ui-custom-dbg.js", + filters, + namespace, + }), + taskUtil + ), + bundleOptions: { + optimize: false, + }, + moduleNameMapping: unoptimizedModuleNameMapping, + allowStringBundling + }; + + if (coreVersion) { + bundleOptions.targetUi5CoreVersion = coreVersion; + bundleDbgOptions.targetUi5CoreVersion = coreVersion; + } + + await Promise.all([ + moduleBundler({ + resources, + options: bundleOptions }), - taskUtil - ), - bundleOptions: { - optimize: false, - }, - moduleNameMapping: unoptimizedModuleNameMapping, - allowStringBundling - }; - - if (coreVersion) { - bundleOptions.targetUi5CoreVersion = coreVersion; - bundleDbgOptions.targetUi5CoreVersion = coreVersion; - } - - await Promise.all([ - moduleBundler({ - resources, - options: bundleOptions - }), - moduleBundler({ - resources: unoptimizedResources, - options: bundleDbgOptions - }) - ]).then((results) => { - const bundles = Array.prototype.concat.apply([], results); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + moduleBundler({ + resources: unoptimizedResources, + options: bundleDbgOptions + }) + ]).then((results) => { + const bundles = Array.prototype.concat.apply([], results); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + if (sourceMap) { + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + } + const writes = [workspace.write(bundle)]; if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + writes.push(workspace.write(sourceMap)); } - } - const writes = [workspace.write(bundle)]; - if (sourceMap) { - writes.push(workspace.write(sourceMap)); - } - return Promise.all(writes); - })); - }); + return Promise.all(writes); + })); + }); + }}; } diff --git a/packages/builder/lib/tasks/enhanceManifest.js b/packages/builder/lib/tasks/enhanceManifest.js index 66ff1e34ee4..4bc389b5af4 100644 --- a/packages/builder/lib/tasks/enhanceManifest.js +++ b/packages/builder/lib/tasks/enhanceManifest.js @@ -22,10 +22,10 @@ import fsInterface from "@ui5/fs/fsInterface"; * * @param {object} options Options * @param {string} options.projectNamespace Namespace of the application - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({projectNamespace}) { - return [{ + return {steps: [{ name: "enhanceManifest", // One key per manifest.json. The i18n bundle reads made by manifestEnhancer go through the step's // workspace and are thus tracked per step. @@ -43,5 +43,5 @@ export default function build({projectNamespace}) { await workspace.write(processed); } }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js index bfe724bd575..fe3aa1f1791 100644 --- a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js @@ -20,7 +20,7 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js"; * @param {object} options Options * @param {string} options.pattern Glob pattern to locate the files to be processed * @param {string} options.encoding source file encoding either "UTF-8" or "ISO-8859-1" - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({pattern, encoding}) { if (!encoding) { @@ -31,7 +31,7 @@ export default function build({pattern, encoding}) { encoding: nonAsciiEscaper.getEncodingFromAlias(encoding) }; - return [{ + return {steps: [{ name: "escapeNonAsciiCharacters", // One key per matched resource, so a delta build re-processes only the resources that changed. keys: async ({workspace}) => workspace.byGlob(pattern), @@ -41,5 +41,5 @@ export default function build({pattern, encoding}) { await workspace.write(processed); } }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index 76610f8cc8f..68f9d13b9bf 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -34,7 +34,7 @@ function getSigner(type) { } } -/* eslint "jsdoc/check-param-names": ["error", {"disableExtraPropertyReporting":true}] */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task to generate the application cachebuster info file. * @@ -47,33 +47,35 @@ function getSigner(type) { * @param {object} parameters.options Options * @param {string} parameters.options.projectNamespace Namespace of the application * @param {string} [parameters.options.signatureType='time'] Type of signature to be used ('time' or 'hash') - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default function({workspace, options}) { - const {signatureType} = options; - const namespace = options.projectNamespace; +export default function() { + return {task: function({workspace, options}) { + const {signatureType} = options; + const namespace = options.projectNamespace; - const basePath = `/resources/${namespace}/`; - return workspace.byGlob(`/resources/${namespace}/**/*`) - .then(async (resources) => { - const cachebusterInfo = Object.create(null); - const signer = getSigner(signatureType); + const basePath = `/resources/${namespace}/`; + return workspace.byGlob(`/resources/${namespace}/**/*`) + .then(async (resources) => { + const cachebusterInfo = Object.create(null); + const signer = getSigner(signatureType); - await Promise.all(resources.map(async (resource) => { - let resourcePath = resource.getPath(); - if (!resourcePath.startsWith(basePath)) { - log.verbose( - `Ignoring resource with path ${resourcePath} since it is not based on path ${basePath}`); - return; - } - // Remove base path. Absolute paths are not allowed in cachebuster info - resourcePath = resourcePath.replace(basePath, ""); - cachebusterInfo[resourcePath] = await signer(resource); - })); - const cachebusterInfoResource = createResource({ - path: `/resources/${namespace}/sap-ui-cachebuster-info.json`, - string: JSON.stringify(cachebusterInfo, null, 2) + await Promise.all(resources.map(async (resource) => { + let resourcePath = resource.getPath(); + if (!resourcePath.startsWith(basePath)) { + log.verbose( + `Ignoring resource with path ${resourcePath} since it is not based on path ${basePath}`); + return; + } + // Remove base path. Absolute paths are not allowed in cachebuster info + resourcePath = resourcePath.replace(basePath, ""); + cachebusterInfo[resourcePath] = await signer(resource); + })); + const cachebusterInfoResource = createResource({ + path: `/resources/${namespace}/sap-ui-cachebuster-info.json`, + string: JSON.stringify(cachebusterInfo, null, 2) + }); + return workspace.write(cachebusterInfoResource); }); - return workspace.write(cachebusterInfoResource); - }); + }}; } diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index 44c464e322b..e5722cd5b89 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -8,6 +8,7 @@ import manifestCreator from "../processors/manifestCreator.js"; * @module @ui5/builder/tasks/generateLibraryManifest */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task for creating a library manifest.json from its .library file. * @@ -20,54 +21,58 @@ import manifestCreator from "../processors/manifestCreator.js"; * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default function({workspace, taskUtil, options: {projectName}}) { - // Note: - // *.library files are needed to identify libraries - // *.json files are needed to avoid overwriting them - // *.js files are needed to identify nested components - // *.less, *.css, *.theming and *.theme files are needed to identify supported themes - // *.properties to identify existence of i18n bundles (e.g. messagebundle.properties) - return workspace.byGlob("/resources/**/*.{js,json,library,less,css,theming,theme,properties}").then((resources) => { - // Find all libraries and create a manifest.json file - return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length < 1) { - // No library found - nothing to do - log.verbose(`Could not find a ".library" file for project ${projectName}. ` + - `Skipping library manifest generation.`); - return; - } - - return Promise.all(libraryIndicatorResources.map((libraryIndicatorResource) => { - // Determine library namespace from library indicator file path - // ending with ".library" - // e.g. /resources/sap/foo/.library => sap/foo - const libraryNamespacePattern = /^\/resources\/(.*)\/\.library$/; - const libraryIndicatorPath = libraryIndicatorResource.getPath(); - const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); - if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { - const libraryNamespace = libraryNamespaceMatch[1]; - return manifestCreator({ - libraryResource: libraryIndicatorResource, - namespace: libraryNamespace, - resources, - getProjectVersion: (projectName) => { - return taskUtil?.getProject(projectName)?.getVersion(); - }, - options: { - } - }).then((manifest) => { - if (manifest) { - return workspace.write(manifest); - } - }); - } else { - log.verbose(`Could not determine library namespace from file "${libraryIndicatorPath}" ` + - `for project ${projectName}. Skipping library manifest generation.`); - return Promise.resolve(); +export default function() { + return {task: function({workspace, taskUtil, options: {projectName}}) { + // Note: + // *.library files are needed to identify libraries + // *.json files are needed to avoid overwriting them + // *.js files are needed to identify nested components + // *.less, *.css, *.theming and *.theme files are needed to identify supported themes + // *.properties to identify existence of i18n bundles (e.g. messagebundle.properties) + return workspace.byGlob( + "/resources/**/*.{js,json,library,less,css,theming,theme,properties}" + ).then((resources) => { + // Find all libraries and create a manifest.json file + return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length < 1) { + // No library found - nothing to do + log.verbose(`Could not find a ".library" file for project ${projectName}. ` + + `Skipping library manifest generation.`); + return; } - })); + + return Promise.all(libraryIndicatorResources.map((libraryIndicatorResource) => { + // Determine library namespace from library indicator file path + // ending with ".library" + // e.g. /resources/sap/foo/.library => sap/foo + const libraryNamespacePattern = /^\/resources\/(.*)\/\.library$/; + const libraryIndicatorPath = libraryIndicatorResource.getPath(); + const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); + if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { + const libraryNamespace = libraryNamespaceMatch[1]; + return manifestCreator({ + libraryResource: libraryIndicatorResource, + namespace: libraryNamespace, + resources, + getProjectVersion: (projectName) => { + return taskUtil?.getProject(projectName)?.getVersion(); + }, + options: { + } + }).then((manifest) => { + if (manifest) { + return workspace.write(manifest); + } + }); + } else { + log.verbose(`Could not determine library namespace from file "${libraryIndicatorPath}" ` + + `for project ${projectName}. Skipping library manifest generation.`); + return Promise.resolve(); + } + })); + }); }); - }); + }}; } diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index aa612455252..8f3b030c025 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -42,6 +42,7 @@ function getCreatorOptions(projectName) { * @module @ui5/builder/tasks/generateResourcesJson */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task for creating a resources.json file, describing all productive build resources. * @@ -109,29 +110,33 @@ function getCreatorOptions(projectName) { * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({workspace, dependencies, taskUtil, options: {projectName}}) { - let resources = await workspace.byGlob(["/resources/**/*"].concat(DEFAULT_EXCLUDES)); - let dependencyResources = - await dependencies.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); +export default function() { + return {requiresDependencies: true, task: async function( + {workspace, dependencies, taskUtil, options: {projectName}} + ) { + let resources = await workspace.byGlob(["/resources/**/*"].concat(DEFAULT_EXCLUDES)); + let dependencyResources = + await dependencies.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - if (taskUtil) { - // Filter out resources that will be omitted from the build results - resources = resources.filter((resource) => { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - }); - dependencyResources = dependencyResources.filter((resource) => { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - }); - } + if (taskUtil) { + // Filter out resources that will be omitted from the build results + resources = resources.filter((resource) => { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }); + dependencyResources = dependencyResources.filter((resource) => { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }); + } - const resourceLists = await resourceListCreator({ - resources, - dependencyResources, - options: getCreatorOptions(projectName), - }); - await Promise.all( - resourceLists.map((resourceList) => workspace.write(resourceList)) - ); + const resourceLists = await resourceListCreator({ + resources, + dependencyResources, + options: getCreatorOptions(projectName), + }); + await Promise.all( + resourceLists.map((resourceList) => workspace.write(resourceList)) + ); + }}; } diff --git a/packages/builder/lib/tasks/generateThemeDesignerResources.js b/packages/builder/lib/tasks/generateThemeDesignerResources.js index 78443b3950b..0d546d5cc0a 100644 --- a/packages/builder/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/lib/tasks/generateThemeDesignerResources.js @@ -112,7 +112,7 @@ async function generateThemeDotTheming({workspace, combo, themeFolder}) { * @param {string} options.version Project version * @param {string} [options.projectNamespace] If the project is of type library, provide its * namespace. Omit for type theme-library - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build(options) { const {projectName, version} = options; @@ -121,7 +121,7 @@ export default function build(options) { // Skip sap.ui.documentation since it is not intended to be available in SAP Theme Designer to create // custom themes if (namespace === "sap/ui/documentation") { - return []; + return {requiresDependencies: true, steps: []}; } let librarySourceLessPattern; @@ -217,5 +217,5 @@ export default function build(options) { }, }); - return steps; + return {requiresDependencies: true, steps}; } diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index 8cb2f8f6cfb..b4b76bee3c4 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -7,6 +7,7 @@ const MANIFEST_JSON = "manifest.json"; * @module @ui5/builder/tasks/generateVersionInfo */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task to create sap-ui-version.json * @@ -20,39 +21,41 @@ const MANIFEST_JSON = "manifest.json"; * @param {object} parameters.options Options * @param {string} parameters.options.pattern Glob pattern for .library resources * @param {object} parameters.options.rootProject DuplexCollection to read and write files - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async ({workspace, dependencies, options: {rootProject, pattern}}) => { - let resources = await dependencies.byGlob(pattern); +export default function() { + return {requiresDependencies: true, task: async ({workspace, dependencies, options: {rootProject, pattern}}) => { + let resources = await dependencies.byGlob(pattern); - resources = resources.filter((res) => res.getProject()?.getType() === "library"); + resources = resources.filter((res) => res.getProject()?.getType() === "library"); - const libraryInfosPromises = resources.map((dotLibResource) => { - const namespace = dotLibResource.getProject().getNamespace(); - // pass all required resources to the processor - // the processor will then filter - return dependencies.byGlob(`/resources/${namespace}/**/${MANIFEST_JSON}`).then((manifestResources) => { - const libraryManifest = manifestResources.find((manifestResource) => { - return manifestResource.getPath() === `/resources/${namespace}/${MANIFEST_JSON}`; + const libraryInfosPromises = resources.map((dotLibResource) => { + const namespace = dotLibResource.getProject().getNamespace(); + // pass all required resources to the processor + // the processor will then filter + return dependencies.byGlob(`/resources/${namespace}/**/${MANIFEST_JSON}`).then((manifestResources) => { + const libraryManifest = manifestResources.find((manifestResource) => { + return manifestResource.getPath() === `/resources/${namespace}/${MANIFEST_JSON}`; + }); + const embeddedManifests = + manifestResources.filter((manifestResource) => manifestResource !== libraryManifest); + return { + libraryManifest, + embeddedManifests, + name: dotLibResource.getProject().getName(), + version: dotLibResource.getProject().getVersion() + }; }); - const embeddedManifests = - manifestResources.filter((manifestResource) => manifestResource !== libraryManifest); - return { - libraryManifest, - embeddedManifests, - name: dotLibResource.getProject().getName(), - version: dotLibResource.getProject().getVersion() - }; }); - }); - const libraryInfos = await Promise.all(libraryInfosPromises); + const libraryInfos = await Promise.all(libraryInfosPromises); - const [versionInfoResource] = await versionInfoGenerator({ - options: { - rootProjectName: rootProject.getName(), - rootProjectVersion: rootProject.getVersion(), - libraryInfos - } - }); - return workspace.write(versionInfoResource); + const [versionInfoResource] = await versionInfoGenerator({ + options: { + rootProjectName: rootProject.getName(), + rootProjectVersion: rootProject.getVersion(), + libraryInfos + } + }); + return workspace.write(versionInfoResource); + }}; }; diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 595b52173c6..24a5a18c5d2 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -9,6 +9,7 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @module @ui5/builder/tasks/jsdoc/executeJsdocSdkTransformation */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task to transform the api.json file as created by the * [generateJsdoc]{@link @ui5/builder/tasks/jsdoc/generateJsdoc} task into a pre-processed api.json @@ -24,61 +25,61 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @param {object} parameters.options Options * @param {string|Array} parameters.options.dotLibraryPattern Pattern to locate the .library resource to be processed * @param {string} parameters.options.projectName Project name - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -const executeJsdocSdkTransformation = async function( - {workspace, dependencies, options: {projectName, dotLibraryPattern}} = {} -) { - if (!projectName || !dotLibraryPattern) { - throw new Error("[executeJsdocSdkTransformation]: One or more mandatory options not provided"); - } +export default function() { + return {requiresDependencies: true, task: async function( + {workspace, dependencies, options: {projectName, dotLibraryPattern}} = {} + ) { + if (!projectName || !dotLibraryPattern) { + throw new Error("[executeJsdocSdkTransformation]: One or more mandatory options not provided"); + } - const [apiJsons, dotLibraries, depApiJsons] = await Promise.all([ - workspace.byGlob("/test-resources/**/designtime/api.json"), - workspace.byGlob(dotLibraryPattern), - dependencies.byGlob("/test-resources/**/designtime/api.json") - ]); - if (!apiJsons.length) { - log.info(`Failed to locate api.json resource for project ${projectName}. ` + - `Skipping SDK Transformation...`); - return; - } else if (apiJsons.length > 1) { - throw new Error(`[executeJsdocSdkTransformation]: Found more than one api.json resources for project ` + - `${projectName}.`); - } - if (!dotLibraries.length) { - throw new Error(`[executeJsdocSdkTransformation]: Failed to locate .library resource for project ` + - `${projectName}.`); - } else if (dotLibraries.length > 1) { - throw new Error(`[executeJsdocSdkTransformation]: Found more than one .library resources for project ` + - `${projectName}.`); - } + const [apiJsons, dotLibraries, depApiJsons] = await Promise.all([ + workspace.byGlob("/test-resources/**/designtime/api.json"), + workspace.byGlob(dotLibraryPattern), + dependencies.byGlob("/test-resources/**/designtime/api.json") + ]); + if (!apiJsons.length) { + log.info(`Failed to locate api.json resource for project ${projectName}. ` + + `Skipping SDK Transformation...`); + return; + } else if (apiJsons.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one api.json resources for project ` + + `${projectName}.`); + } + if (!dotLibraries.length) { + throw new Error(`[executeJsdocSdkTransformation]: Failed to locate .library resource for project ` + + `${projectName}.`); + } else if (dotLibraries.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one .library resources for project ` + + `${projectName}.`); + } - const combo = new ReaderCollectionPrioritized({ - name: `executeJsdocSdkTransformation - custom workspace + dependencies FS: ${projectName}`, - readers: [workspace, dependencies] - }); + const combo = new ReaderCollectionPrioritized({ + name: `executeJsdocSdkTransformation - custom workspace + dependencies FS: ${projectName}`, + readers: [workspace, dependencies] + }); - const apiJsonPath = apiJsons[0].getPath(); - const dotLibraryPath = dotLibraries[0].getPath(); - const dependencyApiJsonPaths = depApiJsons.map((res) => { - return res.getPath(); - }); + const apiJsonPath = apiJsons[0].getPath(); + const dotLibraryPath = dotLibraries[0].getPath(); + const dependencyApiJsonPaths = depApiJsons.map((res) => { + return res.getPath(); + }); - // Target path is typically "/test-resources/${options.namespace}/designtime/apiref/api.json" - const targetApiJsonPath = apiJsonPath.replace(/\/api\.json$/i, "/apiref/api.json"); + // Target path is typically "/test-resources/${options.namespace}/designtime/apiref/api.json" + const targetApiJsonPath = apiJsonPath.replace(/\/api\.json$/i, "/apiref/api.json"); - const createdResources = await sdkTransformer({ - apiJsonPath, - dotLibraryPath, - dependencyApiJsonPaths, - targetApiJsonPath, - fs: fsInterface(combo) - }); + const createdResources = await sdkTransformer({ + apiJsonPath, + dotLibraryPath, + dependencyApiJsonPaths, + targetApiJsonPath, + fs: fsInterface(combo) + }); - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); -}; - -export default executeJsdocSdkTransformation; + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); + }}; +} diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index da742792cdf..1528330b0ae 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -7,11 +7,15 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * @module @ui5/builder/tasks/jsdoc/generateApiIndex */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Compiles an api-index.json resource from all available api.json resources as created by the * [executeJsdocSdkTransformation]{@link @ui5/builder/tasks/jsdoc/executeJsdocSdkTransformation} task. * The resulting api-index.json resource is mainly to be used in the SDK. * + * The default export is a factory returning the task descriptor. The parameters below document the + * task body that receives them at build time. + * * @public * @function default * @static @@ -21,36 +25,38 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({ - workspace, - dependencies, - options: {projectName} -}) { - const combo = new ReaderCollectionPrioritized({ - name: `generateApiIndex - workspace + dependencies: ${projectName}`, - readers: [workspace, dependencies] - }); +export default function() { + return {requiresDependencies: true, task: async function({ + workspace, + dependencies, + options: {projectName} + }) { + const combo = new ReaderCollectionPrioritized({ + name: `generateApiIndex - workspace + dependencies: ${projectName}`, + readers: [workspace, dependencies] + }); - const versionInfoPath = "/resources/sap-ui-version.json"; - const testResourcesRootPath = "/test-resources"; - const targetApiIndexPath = "/docs/api/api-index.json"; - const targetApiIndexDeprecatedPath = "/docs/api/api-index-deprecated.json"; - const targetApiIndexExperimentalPath = "/docs/api/api-index-experimental.json"; - const targetApiIndexSincePath = "/docs/api/api-index-since.json"; + const versionInfoPath = "/resources/sap-ui-version.json"; + const testResourcesRootPath = "/test-resources"; + const targetApiIndexPath = "/docs/api/api-index.json"; + const targetApiIndexDeprecatedPath = "/docs/api/api-index-deprecated.json"; + const targetApiIndexExperimentalPath = "/docs/api/api-index-experimental.json"; + const targetApiIndexSincePath = "/docs/api/api-index-since.json"; - const createdResources = await apiIndexGenerator({ - versionInfoPath, - testResourcesRootPath, - targetApiIndexPath, - targetApiIndexDeprecatedPath, - targetApiIndexExperimentalPath, - targetApiIndexSincePath, - fs: fsInterface(combo), - }); + const createdResources = await apiIndexGenerator({ + versionInfoPath, + testResourcesRootPath, + targetApiIndexPath, + targetApiIndexDeprecatedPath, + targetApiIndexExperimentalPath, + targetApiIndexSincePath, + fs: fsInterface(combo), + }); - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); + }}; } diff --git a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js index 938231b0603..f8506bfbab9 100644 --- a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js @@ -27,68 +27,83 @@ import {createAdapter} from "@ui5/fs/resourceFactory"; /** * Task to execute a JSDoc build for UI5 projects * + * A factory returning the task descriptor. The factory assembles the glob patterns from + * options.jsdocExcludes and captures options.namespace and + * options.version, which the project definition supplies in place of the project-derived + * values that used to be wrapped around this task. The task body then uses these closure + * values; the per-run options.projectName is read from the task-run options. + * + * The factory stays pure and cheap: the heavy jsdocGenerator processor is only reached inside the + * task body, so calling the factory for the build signature loads no processor. + * * @public * @function default * @static * - * @param {object} parameters Parameters - * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files - * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files - * @param {module:@ui5/builder/tasks/jsdoc/generateJsdoc~GenerateJsdocOptions} parameters.options Options - * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil - * @returns {Promise} Promise resolving with undefined once data has been written + * @param {object} options Options + * @param {string} options.namespace Namespace to build (e.g. some/project/name) + * @param {string} options.version Project version + * @param {string[]} [options.jsdocExcludes=[]] List of exclude patterns, relative to /resources/ + * @returns {object} The task descriptor */ -export default async function generateJsdoc({ - taskUtil, - workspace, - dependencies, - options = {} -}) { - const {projectName, namespace, version, pattern} = - /** @type {module:@ui5/builder/tasks/jsdoc/generateJsdoc~GenerateJsdocOptions} */ (options); - - if (!projectName || !namespace || !version || !pattern) { - throw new Error("[generateJsdoc]: One or more mandatory options not provided"); - } - - const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} = - await utils.createTmpDirs(projectName); - - taskUtil?.registerCleanupTask(cleanup); - - const [writtenResourcesCount] = await Promise.all([ - utils.writeResourcesToDir({ +export default function generateJsdoc(options) { + const {version, namespace, jsdocExcludes = []} = options; + const patterns = ["/resources/**/*.js", ...jsdocExcludes.map((p) => `!/resources/${p}`)]; + return { + requiresDependencies: true, + determineBuildSignature: async () => version, + task: async function({ + taskUtil, workspace, - pattern, - targetPath: resourcePath - }), - utils.writeDependencyApisToDir({ dependencies, - targetPath: path.join(tmpPath, "dependency-apis") - }) - ]); - - if (writtenResourcesCount === 0) { - log.info(`Failed to find any input resources for project ${projectName} using pattern ` + - `${pattern}. Skipping JSDoc generation...`); - return; - } + options = {} + }) { + const {projectName} = options; - const createdResources = await jsdocGenerator({ - sourcePath: resourcePath, - targetPath, - tmpPath, - options: { - projectName, - namespace, - version, - variants: ["apijson"] - } - }); + if (!projectName || !namespace || !version || !patterns.length) { + throw new Error("[generateJsdoc]: One or more mandatory options not provided"); + } - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); + const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} = + await utils.createTmpDirs(projectName); + + taskUtil?.registerCleanupTask(cleanup); + + const [writtenResourcesCount] = await Promise.all([ + utils.writeResourcesToDir({ + workspace, + pattern: patterns, + targetPath: resourcePath + }), + utils.writeDependencyApisToDir({ + dependencies, + targetPath: path.join(tmpPath, "dependency-apis") + }) + ]); + + if (writtenResourcesCount === 0) { + log.info(`Failed to find any input resources for project ${projectName} using pattern ` + + `${patterns}. Skipping JSDoc generation...`); + return; + } + + const createdResources = await jsdocGenerator({ + sourcePath: resourcePath, + targetPath, + tmpPath, + options: { + projectName, + namespace, + version, + variants: ["apijson"] + } + }); + + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); + } + }; } const utils = { diff --git a/packages/builder/lib/tasks/minify.js b/packages/builder/lib/tasks/minify.js index 52a8d4af427..275d96bab0a 100644 --- a/packages/builder/lib/tasks/minify.js +++ b/packages/builder/lib/tasks/minify.js @@ -28,7 +28,7 @@ const log = getLogger("builder:tasks:minify"); * @param {boolean} [options.useInputSourceMaps=true] Whether to make use of any existing source * maps referenced in the resources to be minified. Use this option to preserve reference to the original * source files, such as TypeScript files, in the generated source map. - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({pattern, omitSourceMapResources = false, useInputSourceMaps = true}) { const minifierOptions = { @@ -66,7 +66,7 @@ export default function build({pattern, omitSourceMapResources = false, useInput ]); }; - return [{ + return {steps: [{ name: "minify", // One key per matched resource, so a delta build re-minifies only the resources whose inputs // changed. The input source map is read through the step's workspace and thus tracked per step. @@ -118,5 +118,5 @@ export default function build({pattern, omitSourceMapResources = false, useInput }); await tagAndWrite(processed, workspace, taskUtil); }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/replaceBuildtime.js b/packages/builder/lib/tasks/replaceBuildtime.js index e8cad9174eb..53f9bb2d57b 100644 --- a/packages/builder/lib/tasks/replaceBuildtime.js +++ b/packages/builder/lib/tasks/replaceBuildtime.js @@ -34,10 +34,10 @@ function formatTimestamp(date) { * * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({pattern}) { - return [{ + return {steps: [{ name: "replaceBuildtime", // One key per matched resource, so a delta build re-processes only the resources that changed. keys: async ({workspace}) => workspace.byGlob(pattern), @@ -53,5 +53,5 @@ export default function build({pattern}) { await workspace.write(processed); } }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/replaceCopyright.js b/packages/builder/lib/tasks/replaceCopyright.js index a88d6ea313d..7a5731c4a6d 100644 --- a/packages/builder/lib/tasks/replaceCopyright.js +++ b/packages/builder/lib/tasks/replaceCopyright.js @@ -31,16 +31,16 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.copyright Replacement copyright * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({copyright, pattern}) { if (!copyright) { - return []; + return {steps: []}; } const replacePattern = /(?:\$\{copyright\}|@copyright@)/g; - return [{ + return {steps: [{ name: "replaceCopyright", // One key per matched resource, so a delta build re-processes only the resources that changed. keys: async ({workspace}) => workspace.byGlob(pattern), @@ -59,5 +59,5 @@ export default function build({copyright, pattern}) { await workspace.write(processed); } }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/replaceVersion.js b/packages/builder/lib/tasks/replaceVersion.js index 47655c86564..8c1216d227f 100644 --- a/packages/builder/lib/tasks/replaceVersion.js +++ b/packages/builder/lib/tasks/replaceVersion.js @@ -20,7 +20,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed * @param {string} options.version Replacement version - * @returns {object[]} The task's build steps + * @returns {object} The task descriptor with its build steps */ export default function build({pattern, version}) { const replacerOptions = { @@ -28,7 +28,7 @@ export default function build({pattern, version}) { replacement: version }; - return [{ + return {steps: [{ name: "replaceVersion", // One key per matched resource, so a delta build re-processes only the resources that changed. keys: async ({workspace}) => workspace.byGlob(pattern), @@ -38,5 +38,5 @@ export default function build({pattern, version}) { await workspace.write(processed); } }, - }]; + }]}; } diff --git a/packages/builder/lib/tasks/runSteps.js b/packages/builder/lib/tasks/runSteps.js index fd7134eedf0..4420413b4c6 100644 --- a/packages/builder/lib/tasks/runSteps.js +++ b/packages/builder/lib/tasks/runSteps.js @@ -58,9 +58,9 @@ class BufferedWriter extends AbstractReaderWriter { /** * Runs a step-based task's steps without a build cache. * - * A step-based task default-exports a factory build(options) => Step[]. This runner is the - * no-cache counterpart to the cached step runner in @ui5/project (which - * @ui5/builder cannot import, the dependency direction being + * A step-based task default-exports a factory build(options) => descriptor whose descriptor + * carries a steps array. This runner is the no-cache counterpart to the cached step runner in + * @ui5/project (which @ui5/builder cannot import, the dependency direction being * @ui5/cli -> @ui5/project -> @ui5/builder): it runs every step in order, fans out every * map step's keys set, threads needs returns in memory, and buffers a * concurrent map step's writes so they flush in key order. It replaces the per-task batch fallback tasks @@ -71,7 +71,8 @@ class BufferedWriter extends AbstractReaderWriter { * * @public * @module @ui5/builder/tasks/runSteps - * @param {Function} build Task factory build(options) => Step[] + * @param {Function} build Task factory build(options) => descriptor whose descriptor + * carries a steps array * @param {object} parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace Workspace to read and write files * @param {@ui5/fs/AbstractReader} [parameters.dependencies] Reader to read dependency files @@ -80,7 +81,11 @@ class BufferedWriter extends AbstractReaderWriter { * @returns {Promise} Resolves once all steps have run and their writes are flushed */ export default async function runSteps(build, {workspace, dependencies, taskUtil, options} = {}) { - const steps = await build(options); + const {steps} = await build(options); + if (!steps) { + throw new Error( + "runSteps requires a step-based task whose factory returns a descriptor with a 'steps' array"); + } // Each step's return, so a later step's needs can consume it. A scalar step's return is its single // value; a map step's is the array of its per-key returns in key order. const returns = new Map(); diff --git a/packages/builder/lib/tasks/taskRepository.js b/packages/builder/lib/tasks/taskRepository.js index 9b7b968667a..4273e560eb9 100644 --- a/packages/builder/lib/tasks/taskRepository.js +++ b/packages/builder/lib/tasks/taskRepository.js @@ -43,7 +43,9 @@ const taskInfos = { * * @private * @typedef {object} @ui5/builder/tasks/taskRepository~TaskInfo - * @property {Function} task Task function + * @property {Function} task The task module's default-export factory (options) => descriptor. + * The returned descriptor carries the task's steps or task body plus the + * optional requiresDependencies and determineBuildSignature callbacks. */ /** diff --git a/packages/builder/lib/tasks/transformBootstrapHtml.js b/packages/builder/lib/tasks/transformBootstrapHtml.js index bd10a479bc7..53c64d99c4e 100644 --- a/packages/builder/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/lib/tasks/transformBootstrapHtml.js @@ -2,7 +2,7 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("builder:tasks:transformBootstrapHtml"); import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js"; -/* eslint "jsdoc/check-param-names": ["error", {"disableExtraPropertyReporting":true}] */ +/* eslint "jsdoc/check-param-names": "off" */ /** * Task for transforming the application bootstrap HTML file. * @@ -12,28 +12,30 @@ import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js" * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name * @param {string} [parameters.options.projectNamespace] Project namespace - * @returns {Promise} Promise resolving with undefined once data has been written + * @returns {object} The task descriptor */ -export default async function({workspace, options}) { - const {projectName} = options; - const namespace = options.projectNamespace; +export default function() { + return {task: async function({workspace, options}) { + const {projectName} = options; + const namespace = options.projectNamespace; - let indexPath; - if (namespace) { - indexPath = `/resources/${namespace}/index.html`; - } else { - indexPath = "/index.html"; - } - const resource = await workspace.byPath(indexPath); - if (!resource) { - log.warn(`Skipping bootstrap transformation due to missing index.html in project "${projectName}".`); - return; - } - const processedResources = await bootstrapHtmlTransformer({ - resources: [resource], - options: { - src: "resources/sap-ui-custom.js" + let indexPath; + if (namespace) { + indexPath = `/resources/${namespace}/index.html`; + } else { + indexPath = "/index.html"; } - }); - await Promise.all(processedResources.map((resource) => workspace.write(resource))); + const resource = await workspace.byPath(indexPath); + if (!resource) { + log.warn(`Skipping bootstrap transformation due to missing index.html in project "${projectName}".`); + return; + } + const processedResources = await bootstrapHtmlTransformer({ + resources: [resource], + options: { + src: "resources/sap-ui-custom.js" + } + }); + await Promise.all(processedResources.map((resource) => workspace.write(resource))); + }}; } diff --git a/packages/builder/test/lib/tasks/bundlers/generateBundle.js b/packages/builder/test/lib/tasks/bundlers/generateBundle.js index 879e50f1062..0db51bbe876 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/test/lib/tasks/bundlers/generateBundle.js @@ -88,7 +88,7 @@ test.serial("generateBundle: No taskUtil, no bundleOptions", async (t) => { // bundleDefinition can be empty here as the moduleBundler is mocked const bundleDefinition = {}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, options: { @@ -160,7 +160,7 @@ test.serial("generateBundle: No bundleOptions, with taskUtil", async (t) => { // bundleDefinition can be empty here as the moduleBundler is mocked const bundleDefinition = {}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, @@ -277,7 +277,7 @@ test.serial("generateBundle: No bundleOptions, with taskUtil and specVersion < 4 // bundleDefinition can be empty here as the moduleBundler is mocked const bundleDefinition = {}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, @@ -395,7 +395,7 @@ test.serial("generateBundle: bundleOptions: optimize=false, with taskUtil", asyn const bundleDefinition = {}; const bundleOptions = {optimize: false}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, @@ -522,7 +522,7 @@ test.serial("generateBundle: bundleOptions: sourceMap=false, with taskUtil", asy const bundleDefinition = {}; const bundleOptions = {sourceMap: false}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, @@ -621,7 +621,7 @@ test.serial("generateBundle: Empty bundle (skipIfEmpty=true)", async (t) => { const bundleDefinition = {}; const bundleOptions = {skipIfEmpty: true}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, @@ -703,7 +703,7 @@ test.serial("generateBundle: Throws error when non-debug name can't be resolved" taskUtil.getTag.returns(true); getNonDebugName.returns(false); - await t.throwsAsync(generateBundle({ + await t.throwsAsync(generateBundle().task({ workspace, dependencies, taskUtil, @@ -755,7 +755,7 @@ test.serial("generateBundle: No bundleOptions, with taskUtil, UI5 Version >= 2", // bundleDefinition can be empty here as the moduleBundler is mocked const bundleDefinition = {}; - await generateBundle({ + await generateBundle().task({ workspace, dependencies, taskUtil, diff --git a/packages/builder/test/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/test/lib/tasks/bundlers/generateComponentPreload.js index 6231d9adfe3..9338996187f 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/test/lib/tasks/bundlers/generateComponentPreload.js @@ -50,7 +50,7 @@ test.serial("generateComponentPreload - one namespace", async (t) => { } ]); - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, options: { @@ -162,7 +162,7 @@ test.serial("generateComponentPreload - one namespace, with taskUtil and specVer } }; - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, taskUtil, @@ -276,7 +276,7 @@ test.serial("generateComponentPreload - one namespace, with taskUtil and specVer } }; - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, taskUtil, @@ -368,7 +368,7 @@ test.serial("generateComponentPreload - one namespace - excludes", async (t) => } ]); - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, options: { @@ -485,7 +485,7 @@ test.serial("generateComponentPreload - one namespace - excludes w/o namespace", createFilterReader: () => workspace } }; - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, taskUtil, @@ -604,7 +604,7 @@ test.serial("generateComponentPreload - multiple namespaces - excludes", async ( } ]); - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, options: { @@ -754,7 +754,7 @@ test.serial("generateComponentPreload - one namespace - invalid exclude", async ]; byGlob.resolves(resources); - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, options: { @@ -792,7 +792,7 @@ test.serial("generateComponentPreload - nested namespaces - excludes", async (t) ]; byGlob.resolves(resources); - await generateComponentPreload({ + await generateComponentPreload().task({ workspace, dependencies, options: { diff --git a/packages/builder/test/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/test/lib/tasks/bundlers/generateFlexChangesBundle.js index d50b5e18cb5..470a57552f4 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/test/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -87,7 +87,7 @@ function createStubWorkspace(changeList, manifest, { * @returns {Promise} Resolves when the task has finished */ function runTask(workspace, extra = {}) { - return generateFlexChangesBundle({ + return generateFlexChangesBundle().task({ workspace, taskUtil: false, options: {projectNamespace: "sap/ui/demo/app"}, diff --git a/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.integration.js b/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.integration.js index c265f581cea..38768724fd0 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.integration.js +++ b/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.integration.js @@ -193,7 +193,7 @@ test.serial("integration: generateLibraryPreload", async (t) => { virBasePath: "/" }); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace: duplexCollection, dependencies: dependencies, options: { @@ -259,7 +259,7 @@ test.serial("integration: generateLibraryPreload with designtime and support fil virBasePath: "/" }); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace: duplexCollection, dependencies: dependencies, options: { diff --git a/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.js index 7618a8c908f..4fdb2a47401 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/test/lib/tasks/bundlers/generateLibraryPreload.js @@ -47,7 +47,7 @@ test.serial("generateLibraryPreload", async (t) => { {getPath: sinon.stub().returns("/resources/my/lib/.library")} ]); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, options: { @@ -172,7 +172,7 @@ test.serial("generateLibraryPreload for sap.ui.core (w/o ui5loader.js)", async ( {getPath: sinon.stub().returns("/resources/sap/ui/core/.library")} ]); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, options: { @@ -508,7 +508,7 @@ test.serial("generateLibraryPreload for sap.ui.core (/w ui5loader.js)", async (t createFilterReader: () => workspace } }; - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, taskUtil, @@ -934,7 +934,7 @@ test.serial("generateLibraryPreload for sap.ui.core with old specVersion defined taskUtil.getTag .withArgs(sinon.match.any, taskUtil.STANDARD_TAGS.HasDebugVariant) .returns(true); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, taskUtil, @@ -1345,7 +1345,7 @@ test.serial("generateLibraryPreload for sap.ui.core with own bundle configuratio createFilterReader: () => workspace } }; - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, taskUtil, @@ -1536,7 +1536,7 @@ test.serial("generateLibraryPreload for sap.ui.core with own bundle configuratio taskUtil.getTag .withArgs(resources[0], taskUtil.STANDARD_TAGS.HasDebugVariant) .returns(true); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, taskUtil, @@ -1725,7 +1725,7 @@ test.serial("Error: Failed to resolve non-debug name", async (t) => { .withArgs(resources[0], taskUtil.STANDARD_TAGS.IsDebugVariant) .returns(true); - await t.throwsAsync(generateLibraryPreload({ + await t.throwsAsync(generateLibraryPreload().task({ workspace, dependencies, taskUtil, @@ -1754,7 +1754,7 @@ test.serial("generateLibraryPreload with excludes", async (t) => { {getPath: sinon.stub().returns("/resources/my/lib/.library")} ]); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, options: { @@ -1826,7 +1826,7 @@ test.serial("generateLibraryPreload with invalid excludes", async (t) => { {getPath: sinon.stub().returns("/resources/my/lib/.library")} ]); - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, options: { @@ -1926,7 +1926,7 @@ test.serial("generateLibraryPreload for sap.ui.core (/w ui5loader.js), UI5 Versi createFilterReader: () => workspace } }; - await generateLibraryPreload({ + await generateLibraryPreload().task({ workspace, dependencies, taskUtil, diff --git a/packages/builder/test/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/test/lib/tasks/bundlers/generateStandaloneAppBundle.js index 20bf7b73e47..f9e29551b58 100644 --- a/packages/builder/test/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/test/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -71,7 +71,7 @@ test.serial("execute module bundler and write results", async (t) => { projectNamespace: "some/project/namespace" } }; - await generateStandaloneAppBundle(params); + await generateStandaloneAppBundle().task(params); t.is(t.context.moduleBundlerStub.callCount, 2); @@ -118,7 +118,7 @@ test.serial("execute module bundler and write results without namespace", async projectName: "some.project.name" } }; - await generateStandaloneAppBundle(params); + await generateStandaloneAppBundle().task(params); t.is(t.context.moduleBundlerStub.callCount, 2); @@ -165,7 +165,7 @@ test.serial("execute module bundler and write results in evo mode", async (t) => projectNamespace: "some/project/namespace" } }; - await generateStandaloneAppBundle(params); + await generateStandaloneAppBundle().task(params); t.is(t.context.moduleBundlerStub.callCount, 2); @@ -222,7 +222,7 @@ test.serial("execute module bundler with taskUtil", async (t) => { projectNamespace: "some/project/namespace" } }; - await generateStandaloneAppBundle(params); + await generateStandaloneAppBundle().task(params); t.is(taskUtil.getTag.callCount, 5, "TaskUtil#getTag got called six times"); @@ -419,7 +419,7 @@ test.serial("execute module bundler with taskUtil, UI5 Version >= 2", async (t) projectNamespace: "some/project/namespace" } }; - await generateStandaloneAppBundle(params); + await generateStandaloneAppBundle().task(params); t.is(taskUtil.getTag.callCount, 5, "TaskUtil#getTag got called six times"); @@ -590,7 +590,7 @@ test.serial("Error: Failed to resolve non-debug name", async (t) => { } }; - await t.throwsAsync(generateStandaloneAppBundle(params), { + await t.throwsAsync(generateStandaloneAppBundle().task(params), { message: "Failed to resolve non-debug name for /resources/ponyPath1.js" }); }); diff --git a/packages/builder/test/lib/tasks/generateLibraryManifest.js b/packages/builder/test/lib/tasks/generateLibraryManifest.js index bcb6a355d1e..625b1e12d81 100644 --- a/packages/builder/test/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/test/lib/tasks/generateLibraryManifest.js @@ -17,7 +17,7 @@ async function assertCreatedManifest(t, oExpectedManifest) { await Promise.all(resources.map((resource) => workspace.write(resource))); - await generateLibraryManifest({ + await generateLibraryManifest().task({ workspace, taskUtil: { getProject: () => { diff --git a/packages/builder/test/lib/tasks/generateResourcesJson.js b/packages/builder/test/lib/tasks/generateResourcesJson.js index a679d3bbadf..adc613c6023 100644 --- a/packages/builder/test/lib/tasks/generateResourcesJson.js +++ b/packages/builder/test/lib/tasks/generateResourcesJson.js @@ -45,7 +45,7 @@ test.afterEach.always((t) => { test.serial("Missing 'dependencies' parameter", async (t) => { const {generateResourcesJson} = t.context; - await t.throwsAsync(generateResourcesJson({ + await t.throwsAsync(generateResourcesJson().task({ workspace: createWorkspace(), options: { projectName: "sap.ui.core" @@ -59,7 +59,7 @@ test.serial("Missing 'dependencies' parameter", async (t) => { test.serial("empty resources (sap.ui.core)", async (t) => { const {generateResourcesJson, resourceListCreatorStub} = t.context; - const result = await generateResourcesJson({ + const result = await generateResourcesJson().task({ workspace: createWorkspace(), dependencies: createDependencies(), options: { @@ -84,7 +84,7 @@ test.serial("empty resources (sap.ui.core)", async (t) => { test.serial("empty resources (my.lib)", async (t) => { const {generateResourcesJson} = t.context; - const result = await generateResourcesJson({ + const result = await generateResourcesJson().task({ workspace: createWorkspace(), dependencies: createDependencies(), options: { @@ -106,7 +106,7 @@ test.serial("empty resources (my.lib with dependencies)", async (t) => { byGlob: sinon.stub().resolves(dependencyResources) }; - const result = await generateResourcesJson({ + const result = await generateResourcesJson().task({ workspace: createWorkspace(), dependencies, options: { @@ -160,7 +160,7 @@ test.serial("Resources omitted from build result should be ignored", async (t) = .onCall(3).returns(true) // first dependencyResource should be filtered out .onCall(4).returns(false); - const result = await generateResourcesJson({ + const result = await generateResourcesJson().task({ workspace, dependencies, taskUtil, diff --git a/packages/builder/test/lib/tasks/generateVersionInfo.js b/packages/builder/test/lib/tasks/generateVersionInfo.js index bedb63baf19..ff8d53d8619 100644 --- a/packages/builder/test/lib/tasks/generateVersionInfo.js +++ b/packages/builder/test/lib/tasks/generateVersionInfo.js @@ -68,7 +68,7 @@ async function createOptions(t, options) { async function assertCreatedVersionInfo(t, oExpectedVersionInfo, oOptions) { const {generateVersionInfo} = t.context; - await generateVersionInfo(oOptions); + await generateVersionInfo().task(oOptions); const resource = await oOptions.workspace.byPath("/resources/sap-ui-version.json"); if (!resource) { diff --git a/packages/builder/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index abea08068cc..5392af570ba 100644 --- a/packages/builder/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -56,7 +56,7 @@ test.serial("executeJsdocSdkTransformation", async (t) => { const dependencies = { byGlob: byGlobDependenciesStub }; - await executeJsdocSdkTransformation({ + await executeJsdocSdkTransformation().task({ workspace, dependencies, options: { @@ -105,7 +105,7 @@ test.serial("executeJsdocSdkTransformation", async (t) => { test("executeJsdocSdkTransformation with missing parameters", async (t) => { const {executeJsdocSdkTransformation} = t.context; - await t.throwsAsync(executeJsdocSdkTransformation(), { + await t.throwsAsync(executeJsdocSdkTransformation().task(), { instanceOf: TypeError }, "TypeError thrown"); }); @@ -131,7 +131,7 @@ test.serial("executeJsdocSdkTransformation with missing project api.json (skips byGlob: byGlobDependenciesStub }; - await executeJsdocSdkTransformation({ + await executeJsdocSdkTransformation().task({ workspace, dependencies, options: { @@ -170,7 +170,7 @@ test("executeJsdocSdkTransformation too many project api.json resources", async byGlob: byGlobDependenciesStub }; - const error = await t.throwsAsync(executeJsdocSdkTransformation({ + const error = await t.throwsAsync(executeJsdocSdkTransformation().task({ workspace, dependencies, options: { @@ -203,7 +203,7 @@ test("executeJsdocSdkTransformation missing project .library", async (t) => { byGlob: byGlobDependenciesStub }; - const error = await t.throwsAsync(executeJsdocSdkTransformation({ + const error = await t.throwsAsync(executeJsdocSdkTransformation().task({ workspace, dependencies, options: { @@ -240,7 +240,7 @@ test("executeJsdocSdkTransformation too many project .library resources", async byGlob: byGlobDependenciesStub }; - const error = await t.throwsAsync(executeJsdocSdkTransformation({ + const error = await t.throwsAsync(executeJsdocSdkTransformation().task({ workspace, dependencies, options: { diff --git a/packages/builder/test/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/test/lib/tasks/jsdoc/generateApiIndex.js index 262e72fba92..2bcd452f1ff 100644 --- a/packages/builder/test/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/test/lib/tasks/jsdoc/generateApiIndex.js @@ -32,7 +32,7 @@ test.serial("generateApiIndex", async (t) => { write: writeStub }; const dependencies = {}; - await generateApiIndex({ + await generateApiIndex().task({ workspace, dependencies, options: { @@ -69,7 +69,7 @@ test.serial("generateApiIndex", async (t) => { test("generateApiIndex with missing parameters", async (t) => { const {generateApiIndex} = t.context; - await t.throwsAsync(generateApiIndex(), { + await t.throwsAsync(generateApiIndex().task(), { instanceOf: TypeError }, "TypeError thrown"); }); diff --git a/packages/builder/test/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/test/lib/tasks/jsdoc/generateJsdoc.js index 31054cd3e7f..68a47586642 100644 --- a/packages/builder/test/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/test/lib/tasks/jsdoc/generateJsdoc.js @@ -244,15 +244,16 @@ test.serial("generateJsdoc", async (t) => { const taskUtil = { registerCleanupTask: registerCleanupTaskStub }; - await generateJsdoc({ + const {task} = generateJsdoc({ + namespace: "some/project", + version: "some version" + }); + await task({ taskUtil, workspace, dependencies: "dependencies", options: { - pattern: "some pattern", - projectName: "some.project", - namespace: "some/project", - version: "some version" + projectName: "some.project" } }); @@ -267,7 +268,7 @@ test.serial("generateJsdoc", async (t) => { t.is(writeResourcesToDirStub.callCount, 1, "writeResourcesToDir got called once"); t.deepEqual(writeResourcesToDirStub.getCall(0).args[0], { workspace, - pattern: "some pattern", + pattern: ["/resources/**/*.js"], targetPath: path.join("/", "some", "source", "path") // one's target is another one's source }, "writeResourcesToDir got called with correct arguments"); @@ -321,15 +322,16 @@ test.serial("generateJsdoc with missing resources", async (t) => { const taskUtil = { registerCleanupTask: registerCleanupTaskStub }; - await generateJsdoc({ + const {task} = generateJsdoc({ + namespace: "some/project", + version: "some version" + }); + await task({ taskUtil, workspace, dependencies: "dependencies", options: { - pattern: "some pattern", - projectName: "some.project", - namespace: "some/project", - version: "some version" + projectName: "some.project" } }); @@ -339,7 +341,7 @@ test.serial("generateJsdoc with missing resources", async (t) => { t.is(log.info.callCount, 1, "One message has been logged"); t.deepEqual(log.info.getCall(0).args[0], "Failed to find any input resources for project some.project " + - "using pattern some pattern. Skipping JSDoc generation...", + "using pattern /resources/**/*.js. Skipping JSDoc generation...", "Correct message has been logged"); t.is(jsdocGeneratorStub.callCount, 0, "jsdocGenerator processor has *not* been called"); @@ -351,14 +353,15 @@ test.serial("generateJsdoc with missing resources", async (t) => { test.serial("generateJsdoc no parameters", async (t) => { const {generateJsdoc} = t.context; - await t.throwsAsync(generateJsdoc(), { + t.throws(() => generateJsdoc(), { instanceOf: TypeError }, "TypeError thrown"); }); test.serial("generateJsdoc missing parameters", async (t) => { const {generateJsdoc} = t.context; - const error = await t.throwsAsync(generateJsdoc({})); + const {task} = generateJsdoc({}); + const error = await t.throwsAsync(task({options: {}})); t.is(error.message, "[generateJsdoc]: One or more mandatory options not provided", "Correct error message thrown"); }); diff --git a/packages/builder/test/lib/tasks/runSteps.js b/packages/builder/test/lib/tasks/runSteps.js index 1c0859fb2ee..48bdecf734f 100644 --- a/packages/builder/test/lib/tasks/runSteps.js +++ b/packages/builder/test/lib/tasks/runSteps.js @@ -34,13 +34,13 @@ function createWorkspace(initial = []) { test("Runs a scalar step and threads its return into a consumer via needs", async (t) => { const workspace = createWorkspace(); let consumed; - await runSteps((options) => [ + await runSteps((options) => ({steps: [ {name: "scan", run: async () => ({flag: options.flag})}, {name: "use", needs: ["scan"], run: async ({needs, workspace}) => { consumed = needs.scan; await workspace.write(createResource("/out")); }}, - ], {workspace, options: {flag: 42}}); + ]}), {workspace, options: {flag: 42}}); t.deepEqual(consumed, {flag: 42}, "The producer's return arrived as needs.scan"); t.true(workspace.store.has("/out"), "The consumer's write persisted"); @@ -49,12 +49,12 @@ test("Runs a scalar step and threads its return into a consumer via needs", asyn test("Fans out a map step's keys and writes each", async (t) => { const workspace = createWorkspace(); const ran = []; - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "m", keys: async () => ["a", "b", "c"], each: async (key, {workspace}) => { ran.push(key); await workspace.write(createResource(`/out/${key}`)); }}, - ], {workspace}); + ]}), {workspace}); t.deepEqual(ran.sort(), ["a", "b", "c"], "each ran once per key"); t.true(workspace.store.has("/out/a") && workspace.store.has("/out/b") && workspace.store.has("/out/c"), @@ -64,20 +64,20 @@ test("Fans out a map step's keys and writes each", async (t) => { test("A producer return reaches a map step's keys and each", async (t) => { const workspace = createWorkspace(); const eachSaw = []; - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "scan", run: async () => ({wanted: ["x", "y"]})}, {name: "build", needs: ["scan"], keys: async ({needs}) => needs.scan.wanted, each: async (key, {needs}) => { eachSaw.push([key, needs.scan.wanted.length]); }}, - ], {workspace}); + ]}), {workspace}); t.deepEqual(eachSaw.sort(), [["x", 2], ["y", 2]], "each saw the producer return per key"); }); test("A concurrent map step flushes its writes in key order", async (t) => { const workspace = createWorkspace(); - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "m", keys: async () => ["a", "b", "c"], each: async (key, {workspace}) => { // Reverse the natural completion order so the key-order flush is observable. if (key === "a") { @@ -85,7 +85,7 @@ test("A concurrent map step flushes its writes in key order", async (t) => { } await workspace.write(createResource(`/${key}.out`)); }}, - ], {workspace}); + ]}), {workspace}); t.deepEqual(workspace.writeOrder, ["/a.out", "/b.out", "/c.out"], "Buffered writes flushed in key order regardless of completion order"); @@ -94,7 +94,7 @@ test("A concurrent map step flushes its writes in key order", async (t) => { test("A sequential map step makes an earlier key's write visible to a later key", async (t) => { const workspace = createWorkspace(); let secondSawFirst = false; - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "m", sequential: true, keys: async () => ["first", "second"], each: async (key, {workspace}) => { if (key === "first") { await workspace.write(createResource("/shared")); @@ -102,7 +102,7 @@ test("A sequential map step makes an earlier key's write visible to a later key" secondSawFirst = !!(await workspace.byPath("/shared")); } }}, - ], {workspace}); + ]}), {workspace}); t.true(secondSawFirst, "The second key read the first key's write"); }); @@ -111,11 +111,11 @@ test("Concurrent map-step keys writing the same path throw", async (t) => { const workspace = createWorkspace(); const inputA = createResource("/resources/first.js"); const inputB = createResource("/resources/second.js"); - const err = await t.throwsAsync(runSteps(() => [ + const err = await t.throwsAsync(runSteps(() => ({steps: [ {name: "m", keys: async () => [inputA, inputB], each: async (key, {workspace}) => { await workspace.write(createResource("/same")); }}, - ], {workspace})); + ]}), {workspace})); // The exact user-visible message, shared with the cached runner through @ui5/fs/internal/stepWriteBuffer. t.is(err.message, "Concurrent map step 'm' tried to write '/same' for invocations '/resources/first.js' and " + @@ -125,7 +125,7 @@ test("Concurrent map-step keys writing the same path throw", async (t) => { test("A concurrent map step preserves each key's write arguments through the flush", async (t) => { const workspace = createWorkspace(); - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "m", keys: async () => ["with", "without"], each: async (key, {workspace}) => { if (key === "with") { await workspace.write(createResource("/with"), {drain: true}); @@ -134,7 +134,7 @@ test("A concurrent map step preserves each key's write arguments through the flu await workspace.write(createResource("/without")); } }}, - ], {workspace}); + ]}), {workspace}); t.deepEqual(workspace.writeOrder, ["/with", "/without"], "Flushed in key order"); t.deepEqual(workspace.writeArgs, [[{drain: true}], []], @@ -144,14 +144,14 @@ test("A concurrent map step preserves each key's write arguments through the flu test("A later step sees an earlier step's write", async (t) => { const workspace = createWorkspace(); let laterSaw = false; - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "first", run: async ({workspace}) => { await workspace.write(createResource("/from-first")); }}, {name: "second", run: async ({workspace}) => { laterSaw = !!(await workspace.byPath("/from-first")); }}, - ], {workspace}); + ]}), {workspace}); t.true(laterSaw, "The second step read the first step's write"); }); @@ -161,11 +161,11 @@ test("taskUtil and dependencies are passed through to steps", async (t) => { const taskUtil = {marker: "taskUtil"}; const dependencies = {marker: "dependencies"}; let seen; - await runSteps(() => [ + await runSteps(() => ({steps: [ {name: "s", run: async (ctx) => { seen = {taskUtil: ctx.taskUtil, dependencies: ctx.dependencies}; }}, - ], {workspace, taskUtil, dependencies}); + ]}), {workspace, taskUtil, dependencies}); t.is(seen.taskUtil, taskUtil, "taskUtil passed through"); t.is(seen.dependencies, dependencies, "dependencies passed through"); @@ -173,6 +173,14 @@ test("taskUtil and dependencies are passed through to steps", async (t) => { test("An empty step list is a no-op", async (t) => { const workspace = createWorkspace(); - await runSteps(() => [], {workspace}); + await runSteps(() => ({steps: []}), {workspace}); t.is(workspace.store.size, 0, "Nothing written"); }); + +test("A descriptor without a steps array throws", async (t) => { + const workspace = createWorkspace(); + const err = await t.throwsAsync(runSteps(() => ({task: async () => {}}), {workspace})); + t.is(err.message, + "runSteps requires a step-based task whose factory returns a descriptor with a 'steps' array", + "A non-step descriptor surfaces a clear error"); +}); diff --git a/packages/builder/test/lib/tasks/transformBootstrapHtml.js b/packages/builder/test/lib/tasks/transformBootstrapHtml.js index 60be2f402f8..488f85b22ee 100644 --- a/packages/builder/test/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/test/lib/tasks/transformBootstrapHtml.js @@ -44,7 +44,7 @@ test.serial("Transforms index.html resource", async (t) => { t.context.bootstrapHtmlTransformerStub.returns([resource]); - await transformBootstrapHtml({ + await transformBootstrapHtml().task({ workspace, options: { projectName: "sap.ui.demo.app", @@ -86,7 +86,7 @@ test.serial("Transforms index.html resource without namespace", async (t) => { t.context.bootstrapHtmlTransformerStub.returns([resource]); - await transformBootstrapHtml({ + await transformBootstrapHtml().task({ workspace, options: { projectName: "sap.ui.demo.app" @@ -122,7 +122,7 @@ test.serial("No index.html resource exists", async (t) => { } }; - await transformBootstrapHtml({ + await transformBootstrapHtml().task({ workspace, options: { projectName: "sap.ui.demo.app", diff --git a/packages/project/lib/build/TaskDefinitions.js b/packages/project/lib/build/TaskDefinitions.js index a7b20e06221..462420b61ff 100644 --- a/packages/project/lib/build/TaskDefinitions.js +++ b/packages/project/lib/build/TaskDefinitions.js @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import {getLogger} from "@ui5/logger"; +import {validateDescriptor} from "./helpers/StepRunner.js"; export default class TaskDefinitions { #graph; @@ -66,9 +67,47 @@ export default class TaskDefinitions { getTask: this.#taskRepository.getTask }); + // Complete the project-derived options every standard task's factory is called with, before any + // factory runs. The factory is called once per build (early for the build signature, then reused for + // execution), and some factories branch on these values (generateThemeDesignerResources emits its + // libraryTheming step only for a namespace), so an incomplete options object at signature time would + // produce a different descriptor than execution. Completing them here keeps one memoized descriptor + // valid for both phases. + for (const taskDef of this.#standardTasks.values()) { + taskDef.options = taskDef.options ?? {}; + taskDef.options.projectName = this.#project.getName(); + taskDef.options.projectNamespace = this.#project.getNamespace(); + } + this.#customTasks = await this._getCustomTasks(this.#project, this.#taskUtil); } + /** + * Resolves and memoizes a standard task's descriptor by calling its factory (the task module's default + * export) once with the completed options. A task the definition explicitly skips (skip) or + * overrides with its own taskFunction has no descriptor to resolve. + * + * @private + * @param {string} taskName Task name + * @param {object} taskDef Standard task definition + * @returns {Promise} The validated descriptor, or undefined for a skipped + * or taskFunction-overridden task + */ + async #resolveStandardDescriptor(taskName, taskDef) { + if (taskDef.skip || taskDef.taskFunction) { + return undefined; + } + if (!taskDef.descriptorPromise) { + taskDef.descriptorPromise = (async () => { + const {task: factory} = await this.#taskRepository.getTask(taskName); + const descriptor = factory(taskDef.options); + validateDescriptor(descriptor, {taskName}); + return descriptor; + })(); + } + return taskDef.descriptorPromise; + } + async _getCustomTasks() { const projectCustomTasks = this.#project.getCustomTasks(); if (!projectCustomTasks || projectCustomTasks.length === 0) { @@ -129,14 +168,16 @@ export default class TaskDefinitions { await this._initTasks(); const sigKeys = []; - for (const taskDef of this.#standardTasks.values()) { - if (taskDef.determineBuildSignature) { - const sig = await taskDef.determineBuildSignature(); + for (const [taskName, taskDef] of this.#standardTasks) { + const descriptor = await this.#resolveStandardDescriptor(taskName, taskDef); + if (descriptor?.determineBuildSignature) { + const sig = await descriptor.determineBuildSignature(); if (sig) { sigKeys.push(sig); } } else { - // Fallback to task options + // A skipped or taskFunction-overridden task has no descriptor; a descriptor without a + // determineBuildSignature callback falls back to its options. Both hash the task options. sigKeys.push(JSON.stringify(taskDef.options)); } } @@ -145,25 +186,23 @@ export default class TaskDefinitions { const projectNamespace = this.#project.getNamespace(); for (const [taskName, {taskDef, task}] of this.#customTasks) { const specVersion = task.getSpecVersion(); - const determineBuildSignatureCallback = await task.getDetermineBuildSignatureCallback(); + const sigOptions = { + projectName, + projectNamespace, + configuration: taskDef.configuration, + taskName + }; + // The determineBuildSignature callback is a descriptor property, available only from + // Specification Version 5.0. Below 5.0 the default export is the legacy task body, so there is no + // callback to honor and the signature falls back to the task configuration. + const determineBuildSignatureCallback = specVersion.gte("5.0") ? + await task.getDetermineBuildSignatureCallback(taskName, sigOptions) : undefined; if (determineBuildSignatureCallback) { - if (specVersion.lt("5.0")) { - throw new Error( - `Custom task ${taskDef.name} of project ${projectName} uses a ` + - `determineBuildSignature callback, which is not supported for tasks with specVersion < 5.0. ` + - `Please update the task's specVersion to at least 5.0.` - ); - } const taskUtil = this.#taskUtil.getReadOnlyInterface(specVersion); const log = getLogger(`TaskDefinitions:custom-task:${taskName}`); const sig = await determineBuildSignatureCallback({ log, - options: { - projectName, - projectNamespace, - configuration: taskDef.configuration, - taskName - }, + options: sigOptions, taskUtil }); if (sig) { @@ -186,4 +225,18 @@ export default class TaskDefinitions { customTasks: this.#customTasks }; } + + /** + * Returns the memoized descriptor of a standard task (the object its factory returns), or + * undefined for a skipped or taskFunction-overridden task. Shares one factory + * call with {@link #getBuildSignatures}. + * + * @public + * @param {string} taskName Task name + * @param {object} taskDef Standard task definition + * @returns {Promise} The validated descriptor, or undefined + */ + async getStandardDescriptor(taskName, taskDef) { + return this.#resolveStandardDescriptor(taskName, taskDef); + } } diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js index 0524647c5c7..0e3817813c7 100644 --- a/packages/project/lib/build/TaskRunner.js +++ b/packages/project/lib/build/TaskRunner.js @@ -162,7 +162,7 @@ class TaskRunner { const {standardTasks, customTasks} = await this._taskDefinitions.getTaskDefinitions(); for (const [taskName, taskDef] of standardTasks) { - this._addTask(taskName, taskDef); + await this._addTask(taskName, taskDef); } for (const [taskName, {taskDef, task}] of customTasks) { @@ -211,26 +211,24 @@ class TaskRunner { this._log.setTasks(allTasks); // Expand step-based tasks into their per-step stages: each step is its own stage, so the - // stage list must enumerate a step-based task's step names in step order. The factory is pure over - // options (it may not read readers/taskUtil) and options are fixed for this build, so one factory - // call produces the steps that both create the stages here and run at execution. Keep the step array - // on the task so the execution path reuses it instead of calling the factory a second time. Freeze it - // so a custom task cannot mutate the shared value between the two uses; discovery re-derives it on the - // next build, so a surviving TaskRunner never serves a stale array. - const stageTasks = await Promise.all(allTasks.map(async (taskName) => { + // stage list must enumerate a step-based task's step names in step order. The descriptor (and its + // steps array) was resolved once by TaskDefinitions, shared with the build-signature phase, so read + // it here rather than calling the factory again. Freeze the array so a custom task cannot mutate the + // shared value; discovery re-derives it on the next build, so a surviving TaskRunner never serves a + // stale array. + const stageTasks = allTasks.map((taskName) => { const taskDef = this._tasks[taskName]; - if (!taskDef.stepBased) { + const steps = taskDef.descriptor?.steps; + if (!steps) { return {taskName}; } - const factory = await taskDef.stepFactory(); - const steps = await factory(taskDef.options); // Validate the whole step list before it creates any stage: setTasks below derives a stage id per // step name, so a duplicate name or a malformed declaration must be rejected here, not later when // the step runs (by then two steps sharing a name have already created two stages with one id). validateSteps(steps, {taskName}); taskDef.steps = Object.freeze(steps); return {taskName, stepNames: steps.map((step) => step.name)}; - })); + }); this._buildCache.setTasks(stageTasks); for (let i = 0; i < allTasks.length; i++) { @@ -291,23 +289,26 @@ class TaskRunner { * Adds an executable task to the builder * * The order this function is called defines the build order (FIFO). - * Tasks can be explicitly skipped by setting taskFunction to null. + * Tasks can be explicitly skipped by setting skip to true in the definition. + * + * The task's shape comes from the descriptor its factory (the task module's default export) returns: + * a steps array drives the step runner (one stage per step), a task body runs + * as a single legacy stage, and requiresDependencies decides whether a dependencies reader + * is provided. A definition may instead supply its own taskFunction override (for example the + * multi-bundle generateBundle wrapper that cannot live in @ui5/builder); it then runs as the + * single-stage body and the definition's own requiresDependencies flag applies. * * @param {string} taskName Name of the task to add - * @param {object} [parameters] Task parameters - * @param {boolean} [parameters.requiresDependencies=false] - * Whether the task requires access to project dependencies - * @param {boolean} [parameters.stepBased=false] - * Whether the task's default export is a step factory build(options) => Step[] driven - * by the step runner, rather than a legacy task body - * @param {object} [parameters.options={}] Options to pass to the task - * @param {Function|null} [parameters.taskFunction] - * Task function to execute, or null to explicitly skip the task - * @returns {void} + * @param {object} [taskDef] Task definition from the build definition + * @param {boolean} [taskDef.requiresDependencies=false] + * Whether a taskFunction-overridden task requires access to project dependencies + * @param {object} [taskDef.options={}] Options to pass to the task factory + * @param {Function} [taskDef.taskFunction] Task body override; runs as the single legacy stage + * @param {boolean} [taskDef.skip] Whether the task is explicitly skipped + * @returns {Promise} */ - _addTask(taskName, { - requiresDependencies = false, stepBased = false, options = {}, taskFunction - } = {}) { + async _addTask(taskName, taskDef = {}) { + const {options = {}, taskFunction, skip} = taskDef; if (this._tasks[taskName]) { throw new Error(`Failed to add duplicate task ${taskName} for project ${this._project.getName()}`); } @@ -316,39 +317,38 @@ class TaskRunner { `It has already been scheduled for execution`); } - // Complete the options before the task is registered, not when it runs. runTasks calls a step-based - // task's factory at plan time to discover its step names, and the factory may branch on - // projectNamespace (generateThemeDesignerResources emits its libraryTheming step only for a - // namespace). Assigning these at execution time left the plan-time call reading an incomplete - // options object, so the discovered step set missed a stage the step runner then asked for. + // projectName/projectNamespace were completed into the options by TaskDefinitions before the first + // factory call, so the descriptor resolved there matches the options the task body sees here. options.projectName = this._project.getName(); options.projectNamespace = this._project.getNamespace(); + // The descriptor is resolved and memoized by TaskDefinitions (shared with the build-signature phase). + // A skipped or taskFunction-overridden task has no descriptor. + const descriptor = await this._taskDefinitions.getStandardDescriptor(taskName, taskDef); + const steps = descriptor?.steps; + const stepBased = !!steps; + // requiresDependencies comes from the descriptor for a normal task, or from the definition for a + // taskFunction override. + const requiresDependencies = descriptor ? !!descriptor.requiresDependencies : !!taskDef.requiresDependencies; + // The single-stage body is the descriptor's task, or the definition's taskFunction override. + const taskBody = descriptor ? descriptor.task : taskFunction; + let task; - if (taskFunction === null) { + if (skip) { this._log.verbose(`Task ${taskName} is set to be explicitly skipped in definitions.`); task = null; } else { task = async (log) => { - if (!taskFunction) { - const {task} = await this._taskRepository.getTask(taskName); - taskFunction = task; - } - if (stepBased) { - // Step-based task: the default export is a factory build(options) => Step[]. runTasks - // already called the factory at discovery and kept the returned step array on the task, so - // reuse it here instead of calling the factory a second time. The factory is pure over - // options, so one call per build is the single source of truth for both the stage list and - // execution. Fall back to a direct call for a task invoked outside runTasks, where no - // discovery ran. Each step is its own pipeline stage; the step runner drives one stage per - // step via the per-stage hooks below. Every input a step reads arrives through its - // arguments, so no task body closes over the readers or taskUtil. - const steps = this._tasks[taskName].steps ?? await taskFunction(options); + // Step-based task: the descriptor carried a steps array. runTasks already froze it on the + // task at discovery; reuse it here. Each step is its own pipeline stage; the step runner + // drives one stage per step via the per-stage hooks below. Every input a step reads arrives + // through its arguments, so no task body closes over the readers or taskUtil. + const stepList = this._tasks[taskName].steps ?? steps; this._taskStart = performance.now(); const taskReport = this.#createTaskExecutionReport(taskName); const stepDriver = new StepRunner({ - steps, + steps: stepList, taskName, options, ...this.#createStepStageHooks(taskName, requiresDependencies), @@ -375,7 +375,7 @@ class TaskRunner { return; } - // Legacy task: the default export is a task body, not a step factory. It has a single stage. + // Legacy task: the descriptor's task body (or a definition taskFunction override). Single stage. const cacheInfo = await this._buildCache.prepareStageExecutionAndValidateCache(taskName); if (cacheInfo === true) { this._log.skipTask(taskName); @@ -398,7 +398,7 @@ class TaskRunner { } this._log.startTask(taskName, !!cacheInfo); this._taskStart = performance.now(); - await taskFunction(params); + await taskBody(params); if (this._log.isLevelEnabled("perf")) { this._log.perf( `Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`); @@ -423,14 +423,7 @@ class TaskRunner { this._tasks[taskName] = { task, stepBased, - // Lazily resolves the task factory so runTasks can enumerate a step-based task's step names for - // setTasks (called only for tasks that actually run). A legacy task keeps this undefined. - stepFactory: stepBased ? (async () => { - if (!taskFunction) { - taskFunction = (await this._taskRepository.getTask(taskName)).task; - } - return taskFunction; - }) : undefined, + descriptor, options, requiredDependencies: requiresDependencies ? this._directDependencies : new Set() }; @@ -462,11 +455,22 @@ class TaskRunner { const project = this._project; const taskUtil = this._taskUtil; - // Tasks can provide an optional callback to tell build process which dependencies they require - const requiredDependenciesCallback = await task.getRequiredDependenciesCallback(); - // const buildSignatureCallback = await task.getBuildSignatureCallback(); - // const expectedOutputCallback = await task.getExpectedOutputCallback(); const specVersion = task.getSpecVersion(); + // From Specification Version 5.0 the default export is a factory returning a descriptor. Below 5.0 it + // is the legacy task body invoked directly (any value it returns is ignored), so there is no descriptor + // and none of its callbacks apply. The factory options match those used for the build signature in + // TaskDefinitions, so Task#getDescriptor memoizes one descriptor shared by both phases. + const isV5 = specVersion.gte("5.0"); + const descriptorOptions = { + projectName: project.getName(), + projectNamespace: project.getNamespace(), + configuration: taskDef.configuration, + taskName + }; + const descriptor = isV5 ? await task.getDescriptor(taskName, descriptorOptions) : undefined; + + // Tasks can provide an optional callback to tell build process which dependencies they require + const requiredDependenciesCallback = descriptor?.determineRequiredDependencies; let requiredDependencies; // Always provide a dependencies-reader, even if empty. Unless the task is specVersion >=3.0 @@ -528,18 +532,9 @@ class TaskRunner { } }); } - // A custom task opts into the step-factory API with a static `stepBased` export, honored from - // Specification Version 5.0. Below 5.0 the export is ignored and the task runs as a legacy body. - const stepBased = specVersion.gte("5.0") && (await task.getStepBased()) === true; - // Options the factory is called with at step-name discovery (runTasks). The factory is pure over - // options, so discovering step names by calling it early is safe, and the step array it returns is - // kept on the task and reused for execution rather than calling the factory again. - const stepOptions = stepBased ? { - projectName: project.getName(), - projectNamespace: project.getNamespace(), - configuration: taskDef.configuration, - ...(specVersion.gte("3.0") ? {taskName} : {}), - } : undefined; + // A custom task is step-based when its descriptor carries a steps array (Specification Version 5.0 + // and later). Below 5.0 there is no descriptor and the task runs as a legacy body. + const stepBased = !!descriptor?.steps; this._tasks[taskName] = { task: this._createCustomTaskWrapper({ task, @@ -548,16 +543,16 @@ class TaskRunner { taskName, taskConfiguration: taskDef.configuration, provideDependenciesReader, + descriptor, stepBased, - stepOptions, getDependenciesReaderCb: () => { // Create the dependencies reader on-demand return this.getDependenciesReader(requiredDependencies); }, }), stepBased, - stepFactory: stepBased ? (async () => task.getTask()) : undefined, - options: stepOptions, + descriptor, + options: descriptorOptions, requiredDependencies }; @@ -604,18 +599,18 @@ class TaskRunner { * Callback to get dependencies reader on-demand * @param {boolean} parameters.provideDependenciesReader * Whether to provide dependencies reader to the task + * @param {object} [parameters.descriptor] + * The task's resolved descriptor (Specification Version 5.0 and later). Absent below 5.0, where the + * default export is the legacy task body * @param {boolean} parameters.stepBased - * Whether the task's default export is a step factory (honored from Specification Version 5.0) - * @param {object} [parameters.stepOptions] - * The options object a step factory is called with at discovery in {@link #runTasks}. Kept so the - * fallback path (a task invoked outside runTasks) calls the factory with the same options + * Whether the descriptor carries a steps array (Specification Version 5.0 and later) * @param {@ui5/project/specifications/Extension} parameters.task Task extension instance * @param {string} parameters.taskName Runtime name of the task (may include suffix) * @param {object} [parameters.taskConfiguration] Task configuration from ui5.yaml * @returns {Function} Async wrapper function for the custom task */ _createCustomTaskWrapper({ - project, taskUtil, getDependenciesReaderCb, provideDependenciesReader, stepBased, stepOptions, + project, taskUtil, getDependenciesReaderCb, provideDependenciesReader, descriptor, stepBased, task, taskName, taskConfiguration }) { return async () => { @@ -643,7 +638,6 @@ class TaskRunner { */ const specVersion = task.getSpecVersion(); const taskUtilInterface = taskUtil.getInterface(specVersion); - const taskFunction = await task.getTask(); const isSpec3 = specVersion.gte("3.0"); const options = { @@ -656,19 +650,16 @@ class TaskRunner { } if (stepBased) { - // Step-based custom task: gated at Specification Version 5.0 in _addCustomTask, which always - // provides a taskUtil interface. The default export is a factory build(options) => Step[]; - // each step is its own pipeline stage, driven by the step runner via per-stage hooks. The - // factory receives options only. runTasks already called it at discovery and kept the step - // array on the task, so reuse it instead of calling the factory a second time (see the - // standard-task path). Fall back to a direct call for a task invoked outside runTasks. - const factoryOptions = stepOptions ?? options; - const steps = this._tasks[taskName].steps ?? await taskFunction(factoryOptions); + // Step-based custom task (Specification Version 5.0 and later): the descriptor carried a steps + // array. Each step is its own pipeline stage, driven by the step runner via per-stage hooks. + // runTasks already froze the step array on the task at discovery; reuse it, falling back to the + // descriptor's own array for a task invoked outside runTasks. + const steps = this._tasks[taskName].steps ?? descriptor.steps; const taskReport = this.#createTaskExecutionReport(taskName); const stepDriver = new StepRunner({ steps, taskName, - options: factoryOptions, + options, ...this.#createStepStageHooks(taskName, provideDependenciesReader, taskUtilInterface), returnValueStore: this._buildCache.getStepReturnValueStore(), resolveInputValue: this._buildCache.getResolveInputValue(), @@ -688,7 +679,10 @@ class TaskRunner { return; } - // Legacy custom task: the default export is a task body, not a step factory. It has a single stage. + // Legacy custom task: a single-stage body. From Specification Version 5.0 the body is the + // descriptor's task; below 5.0 the default export is the body itself (invoked directly), so any + // value it returns is ignored. + const taskFunction = descriptor ? descriptor.task : await task.getTask(); const cacheInfo = await this._buildCache.prepareStageExecutionAndValidateCache(taskName); if (cacheInfo === true) { this._log.skipTask(taskName); diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index 4740bdba952..4333bd3f1d2 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -11,7 +11,7 @@ const log = getLogger("build:cache:CacheManager"); const cacheManagerInstances = new Map(); // Cache version for compatibility management -export const CACHE_VERSION = "v0_9"; +export const CACHE_VERSION = "v0_10"; /** * Manages persistence for the build cache using a unified SQLite-backed storage diff --git a/packages/project/lib/build/definitions/application.js b/packages/project/lib/build/definitions/application.js index 0fdd3359683..b9599871c6f 100644 --- a/packages/project/lib/build/definitions/application.js +++ b/packages/project/lib/build/definitions/application.js @@ -13,7 +13,6 @@ import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; export default function({project, taskUtil, getTask}) { const tasks = new Map(); tasks.set("escapeNonAsciiCharacters", { - stepBased: true, options: { encoding: project.getPropertiesFileSourceEncoding(), pattern: "/**/*.properties" @@ -21,7 +20,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { - stepBased: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,json}" @@ -29,7 +27,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { - stepBased: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json}" @@ -45,13 +42,12 @@ export default function({project, taskUtil, getTask}) { } } tasks.set("minify", { - stepBased: true, options: { pattern: minificationPattern } }); - tasks.set("enhanceManifest", {stepBased: true}); + tasks.set("enhanceManifest", {}); tasks.set("generateFlexChangesBundle", {}); @@ -82,21 +78,26 @@ export default function({project, taskUtil, getTask}) { }); } - tasks.set("generateStandaloneAppBundle", {requiresDependencies: true}); + tasks.set("generateStandaloneAppBundle", {}); tasks.set("transformBootstrapHtml", {}); if (bundles.length) { + // The generateBundle task module resolves one bundle per call. Resolving the per-bundle defaults + // needs enhanceBundlesWithDefaults, which lives in @ui5/project and cannot move into @ui5/builder, so + // the multi-bundle wrapper stays here. It reads the task module's descriptor via getTask and drives + // descriptor.task once per bundle. tasks.set("generateBundle", { requiresDependencies: true, taskFunction: async ({workspace, dependencies, taskUtil, options}) => { - const generateBundleTask = await getTask("generateBundle"); + const {task} = await getTask("generateBundle"); + const {task: generateBundle} = task(options); // Async resolve default values for bundle definitions and options const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); return bundlesDefaults.reduce(async function(sequence, bundle) { return sequence.then(function() { - return generateBundleTask.task({ + return generateBundle({ workspace, dependencies, taskUtil, @@ -113,12 +114,11 @@ export default function({project, taskUtil, getTask}) { } else { // No bundles defined. Just set task so that it can be referenced by custom tasks tasks.set("generateBundle", { - taskFunction: null + skip: true }); } tasks.set("generateVersionInfo", { - requiresDependencies: true, options: { rootProject: project, pattern: "/resources/**/.library" @@ -131,8 +131,8 @@ export default function({project, taskUtil, getTask}) { } }); - tasks.set("generateApiIndex", {requiresDependencies: true}); - tasks.set("generateResourcesJson", {requiresDependencies: true}); + tasks.set("generateApiIndex", {}); + tasks.set("generateResourcesJson", {}); return tasks; } diff --git a/packages/project/lib/build/definitions/component.js b/packages/project/lib/build/definitions/component.js index 2fbb396bb89..75833f0e490 100644 --- a/packages/project/lib/build/definitions/component.js +++ b/packages/project/lib/build/definitions/component.js @@ -13,7 +13,6 @@ import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; export default function({project, taskUtil, getTask}) { const tasks = new Map(); tasks.set("escapeNonAsciiCharacters", { - stepBased: true, options: { encoding: project.getPropertiesFileSourceEncoding(), pattern: "/**/*.properties" @@ -21,7 +20,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { - stepBased: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,json}" @@ -29,7 +27,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { - stepBased: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json}" @@ -44,13 +41,12 @@ export default function({project, taskUtil, getTask}) { } tasks.set("minify", { - stepBased: true, options: { pattern: minificationPattern } }); - tasks.set("enhanceManifest", {stepBased: true}); + tasks.set("enhanceManifest", {}); tasks.set("generateFlexChangesBundle", {}); @@ -82,16 +78,21 @@ export default function({project, taskUtil, getTask}) { } if (bundles.length) { + // The generateBundle task module resolves one bundle per call. Resolving the per-bundle defaults + // needs enhanceBundlesWithDefaults, which lives in @ui5/project and cannot move into @ui5/builder, so + // the multi-bundle wrapper stays here. It reads the task module's descriptor via getTask and drives + // descriptor.task once per bundle. tasks.set("generateBundle", { requiresDependencies: true, taskFunction: async ({workspace, dependencies, taskUtil, options}) => { - const generateBundleTask = await getTask("generateBundle"); + const {task} = await getTask("generateBundle"); + const {task: generateBundle} = task(options); // Async resolve default values for bundle definitions and options const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); return bundlesDefaults.reduce(async function(sequence, bundle) { return sequence.then(function() { - return generateBundleTask.task({ + return generateBundle({ workspace, dependencies, taskUtil, @@ -108,7 +109,7 @@ export default function({project, taskUtil, getTask}) { } else { // No bundles defined. Just set task so that it can be referenced by custom tasks tasks.set("generateBundle", { - taskFunction: null + skip: true }); } @@ -118,7 +119,7 @@ export default function({project, taskUtil, getTask}) { } }); - tasks.set("generateResourcesJson", {requiresDependencies: true}); + tasks.set("generateResourcesJson", {}); return tasks; } diff --git a/packages/project/lib/build/definitions/library.js b/packages/project/lib/build/definitions/library.js index ec457d11517..46faaac20e5 100644 --- a/packages/project/lib/build/definitions/library.js +++ b/packages/project/lib/build/definitions/library.js @@ -14,7 +14,6 @@ import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; export default function({project, taskUtil, getTask}) { const tasks = new Map(); tasks.set("escapeNonAsciiCharacters", { - stepBased: true, options: { encoding: project.getPropertiesFileSourceEncoding(), pattern: "/**/*.properties" @@ -22,7 +21,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceCopyright", { - stepBased: true, options: { copyright: project.getCopyright(), pattern: "/**/*.{js,library,css,less,theme,html}" @@ -30,7 +28,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { - stepBased: true, options: { version: project.getVersion(), pattern: "/**/*.{js,json,library,css,less,theme,html}" @@ -38,43 +35,22 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceBuildtime", { - stepBased: true, options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" } }); + // The task's requiresDependencies flag and its determineBuildSignature callback live on the task + // module's descriptor now; the definition only supplies the project-derived options the factory needs. tasks.set("generateJsdoc", { - requiresDependencies: true, - determineBuildSignature: async () => { - return project.getVersion(); - }, - taskFunction: async ({workspace, dependencies, taskUtil, options}) => { - const patterns = ["/resources/**/*.js"]; - // Add excludes - const excludes = project.getJsdocExcludes(); - if (excludes.length) { - patterns.push(...excludes.map((pattern) => { - return `!/resources/${pattern}`; - })); - } - const generateJsdocTask = await getTask("generateJsdoc"); - return generateJsdocTask.task({ - workspace, - dependencies, - taskUtil, - options: { - projectName: options.projectName, - namespace: project.getNamespace(), - version: project.getVersion(), - pattern: patterns - } - }); + options: { + namespace: project.getNamespace(), + version: project.getVersion(), + jsdocExcludes: project.getJsdocExcludes() } }); tasks.set("executeJsdocSdkTransformation", { - requiresDependencies: true, options: { dotLibraryPattern: "/resources/**/*.library", } @@ -90,7 +66,6 @@ export default function({project, taskUtil, getTask}) { } tasks.set("minify", { - stepBased: true, options: { pattern: minificationPattern } @@ -100,10 +75,10 @@ export default function({project, taskUtil, getTask}) { if (project.getSpecVersion().lt("5.0") || project.isFrameworkProject()) { tasks.set("generateLibraryManifest", {}); } else { - tasks.set("generateLibraryManifest", {taskFunction: null}); + tasks.set("generateLibraryManifest", {skip: true}); } - tasks.set("enhanceManifest", {stepBased: true}); + tasks.set("enhanceManifest", {}); const bundles = project.getBundles(); const existingBundleDefinitionNames = @@ -121,7 +96,7 @@ export default function({project, taskUtil, getTask}) { } }); } else { - tasks.set("generateComponentPreload", {taskFunction: null}); + tasks.set("generateComponentPreload", {skip: true}); } tasks.set("generateLibraryPreload", { @@ -132,16 +107,21 @@ export default function({project, taskUtil, getTask}) { }); if (bundles.length) { + // The generateBundle task module resolves one bundle per call. Resolving the per-bundle defaults + // needs enhanceBundlesWithDefaults, which lives in @ui5/project and cannot move into @ui5/builder, so + // the multi-bundle wrapper stays here. It reads the task module's descriptor via getTask and drives + // descriptor.task once per bundle. tasks.set("generateBundle", { requiresDependencies: true, taskFunction: async ({workspace, dependencies, taskUtil, options}) => { - const generateBundleTask = await getTask("generateBundle"); + const {task} = await getTask("generateBundle"); + const {task: generateBundle} = task(options); // Async resolve default values for bundle definitions and options const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); return bundlesDefaults.reduce(function(sequence, bundle) { return sequence.then(function() { - return generateBundleTask.task({ + return generateBundle({ workspace, dependencies, taskUtil, @@ -156,12 +136,10 @@ export default function({project, taskUtil, getTask}) { } }); } else { - tasks.set("generateBundle", {taskFunction: null}); + tasks.set("generateBundle", {skip: true}); } tasks.set("buildThemes", { - stepBased: true, - requiresDependencies: true, options: { projectName: project.getName(), librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, @@ -172,19 +150,15 @@ export default function({project, taskUtil, getTask}) { if (project.isFrameworkProject()) { tasks.set("generateThemeDesignerResources", { - stepBased: true, - requiresDependencies: true, options: { version: project.getVersion() } }); } else { - tasks.set("generateThemeDesignerResources", {taskFunction: null}); + tasks.set("generateThemeDesignerResources", {skip: true}); } - tasks.set("generateResourcesJson", { - requiresDependencies: true - }); + tasks.set("generateResourcesJson", {}); return tasks; } diff --git a/packages/project/lib/build/definitions/themeLibrary.js b/packages/project/lib/build/definitions/themeLibrary.js index 76b6eae08ff..79cb73f712f 100644 --- a/packages/project/lib/build/definitions/themeLibrary.js +++ b/packages/project/lib/build/definitions/themeLibrary.js @@ -11,7 +11,6 @@ export default function({project, taskUtil, getTask}) { const tasks = new Map(); tasks.set("replaceCopyright", { - stepBased: true, options: { copyright: project.getCopyright(), pattern: "/resources/**/*.{less,theme}" @@ -19,7 +18,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("replaceVersion", { - stepBased: true, options: { version: project.getVersion(), pattern: "/resources/**/*.{less,theme}" @@ -27,8 +25,6 @@ export default function({project, taskUtil, getTask}) { }); tasks.set("buildThemes", { - stepBased: true, - requiresDependencies: true, options: { projectName: project.getName(), librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, @@ -39,16 +35,14 @@ export default function({project, taskUtil, getTask}) { if (project.isFrameworkProject()) { tasks.set("generateThemeDesignerResources", { - stepBased: true, - requiresDependencies: true, options: { version: project.getVersion() } }); } else { - tasks.set("generateThemeDesignerResources", {taskFunction: null}); + tasks.set("generateThemeDesignerResources", {skip: true}); } - tasks.set("generateResourcesJson", {requiresDependencies: true}); + tasks.set("generateResourcesJson", {}); return tasks; } diff --git a/packages/project/lib/build/helpers/StepRunner.js b/packages/project/lib/build/helpers/StepRunner.js index d5149bdd831..d0292feaec3 100644 --- a/packages/project/lib/build/helpers/StepRunner.js +++ b/packages/project/lib/build/helpers/StepRunner.js @@ -118,6 +118,58 @@ export function validateSteps(steps, {taskName} = {}) { } } +/** + * Validates a task descriptor: the object a task factory returns from its default export, from + * Specification Version 5.0 (and for every built-in task). A descriptor carries the whole task + * contract in one object instead of the former scattered exports and flags: exactly one of + * steps (the step-based path) or task (a legacy single-stage body), plus the + * optional determineRequiredDependencies / determineBuildSignature callbacks + * and an optional declared specVersion. + * + * The rules are static over the descriptor; the factory that produced it must stay pure and cheap (it is + * called once per build, including early for the build signature), so it must not read or write resources + * or import heavy processors. The step-equality check against the task extension's configured + * Specification Version is NOT done here (it needs a + * [SpecificationVersion]{@link @ui5/project/specifications/SpecificationVersion} instance only the caller + * holds); this helper only validates that a declared specVersion is a string. + * + * @param {*} descriptor The value the task factory returned + * @param {object} [parameters] + * @param {string} [parameters.taskName] The task name, named in the error messages + * @throws {Error} If the descriptor is invalid + */ +export function validateDescriptor(descriptor, {taskName} = {}) { + const where = taskName ? ` for task '${taskName}'` : ""; + if (!descriptor || typeof descriptor !== "object") { + throw new Error( + `Task factory${where} must return a descriptor object, got ${describeValue(descriptor)}`); + } + const hasSteps = descriptor.steps !== undefined; + const hasTask = descriptor.task !== undefined; + if (hasSteps === hasTask) { + throw new Error( + `Task descriptor${where} must define exactly one of 'steps' or 'task'`); + } + if (hasSteps) { + validateSteps(descriptor.steps, {taskName}); + } + if (hasTask && typeof descriptor.task !== "function") { + throw new Error( + `Task descriptor${where} 'task' must be a function, got ${describeValue(descriptor.task)}`); + } + for (const cb of ["determineRequiredDependencies", "determineBuildSignature", "determineExpectedOutput"]) { + if (descriptor[cb] !== undefined && typeof descriptor[cb] !== "function") { + throw new Error( + `Task descriptor${where} '${cb}' must be a function, got ${describeValue(descriptor[cb])}`); + } + } + if (descriptor.specVersion !== undefined && typeof descriptor.specVersion !== "string") { + throw new Error( + `Task descriptor${where} 'specVersion' must be a string, ` + + `got ${describeValue(descriptor.specVersion)}`); + } +} + /** * Collects the reads and writes of a single invocation (a scalar step's one invocation, or one key of a map * step). Project reads (workspace) and dependency reads are kept apart so they can be folded back into diff --git a/packages/project/lib/build/helpers/getBuildSignature.js b/packages/project/lib/build/helpers/getBuildSignature.js index 76d93d37691..1283aae2f43 100644 --- a/packages/project/lib/build/helpers/getBuildSignature.js +++ b/packages/project/lib/build/helpers/getBuildSignature.js @@ -3,7 +3,7 @@ import {getPackageVersion} from "./createBuildManifest.js"; // Increment signature version to invalidate all existing build signatures // (e.g. after making a change that impacts build contents) -const BUILD_SIG_VERSION = "0"; +const BUILD_SIG_VERSION = "1"; export function getBaseSignature(buildConfig) { const key = BUILD_SIG_VERSION + JSON.stringify(buildConfig); diff --git a/packages/project/lib/specifications/extensions/Task.js b/packages/project/lib/specifications/extensions/Task.js index ac9ce69f00a..ae18266342a 100644 --- a/packages/project/lib/specifications/extensions/Task.js +++ b/packages/project/lib/specifications/extensions/Task.js @@ -1,6 +1,7 @@ import path from "node:path"; import Extension from "../Extension.js"; import {pathToFileURL} from "node:url"; +import {validateDescriptor} from "../../build/helpers/StepRunner.js"; /** * Task @@ -12,47 +13,126 @@ import {pathToFileURL} from "node:url"; * @hideconstructor */ class Task extends Extension { + #descriptorPromises = new Map(); + constructor(parameters) { super(parameters); } /* === Attributes === */ /** - * @public - */ + * The task module's default export. + * + * From Specification Version 5.0 this is the task factory (options) => descriptor; use + * {@link #getDescriptor} to obtain the validated descriptor. Below 5.0 it is the legacy task body that + * is invoked directly during the build (any value it returns is ignored). + * + * @public + */ async getTask() { return (await this._getImplementation()).default; } /** - * @public - */ - async getRequiredDependenciesCallback() { + * Resolves and validates the task descriptor by calling the task factory (the default export) once with + * the given options. The result is memoized per runtime taskName, so a factory that branches + * on options (for example on projectNamespace) is called exactly once per build + * and the same descriptor is shared by the build-signature phase and task execution. Keyed by + * taskName rather than once per instance because one task extension can back more than one + * runtime task (a repeated configuration gets a --<number> suffix) with different options. + * + * Only valid from Specification Version 5.0. If the descriptor declares a specVersion, it must + * be equal to this task extension's configured Specification Version (the task extension's own + * ui5.yaml value, which is unrelated to the Specification Version of the project being built). + * + * @public + * @param {string} taskName Runtime task name (memoization key) + * @param {object} options Options passed to the factory + * @returns {Promise} The validated descriptor + */ + async getDescriptor(taskName, options) { + let descriptorPromise = this.#descriptorPromises.get(taskName); + if (!descriptorPromise) { + descriptorPromise = (async () => { + const factory = (await this._getImplementation()).default; + const descriptor = await factory(options); + validateDescriptor(descriptor, {taskName}); + if (descriptor.specVersion !== undefined && + !this.getSpecVersion().eq(descriptor.specVersion)) { + throw new Error( + `Custom task ${this.getName()} declares Specification Version ` + + `${descriptor.specVersion} in its default export, which does not match the task ` + + `extension's configured Specification Version ${this.getSpecVersion().toString()}`); + } + return descriptor; + })(); + this.#descriptorPromises.set(taskName, descriptorPromise); + } + return descriptorPromise; + } + + /** + * The task's determineRequiredDependencies callback. From Specification Version 5.0 it is a + * property of the descriptor the factory returns; below 5.0 it is a top-level named export of the task + * module (legacy). Returns undefined if the task does not define it. + * + * @public + * @param {string} [taskName] Runtime task name (required at Specification Version 5.0) + * @param {object} [options] Options passed to the factory (required at Specification Version 5.0) + */ + async getRequiredDependenciesCallback(taskName, options) { + if (this.getSpecVersion().gte("5.0")) { + return (await this.getDescriptor(taskName, options)).determineRequiredDependencies; + } return (await this._getImplementation()).determineRequiredDependencies; } /** - * @public - */ - async getDetermineBuildSignatureCallback() { - return (await this._getImplementation()).determineBuildSignature; + * The task's determineBuildSignature callback. From Specification Version 5.0 it is a + * property of the descriptor the factory returns; below 5.0 the callback is not supported and this + * returns undefined. + * + * @public + * @param {string} [taskName] Runtime task name (required at Specification Version 5.0) + * @param {object} [options] Options passed to the factory (required at Specification Version 5.0) + */ + async getDetermineBuildSignatureCallback(taskName, options) { + if (this.getSpecVersion().gte("5.0")) { + return (await this.getDescriptor(taskName, options)).determineBuildSignature; + } + return undefined; } /** - * Whether the task opts into the step-factory build API, declared as a static stepBased - * export (a plain boolean value, not a callback). Honored from Specification Version 5.0. + * Whether the task opts into the step-based build API, derived from the descriptor carrying a + * steps array (Specification Version 5.0 and later). Always false below 5.0, + * where the default export is the legacy task body. * * @public - * @returns {Promise} The task module's stepBased export + * @param {string} [taskName] Runtime task name (required at Specification Version 5.0) + * @param {object} [options] Options passed to the factory (required at Specification Version 5.0) + * @returns {Promise} true if the descriptor defines steps */ - async getStepBased() { - return (await this._getImplementation()).stepBased; + async getStepBased(taskName, options) { + if (this.getSpecVersion().gte("5.0")) { + return !!(await this.getDescriptor(taskName, options)).steps; + } + return false; } /** - * @public - */ - async getExpectedOutputCallback() { + * The task's determineExpectedOutput callback. From Specification Version 5.0 it is a + * property of the descriptor; below 5.0 it is a top-level named export. Currently parsed but not + * consumed anywhere. + * + * @public + * @param {string} [taskName] Runtime task name (required at Specification Version 5.0) + * @param {object} [options] Options passed to the factory (required at Specification Version 5.0) + */ + async getExpectedOutputCallback(taskName, options) { + if (this.getSpecVersion().gte("5.0")) { + return (await this.getDescriptor(taskName, options)).determineExpectedOutput; + } return (await this._getImplementation()).determineExpectedOutput; } diff --git a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js index 91ef3b68232..5a9e8b5be58 100644 --- a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js +++ b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-reader.js @@ -2,36 +2,40 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:depTagReader"); let buildRanOnce; -module.exports = async function ({taskUtil, dependencies}) { - log.verbose("dep-tag-reader executed"); +// Specification Version 5.0 descriptor. +module.exports = function build() { + return { + determineRequiredDependencies: ({availableDependencies}) => { + return availableDependencies; + }, + task: async ({taskUtil, dependencies}) => { + log.verbose("dep-tag-reader executed"); - // const libraryDProject = taskUtil.getProject("library.d"); - const resources = await dependencies.byGlob("**/some.js"); - if (resources.length === 0) { - throw new Error("dep-tag-reader: some.js not found in library.d"); - } - const someJs = resources[0]; + // const libraryDProject = taskUtil.getProject("library.d"); + const resources = await dependencies.byGlob("**/some.js"); + if (resources.length === 0) { + throw new Error("dep-tag-reader: some.js not found in library.d"); + } + const someJs = resources[0]; - if (buildRanOnce !== true) { - log.verbose("First build: Verifying ui5:IsDebugVariant is set on some.js"); - buildRanOnce = true; - const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); - if (!tag) { - throw new Error( - "dep-tag-reader: Expected ui5:IsDebugVariant tag to be set on some.js in first build" - ); + if (buildRanOnce !== true) { + log.verbose("First build: Verifying ui5:IsDebugVariant is set on some.js"); + buildRanOnce = true; + const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (!tag) { + throw new Error( + "dep-tag-reader: Expected ui5:IsDebugVariant tag to be set on some.js in first build" + ); + } + } else { + log.verbose("Subsequent build: Verifying ui5:HasDebugVariant is set on some.js"); + const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); + if (!tag) { + throw new Error( + "dep-tag-reader: Expected ui5:HasDebugVariant tag to be set on some.js in subsequent build" + ); + } + } } - } else { - log.verbose("Subsequent build: Verifying ui5:HasDebugVariant is set on some.js"); - const tag = taskUtil.getTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); - if (!tag) { - throw new Error( - "dep-tag-reader: Expected ui5:HasDebugVariant tag to be set on some.js in subsequent build" - ); - } - } + }; }; - -module.exports.determineRequiredDependencies = function ({availableDependencies}) { - return availableDependencies; -} diff --git a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js index 8d5b986dbd4..e7f4552b0db 100644 --- a/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js +++ b/packages/project/test/fixtures/application.a/cross-project-tag-tasks/dep-tag-setter.js @@ -2,21 +2,26 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:depTagSetter"); let buildRanOnce; -module.exports = async function ({workspace, taskUtil}) { - log.verbose("dep-tag-setter executed"); +// Specification Version 5.0 descriptor. +module.exports = function build() { + return { + task: async ({workspace, taskUtil}) => { + log.verbose("dep-tag-setter executed"); - const resources = await workspace.byGlob("**/some.js"); - if (resources.length === 0) { - throw new Error("dep-tag-setter: some.js not found in workspace"); - } - const someJs = resources[0]; + const resources = await workspace.byGlob("**/some.js"); + if (resources.length === 0) { + throw new Error("dep-tag-setter: some.js not found in workspace"); + } + const someJs = resources[0]; - if (buildRanOnce !== true) { - log.verbose("First build: Setting ui5:IsDebugVariant on some.js"); - buildRanOnce = true; - taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); - } else { - log.verbose("Subsequent build: Setting ui5:HasDebugVariant on some.js"); - taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); - } + if (buildRanOnce !== true) { + log.verbose("First build: Setting ui5:IsDebugVariant on some.js"); + buildRanOnce = true; + taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.IsDebugVariant); + } else { + log.verbose("Subsequent build: Setting ui5:HasDebugVariant on some.js"); + taskUtil.setTag(someJs, taskUtil.STANDARD_TAGS.HasDebugVariant); + } + } + }; }; diff --git a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js index 78495298e3a..e8e13e68661 100644 --- a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js +++ b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-0.js @@ -2,21 +2,23 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:customTask0"); let buildRanOnce; -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Custom task 0 executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Custom task 0 executed"); - // Read a file to trigger execution of this task: - const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + // Read a file to trigger execution of this task: + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - if (buildRanOnce != true) { - log.verbose("Flag NOT set -> We are in #1 Build still"); - buildRanOnce = true; - taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); - } else { - log.verbose("Flag set -> We are in #2 Build"); - taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - } + if (buildRanOnce != true) { + log.verbose("Flag NOT set -> We are in #1 Build still"); + buildRanOnce = true; + taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + } else { + log.verbose("Flag set -> We are in #2 Build"); + taskUtil.setTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + } + }; }; diff --git a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js index 98f8b94c848..3c294808b1b 100644 --- a/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js +++ b/packages/project/test/fixtures/application.a/custom-tasks-2/custom-task-1.js @@ -2,31 +2,33 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:customTask1"); let buildRanOnce; -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Custom task 1 executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Custom task 1 executed"); - // Read a file to trigger execution of this task: - const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + // Read a file to trigger execution of this task: + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - if (buildRanOnce != true) { - log.verbose("Flag NOT set -> We are in #1 Build still"); - buildRanOnce = true; - const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); - if (!tag) { - throw new Error("Tag set during #1 Build is not readable, which is UNEXPECTED."); + if (buildRanOnce != true) { + log.verbose("Flag NOT set -> We are in #1 Build still"); + buildRanOnce = true; + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (!tag) { + throw new Error("Tag set during #1 Build is not readable, which is UNEXPECTED."); + } + } else { + const previousTag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (previousTag) { + throw new Error("Tag set during #1 Build is still readable, which is UNEXPECTED."); + } + log.verbose("Flag set -> We are in #2 Build"); + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + if (!tag) { + throw new Error("Tag set during #2 Build is not readable, which is UNEXPECTED."); + } + } } - } else { - const previousTag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); - if (previousTag) { - throw new Error("Tag set during #1 Build is still readable, which is UNEXPECTED."); - } - log.verbose("Flag set -> We are in #2 Build"); - const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - if (!tag) { - throw new Error("Tag set during #2 Build is not readable, which is UNEXPECTED."); - } - } + }; }; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js index c19f110113c..376b6b602df 100644 --- a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js @@ -1,22 +1,24 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:customTask0"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Custom task 0 executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Custom task 0 executed"); - // Read a file which is an input of custom-task-1 (which sets a tag on it): - const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + // Read a file which is an input of custom-task-1 (which sets a tag on it): + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); - // For #1 & #3 build: - if (tag) { - throw new Error("Tag set by custom-task-1 is present in custom-task-0, which is UNEXPECTED."); - } + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + // For #1 & #3 build: + if (tag) { + throw new Error("Tag set by custom-task-1 is present in custom-task-0, which is UNEXPECTED."); + } - // For #3 build: Read a different file which is not an input of custom-task-1 - // (ensures that this task is executed): - const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); + // For #3 build: Read a different file which is not an input of custom-task-1 + // (ensures that this task is executed): + const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js index 58030f3ea06..388d9f91a38 100644 --- a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-1.js @@ -1,15 +1,17 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:customTask1"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Custom task 1 executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Custom task 1 executed"); - // Set a tag on a specific resource: - const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - if (resource) { - taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + // Set a tag on a specific resource: + const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + if (resource) { + taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + }; + } }; }; diff --git a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js index de5a39068a2..82475603c18 100644 --- a/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js +++ b/packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js @@ -1,22 +1,24 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:customTask2"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Custom task 2 executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Custom task 2 executed"); - // Read a file which is an input of custom-task-1 (which sets a tag on it): - const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + // Read a file which is an input of custom-task-1 (which sets a tag on it): + const testJS = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); - // For #1 & #3 build: - if (!tag) { - throw new Error("Tag set by custom-task-1 is NOT present in custom-task-2, which is UNEXPECTED."); - } + const tag = taskUtil.getTag(testJS, taskUtil.STANDARD_TAGS.IsDebugVariant); + // For #1 & #3 build: + if (!tag) { + throw new Error("Tag set by custom-task-1 is NOT present in custom-task-2, which is UNEXPECTED."); + } - // For #3 build: Read a different file which is not an input of custom-task-1 - // (ensures that this task is executed): - const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); + // For #3 build: Read a different file which is not an input of custom-task-1 + // (ensures that this task is executed): + const test2JS = await workspace.byPath(`/resources/${projectNamespace}/test2.js`); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/dependency-race-condition-task.js b/packages/project/test/fixtures/application.a/dependency-race-condition-task.js index 9fac2ae248d..58cfd537786 100644 --- a/packages/project/test/fixtures/application.a/dependency-race-condition-task.js +++ b/packages/project/test/fixtures/application.a/dependency-race-condition-task.js @@ -16,46 +16,52 @@ const path = require("path"); * 3. Re-read via the dependency reader * 4. Assert the content is still the original (CAS-served) * 5. Restore the original file on disk + * + * Specification Version 5.0 descriptor: the default export is a factory that returns the task body. */ -module.exports = async function ({taskUtil}) { - const libDProject = taskUtil.getProject("library.d"); - const libDReader = libDProject.getReader(); +module.exports = function build() { + return { + task: async ({taskUtil}) => { + const libDProject = taskUtil.getProject("library.d"); + const libDReader = libDProject.getReader(); - // Step 1: Read the original content via the dependency reader (CAS-backed) - // data.json is untransformed (no placeholders, not a .js file subject to minification) - // so it is only served by the frozen CAS reader or the filesystem reader - const resourcePath = "/resources/library/d/data.json"; - const originalResource = await libDReader.byPath(resourcePath); - if (!originalResource) { - throw new Error(`Resource ${resourcePath} not found via dependency reader`); - } - const originalContent = await originalResource.getString(); + // Step 1: Read the original content via the dependency reader (CAS-backed) + // data.json is untransformed (no placeholders, not a .js file subject to minification) + // so it is only served by the frozen CAS reader or the filesystem reader + const resourcePath = "/resources/library/d/data.json"; + const originalResource = await libDReader.byPath(resourcePath); + if (!originalResource) { + throw new Error(`Resource ${resourcePath} not found via dependency reader`); + } + const originalContent = await originalResource.getString(); - // Step 2: Overwrite the file on disk - const sourcePath = libDProject.getSourcePath(); - const diskFilePath = path.join(sourcePath, "library", "d", "data.json"); - const diskOriginalContent = await readFile(diskFilePath, {encoding: "utf8"}); - await writeFile(diskFilePath, JSON.stringify({key: "modified-by-race-condition"})); + // Step 2: Overwrite the file on disk + const sourcePath = libDProject.getSourcePath(); + const diskFilePath = path.join(sourcePath, "library", "d", "data.json"); + const diskOriginalContent = await readFile(diskFilePath, {encoding: "utf8"}); + await writeFile(diskFilePath, JSON.stringify({key: "modified-by-race-condition"})); - try { - // Step 3: Re-read via the dependency reader — should still return CAS-frozen content - // Without the frozen reader, this would read the modified disk content - const reReadResource = await libDReader.byPath(resourcePath); - if (!reReadResource) { - throw new Error(`Resource ${resourcePath} not found on re-read via dependency reader`); - } - const reReadContent = await reReadResource.getString(); + try { + // Step 3: Re-read via the dependency reader — should still return CAS-frozen content + // Without the frozen reader, this would read the modified disk content + const reReadResource = await libDReader.byPath(resourcePath); + if (!reReadResource) { + throw new Error(`Resource ${resourcePath} not found on re-read via dependency reader`); + } + const reReadContent = await reReadResource.getString(); - // Step 4: Assert the content is still the original (not modified disk content) - if (reReadContent !== originalContent) { - throw new Error( - "Frozen source reader protection failed: dependency reader returned modified disk content " + - "instead of the original CAS-frozen content. " + - `Expected: ${JSON.stringify(originalContent)}, Got: ${JSON.stringify(reReadContent)}` - ); + // Step 4: Assert the content is still the original (not modified disk content) + if (reReadContent !== originalContent) { + throw new Error( + "Frozen source reader protection failed: dependency reader returned modified disk " + + "content instead of the original CAS-frozen content. " + + `Expected: ${JSON.stringify(originalContent)}, Got: ${JSON.stringify(reReadContent)}` + ); + } + } finally { + // Step 5: Always restore the original file on disk + await writeFile(diskFilePath, diskOriginalContent); + } } - } finally { - // Step 5: Always restore the original file on disk - await writeFile(diskFilePath, diskOriginalContent); - } + }; }; diff --git a/packages/project/test/fixtures/application.a/race-condition-add-file-task.js b/packages/project/test/fixtures/application.a/race-condition-add-file-task.js index c4572b0aae1..a320041d9fd 100644 --- a/packages/project/test/fixtures/application.a/race-condition-add-file-task.js +++ b/packages/project/test/fixtures/application.a/race-condition-add-file-task.js @@ -1,11 +1,13 @@ const {writeFile} = require("fs/promises"); const path = require("path"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - const webappPath = taskUtil.getProject().getSourcePath(); - const addedFilePath = path.join(webappPath, "added-during-build.js"); - await writeFile(addedFilePath, `console.log("RACE CONDITION ADDED FILE");\n`); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + const webappPath = taskUtil.getProject().getSourcePath(); + const addedFilePath = path.join(webappPath, "added-during-build.js"); + await writeFile(addedFilePath, `console.log("RACE CONDITION ADDED FILE");\n`); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js b/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js index bad0440f7c4..5d1760d74c8 100644 --- a/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js +++ b/packages/project/test/fixtures/application.a/race-condition-delete-file-task.js @@ -1,11 +1,13 @@ const {rm} = require("fs/promises"); const path = require("path"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - const webappPath = taskUtil.getProject().getSourcePath(); - const deletedFilePath = path.join(webappPath, "test.js"); - await rm(deletedFilePath, {force: true}); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + const webappPath = taskUtil.getProject().getSourcePath(); + const deletedFilePath = path.join(webappPath, "test.js"); + await rm(deletedFilePath, {force: true}); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/race-condition-task.js b/packages/project/test/fixtures/application.a/race-condition-task.js index e70ed51ce7f..571e1c3e1bb 100644 --- a/packages/project/test/fixtures/application.a/race-condition-task.js +++ b/packages/project/test/fixtures/application.a/race-condition-task.js @@ -1,13 +1,15 @@ const {readFile, writeFile} = require("fs/promises"); const path = require("path"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - const webappPath = taskUtil.getProject().getSourcePath(); - // Modify source file during build - const testFilePath = path.join(webappPath, "test.js"); - const originalContent = await readFile(testFilePath, {encoding: "utf8"}); - await writeFile(testFilePath, originalContent + `\nconsole.log("RACE CONDITION MODIFICATION");\n`); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + const webappPath = taskUtil.getProject().getSourcePath(); + // Modify source file during build + const testFilePath = path.join(webappPath, "test.js"); + const originalContent = await readFile(testFilePath, {encoding: "utf8"}); + await writeFile(testFilePath, originalContent + `\nconsole.log("RACE CONDITION MODIFICATION");\n`); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/task.build-signature.js b/packages/project/test/fixtures/application.a/task.build-signature.js index 9b78d20c3b5..b3406523de2 100644 --- a/packages/project/test/fixtures/application.a/task.build-signature.js +++ b/packages/project/test/fixtures/application.a/task.build-signature.js @@ -19,25 +19,28 @@ async function readControlValue(rootPath) { } } -// Task body: appends the current control value to the application's test.js. This makes the built -// output observably depend on the control file, so a served resource reflects its value. -module.exports = async function ({taskUtil, workspace, options: {projectNamespace}}) { - log.verbose("build-signature-task executed"); +// Specification Version 5.0 descriptor: the task body and the determineBuildSignature callback are +// properties of the object the default-export factory returns. Both derive their value from the control +// file, so a test can change the returned value / produced output independently of any source change. +module.exports = function build() { + return { + determineBuildSignature: async ({taskUtil}) => { + const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); + log.verbose(`build-signature-task determineBuildSignature: ${controlValue}`); + return controlValue; + }, + // Task body: appends the current control value to the application's test.js. This makes the built + // output observably depend on the control file, so a served resource reflects its value. + task: async ({taskUtil, workspace, options: {projectNamespace}}) => { + log.verbose("build-signature-task executed"); - const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); - const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); - if (resource) { - const content = `${await resource.getString()}\n// build-signature-control: ${controlValue}\n`; - resource.setString(content); - await workspace.write(resource); - } -}; - -// determineBuildSignature is invoked by TaskDefinitions#getBuildSignatures() to contribute a value -// to the project's build signature. We derive it from the same control file so that changing the -// file changes the returned signature (and thus must invalidate the project's build cache). -module.exports.determineBuildSignature = async function ({taskUtil}) { - const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); - log.verbose(`build-signature-task determineBuildSignature: ${controlValue}`); - return controlValue; + const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); + const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + if (resource) { + const content = `${await resource.getString()}\n// build-signature-control: ${controlValue}\n`; + resource.setString(content); + await workspace.write(resource); + } + } + }; }; diff --git a/packages/project/test/fixtures/application.a/task.dependency-change.js b/packages/project/test/fixtures/application.a/task.dependency-change.js index 8007b936df7..c2463a890d9 100644 --- a/packages/project/test/fixtures/application.a/task.dependency-change.js +++ b/packages/project/test/fixtures/application.a/task.dependency-change.js @@ -1,40 +1,45 @@ // This is a modified version of the compileLicenseSummary example of the UI5 CLI documentation. // (https://github.com/UI5/cli/blob/b72919469d856508dd757ecf325a5fb45f15e56d/internal/documentation/docs/pages/extensibility/CustomTasks.md#example-libtaskscompilelicensesummaryjs) -module.exports = async function ({log, taskUtil, workspace}) { - const {createResource} = taskUtil.resourceFactory; - const projectsVisited = new Set(); +// Specification Version 5.0 descriptor: the task body and the determineRequiredDependencies callback are +// properties of the object the default-export factory returns. +module.exports = function build() { + return { + determineRequiredDependencies: ({availableDependencies}) => { + return availableDependencies; + }, + task: async ({log, taskUtil, workspace}) => { + const {createResource} = taskUtil.resourceFactory; + const projectsVisited = new Set(); - async function processProject() { - return Promise.all(taskUtil.getDependencies().map(async (projectName) => { - if (projectName !== "library.d") { - return; + async function processProject() { + return Promise.all(taskUtil.getDependencies().map(async (projectName) => { + if (projectName !== "library.d") { + return; + } + if (projectsVisited.has(projectName)) { + return; + } + projectsVisited.add(projectName); + const project = taskUtil.getProject(projectName); + const newLibraryFile = await project.getReader().byGlob("**/newLibraryFile.js"); + if (newLibraryFile.length > 0) { + log.verbose('New Library file found. We are in #4 build.'); + // Change content of application.a: + const applicationResource = await workspace.byPath("/resources/id1/test.js"); + const content = (await applicationResource.getString()) + "\n console.log('something new');"; + await workspace.write(createResource({ + path: "/test.js", + string: content + })); + } else { + log.verbose(`New Library file not found. We are still in an earlier build.`); + } + return processProject(project); + })); } - if (projectsVisited.has(projectName)) { - return; - } - projectsVisited.add(projectName); - const project = taskUtil.getProject(projectName); - const newLibraryFile = await project.getReader().byGlob("**/newLibraryFile.js"); - if (newLibraryFile.length > 0) { - log.verbose('New Library file found. We are in #4 build.'); - // Change content of application.a: - const applicationResource = await workspace.byPath("/resources/id1/test.js"); - const content = (await applicationResource.getString()) + "\n console.log('something new');"; - await workspace.write(createResource({ - path: "/test.js", - string: content - })); - } else { - log.verbose(`New Library file not found. We are still in an earlier build.`); - } - return processProject(project); - })); - } - // Start processing dependencies of the root project - await processProject(taskUtil.getProject()); + // Start processing dependencies of the root project + await processProject(taskUtil.getProject()); + } + }; }; - -module.exports.determineRequiredDependencies = function ({availableDependencies}) { - return availableDependencies; -} diff --git a/packages/project/test/fixtures/application.a/task.example.js b/packages/project/test/fixtures/application.a/task.example.js index 669c19c3fbf..6f548d10218 100644 --- a/packages/project/test/fixtures/application.a/task.example.js +++ b/packages/project/test/fixtures/application.a/task.example.js @@ -1,15 +1,17 @@ const Logger = require("@ui5/logger"); const log = Logger.getLogger("builder:tasks:exampleTask"); -module.exports = async function ({ - workspace, taskUtil, - options: {projectNamespace} -}) { - log.verbose("Example task executed"); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({workspace, taskUtil, options: {projectNamespace}}) => { + log.verbose("Example task executed"); - // Omit a specific resource from the build result - const omittedResource = await workspace.byPath(`/resources/${projectNamespace}/fileToBeOmitted.js`); - if (omittedResource) { - taskUtil.setTag(omittedResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + // Omit a specific resource from the build result + const omittedResource = await workspace.byPath(`/resources/${projectNamespace}/fileToBeOmitted.js`); + if (omittedResource) { + taskUtil.setTag(omittedResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }; + } }; }; diff --git a/packages/project/test/fixtures/application.a/task.root-conditional.js b/packages/project/test/fixtures/application.a/task.root-conditional.js index fa15cac1527..e9faebad40c 100644 --- a/packages/project/test/fixtures/application.a/task.root-conditional.js +++ b/packages/project/test/fixtures/application.a/task.root-conditional.js @@ -3,17 +3,22 @@ // Once /toggle.js is gone the task records no root read, so the previously recorded root request must // be cleared (and the emptied request set re-persisted), and a later change to /tsconfig.json must then // no longer invalidate this stage. -module.exports = async function ({taskUtil, workspace, options: {projectNamespace}}) { - const {createResource} = taskUtil.resourceFactory; - const toggle = await workspace.byPath(`/resources/${projectNamespace}/toggle.js`); - let content = "root-not-read"; - if (toggle) { - const rootReader = taskUtil.getProject().getRootReader(); - const tsconfig = await rootReader.byPath("/tsconfig.json"); - content = tsconfig ? await tsconfig.getString() : "no-tsconfig"; - } - await workspace.write(createResource({ - path: "/rootConditionalDigest.js", - string: `export const content = ${JSON.stringify(content)};\n`, - })); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({taskUtil, workspace, options: {projectNamespace}}) => { + const {createResource} = taskUtil.resourceFactory; + const toggle = await workspace.byPath(`/resources/${projectNamespace}/toggle.js`); + let content = "root-not-read"; + if (toggle) { + const rootReader = taskUtil.getProject().getRootReader(); + const tsconfig = await rootReader.byPath("/tsconfig.json"); + content = tsconfig ? await tsconfig.getString() : "no-tsconfig"; + } + await workspace.write(createResource({ + path: "/rootConditionalDigest.js", + string: `export const content = ${JSON.stringify(content)};\n`, + })); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/task.root-config.js b/packages/project/test/fixtures/application.a/task.root-config.js index 20fe7b00a15..748d605ce94 100644 --- a/packages/project/test/fixtures/application.a/task.root-config.js +++ b/packages/project/test/fixtures/application.a/task.root-config.js @@ -1,13 +1,18 @@ // Custom task that reads a configuration file from the project root (outside the UI5 resource model) // through taskUtil.getProject().getRootReader() and derives its output from that file's content. // Exercises root-resource tracking: a change to the root config must invalidate this task's cache. -module.exports = async function ({taskUtil, workspace}) { - const {createResource} = taskUtil.resourceFactory; - const rootReader = taskUtil.getProject().getRootReader(); - const tsconfig = await rootReader.byPath("/tsconfig.json"); - const content = tsconfig ? await tsconfig.getString() : "no-tsconfig"; - await workspace.write(createResource({ - path: "/tsconfigDigest.js", - string: `export const tsconfig = ${JSON.stringify(content)};\n`, - })); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({taskUtil, workspace}) => { + const {createResource} = taskUtil.resourceFactory; + const rootReader = taskUtil.getProject().getRootReader(); + const tsconfig = await rootReader.byPath("/tsconfig.json"); + const content = tsconfig ? await tsconfig.getString() : "no-tsconfig"; + await workspace.write(createResource({ + path: "/tsconfigDigest.js", + string: `export const tsconfig = ${JSON.stringify(content)};\n`, + })); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/task.root-glob.js b/packages/project/test/fixtures/application.a/task.root-glob.js index 931eb65bb73..fe8c3b7abdf 100644 --- a/packages/project/test/fixtures/application.a/task.root-glob.js +++ b/packages/project/test/fixtures/application.a/task.root-glob.js @@ -2,13 +2,18 @@ // the read is recorded against the useGitignore:false root manager. Exercises glob-based root tracking: // adding or removing a file matching the glob must invalidate the task's cache (root indices refresh by // re-globbing, so a newly matching file is detected). -module.exports = async function ({taskUtil, workspace}) { - const {createResource} = taskUtil.resourceFactory; - const rootReader = taskUtil.getProject().getRootReader({useGitignore: false}); - const configs = await rootReader.byGlob("/rootcfg/**/*.json"); - const names = configs.map((r) => r.getPath()).sort(); - await workspace.write(createResource({ - path: "/rootGlobDigest.js", - string: `export const configs = ${JSON.stringify(names)};\n`, - })); +// Specification Version 5.0 descriptor: the default export is a factory that returns the task body. +module.exports = function build() { + return { + task: async ({taskUtil, workspace}) => { + const {createResource} = taskUtil.resourceFactory; + const rootReader = taskUtil.getProject().getRootReader({useGitignore: false}); + const configs = await rootReader.byGlob("/rootcfg/**/*.json"); + const names = configs.map((r) => r.getPath()).sort(); + await workspace.write(createResource({ + path: "/rootGlobDigest.js", + string: `export const configs = ${JSON.stringify(names)};\n`, + })); + } + }; }; diff --git a/packages/project/test/fixtures/application.a/task.step-based.js b/packages/project/test/fixtures/application.a/task.step-based.js index 8881955e72b..b81ec6e59ff 100644 --- a/packages/project/test/fixtures/application.a/task.step-based.js +++ b/packages/project/test/fixtures/application.a/task.step-based.js @@ -8,33 +8,34 @@ const log = Logger.getLogger("builder:tasks:stepBasedTask"); // a `.dep` re-runs its owning invocation and leaves the others served from cache. This is the same reverse // mapping the built-in minify relies on for its `.js.map` -> `.js` relation, expressed by a custom task. module.exports = function build() { - return [{ - name: "procEach", - keys: async ({workspace, taskUtil, options: {projectNamespace}}) => { - // Tag from the enumerator, not from an invocation: the enumerator runs whenever the stage runs, but - // it owns no key, so its tags have no per-key invocation entry to be replayed from. They - // survive a fully cached stage through the stage's own recorded tag operations. - const omittedResources = await workspace.byGlob(`/resources/${projectNamespace}/procEach/*.omitme`); - for (const omittedResource of omittedResources) { - taskUtil.setTag(omittedResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - } - const srcResources = await workspace.byGlob(`/resources/${projectNamespace}/procEach/*.src`); - log.verbose(`step-based-task processing ${srcResources.length} source(s)`); - return srcResources; - }, - each: async (srcResource, {workspace, taskUtil}) => { - const srcPath = srcResource.getPath(); - const depPath = srcPath.replace(/\.src$/, ".dep"); - // Read the cross-resource input through the step workspace so it is tracked as this invocation's input. - const depResource = await workspace.byPath(depPath); - const depContent = depResource ? await depResource.getString() : ""; - const srcContent = await srcResource.getString(); - const outResource = taskUtil.resourceFactory.createResource({ - path: srcPath.replace(/\.src$/, ".out"), - string: `${srcContent}\n// dep: ${depContent}\n`, - }); - await workspace.write(outResource); - }, - }]; + return { + steps: [{ + name: "procEach", + keys: async ({workspace, taskUtil, options: {projectNamespace}}) => { + // Tag from the enumerator, not from an invocation: the enumerator runs whenever the stage runs, but + // it owns no key, so its tags have no per-key invocation entry to be replayed from. They + // survive a fully cached stage through the stage's own recorded tag operations. + const omittedResources = await workspace.byGlob(`/resources/${projectNamespace}/procEach/*.omitme`); + for (const omittedResource of omittedResources) { + taskUtil.setTag(omittedResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + const srcResources = await workspace.byGlob(`/resources/${projectNamespace}/procEach/*.src`); + log.verbose(`step-based-task processing ${srcResources.length} source(s)`); + return srcResources; + }, + each: async (srcResource, {workspace, taskUtil}) => { + const srcPath = srcResource.getPath(); + const depPath = srcPath.replace(/\.src$/, ".dep"); + // Read the cross-resource input through the step workspace so it is tracked as this invocation's input. + const depResource = await workspace.byPath(depPath); + const depContent = depResource ? await depResource.getString() : ""; + const srcContent = await srcResource.getString(); + const outResource = taskUtil.resourceFactory.createResource({ + path: srcPath.replace(/\.src$/, ".out"), + string: `${srcContent}\n// dep: ${depContent}\n`, + }); + await workspace.write(outResource); + }, + }], + }; }; -module.exports.stepBased = true; diff --git a/packages/project/test/lib/build/TaskDefinitions.js b/packages/project/test/lib/build/TaskDefinitions.js index b155aebc455..659bed93378 100644 --- a/packages/project/test/lib/build/TaskDefinitions.js +++ b/packages/project/test/lib/build/TaskDefinitions.js @@ -57,9 +57,9 @@ test.beforeEach(async (t) => { getAllTaskNames: sinon.stub().returns(["replaceVersion", "minify"]), }; - t.context.customTaskSpecVersionLtStub = sinon.stub().returns(false); + t.context.customTaskSpecVersionGteStub = sinon.stub().returns(false); t.context.customTaskSpecVersion = { - lt: t.context.customTaskSpecVersionLtStub + gte: t.context.customTaskSpecVersionGteStub }; t.context.getDetermineBuildSignatureCallbackStub = sinon.stub().resolves(undefined); t.context.customTask = { @@ -412,6 +412,10 @@ test("getBuildSignatures: Standard tasks without determineBuildSignature use opt ["taskA", {options: {foo: "bar"}}], ["taskB", {options: {hello: "world"}}], ]); + // The factory for each task returns a descriptor with no determineBuildSignature, so the + // signature loop falls back to JSON.stringify(taskDef.options). + taskRepository.getTask.withArgs("taskA").resolves({task: (opts) => ({task: async () => {}})}); + taskRepository.getTask.withArgs("taskB").resolves({task: (opts) => ({task: async () => {}})}); const definitionStub = sinon.stub().returns(standardTasks); const MockedTaskDefinitions = await esmock.p("../../../lib/build/TaskDefinitions.js", {}, { "../../../lib/build/definitions/module.js": { @@ -424,8 +428,8 @@ test("getBuildSignatures: Standard tasks without determineBuildSignature use opt const sig = await taskDefinitions.getBuildSignatures(); const expectedKey = [ - JSON.stringify({foo: "bar"}), - JSON.stringify({hello: "world"}) + JSON.stringify({foo: "bar", projectName: "project.b", projectNamespace: "project/b"}), + JSON.stringify({hello: "world", projectName: "project.b", projectNamespace: "project/b"}), ].join("|"); const expectedHash = crypto.createHash("sha256").update(expectedKey).digest("hex"); t.is(sig, expectedHash, "Returns sha256 hash of joined task options"); @@ -435,8 +439,12 @@ test("getBuildSignatures: Standard task determineBuildSignature is awaited and u const {graph, taskUtil, taskRepository} = t.context; const sinon = t.context.sinon; const determineBuildSignatureStub = sinon.stub().resolves("custom-sig"); + // The factory returns a descriptor carrying the determineBuildSignature callback. + taskRepository.getTask.withArgs("taskA").resolves({ + task: (opts) => ({task: async () => {}, determineBuildSignature: determineBuildSignatureStub}) + }); const standardTasks = new Map([ - ["taskA", {determineBuildSignature: determineBuildSignatureStub}], + ["taskA", {options: {}}], ]); const definitionStub = sinon.stub().returns(standardTasks); const MockedTaskDefinitions = await esmock.p("../../../lib/build/TaskDefinitions.js", {}, { @@ -457,8 +465,14 @@ test("getBuildSignatures: Standard task determineBuildSignature is awaited and u test("getBuildSignatures: Standard task determineBuildSignature returning falsy is skipped", async (t) => { const {graph, taskUtil, taskRepository} = t.context; const sinon = t.context.sinon; + // taskA has a determineBuildSignature that returns undefined; taskB has no callback and falls + // back to its options JSON. + taskRepository.getTask.withArgs("taskA").resolves({ + task: (opts) => ({task: async () => {}, determineBuildSignature: sinon.stub().resolves(undefined)}) + }); + taskRepository.getTask.withArgs("taskB").resolves({task: (opts) => ({task: async () => {}})}); const standardTasks = new Map([ - ["taskA", {determineBuildSignature: sinon.stub().resolves(undefined)}], + ["taskA", {options: {}}], ["taskB", {options: {a: 1}}], ]); const definitionStub = sinon.stub().returns(standardTasks); @@ -472,7 +486,9 @@ test("getBuildSignatures: Standard task determineBuildSignature returning falsy const taskDefinitions = new MockedTaskDefinitions(graph, project, taskUtil, taskRepository); const sig = await taskDefinitions.getBuildSignatures(); - const expectedHash = crypto.createHash("sha256").update(JSON.stringify({a: 1})).digest("hex"); + const expectedHash = crypto.createHash("sha256").update( + JSON.stringify({a: 1, projectName: "project.b", projectNamespace: "project/b"}) + ).digest("hex"); t.is(sig, expectedHash, "Falsy signature returned by callback is skipped"); }); @@ -492,8 +508,10 @@ test("getBuildSignatures: Custom task without determineBuildSignature falls back }); test("getBuildSignatures: Custom task determineBuildSignature gets invoked correctly", async (t) => { - const {TaskDefinitions, graph, taskUtil, taskRepository, customTask, customTaskSpecVersion, sinon} = - t.context; + const {TaskDefinitions, graph, taskUtil, taskRepository, customTask, customTaskSpecVersion, + customTaskSpecVersionGteStub, sinon} = t.context; + // Make the task specVersion report >= 5.0 so the new code takes the descriptor path. + customTaskSpecVersionGteStub.withArgs("5.0").returns(true); const determineBuildSignatureCallback = sinon.stub().resolves("my-custom-sig"); customTask.getDetermineBuildSignatureCallback = sinon.stub().resolves(determineBuildSignatureCallback); @@ -525,7 +543,10 @@ test("getBuildSignatures: Custom task determineBuildSignature gets invoked corre }); test("getBuildSignatures: Custom task determineBuildSignature returning falsy is skipped", async (t) => { - const {TaskDefinitions, graph, taskUtil, taskRepository, customTask, sinon} = t.context; + const {TaskDefinitions, graph, taskUtil, taskRepository, customTask, customTaskSpecVersionGteStub, + sinon} = t.context; + // Make the task specVersion report >= 5.0 so the new code takes the descriptor path. + customTaskSpecVersionGteStub.withArgs("5.0").returns(true); const determineBuildSignatureCallback = sinon.stub().resolves(null); customTask.getDetermineBuildSignatureCallback = sinon.stub().resolves(determineBuildSignatureCallback); @@ -541,11 +562,12 @@ test("getBuildSignatures: Custom task determineBuildSignature returning falsy is t.is(sig, expectedHash, "Falsy signature is omitted from hash input"); }); -test("getBuildSignatures: Custom task with specVersion < 5.0 and determineBuildSignature throws", async (t) => { +test("getBuildSignatures: Custom task with specVersion < 5.0 falls back to configuration", async (t) => { const {TaskDefinitions, graph, taskUtil, taskRepository, customTask, sinon} = t.context; + // gte("5.0") returns false: below 5.0, the descriptor path is skipped and the signature falls + // back to JSON.stringify(taskDef.configuration) regardless of any callback. const determineBuildSignatureCallback = sinon.stub().resolves("custom-sig"); customTask.getDetermineBuildSignatureCallback = sinon.stub().resolves(determineBuildSignatureCallback); - t.context.customTaskSpecVersionLtStub.withArgs("5.0").returns(true); const project = getMockProject("module"); project.getCustomTasks = () => [ @@ -553,21 +575,27 @@ test("getBuildSignatures: Custom task with specVersion < 5.0 and determineBuildS ]; const taskDefinitions = new TaskDefinitions(graph, project, taskUtil, taskRepository); - const err = await t.throwsAsync(taskDefinitions.getBuildSignatures()); - t.is(err.message, - `Custom task myTask of project project.b uses a determineBuildSignature callback, ` + - `which is not supported for tasks with specVersion < 5.0. ` + - `Please update the task's specVersion to at least 5.0.`, - "Threw with expected error message"); + const sig = await taskDefinitions.getBuildSignatures(); + const expectedHash = crypto.createHash("sha256").update( + JSON.stringify("configuration") + ).digest("hex"); + t.is(sig, expectedHash, "Below specVersion 5.0, signature falls back to configuration JSON"); t.is(determineBuildSignatureCallback.callCount, 0, - "determineBuildSignature callback was not invoked"); + "determineBuildSignature callback is not invoked below specVersion 5.0"); }); test("getBuildSignatures: Combines standard and custom task signatures", async (t) => { - const {graph, taskUtil, taskRepository, customTask, sinon} = t.context; + const {graph, taskUtil, taskRepository, customTask, customTaskSpecVersionGteStub, sinon} = t.context; + // taskA: no determineBuildSignature → falls back to options JSON. + // taskB: descriptor carries determineBuildSignature returning "std-sig". + const stdSigStub = sinon.stub().resolves("std-sig"); + taskRepository.getTask.withArgs("taskA").resolves({task: (opts) => ({task: async () => {}})}); + taskRepository.getTask.withArgs("taskB").resolves({ + task: (opts) => ({task: async () => {}, determineBuildSignature: stdSigStub}) + }); const standardTasks = new Map([ ["taskA", {options: {a: 1}}], - ["taskB", {determineBuildSignature: sinon.stub().resolves("std-sig")}], + ["taskB", {options: {}}], ]); const definitionStub = sinon.stub().returns(standardTasks); const MockedTaskDefinitions = await esmock.p("../../../lib/build/TaskDefinitions.js", {}, { @@ -576,6 +604,8 @@ test("getBuildSignatures: Combines standard and custom task signatures", async ( } }); + // Make the custom task specVersion >= 5.0 so its determineBuildSignature is honored. + customTaskSpecVersionGteStub.withArgs("5.0").returns(true); const determineBuildSignatureCallback = sinon.stub().resolves("custom-sig"); customTask.getDetermineBuildSignatureCallback = sinon.stub().resolves(determineBuildSignatureCallback); @@ -587,7 +617,7 @@ test("getBuildSignatures: Combines standard and custom task signatures", async ( const sig = await taskDefinitions.getBuildSignatures(); const expectedKey = [ - JSON.stringify({a: 1}), + JSON.stringify({a: 1, projectName: "project.b", projectNamespace: "project/b"}), "std-sig", "custom-sig" ].join("|"); diff --git a/packages/project/test/lib/build/TaskRunner.js b/packages/project/test/lib/build/TaskRunner.js index 1b364a71f3b..3847c8f4436 100644 --- a/packages/project/test/lib/build/TaskRunner.js +++ b/packages/project/test/lib/build/TaskRunner.js @@ -2,6 +2,7 @@ import test from "ava"; import sinonGlobal from "sinon"; import esmock from "esmock"; import {setLogLevel} from "@ui5/logger"; +import {validateSteps} from "../../../lib/build/helpers/StepRunner.js"; setLogLevel("perf"); function noop() {} @@ -29,6 +30,30 @@ const buildConfig = { excludedTasks: [] }; +/** + * Returns a getStandardDescriptor implementation for mock taskDefinitions objects used in step-based tests. + * + * Step-based standard task mocks store the step factory in taskDef.taskFunction instead of in a real + * taskRepository. This helper calls the factory with the completed options, validates the steps, and returns + * the descriptor so _addTask treats the task as step-based. + * + * For tasks with skip or no taskFunction, returns undefined so _addTask falls back to taskDef properties. + * + * @returns {Function} Async getStandardDescriptor implementation + */ +function makeStepBasedGetStandardDescriptor() { + return async function getStandardDescriptor(taskName, taskDef) { + if (taskDef.skip || !taskDef.taskFunction) { + return undefined; + } + // The mock stores the step factory in taskFunction. Call it with the completed options. + const steps = taskDef.taskFunction(taskDef.options ?? {}); + // validateSteps throws if the factory returned a non-array or a malformed step. + validateSteps(steps, {taskName}); + return {steps, requiresDependencies: !!taskDef.requiresDependencies}; + }; +} + function getMockProject(type) { return { getName: () => "project.b", @@ -79,6 +104,7 @@ function getMockProject(type) { isFrameworkProject: () => false, sealWorkspace: noop, createNewWorkspaceVersion: noop, + getJsdocExcludes: emptyarray, }; } @@ -95,9 +121,9 @@ test.beforeEach(async (t) => { t.context.taskUtil.getInterface.returns(t.context.taskUtil); t.context.taskRepository = { - getTask: sinon.stub().callsFake(async (taskName) => { - throw new Error(`taskRepository: Unknown Task ${taskName}`); - }), + getTask: sinon.stub().callsFake((taskName) => ({ + task: (opts) => ({task: async () => {}}) + })), getAllTaskNames: sinon.stub().returns(["replaceVersion"]), getRemovedTaskNames: sinon.stub().returns(["removedTask"]), }; @@ -111,6 +137,8 @@ test.beforeEach(async (t) => { gte: t.context.customTaskSpecVersionGteStub }; }, + getTask: async () => sinon.stub(), + getDescriptor: sinon.stub().resolves({task: async () => {}}), getRequiredDependenciesCallback: t.context.getRequiredDependenciesCallbackStub, getStepBased: async () => false, }; @@ -703,10 +731,10 @@ test("Custom task is called correctly", async (t) => { await taskRunner._tasks["myTask"].task(); t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); - t.is(specVersionGteStub.getCall(0).args[0], "3.0", + t.is(specVersionGteStub.getCall(0).args[0], "5.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "5.0", - "SpecificationVersion#gte got called with correct arguments on second call (step-based opt-in)"); + t.is(specVersionGteStub.getCall(1).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call (no-callback branch)"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); @@ -763,10 +791,10 @@ test("Custom task with legacy spec version", async (t) => { await taskRunner._tasks["myTask"].task(); t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); - t.is(specVersionGteStub.getCall(0).args[0], "3.0", + t.is(specVersionGteStub.getCall(0).args[0], "5.0", "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "5.0", - "SpecificationVersion#gte got called with correct arguments on second call (step-based opt-in)"); + t.is(specVersionGteStub.getCall(1).args[0], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call (no-callback branch)"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); @@ -789,7 +817,10 @@ test("Custom task with legacy spec version", async (t) => { "taskUtil#getInterface got called with correct argument"); }); -test("Custom task with legacy spec version and requiredDependenciesCallback", async (t) => { +// Below specVersion 5.0 the descriptor path is not taken, so the task extension's +// getRequiredDependenciesCallback is never invoked by the runner. All direct dependencies +// are provided by default when the task's specVersion is also below 3.0. +test("Custom task with legacy spec version: requiredDependenciesCallback is not honored", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(false); @@ -797,8 +828,8 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as toString: () => "1.0", gte: specVersionGteStub }; - const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); - const getRequiredDependenciesCallbackStub = sinon.stub().resolves(requiredDependenciesCallbackStub); + // The old getRequiredDependenciesCallback is no longer called below specVersion 5.0. + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(sinon.stub().resolves(new Set(["dep.b"]))); graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, @@ -815,36 +846,28 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); - t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), - "Custom tasks requires all dependencies by default"); - - t.is(requiredDependenciesCallbackStub.callCount, 1, "requiredDependenciesCallback got called once"); - t.deepEqual(requiredDependenciesCallbackStub.getCall(0).args[0], { - availableDependencies: new Set(["dep.a", "dep.b"]), - options: { - projectName: "project.b", - projectNamespace: "project/b", - configuration: "configuration", - taskName: "myTask" - } - }, "requiredDependenciesCallback got called with expected arguments"); + // The callback is not honored below 5.0: the task defaults to all direct dependencies. + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), + "Below specVersion 5.0, all direct dependencies are provided (callback not honored)"); + t.is(getRequiredDependenciesCallbackStub.callCount, 0, + "getRequiredDependenciesCallback is not invoked below specVersion 5.0"); const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); - t.is(specVersionGteStub.getCall(0).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "5.0", - "SpecificationVersion#gte got called with correct arguments on second call (step-based opt-in)"); + t.is(specVersionGteStub.getCall(0).args[0], "5.0", + "SpecificationVersion#gte first call checks 5.0"); + t.is(specVersionGteStub.getCall(1).args[0], "3.0", + "SpecificationVersion#gte second call checks 3.0 (no-callback branch)"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); + "SpecificationVersion#gte third call checks 3.0 (task execution)"); t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], - new Set(["dep.b"]), - "getDependenciesReader got called with correct arguments"); + new Set(["dep.a", "dep.b"]), + "All direct dependencies provided"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); @@ -860,20 +883,24 @@ test("Custom task with legacy spec version and requiredDependenciesCallback", as "taskUtil#getInterface got called with correct argument"); }); +// Below specVersion 5.0 there is no descriptor, so determineRequiredDependencies is not +// available via the descriptor. A task at specVersion 3.0-4.x gets no dependencies by +// default (gte("3.0") = true with no descriptor callback → empty set, no reader). test("Custom task with specVersion 3.0", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); - const specVersionGteStub = sinon.stub().returns(true); + // gte("3.0") = true, gte("5.0") = false: V3 range, no descriptor. + const specVersionGteStub = sinon.stub().callsFake((v) => v === "3.0"); const mockSpecVersion = { toString: () => "3.0", gte: specVersionGteStub }; - const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); - const getRequiredDependenciesCallbackStub = sinon.stub() - .resolves(requiredDependenciesCallbackStub); + // Old getRequiredDependenciesCallback API is not called below V5. + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(sinon.stub().resolves(new Set(["dep.b"]))); graph.getExtension.returns({ + getName: () => "custom task name", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, @@ -888,66 +915,35 @@ test("Custom task with specVersion 3.0", async (t) => { const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - t.is(requiredDependenciesCallbackStub.callCount, 1, "requiredDependenciesCallback got called once"); - const requiredDependenciesCallbackArgs = requiredDependenciesCallbackStub.getCall(0).args[0]; - - t.is(typeof requiredDependenciesCallbackArgs.getProject, "function", "getProject function provided"); - requiredDependenciesCallbackArgs.getProject("some.project"); - t.is(taskUtil.getProject.callCount, 1, "taskUtil.getProject got called once"); - t.is(taskUtil.getProject.getCall(0).args[0], "some.project", - "taskUtil.getProject got called with expected arguments"); - requiredDependenciesCallbackArgs.getProject = "getProject function"; - - t.is(typeof requiredDependenciesCallbackArgs.getDependencies, "function", "getDependencies function provided"); - requiredDependenciesCallbackArgs.getDependencies("some.project"); - t.is(taskUtil.getDependencies.callCount, 2, "taskUtil.getDependencies got called twice"); - t.is(taskUtil.getDependencies.getCall(1).args[0], "some.project", - "taskUtil.getDependencies got called with expected arguments"); - requiredDependenciesCallbackArgs.getDependencies = "getDependencies function"; - - t.deepEqual(requiredDependenciesCallbackArgs, { - availableDependencies: new Set(["dep.a", "dep.b"]), - getProject: "getProject function", - getDependencies: "getDependencies function", - options: { - projectName: "project.b", - projectNamespace: "project/b", - taskName: "myTask", - configuration: "configuration", - } - }, "requiredDependenciesCallback got called with expected arguments"); - t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); - t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), - "Custom tasks requires all dependencies by default"); + // Below V5, no descriptor callback; gte("3.0") = true → empty set (no reader). + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), + "Below V5, a V3 task with no descriptor callback gets no dependencies"); + t.is(getRequiredDependenciesCallbackStub.callCount, 0, + "getRequiredDependenciesCallback is not called below V5"); + const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") .resolves({getName: () => "dependencies"}); await taskRunner._tasks["myTask"].task(); t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); - t.is(specVersionGteStub.getCall(0).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "5.0", - "SpecificationVersion#gte got called with correct arguments on second call (step-based opt-in)"); + t.is(specVersionGteStub.getCall(0).args[0], "5.0", + "SpecificationVersion#gte first call checks 5.0 (isV5)"); + t.is(specVersionGteStub.getCall(1).args[0], "3.0", + "SpecificationVersion#gte second call checks 3.0 (no-callback branch)"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); + "SpecificationVersion#gte third call checks 3.0 (task execution)"); - t.is(taskUtil.getInterface.callCount, 2, "taskUtil#getInterface got called twice"); + t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, "taskUtil#getInterface got called with correct argument on first call"); - t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersion, - "taskUtil#getInterface got called with correct argument on second call"); - t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); - t.deepEqual(createDependencyReaderStub.getCall(0).args[0], - new Set(["dep.b"]), - "getDependenciesReader got called with correct arguments"); + t.is(createDependencyReaderStub.callCount, 0, "getDependenciesReader did not get called"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); const taskArgs = taskStub.getCall(0).args[0]; t.is(taskArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); - t.is(taskArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); t.is(taskArgs.log, "group logger", "log is correct"); assertMonitoredTaskUtil(t, taskArgs.taskUtil); t.is(taskArgs.options.projectName, "project.b", "projectName is correct"); @@ -959,19 +955,19 @@ test("Custom task with specVersion 3.0", async (t) => { test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); + // All gte calls return true; this is treated as >= 5.0 and takes the descriptor path. const specVersionGteStub = sinon.stub().returns(true); const mockSpecVersion = { toString: () => "3.0", gte: specVersionGteStub }; - const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined); - graph.getExtension.returns({ getName: () => "custom task name", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + // Descriptor carries no determineRequiredDependencies callback, so the task gets no deps. + getDescriptor: sinon.stub().resolves({task: taskStub}), getStepBased: async () => false, }); @@ -984,6 +980,7 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); + // Descriptor has no determineRequiredDependencies → gte("3.0") check → no reader. t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), "Custom tasks requires no dependencies by default"); const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") @@ -991,12 +988,12 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy await taskRunner._tasks["myTask"].task(); t.is(specVersionGteStub.callCount, 3, "SpecificationVersion#gte got called three times"); - t.is(specVersionGteStub.getCall(0).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on first call"); - t.is(specVersionGteStub.getCall(1).args[0], "5.0", - "SpecificationVersion#gte got called with correct arguments on second call (step-based opt-in)"); + t.is(specVersionGteStub.getCall(0).args[0], "5.0", + "SpecificationVersion#gte first call checks 5.0 (isV5)"); + t.is(specVersionGteStub.getCall(1).args[0], "3.0", + "SpecificationVersion#gte second call checks 3.0 (no-callback branch)"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", - "SpecificationVersion#gte got called with correct arguments on third call (task execution)"); + "SpecificationVersion#gte third call checks 3.0 (task execution)"); t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, @@ -1022,6 +1019,7 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { const taskStubB = sinon.stub(); const taskStubC = sinon.stub(); const taskStubD = sinon.stub(); + // Tasks A and B are below V5 (legacy body path, gte all false). const mockSpecVersionA = { toString: () => "2.5", gte: () => false @@ -1030,6 +1028,8 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { toString: () => "2.6", gte: () => false }; + // Tasks C and D are V5 (gte all true, descriptor path). Their descriptors carry no + // determineRequiredDependencies, so both get no dependencies and no reader. const mockSpecVersionC = { toString: () => "3.0", gte: () => true @@ -1038,39 +1038,33 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { toString: () => "3.0", gte: () => true }; - const requiredDependenciesCallbackStubA = sinon.stub().resolves(new Set(["dep.b"])); - const requiredDependenciesCallbackStubD = sinon.stub().resolves(new Set(["dep.a"])); - const getRequiredDependenciesCallbackStub = sinon.stub() - .resolves(null) - .onCall(0).resolves(requiredDependenciesCallbackStubA) - .onCall(3).resolves(requiredDependenciesCallbackStubD); graph.getExtension.onFirstCall().returns({ getName: () => "Task Name A", getTask: () => taskStubA, getSpecVersion: () => mockSpecVersionA, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, getStepBased: async () => false, }); graph.getExtension.onSecondCall().returns({ getName: () => "Task Name B", getTask: () => taskStubB, getSpecVersion: () => mockSpecVersionB, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, getStepBased: async () => false, }); graph.getExtension.onThirdCall().returns({ getName: () => "Task Name C", getTask: () => taskStubC, getSpecVersion: () => mockSpecVersionC, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + // V5 descriptor with no determineRequiredDependencies → no deps. + getDescriptor: sinon.stub().resolves({task: taskStubC}), getStepBased: async () => false, }); graph.getExtension.onCall(3).returns({ getName: () => "Task Name D", getTask: () => taskStubD, getSpecVersion: () => mockSpecVersionD, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + // V5 descriptor with no determineRequiredDependencies → no deps. + getDescriptor: sinon.stub().resolves({task: taskStubD}), getStepBased: async () => false, }); const project = getMockProject("module"); @@ -1083,29 +1077,20 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - // getRequiredDependenciesCallbackStub is only called for specVersion >= 3.0 - t.is(getRequiredDependenciesCallbackStub.callCount, 4, - "getRequiredDependenciesCallback stub was called for all tasks"); - t.is(requiredDependenciesCallbackStubA.callCount, 1, - "requiredDependenciesCallback stub for task A was called once"); - t.is(requiredDependenciesCallbackStubD.callCount, 1, - "requiredDependenciesCallback stub for Task D stub was called once"); - t.truthy(taskRunner._tasks["myTask"], "Custom tasks A has been added to task map"); t.truthy(taskRunner._tasks["myTask--2"], "Custom tasks B has been added to task map"); t.truthy(taskRunner._tasks["myTask--3"], "Custom tasks C has been added to task map"); t.truthy(taskRunner._tasks["myTask--4"], "Custom tasks D has been added to task map"); - t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), - "Custom tasks with legacy specVersion and requiredDependenciesCallback defines " + - "required dependencies"); + // A, B (legacy, gte all false → < 3.0 path): all deps. + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), + "Legacy specVersion task A gets all dependencies by default"); t.deepEqual(taskRunner._tasks["myTask--2"].requiredDependencies, new Set(["dep.a", "dep.b"]), - "Custom tasks with legacy specVersion require all dependencies by default"); + "Legacy specVersion task B gets all dependencies by default"); + // C, D (V5, descriptor with no callback → gte("3.0")=true → no reader): empty set. t.deepEqual(taskRunner._tasks["myTask--3"].requiredDependencies, new Set([]), - "Custom tasks with specVersion 3.0 but no requiredDependenciesCallback " + - "require no dependencies by default"); - t.deepEqual(taskRunner._tasks["myTask--4"].requiredDependencies, new Set(["dep.a"]), - "Custom tasks with specVersion 3.0 and requiredDependenciesCallback defines " + - "required dependencies"); + "V5 task C with no determineRequiredDependencies gets no dependencies"); + t.deepEqual(taskRunner._tasks["myTask--4"].requiredDependencies, new Set([]), + "V5 task D with no determineRequiredDependencies gets no dependencies"); // "Last in is the first out" t.deepEqual(taskRunner._taskExecutionOrder, [ @@ -1142,31 +1127,29 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { "myTask--2", ], "ProjectBuildLogger#endTask got called with expected arguments"); - t.is(taskUtil.getInterface.callCount, 5, "taskUtil#getInterface got called three times"); - t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersionD, - "taskUtil#getInterface got called with correct argument on first call"); - t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersionA, - "taskUtil#getInterface got called with correct argument on second call"); - t.is(taskUtil.getInterface.getCall(2).args[0], mockSpecVersionD, - "taskUtil#getInterface got called with correct argument on third call"); - t.is(taskUtil.getInterface.getCall(3).args[0], mockSpecVersionC, - "taskUtil#getInterface got called with correct argument on fourth call"); - t.is(taskUtil.getInterface.getCall(4).args[0], mockSpecVersionB, - "taskUtil#getInterface got called with correct argument on fifth call"); - - t.is(createDependencyReaderStub.callCount, 4, "getDependenciesReader got called four times"); + // One getInterface call per task execution (four tasks run). + t.is(taskUtil.getInterface.callCount, 4, "taskUtil#getInterface got called four times"); + t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersionA, + "taskUtil#getInterface got called with correct argument on first call (Task A)"); + t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersionD, + "taskUtil#getInterface got called with correct argument on second call (Task D)"); + t.is(taskUtil.getInterface.getCall(2).args[0], mockSpecVersionC, + "taskUtil#getInterface got called with correct argument on third call (Task C)"); + t.is(taskUtil.getInterface.getCall(3).args[0], mockSpecVersionB, + "taskUtil#getInterface got called with correct argument on fourth call (Task B)"); + + // Calls: (1) runTasks init pass with all direct deps, (2) Task A all deps, (3) Task B all deps. + // Tasks C and D have no reader (provideDependenciesReader = false). + t.is(createDependencyReaderStub.callCount, 3, "getDependenciesReader got called three times"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.a", "dep.b"]), "getDependenciesReader got called with correct arguments on first call (runTasks init)"); t.deepEqual(createDependencyReaderStub.getCall(1).args[0], - new Set(["dep.b"]), + new Set(["dep.a", "dep.b"]), "getDependenciesReader got called with correct arguments on second call (Task A)"); t.deepEqual(createDependencyReaderStub.getCall(2).args[0], - new Set(["dep.a"]), - "getDependenciesReader got called with correct arguments on third call (Task D)"); - t.deepEqual(createDependencyReaderStub.getCall(3).args[0], new Set(["dep.a", "dep.b"]), - "getDependenciesReader got called with correct arguments on fourth call (Task B)"); + "getDependenciesReader got called with correct arguments on third call (Task B)"); t.is(taskStubA.callCount, 1, "Task A got called once"); t.is(taskStubA.getCall(0).args.length, 1, "Task A got called with one argument"); @@ -1201,7 +1184,6 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { t.is(taskStubD.getCall(0).args.length, 1, "Task D got called with one argument"); const taskDArgs = taskStubD.getCall(0).args[0]; t.is(taskDArgs.workspace.constructor.name, "MonitoredReader", "workspace is MonitoredReader"); - t.is(taskDArgs.dependencies.constructor.name, "MonitoredReader", "dependencies is MonitoredReader"); t.is(taskDArgs.log, "group logger", "log is correct"); assertMonitoredTaskUtil(t, taskDArgs.taskUtil); t.is(taskDArgs.options.projectName, "project.b", "projectName is correct"); @@ -1219,15 +1201,18 @@ test("Custom task: requiredDependenciesCallback returns unknown dependency", asy gte: specVersionGteStub }; + // At specVersion 5.0+, determineRequiredDependencies lives on the descriptor. const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b", "other.dep"])); - const getRequiredDependenciesCallbackStub = sinon.stub() - .resolves(requiredDependenciesCallbackStub); + const getDescriptorStub = sinon.stub().resolves({ + task: taskStub, + determineRequiredDependencies: requiredDependenciesCallbackStub + }); graph.getExtension.returns({ getName: () => "custom.task.a", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getDescriptor: getDescriptorStub, getStepBased: async () => false, }); @@ -1255,15 +1240,18 @@ test("Custom task: requiredDependenciesCallback returns Array instead of Set", a gte: specVersionGteStub }; + // At specVersion 5.0+, determineRequiredDependencies lives on the descriptor. const requiredDependenciesCallbackStub = sinon.stub().resolves(["dep.b"]); - const getRequiredDependenciesCallbackStub = sinon.stub() - .resolves(requiredDependenciesCallbackStub); + const getDescriptorStub = sinon.stub().resolves({ + task: taskStub, + determineRequiredDependencies: requiredDependenciesCallbackStub + }); graph.getExtension.returns({ getName: () => "custom.task.a", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub, + getDescriptor: getDescriptorStub, getStepBased: async () => false, }); @@ -1292,8 +1280,9 @@ test("Custom task attached to a disabled task", async (t) => { // Standard tasks are step-based factories; the stub returns an empty step list so the step runner is // a no-op and this test only exercises task ordering and the custom task's execution. - taskRepository.getTask = sinon.stub().returns({task: sinon.stub().returns([])}); - customTask.getTask = () => customTaskFnStub; + taskRepository.getTask = sinon.stub().returns({task: sinon.stub().returns({steps: []})}); + // For V5 custom tasks the task body comes from descriptor.task, not getTask(). + customTask.getDescriptor = sinon.stub().resolves({task: customTaskFnStub}); const taskRunner = createTaskRunner(t, project); @@ -1325,15 +1314,16 @@ test.serial("_addTask", async (t) => { const {sinon, taskRepository} = t.context; const taskStub = sinon.stub(); - taskRepository.getTask.withArgs("standardTask").resolves({ - task: taskStub + // The factory wraps the task stub as the single-stage body in the descriptor. + taskRepository.getTask.withArgs("standardTask").returns({ + task: (opts) => ({task: taskStub}) }); const project = getMockProject("module"); const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - taskRunner._addTask("standardTask"); + await taskRunner._addTask("standardTask"); t.truthy(taskRunner._tasks["standardTask"], "Task has been added to task map"); t.deepEqual(taskRunner._tasks["standardTask"].requiredDependencies, new Set(), @@ -1365,7 +1355,7 @@ test.serial("_addTask with options", async (t) => { const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - taskRunner._addTask("standardTask", { + await taskRunner._addTask("standardTask", { requiresDependencies: true, options: { myTaskOption: "cat", @@ -1433,7 +1423,7 @@ test.serial("Folds taskUtil project-reader reads into the recorded resource requ const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - taskRunner._addTask("standardTask", {requiresDependencies: true, taskFunction: taskStub}); + await taskRunner._addTask("standardTask", {requiresDependencies: true, taskFunction: taskStub}); // Warm the cached dependencies reader (normally done by runTasks) await taskRunner.getDependenciesReader(new Set(["dep.a", "dep.b"]), true); @@ -1457,15 +1447,13 @@ test("_addTask: Duplicate task", async (t) => { const taskRunner = createTaskRunner(t, project); await taskRunner._initTasks(); - taskRunner._addTask("standardTask", { + await taskRunner._addTask("standardTask", { taskFunction: () => {} }); - const err = t.throws(() => { - taskRunner._addTask("standardTask", { - taskFunction: () => {} - }); - }); + const err = await t.throwsAsync(taskRunner._addTask("standardTask", { + taskFunction: () => {} + })); t.is(err.message, "Failed to add duplicate task standardTask for project project.b", "Threw with expected error message"); }); @@ -1476,11 +1464,9 @@ test("_addTask: Task already added to execution order", async (t) => { await taskRunner._initTasks(); taskRunner._taskExecutionOrder.push("standardTask"); - const err = t.throws(() => { - taskRunner._addTask("standardTask", { - taskFunction: () => {} - }); - }); + const err = await t.throwsAsync(taskRunner._addTask("standardTask", { + taskFunction: () => {} + })); t.is(err.message, "Failed to add task standardTask for project project.b. It has already been scheduled for execution", "Threw with expected error message"); @@ -1502,6 +1488,13 @@ test("getRequiredDependencies: Default application", async (t) => { // By default, builds of this project type always contain the "generateVersionInfo" task, // which requires dependencies to be built. + const {taskRepository} = t.context; + // generateVersionInfo's factory returns requiresDependencies: true in the real module. + // Override the default stub so _addTask reads the correct flag from the descriptor. + taskRepository.getTask.withArgs("generateVersionInfo").returns({ + task: () => ({task: async () => {}, requiresDependencies: true}) + }); + const project = getMockProject("application"); project.getBundles = () => []; const taskRunner = createTaskRunner(t, project); @@ -1520,6 +1513,16 @@ test("getRequiredDependencies: Default component", async (t) => { }); test("getRequiredDependencies: Default library", async (t) => { + const {taskRepository} = t.context; + // Several library tasks return requiresDependencies: true from their descriptors. + // Override the stubs so _addTask reads the correct flag. + const requiresDepFactory = {task: () => ({task: async () => {}, requiresDependencies: true})}; + taskRepository.getTask.withArgs("generateJsdoc").returns(requiresDepFactory); + taskRepository.getTask.withArgs("executeJsdocSdkTransformation").returns(requiresDepFactory); + taskRepository.getTask.withArgs("generateLibraryPreload").returns(requiresDepFactory); + taskRepository.getTask.withArgs("buildThemes").returns(requiresDepFactory); + taskRepository.getTask.withArgs("generateCachebusterInfo").returns(requiresDepFactory); + const project = getMockProject("library"); project.getBundles = () => []; const taskRunner = createTaskRunner(t, project); @@ -1528,6 +1531,13 @@ test("getRequiredDependencies: Default library", async (t) => { }); test("getRequiredDependencies: Default theme-library", async (t) => { + const {taskRepository} = t.context; + // buildThemes and generateThemeDesignerResources return requiresDependencies: true from their descriptors. + const requiresDepFactory = {task: () => ({task: async () => {}, requiresDependencies: true})}; + taskRepository.getTask.withArgs("buildThemes").returns(requiresDepFactory); + taskRepository.getTask.withArgs("generateThemeDesignerResources").returns(requiresDepFactory); + taskRepository.getTask.withArgs("generateCachebusterInfo").returns(requiresDepFactory); + const project = getMockProject("theme-library"); const taskRunner = createTaskRunner(t, project); @@ -1681,6 +1691,7 @@ test("Step-based task: a per-step input change re-runs only that step; a restore ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; let capturedInvocationData; @@ -1737,6 +1748,10 @@ function createCustomTaskExtension(sinon, {taskFunction, gte, stepBased = false} getSpecVersion: () => ({gte}), getTask: async () => taskFunction, getRequiredDependenciesCallback: sinon.stub().resolves(undefined), + // V5 path: descriptor carries either steps (step-based) or task (legacy body). + getDescriptor: sinon.stub().resolves( + stepBased ? {steps: taskFunction()} : {task: taskFunction} + ), getStepBased: async () => stepBased, }; } @@ -1773,6 +1788,7 @@ test("Step-based task: a full stage-cache hit re-runs a read-free consumer when ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; // Per-stage invocation data, keyed by stage id so 'scan' and 'use' carry their own data forward. @@ -1851,6 +1867,7 @@ test("Step-based task: the factory runs once per build and its steps are reused ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; // Only stages that setTasks created can be prepared, as in ProjectResources#useStage. @@ -1906,6 +1923,7 @@ test("Step-based task: a factory returning a non-array throws a named error at d ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const project = getMockProject("module"); @@ -1936,6 +1954,7 @@ test("Step-based task: a duplicate step name throws at discovery, before setTask ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const project = getMockProject("module"); @@ -1974,6 +1993,7 @@ test("Step-based task: the factory is called once, so discovery and execution ca ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const createdStages = new Set(); @@ -2013,8 +2033,10 @@ test("Step-based task: the factory is called once, so discovery and execution ca // The discovered step array is kept on the task, frozen so a custom task cannot mutate the shared value, and // re-derived on the next build so a surviving TaskRunner (reused across ui5 serve rebuilds) never serves a // stale array. Options are fixed for a TaskRunner's lifetime (a changed ui5.yaml rebuilds the whole stack), -// so re-deriving per build keeps the steps in step with the options without any explicit invalidation. -test("Step-based task: the kept step array is frozen and re-derived on each build", async (t) => { +// The step array in the descriptor is frozen at discovery time so a custom task cannot mutate the shared +// value. Because the descriptor is memoized by TaskDefinitions (shared with the build-signature phase), the +// same frozen array is reused across builds — a standard task factory is never called more than once. +test("Step-based task: the kept step array is frozen and reused across builds", async (t) => { const {sinon, taskUtil} = t.context; let callCount = 0; @@ -2030,6 +2052,7 @@ test("Step-based task: the kept step array is frozen and re-derived on each buil ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const createdStages = new Set(); @@ -2057,20 +2080,21 @@ test("Step-based task: the kept step array is frozen and re-derived on each buil const taskRunner = createTaskRunner(t, project, {taskUtil, buildCache, taskDefinitions}); sinon.stub(taskRunner, "getDependenciesReader").resolves({getName: () => "dependencies"}); - // First build: one factory call, the kept array is frozen. + // First build: factory called once via getStandardDescriptor, the kept array is frozen. await taskRunner.runTasks(); - t.is(callCount, 1, "The factory ran once for the first build"); + t.is(callCount, 1, "The factory ran once at discovery"); const firstSteps = taskRunner._tasks["stepTask"].steps; t.true(Object.isFrozen(firstSteps), "The kept step array is frozen"); t.throws(() => firstSteps.push({name: "injected"}), undefined, "A custom task cannot mutate the frozen step array"); - // Second build through the same TaskRunner: the factory runs again and the kept array is a fresh one. + // Second build through the same TaskRunner: the descriptor is memoized so the factory does not run again. + // The same frozen array is reused, so secondSteps === firstSteps. await taskRunner.runTasks(); - t.is(callCount, 2, "The factory ran once more for the second build (re-derived, not reused across builds)"); + t.is(callCount, 1, "The factory is not called again on the second build (descriptor memoized)"); const secondSteps = taskRunner._tasks["stepTask"].steps; - t.not(secondSteps, firstSteps, "The second build keeps a fresh step array, not the previous one"); - t.true(Object.isFrozen(secondSteps), "The re-derived array is frozen too"); + t.is(secondSteps, firstSteps, "The second build reuses the same frozen step array from the memoized descriptor"); + t.true(Object.isFrozen(secondSteps), "The reused array is still frozen"); }); // Integration: the custom-task path drives the same real MonitoredTaskUtil + StepRunner as the standard-task @@ -2207,6 +2231,7 @@ test("Step-based task: recordStage passes stale outputs without mutating the del ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const buildCache = { setTasks: sinon.stub(), @@ -2293,6 +2318,7 @@ test("Step-based task: recordStage dedups the fold against the monitored request ]), customTasks: new Map(), }), + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), }; const buildCache = { setTasks: sinon.stub(), @@ -2389,7 +2415,11 @@ function createStepReportingFixture(t, {stepBased, fullyCached = false}) { task: createCustomTaskExtension(sinon, {taskFunction: build, gte: () => true, stepBased: true}), }], ]) : new Map(); - const taskDefinitions = {getTaskDefinitions: async () => ({standardTasks, customTasks})}; + const taskDefinitions = { + getTaskDefinitions: async () => ({standardTasks, customTasks}), + // Standard step-based tasks use taskFunction as the factory; resolve it to a descriptor. + getStandardDescriptor: makeStepBasedGetStandardDescriptor(), + }; const buildCache = { ...t.context.buildCache, diff --git a/packages/project/test/lib/build/definitions/application.js b/packages/project/test/lib/build/definitions/application.js index e0267bf634d..9006acc235d 100644 --- a/packages/project/test/lib/build/definitions/application.js +++ b/packages/project/test/lib/build/definitions/application.js @@ -50,25 +50,21 @@ test("Standard build", (t) => { t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,json}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json}" }, }, minify: { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -76,7 +72,7 @@ test("Standard build", (t) => { ] }, }, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, generateFlexChangesBundle: {}, generateComponentPreload: { options: { @@ -85,15 +81,10 @@ test("Standard build", (t) => { skipBundles: [] } }, - generateStandaloneAppBundle: { - requiresDependencies: true - }, + generateStandaloneAppBundle: {}, transformBootstrapHtml: {}, - generateBundle: { - taskFunction: null - }, + generateBundle: {skip: true}, generateVersionInfo: { - requiresDependencies: true, options: { rootProject: project, pattern: "/resources/**/.library" @@ -104,12 +95,8 @@ test("Standard build", (t) => { signatureType: "PONY" } }, - generateApiIndex: { - requiresDependencies: true - }, - generateResourcesJson: { - requiresDependencies: true - } + generateApiIndex: {}, + generateResourcesJson: {} }, "Correct task definitions"); t.is(taskUtil.getBuildOption.callCount, 0, "taskUtil#getBuildOption has not been called"); @@ -134,25 +121,21 @@ test("Standard build with legacy spec version", (t) => { t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,json}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json}" }, }, minify: { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -160,7 +143,7 @@ test("Standard build with legacy spec version", (t) => { ] }, }, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, generateFlexChangesBundle: {}, generateComponentPreload: { options: { @@ -169,15 +152,10 @@ test("Standard build with legacy spec version", (t) => { skipBundles: [] } }, - generateStandaloneAppBundle: { - requiresDependencies: true - }, + generateStandaloneAppBundle: {}, transformBootstrapHtml: {}, - generateBundle: { - taskFunction: null - }, + generateBundle: {skip: true}, generateVersionInfo: { - requiresDependencies: true, options: { rootProject: project, pattern: "/resources/**/.library" @@ -188,12 +166,8 @@ test("Standard build with legacy spec version", (t) => { signatureType: "PONY" } }, - generateApiIndex: { - requiresDependencies: true - }, - generateResourcesJson: { - requiresDependencies: true - } + generateApiIndex: {}, + generateResourcesJson: {} }, "Correct task definitions"); }); @@ -240,8 +214,9 @@ test("Custom bundles", async (t) => { }]; const generateBundleTaskStub = sinon.stub(); + const generateBundleFactoryStub = sinon.stub().returns({task: generateBundleTaskStub}); getTask.returns({ - task: generateBundleTaskStub + task: generateBundleFactoryStub }); const tasks = application({ @@ -251,25 +226,21 @@ test("Custom bundles", async (t) => { t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,json}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json}" }, }, minify: { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -277,7 +248,7 @@ test("Custom bundles", async (t) => { ] }, }, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, generateFlexChangesBundle: {}, generateComponentPreload: { options: { @@ -289,16 +260,13 @@ test("Custom bundles", async (t) => { ] } }, - generateStandaloneAppBundle: { - requiresDependencies: true - }, + generateStandaloneAppBundle: {}, transformBootstrapHtml: {}, generateBundle: { requiresDependencies: true, taskFunction: generateBundleTaskDefinition.taskFunction }, generateVersionInfo: { - requiresDependencies: true, options: { rootProject: project, pattern: "/resources/**/.library" @@ -309,12 +277,8 @@ test("Custom bundles", async (t) => { signatureType: "PONY" } }, - generateApiIndex: { - requiresDependencies: true - }, - generateResourcesJson: { - requiresDependencies: true - } + generateApiIndex: {}, + generateResourcesJson: {} }, "Correct task definitions"); @@ -402,7 +366,6 @@ test("Minification excludes", (t) => { const taskDefinition = tasks.get("minify"); t.deepEqual(taskDefinition, { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -429,7 +392,6 @@ test("Minification excludes not applied for legacy specVersion", (t) => { const taskDefinition = tasks.get("minify"); t.deepEqual(taskDefinition, { - stepBased: true, options: { pattern: [ "/**/*.js", diff --git a/packages/project/test/lib/build/definitions/component.js b/packages/project/test/lib/build/definitions/component.js index 4baf6ca9a13..9440c279354 100644 --- a/packages/project/test/lib/build/definitions/component.js +++ b/packages/project/test/lib/build/definitions/component.js @@ -49,25 +49,21 @@ test("Standard build", (t) => { t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,json}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json}" }, }, minify: { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -75,7 +71,7 @@ test("Standard build", (t) => { ] }, }, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, generateFlexChangesBundle: {}, generateComponentPreload: { options: { @@ -84,17 +80,13 @@ test("Standard build", (t) => { skipBundles: [] } }, - generateBundle: { - taskFunction: null - }, + generateBundle: {skip: true}, generateCachebusterInfo: { options: { signatureType: "PONY" } }, - generateResourcesJson: { - requiresDependencies: true - } + generateResourcesJson: {} }, "Correct task definitions"); t.is(taskUtil.getBuildOption.callCount, 0, "taskUtil#getBuildOption has not been called"); @@ -141,8 +133,9 @@ test("Custom bundles", async (t) => { }]; const generateBundleTaskStub = sinon.stub(); + const generateBundleFactoryStub = sinon.stub().returns({task: generateBundleTaskStub}); getTask.returns({ - task: generateBundleTaskStub + task: generateBundleFactoryStub }); const tasks = component({ @@ -152,25 +145,21 @@ test("Custom bundles", async (t) => { t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,json}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json}" }, }, minify: { - stepBased: true, options: { pattern: [ "/**/*.js", @@ -178,7 +167,7 @@ test("Custom bundles", async (t) => { ] }, }, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, generateFlexChangesBundle: {}, generateComponentPreload: { options: { @@ -199,9 +188,7 @@ test("Custom bundles", async (t) => { signatureType: "PONY" } }, - generateResourcesJson: { - requiresDependencies: true - } + generateResourcesJson: {} }, "Correct task definitions"); @@ -289,7 +276,6 @@ test("Minification excludes", (t) => { const taskDefinition = tasks.get("minify"); t.deepEqual(taskDefinition, { - stepBased: true, options: { pattern: [ "/**/*.js", diff --git a/packages/project/test/lib/build/definitions/library.js b/packages/project/test/lib/build/definitions/library.js index c02b1cd1b26..664385b00ad 100644 --- a/packages/project/test/lib/build/definitions/library.js +++ b/packages/project/test/lib/build/definitions/library.js @@ -46,59 +46,49 @@ test.beforeEach((t) => { t.context.getTask = sinon.stub(); }); -test("Standard build", async (t) => { +test("Standard build", (t) => { const {project, taskUtil, getTask} = t.context; project.getJsdocExcludes = () => ["**.html"]; - const generateJsdocTaskStub = sinon.stub(); - getTask.returns({ - task: generateJsdocTaskStub - }); - const tasks = library({ project, taskUtil, getTask }); - const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" }, }, replaceBuildtime: { - stepBased: true, options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" }, }, generateJsdoc: { - determineBuildSignature: generateJsdocTaskDefinition.determineBuildSignature, - requiresDependencies: true, - taskFunction: generateJsdocTaskDefinition.taskFunction + options: { + namespace: "project/b", + version: "version", + jsdocExcludes: ["**.html"] + } }, executeJsdocSdkTransformation: { - requiresDependencies: true, options: { dotLibraryPattern: "/resources/**/*.library" } }, minify: { - stepBased: true, options: { pattern: [ "/resources/**/*.js", @@ -107,15 +97,15 @@ test("Standard build", async (t) => { }, }, generateLibraryManifest: {}, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, + generateComponentPreload: {skip: true}, generateLibraryPreload: { options: { excludes: [], skipBundles: [] } }, + generateBundle: {skip: true}, buildThemes: { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, @@ -123,45 +113,9 @@ test("Standard build", async (t) => { inputPattern: "/resources/project/b/themes/*/library.source.less", } }, - generateBundle: { - taskFunction: null - }, - generateComponentPreload: { - taskFunction: null - }, - generateThemeDesignerResources: { - taskFunction: null - }, - generateResourcesJson: { - requiresDependencies: true - } + generateThemeDesignerResources: {skip: true}, + generateResourcesJson: {} }, "Correct task definitions"); - - - await generateJsdocTaskDefinition.taskFunction({ - workspace: "workspace", - dependencies: "dependencies", - taskUtil, - options: { - projectName: "projectName" - } - }); - - t.is(generateJsdocTaskStub.callCount, 1, "generateJsdoc task got called once"); - t.deepEqual(generateJsdocTaskStub.getCall(0).args[0], { - workspace: "workspace", - dependencies: "dependencies", - taskUtil, - options: { - projectName: "projectName", - namespace: "project/b", - version: "version", - pattern: [ - "/resources/**/*.js", - "!/resources/**.html" - ], - } - }, "generateBundle task got called with correct arguments"); }); test("Standard build (framework project)", (t) => { @@ -169,18 +123,12 @@ test("Standard build (framework project)", (t) => { project.isFrameworkProject = () => true; - const generateJsdocTaskStub = sinon.stub(); - getTask.returns({ - task: generateJsdocTaskStub - }); - const tasks = library({ project, taskUtil, getTask }); t.deepEqual(tasks.get("generateThemeDesignerResources"), { - stepBased: true, - requiresDependencies: true, options: { + options: { version: "version" } }); @@ -199,47 +147,42 @@ test("Standard build with legacy spec version", (t) => { const tasks = library({ project, taskUtil, getTask }); - const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" }, }, replaceBuildtime: { - stepBased: true, options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" }, }, generateJsdoc: { - determineBuildSignature: generateJsdocTaskDefinition.determineBuildSignature, - requiresDependencies: true, - taskFunction: generateJsdocTaskDefinition.taskFunction + options: { + namespace: "project/b", + version: "version", + jsdocExcludes: [] + } }, executeJsdocSdkTransformation: { - requiresDependencies: true, options: { dotLibraryPattern: "/resources/**/*.library" } }, minify: { - stepBased: true, options: { pattern: [ "/resources/**/*.js", @@ -248,15 +191,15 @@ test("Standard build with legacy spec version", (t) => { }, }, generateLibraryManifest: {}, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, + generateComponentPreload: {skip: true}, generateLibraryPreload: { options: { excludes: [], skipBundles: [] } }, + generateBundle: {skip: true}, buildThemes: { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, @@ -264,18 +207,8 @@ test("Standard build with legacy spec version", (t) => { inputPattern: "/resources/project/b/themes/*/library.source.less", } }, - generateBundle: { - taskFunction: null - }, - generateComponentPreload: { - taskFunction: null - }, - generateThemeDesignerResources: { - taskFunction: null - }, - generateResourcesJson: { - requiresDependencies: true - } + generateThemeDesignerResources: {skip: true}, + generateResourcesJson: {} }, "Correct task definitions"); }); @@ -322,56 +255,52 @@ test("Custom bundles", async (t) => { }]; const generateBundleTaskStub = sinon.stub(); + const generateBundleFactoryStub = sinon.stub().returns({task: generateBundleTaskStub}); getTask.returns({ - task: generateBundleTaskStub + task: generateBundleFactoryStub }); const tasks = library({ project, taskUtil, getTask }); - const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); const generateBundleTaskDefinition = tasks.get("generateBundle"); t.deepEqual(Object.fromEntries(tasks), { escapeNonAsciiCharacters: { - stepBased: true, options: { encoding: "UTF-412", pattern: "/**/*.properties" } }, replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/**/*.{js,library,css,less,theme,html}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/**/*.{js,json,library,css,less,theme,html}" }, }, replaceBuildtime: { - stepBased: true, options: { pattern: "/resources/sap/ui/{Global,core/Core}.js" }, }, generateJsdoc: { - determineBuildSignature: generateJsdocTaskDefinition.determineBuildSignature, - requiresDependencies: true, - taskFunction: generateJsdocTaskDefinition.taskFunction + options: { + namespace: "project/b", + version: "version", + jsdocExcludes: [] + } }, executeJsdocSdkTransformation: { - requiresDependencies: true, options: { dotLibraryPattern: "/resources/**/*.library" } }, minify: { - stepBased: true, options: { pattern: [ "/resources/**/*.js", @@ -380,7 +309,8 @@ test("Custom bundles", async (t) => { }, }, generateLibraryManifest: {}, - enhanceManifest: {stepBased: true}, + enhanceManifest: {}, + generateComponentPreload: {skip: true}, generateLibraryPreload: { options: { excludes: [], @@ -395,8 +325,6 @@ test("Custom bundles", async (t) => { taskFunction: generateBundleTaskDefinition.taskFunction }, buildThemes: { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, @@ -404,15 +332,8 @@ test("Custom bundles", async (t) => { inputPattern: "/resources/project/b/themes/*/library.source.less", } }, - generateComponentPreload: { - taskFunction: null - }, - generateThemeDesignerResources: { - taskFunction: null - }, - generateResourcesJson: { - requiresDependencies: true - } + generateThemeDesignerResources: {skip: true}, + generateResourcesJson: {} }, "Correct task definitions"); @@ -500,7 +421,6 @@ test("Minification excludes", (t) => { const taskDefinition = tasks.get("minify"); t.deepEqual(taskDefinition, { - stepBased: true, options: { pattern: [ "/resources/**/*.js", @@ -528,7 +448,6 @@ test("Minification excludes not applied for legacy specVersion", (t) => { const taskDefinition = tasks.get("minify"); t.deepEqual(taskDefinition, { - stepBased: true, options: { pattern: [ "/resources/**/*.js", @@ -640,8 +559,6 @@ test("buildThemes: Project is not root", (t) => { const taskDefinition = tasks.get("buildThemes"); t.deepEqual(taskDefinition, { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: "/resources/**/(*.library|library.js)", @@ -650,6 +567,7 @@ test("buildThemes: Project is not root", (t) => { } }, "Correct buildThemes task definition"); }); + test("generateLibraryManifest: specVersion 5.0, non-framework project", (t) => { const {project, taskUtil, getTask} = t.context; @@ -664,7 +582,7 @@ test("generateLibraryManifest: specVersion 5.0, non-framework project", (t) => { const tasks = library({project, taskUtil, getTask}); - t.deepEqual(tasks.get("generateLibraryManifest"), {taskFunction: null}, + t.deepEqual(tasks.get("generateLibraryManifest"), {skip: true}, "generateLibraryManifest is skipped for non-framework libraries on specVersion 5.0"); }); @@ -696,8 +614,6 @@ test("buildThemes: CSS Variables enabled", (t) => { const taskDefinition = tasks.get("buildThemes"); t.deepEqual(taskDefinition, { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, @@ -707,96 +623,17 @@ test("buildThemes: CSS Variables enabled", (t) => { }, "Correct buildThemes task definition"); }); -test("Standard build: nulled taskFunction to skip tasks", (t) => { +test("Standard build: skipped tasks use skip flag", (t) => { const {project, taskUtil, getTask} = t.context; - project.getJsdocExcludes = () => ["**.html"]; const tasks = library({ project, taskUtil, getTask }); - const generateComponentPreloadTaskDefinition = tasks.get("generateComponentPreload"); - const generateBundleTaskDefinition = tasks.get("generateBundle"); - const generateThemeDesignerResourcesTaskDefinition = tasks.get("generateThemeDesignerResources"); - const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); - t.deepEqual(Object.fromEntries(tasks), { - escapeNonAsciiCharacters: { - stepBased: true, - options: { - encoding: "UTF-412", pattern: "/**/*.properties" - } - }, - replaceCopyright: { - stepBased: true, - options: { - copyright: "copyright", - pattern: "/**/*.{js,library,css,less,theme,html}" - }, - }, - replaceVersion: { - stepBased: true, - options: { - version: "version", - pattern: "/**/*.{js,json,library,css,less,theme,html}" - }, - }, - replaceBuildtime: { - stepBased: true, - options: { - pattern: "/resources/sap/ui/{Global,core/Core}.js" - }, - }, - generateJsdoc: { - determineBuildSignature: generateJsdocTaskDefinition.determineBuildSignature, - requiresDependencies: true, - taskFunction: async () => {}, - }, - executeJsdocSdkTransformation: { - requiresDependencies: true, - options: { - dotLibraryPattern: "/resources/**/*.library" - } - }, - minify: { - stepBased: true, - options: { - pattern: [ - "/resources/**/*.js", - "!**/*.support.js", - ] - }, - }, - generateLibraryManifest: {}, - enhanceManifest: {stepBased: true}, - generateLibraryPreload: { - options: { - excludes: [], skipBundles: [] - } - }, - buildThemes: { - stepBased: true, - requiresDependencies: true, - options: { - projectName: "project.b", - librariesPattern: undefined, - themesPattern: undefined, - inputPattern: "/resources/project/b/themes/*/library.source.less", - } - }, - generateBundle: { - taskFunction: null - }, - generateComponentPreload: { - taskFunction: null - }, - generateThemeDesignerResources: { - taskFunction: null - }, - generateResourcesJson: { - requiresDependencies: true - } - }, "Correct task definitions"); - t.is(generateComponentPreloadTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null"); - t.is(generateBundleTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null"); - t.is(generateThemeDesignerResourcesTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null"); + t.deepEqual(tasks.get("generateComponentPreload"), {skip: true}, + "generateComponentPreload is skipped when no paths or namespaces"); + t.deepEqual(tasks.get("generateBundle"), {skip: true}, + "generateBundle is skipped when no bundles"); + t.deepEqual(tasks.get("generateThemeDesignerResources"), {skip: true}, + "generateThemeDesignerResources is skipped for non-framework projects"); }); diff --git a/packages/project/test/lib/build/definitions/themeLibrary.js b/packages/project/test/lib/build/definitions/themeLibrary.js index cd9209a03c6..6c321828de0 100644 --- a/packages/project/test/lib/build/definitions/themeLibrary.js +++ b/packages/project/test/lib/build/definitions/themeLibrary.js @@ -47,25 +47,20 @@ test("Standard build", (t) => { const tasks = themeLibrary({ project, taskUtil, getTask }); - const generateThemeDesignerResourcesTaskFunction = tasks.get("generateThemeDesignerResources"); t.deepEqual(Object.fromEntries(tasks), { replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/resources/**/*.{less,theme}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/resources/**/*.{less,theme}" }, }, buildThemes: { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, @@ -73,16 +68,9 @@ test("Standard build", (t) => { inputPattern: "/resources/**/themes/*/library.source.less", } }, - generateResourcesJson: { - requiresDependencies: true - }, - generateThemeDesignerResources: { - taskFunction: null - } + generateThemeDesignerResources: {skip: true}, + generateResourcesJson: {} }, "Correct task definitions"); - - - t.is(generateThemeDesignerResourcesTaskFunction.taskFunction, null, "taskFunction is explicitly set to null"); }); test("Standard build (framework project)", (t) => { @@ -95,8 +83,7 @@ test("Standard build (framework project)", (t) => { }); t.deepEqual(tasks.get("generateThemeDesignerResources"), { - stepBased: true, - requiresDependencies: true, options: { + options: { version: "version" } }); @@ -111,22 +98,18 @@ test("Standard build for non root project", (t) => { }); t.deepEqual(Object.fromEntries(tasks), { replaceCopyright: { - stepBased: true, options: { copyright: "copyright", pattern: "/resources/**/*.{less,theme}" }, }, replaceVersion: { - stepBased: true, options: { version: "version", pattern: "/resources/**/*.{less,theme}" }, }, buildThemes: { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: "/resources/**/(*.library|library.js)", @@ -134,12 +117,8 @@ test("Standard build for non root project", (t) => { inputPattern: "/resources/**/themes/*/library.source.less", } }, - generateResourcesJson: { - requiresDependencies: true - }, - generateThemeDesignerResources: { - taskFunction: null - } + generateThemeDesignerResources: {skip: true}, + generateResourcesJson: {} }, "Correct task definitions"); }); @@ -153,8 +132,6 @@ test("CSS variables enabled", (t) => { const taskDefinition = tasks.get("buildThemes"); t.deepEqual(taskDefinition, { - stepBased: true, - requiresDependencies: true, options: { projectName: "project.b", librariesPattern: undefined, diff --git a/packages/project/test/lib/build/helpers/getBuildSignature.js b/packages/project/test/lib/build/helpers/getBuildSignature.js index de747517386..34ac637fdd7 100644 --- a/packages/project/test/lib/build/helpers/getBuildSignature.js +++ b/packages/project/test/lib/build/helpers/getBuildSignature.js @@ -2,7 +2,7 @@ import test from "ava"; import crypto from "node:crypto"; import esmock from "esmock"; -const BUILD_SIG_VERSION = "0"; +const BUILD_SIG_VERSION = "1"; function hash(key) { return crypto.createHash("sha256").update(key).digest("hex"); From 5e0a77e42f90c3f85614bff9c0d0f76bbe40c6b5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 14 Oct 2026 09:44:18 +0200 Subject: [PATCH 2/8] refactor(builder): Extract task bodies and fix descriptor JSDoc The specVersion 5.0 task-descriptor refactor left the legacy `task`-based standard tasks with the body inline in the returned descriptor and a JSDoc block that still documented the task-body parameters on the factory (the default export), guarded by `/* eslint "jsdoc/check-param-names": "off" */`. For every `task`-based standard task, move the body to a module-scoped `task` function (a local `const` in generateJsdoc, where it closes over factory-precomputed values) and carry the parameter JSDoc with it. The factory JSDoc now documents only its own contract (`@returns {object} The task descriptor`), so the eslint-disable is removed. Options the body reads are destructured in its signature so the documented names match. Split descriptors that packed multiple properties onto one line (`{requiresDependencies: true, steps: [...]}`) onto separate lines, lifting a single step into a named const where that keeps the step body from gaining a further indent level. No behavior change: the descriptor shape each factory returns is unchanged, so `generateX().task(...)` and the step arrays stay identical. Builder lint and unit tests pass. --- packages/builder/lib/tasks/buildThemes.js | 9 +- .../bundlers/generateComponentPreload.js | 286 ++++----- .../bundlers/generateFlexChangesBundle.js | 244 ++++---- .../tasks/bundlers/generateLibraryPreload.js | 556 +++++++++--------- .../bundlers/generateStandaloneAppBundle.js | 238 ++++---- .../lib/tasks/generateCachebusterInfo.js | 59 +- .../lib/tasks/generateLibraryManifest.js | 108 ++-- .../lib/tasks/generateResourcesJson.js | 59 +- .../tasks/generateThemeDesignerResources.js | 10 +- .../builder/lib/tasks/generateVersionInfo.js | 75 +-- .../jsdoc/executeJsdocSdkTransformation.js | 107 ++-- .../lib/tasks/jsdoc/generateApiIndex.js | 68 +-- .../builder/lib/tasks/jsdoc/generateJsdoc.js | 100 ++-- .../lib/tasks/transformBootstrapHtml.js | 51 +- 14 files changed, 1009 insertions(+), 961 deletions(-) diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index b282d0d3378..72cb390d1b5 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -185,7 +185,7 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP export default function build({projectName, inputPattern, librariesPattern, themesPattern, compress}) { compress = compress === undefined ? true : compress; - return {requiresDependencies: true, steps: [{ + const buildThemesStep = { name: "buildThemes", // One key per theme's library.source.less, so a delta build rebuilds only the affected theme. keys: async ({workspace}) => workspace.byGlob(inputPattern), @@ -204,5 +204,10 @@ export default function build({projectName, inputPattern, librariesPattern, them const processedResources = await buildThemeResources([themeResource], combo, compress, taskUtil); await Promise.all(processedResources.map((resource) => workspace.write(resource))); }, - }]}; + }; + + return { + requiresDependencies: true, + steps: [buildThemesStep] + }; } diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index b050aaefaa4..d09c58a177f 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -10,7 +10,6 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @module @ui5/builder/tasks/bundlers/generateComponentPreload */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task to for application bundling. * @@ -18,6 +17,13 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @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 @@ -32,157 +38,155 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @param {string[]} [parameters.options.paths] Array of paths (or glob patterns) for component files * @param {string[]} [parameters.options.namespaces] Array of component namespaces * @param {string[]} [parameters.options.skipBundles] Names of bundles that should not be created - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the component preload bundles have been written */ -export default function() { - return {task: async function({ - workspace, taskUtil, options: {projectName, paths, namespaces, skipBundles = [], excludes = []} - }) { - let nonDbgWorkspace = workspace; - if (taskUtil) { - nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } +async function task({ + workspace, taskUtil, options: {projectName, paths, namespaces, skipBundles = [], excludes = []} +}) { + let nonDbgWorkspace = workspace; + if (taskUtil) { + nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + } + }); + } - return nonDbgWorkspace.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}") - .then(async (resources) => { - let allNamespaces = []; - if (paths) { - allNamespaces = await Promise.all(paths.map(async (componentPath) => { - const globPath = "/resources/" + componentPath; - log.verbose(`Globbing for Components directories with configured path ${globPath}...`); - const components = await nonDbgWorkspace.byGlob(globPath); - return components.map((component) => { - const compDir = path.dirname(component.getPath()).replace(/^\/resources\//i, ""); - log.verbose(`Found component namespace ${compDir}`); - return compDir; - }); - })); - } - if (namespaces) { - allNamespaces.push(...namespaces); - } + return nonDbgWorkspace.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}") + .then(async (resources) => { + let allNamespaces = []; + if (paths) { + allNamespaces = await Promise.all(paths.map(async (componentPath) => { + const globPath = "/resources/" + componentPath; + log.verbose(`Globbing for Components directories with configured path ${globPath}...`); + const components = await nonDbgWorkspace.byGlob(globPath); + return components.map((component) => { + const compDir = path.dirname(component.getPath()).replace(/^\/resources\//i, ""); + log.verbose(`Found component namespace ${compDir}`); + return compDir; + }); + })); + } + if (namespaces) { + allNamespaces.push(...namespaces); + } + + allNamespaces = Array.prototype.concat.apply([], allNamespaces); + // As this task is often called with a single namespace, also check + // for bad calls like "namespaces: [undefined]" + if (!allNamespaces || !allNamespaces.length || !allNamespaces[0]) { + throw new Error("generateComponentPreload: No component namespace(s) " + + `found for project: ${projectName}`); + } - allNamespaces = Array.prototype.concat.apply([], allNamespaces); - // As this task is often called with a single namespace, also check - // for bad calls like "namespaces: [undefined]" - if (!allNamespaces || !allNamespaces.length || !allNamespaces[0]) { - throw new Error("generateComponentPreload: No component namespace(s) " + - `found for project: ${projectName}`); + const allFilterExcludes = negateFilters(excludes); + const unusedFilterExcludes = new Set(allFilterExcludes); + + const bundleDefinitions = allNamespaces.map((namespace) => { + const bundleName = `${namespace}/Component-preload.js`; + if (skipBundles.includes(bundleName)) { + log.verbose(`Skipping generation of bundle ${bundleName}`); + return null; } - const allFilterExcludes = negateFilters(excludes); - const unusedFilterExcludes = new Set(allFilterExcludes); + const filters = [ + `${namespace}/`, + `${namespace}/**/manifest.json`, + `${namespace}/changes/changes-bundle.json`, + `${namespace}/changes/flexibility-bundle.json`, + `!${namespace}/test/` + ]; - const bundleDefinitions = allNamespaces.map((namespace) => { - const bundleName = `${namespace}/Component-preload.js`; - if (skipBundles.includes(bundleName)) { - log.verbose(`Skipping generation of bundle ${bundleName}`); - return null; + // Add configured excludes for namespace + allFilterExcludes.forEach((filterExclude) => { + // Allow all excludes (!) and limit re-includes (+) to the component namespace + if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) { + filters.push(filterExclude); + unusedFilterExcludes.delete(filterExclude); } + }); - const filters = [ - `${namespace}/`, - `${namespace}/**/manifest.json`, - `${namespace}/changes/changes-bundle.json`, - `${namespace}/changes/flexibility-bundle.json`, - `!${namespace}/test/` - ]; - - // Add configured excludes for namespace - allFilterExcludes.forEach((filterExclude) => { - // Allow all excludes (!) and limit re-includes (+) to the component namespace - if (filterExclude.startsWith("!") || filterExclude.startsWith(`+${namespace}/`)) { - filters.push(filterExclude); - unusedFilterExcludes.delete(filterExclude); - } - }); + // Exclude other namespaces at the end of filter list to override potential re-includes + // from "excludes" config + allNamespaces.forEach((ns) => { + if (ns !== namespace && ns.startsWith(`${namespace}/`)) { + filters.push(`!${ns}/`); + // Explicitly exclude manifest.json files of subcomponents since the general exclude + // above this comment only applies to the configured default file types, which do + // not include ".json" + filters.push(`!${ns}/**/manifest.json`); + } + }); - // Exclude other namespaces at the end of filter list to override potential re-includes - // from "excludes" config - allNamespaces.forEach((ns) => { - if (ns !== namespace && ns.startsWith(`${namespace}/`)) { - filters.push(`!${ns}/`); - // Explicitly exclude manifest.json files of subcomponents since the general exclude - // above this comment only applies to the configured default file types, which do - // not include ".json" - filters.push(`!${ns}/**/manifest.json`); + return { + name: bundleName, + defaultFileTypes: [ + ".js", + ".control.xml", + ".fragment.html", + ".fragment.json", + ".fragment.xml", + ".view.html", + ".view.json", + ".view.xml", + ".properties" + ], + sections: [ + { + mode: "preload", + filters: filters, + resolve: false, + resolveConditional: false, + renderer: false } - }); + ] + }; + }); - return { - name: bundleName, - defaultFileTypes: [ - ".js", - ".control.xml", - ".fragment.html", - ".fragment.json", - ".fragment.xml", - ".view.html", - ".view.json", - ".view.xml", - ".properties" - ], - sections: [ - { - mode: "preload", - filters: filters, - resolve: false, - resolveConditional: false, - renderer: false - } - ] - }; + if (unusedFilterExcludes.size > 0) { + unusedFilterExcludes.forEach((filterExclude) => { + log.warn( + `Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` + + `Re-includes must start with a component namespace (${allNamespaces.join(" or ")})` + ); }); - - if (unusedFilterExcludes.size > 0) { - unusedFilterExcludes.forEach((filterExclude) => { - log.warn( - `Configured preload exclude contains invalid re-include: !${filterExclude.substr(1)}. ` + - `Re-includes must start with a component namespace (${allNamespaces.join(" or ")})` - ); - }); + } + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => { + log.verbose(`Generating ${bundleDefinition.name}...`); + const options = { + bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), + bundleOptions: { + ignoreMissingModules: true, + optimize: true + }, + allowStringBundling + }; + if (coreVersion) { + options.targetUi5CoreVersion = coreVersion; } - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => { - log.verbose(`Generating ${bundleDefinition.name}...`); - const options = { - bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), - bundleOptions: { - ignoreMissingModules: true, - optimize: true - }, - allowStringBundling - }; - if (coreVersion) { - options.targetUi5CoreVersion = coreVersion; - } - return moduleBundler({ - resources, - options - }); - })); - }) - .then((results) => { - const bundles = Array.prototype.concat.apply([], results); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - } - return Promise.all([ - workspace.write(bundle), - workspace.write(sourceMap) - ]); - })); - }); - }}; + return moduleBundler({ + resources, + options + }); + })); + }) + .then((results) => { + const bundles = Array.prototype.concat.apply([], results); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + return Promise.all([ + workspace.write(bundle), + workspace.write(sourceMap) + ]); + })); + }); } diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index 1efd51589ad..85f0344a711 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -8,7 +8,6 @@ import semver from "semver"; * @module @ui5/builder/tasks/bundlers/generateFlexChangesBundle */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task to create changes-bundle.json file containing all changes stored in the /changes folder for easier consumption * at runtime. @@ -21,141 +20,146 @@ import semver from "semver"; * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @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 {object} [parameters.options] Options * @param {string} [parameters.options.projectNamespace] Project Namespace - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the flex changes bundle has been written */ -export default function() { - return {task: async function({workspace, taskUtil, options = {}}) { - const namespace = options.projectNamespace; - - // Use the given namespace if available, otherwise use no namespace - // (e.g. in case no manifest.json is present) - let pathPrefix = ""; - if (namespace) { - pathPrefix = `/resources/${namespace}`; - } - - /** - * Update the manifest.json JSON in-place with the flexBundle flag and, if a bundle was - * written, ensure the sap.ui.fl dependency is present (and lazy loading disabled). - * - * @param {object} data Parsed manifest.json content - * @param {boolean|undefined} flexBundleFlag Result flag from the flex changes bundler: - * - `true`: bundle written including annotation changes - * - `undefined`: bundle written but without annotation changes - * - `false`: no bundle written - */ - function updateJson(data, flexBundleFlag) { - data["sap.ui5"] = data["sap.ui5"] || {}; - // explicit set the flag to inform the runtime if a bundle is present before the preload can be interpreted - data["sap.ui5"].flexBundle = flexBundleFlag; - - // A bundle has been written whenever the flag is not explicitly `false` - this includes the `undefined`. - // In all these cases the sap.ui.fl library dependency is required. - if (flexBundleFlag !== false) { - // ensure the existence of the libs section in the dependencies - data["sap.ui5"].dependencies = data["sap.ui5"].dependencies || {}; - const mLibs = data["sap.ui5"].dependencies.libs = data["sap.ui5"].dependencies.libs || {}; - - if (mLibs["sap.ui.fl"]) { - log.verbose("sap.ui.fl found in manifest.json"); - if (mLibs["sap.ui.fl"].lazy) { - log.verbose("sap.ui.fl 'lazy' attribute found in manifest.json, setting it to false..."); - mLibs["sap.ui.fl"].lazy = false; - } - } else { - log.verbose("sap.ui.fl not found in manifest.json, inserting it..."); - mLibs["sap.ui.fl"] = {}; +async function task({workspace, taskUtil, options: {projectNamespace} = {}}) { + const namespace = projectNamespace; + + // Use the given namespace if available, otherwise use no namespace + // (e.g. in case no manifest.json is present) + let pathPrefix = ""; + if (namespace) { + pathPrefix = `/resources/${namespace}`; + } + + /** + * Update the manifest.json JSON in-place with the flexBundle flag and, if a bundle was + * written, ensure the sap.ui.fl dependency is present (and lazy loading disabled). + * + * @param {object} data Parsed manifest.json content + * @param {boolean|undefined} flexBundleFlag Result flag from the flex changes bundler: + * - `true`: bundle written including annotation changes + * - `undefined`: bundle written but without annotation changes + * - `false`: no bundle written + */ + function updateJson(data, flexBundleFlag) { + data["sap.ui5"] = data["sap.ui5"] || {}; + // explicit set the flag to inform the runtime if a bundle is present before the preload can be interpreted + data["sap.ui5"].flexBundle = flexBundleFlag; + + // A bundle has been written whenever the flag is not explicitly `false` - this includes the `undefined`. + // In all these cases the sap.ui.fl library dependency is required. + if (flexBundleFlag !== false) { + // ensure the existence of the libs section in the dependencies + data["sap.ui5"].dependencies = data["sap.ui5"].dependencies || {}; + const mLibs = data["sap.ui5"].dependencies.libs = data["sap.ui5"].dependencies.libs || {}; + + if (mLibs["sap.ui.fl"]) { + log.verbose("sap.ui.fl found in manifest.json"); + if (mLibs["sap.ui.fl"].lazy) { + log.verbose("sap.ui.fl 'lazy' attribute found in manifest.json, setting it to false..."); + mLibs["sap.ui.fl"].lazy = false; } + } else { + log.verbose("sap.ui.fl not found in manifest.json, inserting it..."); + mLibs["sap.ui.fl"] = {}; } } + } - async function updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag) { - const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); - if (!manifestResource) { - log.verbose("No manifest.json found, skipping update of sap.ui.fl dependency and flexBundle flag"); - return; - } - const manifestContent = JSON.parse(await manifestResource.getString()); + async function updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag) { + const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); + if (!manifestResource) { + log.verbose("No manifest.json found, skipping update of sap.ui.fl dependency and flexBundle flag"); + return; + } + const manifestContent = JSON.parse(await manifestResource.getString()); - updateJson(manifestContent, flexBundleFlag); - manifestResource.setString(JSON.stringify(manifestContent, null, "\t")); + updateJson(manifestContent, flexBundleFlag); + manifestResource.setString(JSON.stringify(manifestContent, null, "\t")); - await workspace.write(manifestResource); - } + await workspace.write(manifestResource); + } - async function readManifestMinUI5Version() { - const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); - if (!manifestResource) { - log.verbose("No manifest.json found, cannot read minUI5Version"); - return []; - } - const manifestContent = JSON.parse(await manifestResource.getString()); - - manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {}; - manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {}; - if (!Array.isArray(manifestContent["sap.ui5"].dependencies.minUI5Version)) { - manifestContent["sap.ui5"].dependencies.minUI5Version = - // eslint-disable-next-line no-constant-binary-expression - [manifestContent["sap.ui5"].dependencies.minUI5Version] || [""]; - } - return manifestContent["sap.ui5"].dependencies.minUI5Version; + async function readManifestMinUI5Version() { + const manifestResource = await workspace.byPath(`${pathPrefix}/manifest.json`); + if (!manifestResource) { + log.verbose("No manifest.json found, cannot read minUI5Version"); + return []; } - - log.verbose("Collecting flexibility changes"); - const allResources = await workspace.byGlob( - pathPrefix + "/changes/*.{annotation_change,change,variant,ctrl_variant," + - "ctrl_variant_change,ctrl_variant_management_change}" - ); - - let flexChangesBundleResult; - - if (allResources.length > 0) { - const versionArray = await readManifestMinUI5Version(); - const versions = versionArray.map((version) => semver.coerce(version)); - const versionsAllSuitableForFlexBundle = - versions.every((version) => semver.compare(version, "1.73.0") >= 0); - let hasFlexBundleVersion = false; - let flexBundle = {}; - if (versionsAllSuitableForFlexBundle) { - hasFlexBundleVersion = true; - const flexBundleResource = await workspace.byPath(`${pathPrefix}/changes/flexibility-bundle.json`); - if (flexBundleResource) { - flexBundle = JSON.parse(await flexBundleResource.getString()); - } + const manifestContent = JSON.parse(await manifestResource.getString()); + + manifestContent["sap.ui5"] = manifestContent["sap.ui5"] || {}; + manifestContent["sap.ui5"].dependencies = manifestContent["sap.ui5"].dependencies || {}; + if (!Array.isArray(manifestContent["sap.ui5"].dependencies.minUI5Version)) { + manifestContent["sap.ui5"].dependencies.minUI5Version = + // eslint-disable-next-line no-constant-binary-expression + [manifestContent["sap.ui5"].dependencies.minUI5Version] || [""]; + } + return manifestContent["sap.ui5"].dependencies.minUI5Version; + } + + log.verbose("Collecting flexibility changes"); + const allResources = await workspace.byGlob( + pathPrefix + "/changes/*.{annotation_change,change,variant,ctrl_variant," + + "ctrl_variant_change,ctrl_variant_management_change}" + ); + + let flexChangesBundleResult; + + if (allResources.length > 0) { + const versionArray = await readManifestMinUI5Version(); + const versions = versionArray.map((version) => semver.coerce(version)); + const versionsAllSuitableForFlexBundle = + versions.every((version) => semver.compare(version, "1.73.0") >= 0); + let hasFlexBundleVersion = false; + let flexBundle = {}; + if (versionsAllSuitableForFlexBundle) { + hasFlexBundleVersion = true; + const flexBundleResource = await workspace.byPath(`${pathPrefix}/changes/flexibility-bundle.json`); + if (flexBundleResource) { + flexBundle = JSON.parse(await flexBundleResource.getString()); } - flexChangesBundleResult = await flexChangesBundler({ - resources: allResources, - options: { - pathPrefix, - hasFlexBundleVersion - }, - existingFlexBundle: flexBundle + } + flexChangesBundleResult = await flexChangesBundler({ + resources: allResources, + options: { + pathPrefix, + hasFlexBundleVersion + }, + existingFlexBundle: flexBundle + }); + + const bundleResources = flexChangesBundleResult.bundleResources; + + await Promise.all(bundleResources.map((resource) => { + log.verbose("Writing flexibility changes bundle"); + return workspace.write(resource); + })); + + // Do not write bundled source files to build result + if (taskUtil) { + allResources.forEach((resource) => { + taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); }); - - const bundleResources = flexChangesBundleResult.bundleResources; - - await Promise.all(bundleResources.map((resource) => { - log.verbose("Writing flexibility changes bundle"); - return workspace.write(resource); - })); - - // Do not write bundled source files to build result - if (taskUtil) { - allResources.forEach((resource) => { - taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - }); - } } + } - // Always append the flexBundle flag to the manifest.json, even if no bundle was created. - // When no flex changes have been processed at all, no bundle is created and the flag - // is set to `false`. - const flexBundleFlag = flexChangesBundleResult ? flexChangesBundleResult.flexBundle : false; - await updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag); - }}; + // Always append the flexBundle flag to the manifest.json, even if no bundle was created. + // When no flex changes have been processed at all, no bundle is created and the flag + // is set to `false`. + const flexBundleFlag = flexChangesBundleResult ? flexChangesBundleResult.flexBundle : false; + await updateManifestWithFlDependencyAndFlexBundleFlag(flexBundleFlag); } diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index 416b067962b..05f3071b78b 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -314,7 +314,6 @@ function getSapUiCoreBunDef(name, filters, preload) { * @module @ui5/builder/tasks/bundlers/generateLibraryPreload */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task for library bundling. * @@ -322,6 +321,13 @@ function getSapUiCoreBunDef(name, filters, preload) { * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/project/build/helpers/TaskUtil} [parameters.taskUtil] TaskUtil @@ -334,303 +340,299 @@ function getSapUiCoreBunDef(name, filters, preload) { * denote an arbitrary number of characters or folder names. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later * inclusion overrides an earlier exclusion, and vice versa. - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the library preload bundles have been written */ -export default function() { - return {task: async function( - {workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}} - ) { - let nonDbgWorkspace = workspace; - if (taskUtil) { - nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - // Read the experimental flag through taskUtil.getEnv so that its usage is tracked as a task - // input by the incremental build cache (a changed value then invalidates this task's cached - // result). Fall back to a direct process.env read when the task runs without a TaskUtil. - const createBundleInfoPreload = taskUtil?.getEnv ? - !!taskUtil.getEnv("UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD") : - !!process.env.UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD; - const execModuleBundlerIfNeeded = ({options, resources}) => { - if (skipBundles.includes(options.bundleDefinition.name)) { - log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`); - return null; - } - if (coreVersion) { - options.targetUi5CoreVersion = coreVersion; +async function task({workspace, taskUtil, options: {skipBundles = [], excludes = [], projectName}}) { + let nonDbgWorkspace = workspace; + if (taskUtil) { + nonDbgWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); } - options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil); - options.allowStringBundling = allowStringBundling; - return moduleBundler({options, resources}); - }; + }); + } + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + // Read the experimental flag through taskUtil.getEnv so that its usage is tracked as a task + // input by the incremental build cache (a changed value then invalidates this task's cached + // result). Fall back to a direct process.env read when the task runs without a TaskUtil. + const createBundleInfoPreload = taskUtil?.getEnv ? + !!taskUtil.getEnv("UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD") : + !!process.env.UI5_CLI_EXPERIMENTAL_BUNDLE_INFO_PRELOAD; + const execModuleBundlerIfNeeded = ({options, resources}) => { + if (skipBundles.includes(options.bundleDefinition.name)) { + log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`); + return null; + } + if (coreVersion) { + options.targetUi5CoreVersion = coreVersion; + } + options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil); + options.allowStringBundling = allowStringBundling; + return moduleBundler({options, resources}); + }; + + return nonDbgWorkspace.byGlob( + "/resources/**/*.{js,json,xml,html,properties,library,js.map}" + ).then(async (resources) => { + // Find all libraries and create a library-preload.js bundle + + let p = Promise.resolve(); + + // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml + // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0 + if (projectName === "sap.ui.core") { + // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions + // that have been defined for sap.ui.core before the bundle configuration has been introduced. + // This is mainly to have an easier check without version parsing or using semver. + // If no project/specVersion is available, the bundles should also be created to not break potential + // existing use cases without a properly formed/formatted project tree. + if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) { + const isEvo = resources.find((resource) => { + return resource.getPath() === "/resources/ui5loader.js"; + }); - return nonDbgWorkspace.byGlob( - "/resources/**/*.{js,json,xml,html,properties,library,js.map}" - ).then(async (resources) => { - // Find all libraries and create a library-preload.js bundle - - let p = Promise.resolve(); - - // Create core bundles for older versions (<1.97.0) which don't define bundle configuration in the ui5.yaml - // See: https://github.com/SAP/openui5/commit/ff127fd2d009162ea43ad312dec99d759ebc23a0 - if (projectName === "sap.ui.core") { - // Instead of checking the sap.ui.core library version, the specVersion is checked against all versions - // that have been defined for sap.ui.core before the bundle configuration has been introduced. - // This is mainly to have an easier check without version parsing or using semver. - // If no project/specVersion is available, the bundles should also be created to not break potential - // existing use cases without a properly formed/formatted project tree. - if (!taskUtil || taskUtil.getProject().getSpecVersion().lte("2.0")) { - const isEvo = resources.find((resource) => { - return resource.getPath() === "/resources/ui5loader.js"; + let unoptimizedModuleNameMapping; + let unoptimizedResources = resources; + if (taskUtil) { + const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({ + reader: workspace, + callback: function(resource) { + // Remove any non-debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); + } }); + unoptimizedResources = await unoptimizedWorkspace.byGlob( + "/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - let unoptimizedModuleNameMapping; - let unoptimizedResources = resources; - if (taskUtil) { - const unoptimizedWorkspace = taskUtil.resourceFactory.createFilterReader({ - reader: workspace, - callback: function(resource) { - // Remove any non-debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); - } - }); - unoptimizedResources = await unoptimizedWorkspace.byGlob( - "/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - - unoptimizedModuleNameMapping = createModuleNameMapping({ - resources: unoptimizedResources, - taskUtil - }); - } + unoptimizedModuleNameMapping = createModuleNameMapping({ + resources: unoptimizedResources, + taskUtil + }); + } - let filters; - if (isEvo) { - filters = ["ui5loader-autoconfig.js"]; - } else { - filters = ["jquery.sap.global.js"]; - } - p = Promise.all([ - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}), - resources - }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-dbg.js", filters, preload: false, - moduleNameMapping: unoptimizedModuleNameMapping - }), - resources: unoptimizedResources + let filters; + if (isEvo) { + filters = ["ui5loader-autoconfig.js"]; + } else { + filters = ["jquery.sap.global.js"]; + } + p = Promise.all([ + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({name: "sap-ui-core.js", filters, preload: true}), + resources + }), + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-dbg.js", filters, preload: false, + moduleNameMapping: unoptimizedModuleNameMapping }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true - }), - resources + resources: unoptimizedResources + }), + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-nojQuery.js", filters, preload: true, provided: true }), - execModuleBundlerIfNeeded({ - options: getModuleBundlerOptions({ - name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true, - moduleNameMapping: unoptimizedModuleNameMapping - }), - resources: unoptimizedResources + resources + }), + execModuleBundlerIfNeeded({ + options: getModuleBundlerOptions({ + name: "sap-ui-core-nojQuery-dbg.js", filters, preload: false, provided: true, + moduleNameMapping: unoptimizedModuleNameMapping }), - ]).then((results) => { - const bundles = Array.prototype.concat.apply([], results).filter(Boolean); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); - if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - } - } - const writes = [workspace.write(bundle)]; + resources: unoptimizedResources + }), + ]).then((results) => { + const bundles = Array.prototype.concat.apply([], results).filter(Boolean); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); if (sourceMap) { - writes.push(workspace.write(sourceMap)); + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); } - return Promise.all(writes); - })); - }); - } + } + const writes = [workspace.write(bundle)]; + if (sourceMap) { + writes.push(workspace.write(sourceMap)); + } + return Promise.all(writes); + })); + }); } + } - return p.then(() => { - return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length > 0) { - return libraryIndicatorResources; - } else { - // Fallback to "library.js" as library indicator - log.verbose( - `Could not find a ".library" file for project ${projectName}, ` + - `falling back to "library.js".`); - return workspace.byGlob("/resources/**/library.js"); - } - }).then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length < 1) { - // No library found - nothing to do - log.verbose( - `Could not find a ".library" or "library.js" file for project ${projectName}. ` + - `Skipping library preload bundling.`); - return; - } + return p.then(() => { + return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length > 0) { + return libraryIndicatorResources; + } else { + // Fallback to "library.js" as library indicator + log.verbose( + `Could not find a ".library" file for project ${projectName}, ` + + `falling back to "library.js".`); + return workspace.byGlob("/resources/**/library.js"); + } + }).then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length < 1) { + // No library found - nothing to do + log.verbose( + `Could not find a ".library" or "library.js" file for project ${projectName}. ` + + `Skipping library preload bundling.`); + return; + } - return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => { - // Determine library namespace from library indicator file path - // ending with either ".library" or "library.js" (see fallback logic above) - // e.g. /resources/sap/foo/.library => sap/foo - // /resources/sap/bar/library.js => sap/bar - const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/; - const libraryIndicatorPath = libraryIndicatorResource.getPath(); - const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); - if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { - const libraryNamespace = libraryNamespaceMatch[1]; - let results; - if (!createBundleInfoPreload) { - // Regular bundling - results = await Promise.all([ - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getBundleDefinition(libraryNamespace, excludes), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: true, - ignoreMissingModules: true, - skipIfEmpty: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: false, - ignoreMissingModules: true, - skipIfEmpty: true - } - // Note: Although the bundle uses optimize=false, there is - // no moduleNameMapping needed, as support files are excluded from minify. - }, - resources - }) - ]); - } else { - log.info( - `Using experimental bundling with bundle info preload ` + - `for library ${libraryNamespace} in project ${projectName}`); - log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); - - - if (skipBundles.includes(`${libraryNamespace}/library-preload.js`) && - !skipBundles.includes(`${libraryNamespace}/_library-content.js`)) { - // If the standard preload bundle is skipped, also skip the content bundle, - // since they depend on each other - skipBundles.push(`${libraryNamespace}/_library-content.js`); - } + return Promise.all(libraryIndicatorResources.map(async (libraryIndicatorResource) => { + // Determine library namespace from library indicator file path + // ending with either ".library" or "library.js" (see fallback logic above) + // e.g. /resources/sap/foo/.library => sap/foo + // /resources/sap/bar/library.js => sap/bar + const libraryNamespacePattern = /^\/resources\/(.*)\/(?:\.library|library\.js)$/; + const libraryIndicatorPath = libraryIndicatorResource.getPath(); + const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); + if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { + const libraryNamespace = libraryNamespaceMatch[1]; + let results; + if (!createBundleInfoPreload) { + // Regular bundling + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minify. + }, + resources + }) + ]); + } else { + log.info( + `Using experimental bundling with bundle info preload ` + + `for library ${libraryNamespace} in project ${projectName}`); + log.info(`Detected sap.ui.core version is ${coreVersion || "unknown"}`); - if (skipBundles.includes(`${libraryNamespace}/_library-content.js`) && - !skipBundles.includes(`${libraryNamespace}/library-preload.js`)) { - // If the content bundle is skipped, the default preload bundle must be skipped too - throw new Error( - `A custom bundle '${libraryNamespace}/_library-content.js' has been defined, ` + - `but it also requires a corresponding custom bundle definition for ` + - `'${libraryNamespace}/library-preload.js'`); - } - // Experimental bundling with bundle info preload - results = await Promise.all([ - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: - getBundleInfoPreloadDefinition(libraryNamespace, excludes, coreVersion), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getContentBundleDefinition(libraryNamespace, excludes), - bundleOptions: { - optimize: true, - ignoreMissingModules: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: true, - ignoreMissingModules: true, - skipIfEmpty: true - } - }, - resources - }), - execModuleBundlerIfNeeded({ - options: { - bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), - bundleOptions: { - optimize: false, - ignoreMissingModules: true, - skipIfEmpty: true - } - // Note: Although the bundle uses optimize=false, there is - // no moduleNameMapping needed, as support files are excluded from minify. - }, - resources - }) - ]); + if (skipBundles.includes(`${libraryNamespace}/library-preload.js`) && + !skipBundles.includes(`${libraryNamespace}/_library-content.js`)) { + // If the standard preload bundle is skipped, also skip the content bundle, + // since they depend on each other + skipBundles.push(`${libraryNamespace}/_library-content.js`); + } + + if (skipBundles.includes(`${libraryNamespace}/_library-content.js`) && + !skipBundles.includes(`${libraryNamespace}/library-preload.js`)) { + // If the content bundle is skipped, the default preload bundle must be skipped too + throw new Error( + `A custom bundle '${libraryNamespace}/_library-content.js' has been defined, ` + + `but it also requires a corresponding custom bundle definition for ` + + `'${libraryNamespace}/library-preload.js'`); } - const bundles = Array.prototype.concat.apply([], results).filter(Boolean); - return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { - if (bundle) { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); - if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, - taskUtil.STANDARD_TAGS.OmitFromBuildResult); + + // Experimental bundling with bundle info preload + results = await Promise.all([ + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: + getBundleInfoPreloadDefinition(libraryNamespace, excludes, coreVersion), + bundleOptions: { + optimize: true, + ignoreMissingModules: true } - } - const writes = [workspace.write(bundle)]; + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getContentBundleDefinition(libraryNamespace, excludes), + bundleOptions: { + optimize: true, + ignoreMissingModules: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getDesigntimeBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: true, + ignoreMissingModules: true, + skipIfEmpty: true + } + }, + resources + }), + execModuleBundlerIfNeeded({ + options: { + bundleDefinition: getSupportFilesBundleDefinition(libraryNamespace), + bundleOptions: { + optimize: false, + ignoreMissingModules: true, + skipIfEmpty: true + } + // Note: Although the bundle uses optimize=false, there is + // no moduleNameMapping needed, as support files are excluded from minify. + }, + resources + }) + ]); + } + const bundles = Array.prototype.concat.apply([], results).filter(Boolean); + return Promise.all(bundles.map(({bundle, sourceMap} = {}) => { + if (bundle) { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); if (sourceMap) { - writes.push(workspace.write(sourceMap)); + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, + taskUtil.STANDARD_TAGS.OmitFromBuildResult); } - return Promise.all(writes); } - })); - } else { - log.verbose( - `Could not determine library namespace from file "${libraryIndicatorPath}" ` + - `for project ${projectName}. Skipping library preload bundling.`); - return Promise.resolve(); - } - })); - }); + const writes = [workspace.write(bundle)]; + if (sourceMap) { + writes.push(workspace.write(sourceMap)); + } + return Promise.all(writes); + } + })); + } else { + log.verbose( + `Could not determine library namespace from file "${libraryIndicatorPath}" ` + + `for project ${projectName}. Skipping library preload bundling.`); + return Promise.resolve(); + } + })); }); }); - }}; + }); } diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index f50ae86f8ad..bf558d8a01a 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -65,7 +65,6 @@ function getBundleDefinition(config) { * @module @ui5/builder/tasks/bundlers/generateStandaloneAppBundle */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task for bundling standalone applications. * @@ -73,6 +72,16 @@ function getBundleDefinition(config) { * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return { + requiresDependencies: true, + task + }; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files @@ -80,127 +89,124 @@ function getBundleDefinition(config) { * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name * @param {string} [parameters.options.projectNamespace] Project namespace - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the standalone app bundle has been written */ -export default function() { - return {requiresDependencies: true, task: async function({workspace, dependencies, taskUtil, options}) { - const {projectName} = options; - const namespace = options.projectNamespace; - const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); - - if (!namespace) { - log.warn(`Namespace of project ${projectName} is not known. Self contained bundling is currently ` + - `unable to generate complete bundles for such projects.`); - } - - const combo = new ReaderCollectionPrioritized({ - name: `generateStandaloneAppBundle - prioritize workspace over dependencies: ${projectName}`, - readers: [workspace, dependencies] +async function task({workspace, dependencies, taskUtil, options: {projectName, projectNamespace}}) { + const namespace = projectNamespace; + const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion(); + + if (!namespace) { + log.warn(`Namespace of project ${projectName} is not known. Self contained bundling is currently ` + + `unable to generate complete bundles for such projects.`); + } + + const combo = new ReaderCollectionPrioritized({ + name: `generateStandaloneAppBundle - prioritize workspace over dependencies: ${projectName}`, + readers: [workspace, dependencies] + }); + + let resourceReader = combo; + if (taskUtil) { + // Omit -dbg files + resourceReader = await taskUtil.resourceFactory.createFilterReader({ + reader: combo, + callback: function(resource) { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); + } }); + } + const resources = await resourceReader.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - let resourceReader = combo; - if (taskUtil) { - // Omit -dbg files - resourceReader = await taskUtil.resourceFactory.createFilterReader({ - reader: combo, - callback: function(resource) { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.IsDebugVariant); - } - }); - } - const resources = await resourceReader.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); + const isEvo = resources.find((resource) => { + return resource.getPath() === "/resources/ui5loader.js"; + }); + let filters; + if (isEvo) { + filters = ["ui5loader-autoconfig.js"]; + } else { + filters = ["jquery.sap.global.js"]; + } - const isEvo = resources.find((resource) => { - return resource.getPath() === "/resources/ui5loader.js"; + let unoptimizedModuleNameMapping; + let unoptimizedResources = resources; + if (taskUtil) { + const unoptimizedResourceReader = await taskUtil.resourceFactory.createFilterReader({ + reader: combo, + callback: function(resource) { + // Remove any non-debug variants + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); + } }); - let filters; - if (isEvo) { - filters = ["ui5loader-autoconfig.js"]; - } else { - filters = ["jquery.sap.global.js"]; - } - - let unoptimizedModuleNameMapping; - let unoptimizedResources = resources; - if (taskUtil) { - const unoptimizedResourceReader = await taskUtil.resourceFactory.createFilterReader({ - reader: combo, - callback: function(resource) { - // Remove any non-debug variants - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); - } - }); - - unoptimizedResources = await unoptimizedResourceReader - .byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - - unoptimizedModuleNameMapping = createModuleNameMapping({ - resources: unoptimizedResources, - taskUtil - }); - } - - const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); - const bundleOptions = { - bundleDefinition: applyDefaultsToBundleDefinition( - getBundleDefinition({ - name: "sap-ui-custom.js", - filters, - namespace, - preloadSection: true, - }), - taskUtil - ), - allowStringBundling - }; - - const bundleDbgOptions = { - bundleDefinition: applyDefaultsToBundleDefinition( - getBundleDefinition({ - name: "sap-ui-custom-dbg.js", - filters, - namespace, - }), - taskUtil - ), - bundleOptions: { - optimize: false, - }, - moduleNameMapping: unoptimizedModuleNameMapping, - allowStringBundling - }; - - if (coreVersion) { - bundleOptions.targetUi5CoreVersion = coreVersion; - bundleDbgOptions.targetUi5CoreVersion = coreVersion; - } - - await Promise.all([ - moduleBundler({ - resources, - options: bundleOptions + + unoptimizedResources = await unoptimizedResourceReader + .byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); + + unoptimizedModuleNameMapping = createModuleNameMapping({ + resources: unoptimizedResources, + taskUtil + }); + } + + const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0"); + const bundleOptions = { + bundleDefinition: applyDefaultsToBundleDefinition( + getBundleDefinition({ + name: "sap-ui-custom.js", + filters, + namespace, + preloadSection: true, }), - moduleBundler({ - resources: unoptimizedResources, - options: bundleDbgOptions - }) - ]).then((results) => { - const bundles = Array.prototype.concat.apply([], results); - return Promise.all(bundles.map(({bundle, sourceMap}) => { - if (taskUtil) { - taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); - if (sourceMap) { - // Clear tag that might have been set by the minify task, in cases where - // the bundle name is identical to a source file - taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - } - } - const writes = [workspace.write(bundle)]; + taskUtil + ), + allowStringBundling + }; + + const bundleDbgOptions = { + bundleDefinition: applyDefaultsToBundleDefinition( + getBundleDefinition({ + name: "sap-ui-custom-dbg.js", + filters, + namespace, + }), + taskUtil + ), + bundleOptions: { + optimize: false, + }, + moduleNameMapping: unoptimizedModuleNameMapping, + allowStringBundling + }; + + if (coreVersion) { + bundleOptions.targetUi5CoreVersion = coreVersion; + bundleDbgOptions.targetUi5CoreVersion = coreVersion; + } + + await Promise.all([ + moduleBundler({ + resources, + options: bundleOptions + }), + moduleBundler({ + resources: unoptimizedResources, + options: bundleDbgOptions + }) + ]).then((results) => { + const bundles = Array.prototype.concat.apply([], results); + return Promise.all(bundles.map(({bundle, sourceMap}) => { + if (taskUtil) { + taskUtil.setTag(bundle, taskUtil.STANDARD_TAGS.IsBundle); if (sourceMap) { - writes.push(workspace.write(sourceMap)); + // Clear tag that might have been set by the minify task, in cases where + // the bundle name is identical to a source file + taskUtil.clearTag(sourceMap, taskUtil.STANDARD_TAGS.OmitFromBuildResult); } - return Promise.all(writes); - })); - }); - }}; + } + const writes = [workspace.write(bundle)]; + if (sourceMap) { + writes.push(workspace.write(sourceMap)); + } + return Promise.all(writes); + })); + }); } diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index 68f9d13b9bf..bb32694882a 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -34,7 +34,6 @@ function getSigner(type) { } } -/* eslint "jsdoc/check-param-names": "off" */ /** * Task to generate the application cachebuster info file. * @@ -42,40 +41,42 @@ function getSigner(type) { * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {object} parameters.options Options * @param {string} parameters.options.projectNamespace Namespace of the application * @param {string} [parameters.options.signatureType='time'] Type of signature to be used ('time' or 'hash') - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the cachebuster info has been written */ -export default function() { - return {task: function({workspace, options}) { - const {signatureType} = options; - const namespace = options.projectNamespace; - - const basePath = `/resources/${namespace}/`; - return workspace.byGlob(`/resources/${namespace}/**/*`) - .then(async (resources) => { - const cachebusterInfo = Object.create(null); - const signer = getSigner(signatureType); +function task({workspace, options: {projectNamespace, signatureType}}) { + const basePath = `/resources/${projectNamespace}/`; + return workspace.byGlob(`/resources/${projectNamespace}/**/*`) + .then(async (resources) => { + const cachebusterInfo = Object.create(null); + const signer = getSigner(signatureType); - await Promise.all(resources.map(async (resource) => { - let resourcePath = resource.getPath(); - if (!resourcePath.startsWith(basePath)) { - log.verbose( - `Ignoring resource with path ${resourcePath} since it is not based on path ${basePath}`); - return; - } - // Remove base path. Absolute paths are not allowed in cachebuster info - resourcePath = resourcePath.replace(basePath, ""); - cachebusterInfo[resourcePath] = await signer(resource); - })); - const cachebusterInfoResource = createResource({ - path: `/resources/${namespace}/sap-ui-cachebuster-info.json`, - string: JSON.stringify(cachebusterInfo, null, 2) - }); - return workspace.write(cachebusterInfoResource); + await Promise.all(resources.map(async (resource) => { + let resourcePath = resource.getPath(); + if (!resourcePath.startsWith(basePath)) { + log.verbose( + `Ignoring resource with path ${resourcePath} since it is not based on path ${basePath}`); + return; + } + // Remove base path. Absolute paths are not allowed in cachebuster info + resourcePath = resourcePath.replace(basePath, ""); + cachebusterInfo[resourcePath] = await signer(resource); + })); + const cachebusterInfoResource = createResource({ + path: `/resources/${projectNamespace}/sap-ui-cachebuster-info.json`, + string: JSON.stringify(cachebusterInfo, null, 2) }); - }}; + return workspace.write(cachebusterInfoResource); + }); } diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index e5722cd5b89..7e88c5e2430 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -8,7 +8,6 @@ import manifestCreator from "../processors/manifestCreator.js"; * @module @ui5/builder/tasks/generateLibraryManifest */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task for creating a library manifest.json from its .library file. * @@ -16,63 +15,68 @@ import manifestCreator from "../processors/manifestCreator.js"; * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @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 {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the library manifest has been written */ -export default function() { - return {task: function({workspace, taskUtil, options: {projectName}}) { - // Note: - // *.library files are needed to identify libraries - // *.json files are needed to avoid overwriting them - // *.js files are needed to identify nested components - // *.less, *.css, *.theming and *.theme files are needed to identify supported themes - // *.properties to identify existence of i18n bundles (e.g. messagebundle.properties) - return workspace.byGlob( - "/resources/**/*.{js,json,library,less,css,theming,theme,properties}" - ).then((resources) => { - // Find all libraries and create a manifest.json file - return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { - if (libraryIndicatorResources.length < 1) { - // No library found - nothing to do - log.verbose(`Could not find a ".library" file for project ${projectName}. ` + - `Skipping library manifest generation.`); - return; - } +function task({workspace, taskUtil, options: {projectName}}) { + // Note: + // *.library files are needed to identify libraries + // *.json files are needed to avoid overwriting them + // *.js files are needed to identify nested components + // *.less, *.css, *.theming and *.theme files are needed to identify supported themes + // *.properties to identify existence of i18n bundles (e.g. messagebundle.properties) + return workspace.byGlob( + "/resources/**/*.{js,json,library,less,css,theming,theme,properties}" + ).then((resources) => { + // Find all libraries and create a manifest.json file + return workspace.byGlob("/resources/**/.library").then((libraryIndicatorResources) => { + if (libraryIndicatorResources.length < 1) { + // No library found - nothing to do + log.verbose(`Could not find a ".library" file for project ${projectName}. ` + + `Skipping library manifest generation.`); + return; + } - return Promise.all(libraryIndicatorResources.map((libraryIndicatorResource) => { - // Determine library namespace from library indicator file path - // ending with ".library" - // e.g. /resources/sap/foo/.library => sap/foo - const libraryNamespacePattern = /^\/resources\/(.*)\/\.library$/; - const libraryIndicatorPath = libraryIndicatorResource.getPath(); - const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); - if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { - const libraryNamespace = libraryNamespaceMatch[1]; - return manifestCreator({ - libraryResource: libraryIndicatorResource, - namespace: libraryNamespace, - resources, - getProjectVersion: (projectName) => { - return taskUtil?.getProject(projectName)?.getVersion(); - }, - options: { - } - }).then((manifest) => { - if (manifest) { - return workspace.write(manifest); - } - }); - } else { - log.verbose(`Could not determine library namespace from file "${libraryIndicatorPath}" ` + - `for project ${projectName}. Skipping library manifest generation.`); - return Promise.resolve(); - } - })); - }); + return Promise.all(libraryIndicatorResources.map((libraryIndicatorResource) => { + // Determine library namespace from library indicator file path + // ending with ".library" + // e.g. /resources/sap/foo/.library => sap/foo + const libraryNamespacePattern = /^\/resources\/(.*)\/\.library$/; + const libraryIndicatorPath = libraryIndicatorResource.getPath(); + const libraryNamespaceMatch = libraryIndicatorPath.match(libraryNamespacePattern); + if (libraryNamespaceMatch && libraryNamespaceMatch[1]) { + const libraryNamespace = libraryNamespaceMatch[1]; + return manifestCreator({ + libraryResource: libraryIndicatorResource, + namespace: libraryNamespace, + resources, + getProjectVersion: (projectName) => { + return taskUtil?.getProject(projectName)?.getVersion(); + }, + options: { + } + }).then((manifest) => { + if (manifest) { + return workspace.write(manifest); + } + }); + } else { + log.verbose(`Could not determine library namespace from file "${libraryIndicatorPath}" ` + + `for project ${projectName}. Skipping library manifest generation.`); + return Promise.resolve(); + } + })); }); - }}; + }); } diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index 8f3b030c025..020ddf17cff 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -42,7 +42,6 @@ function getCreatorOptions(projectName) { * @module @ui5/builder/tasks/generateResourcesJson */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task for creating a resources.json file, describing all productive build resources. * @@ -104,39 +103,45 @@ function getCreatorOptions(projectName) { * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return { + requiresDependencies: true, + task + }; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the resources.json has been written */ -export default function() { - return {requiresDependencies: true, task: async function( - {workspace, dependencies, taskUtil, options: {projectName}} - ) { - let resources = await workspace.byGlob(["/resources/**/*"].concat(DEFAULT_EXCLUDES)); - let dependencyResources = - await dependencies.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - - if (taskUtil) { - // Filter out resources that will be omitted from the build results - resources = resources.filter((resource) => { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - }); - dependencyResources = dependencyResources.filter((resource) => { - return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); - }); - } +async function task({workspace, dependencies, taskUtil, options: {projectName}}) { + let resources = await workspace.byGlob(["/resources/**/*"].concat(DEFAULT_EXCLUDES)); + let dependencyResources = + await dependencies.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}"); - const resourceLists = await resourceListCreator({ - resources, - dependencyResources, - options: getCreatorOptions(projectName), + if (taskUtil) { + // Filter out resources that will be omitted from the build results + resources = resources.filter((resource) => { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); }); - await Promise.all( - resourceLists.map((resourceList) => workspace.write(resourceList)) - ); - }}; + dependencyResources = dependencyResources.filter((resource) => { + return !taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + }); + } + + const resourceLists = await resourceListCreator({ + resources, + dependencyResources, + options: getCreatorOptions(projectName), + }); + await Promise.all( + resourceLists.map((resourceList) => workspace.write(resourceList)) + ); } diff --git a/packages/builder/lib/tasks/generateThemeDesignerResources.js b/packages/builder/lib/tasks/generateThemeDesignerResources.js index 0d546d5cc0a..a899e564067 100644 --- a/packages/builder/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/lib/tasks/generateThemeDesignerResources.js @@ -121,7 +121,10 @@ export default function build(options) { // Skip sap.ui.documentation since it is not intended to be available in SAP Theme Designer to create // custom themes if (namespace === "sap/ui/documentation") { - return {requiresDependencies: true, steps: []}; + return { + requiresDependencies: true, + steps: [] + }; } let librarySourceLessPattern; @@ -217,5 +220,8 @@ export default function build(options) { }, }); - return {requiresDependencies: true, steps}; + return { + requiresDependencies: true, + steps + }; } diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index b4b76bee3c4..cd413a31dbf 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -7,7 +7,6 @@ const MANIFEST_JSON = "manifest.json"; * @module @ui5/builder/tasks/generateVersionInfo */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task to create sap-ui-version.json * @@ -15,47 +14,55 @@ const MANIFEST_JSON = "manifest.json"; * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return { + requiresDependencies: true, + task + }; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {object} parameters.options Options * @param {string} parameters.options.pattern Glob pattern for .library resources - * @param {object} parameters.options.rootProject DuplexCollection to read and write files - * @returns {object} The task descriptor + * @param {object} parameters.options.rootProject Root project (used for its name and version) + * @returns {Promise} Promise resolving once sap-ui-version.json has been written */ -export default function() { - return {requiresDependencies: true, task: async ({workspace, dependencies, options: {rootProject, pattern}}) => { - let resources = await dependencies.byGlob(pattern); +async function task({workspace, dependencies, options: {rootProject, pattern}}) { + let resources = await dependencies.byGlob(pattern); - resources = resources.filter((res) => res.getProject()?.getType() === "library"); + resources = resources.filter((res) => res.getProject()?.getType() === "library"); - const libraryInfosPromises = resources.map((dotLibResource) => { - const namespace = dotLibResource.getProject().getNamespace(); - // pass all required resources to the processor - // the processor will then filter - return dependencies.byGlob(`/resources/${namespace}/**/${MANIFEST_JSON}`).then((manifestResources) => { - const libraryManifest = manifestResources.find((manifestResource) => { - return manifestResource.getPath() === `/resources/${namespace}/${MANIFEST_JSON}`; - }); - const embeddedManifests = - manifestResources.filter((manifestResource) => manifestResource !== libraryManifest); - return { - libraryManifest, - embeddedManifests, - name: dotLibResource.getProject().getName(), - version: dotLibResource.getProject().getVersion() - }; + const libraryInfosPromises = resources.map((dotLibResource) => { + const namespace = dotLibResource.getProject().getNamespace(); + // pass all required resources to the processor + // the processor will then filter + return dependencies.byGlob(`/resources/${namespace}/**/${MANIFEST_JSON}`).then((manifestResources) => { + const libraryManifest = manifestResources.find((manifestResource) => { + return manifestResource.getPath() === `/resources/${namespace}/${MANIFEST_JSON}`; }); + const embeddedManifests = + manifestResources.filter((manifestResource) => manifestResource !== libraryManifest); + return { + libraryManifest, + embeddedManifests, + name: dotLibResource.getProject().getName(), + version: dotLibResource.getProject().getVersion() + }; }); - const libraryInfos = await Promise.all(libraryInfosPromises); + }); + const libraryInfos = await Promise.all(libraryInfosPromises); - const [versionInfoResource] = await versionInfoGenerator({ - options: { - rootProjectName: rootProject.getName(), - rootProjectVersion: rootProject.getVersion(), - libraryInfos - } - }); - return workspace.write(versionInfoResource); - }}; -}; + const [versionInfoResource] = await versionInfoGenerator({ + options: { + rootProjectName: rootProject.getName(), + rootProjectVersion: rootProject.getVersion(), + libraryInfos + } + }); + return workspace.write(versionInfoResource); +} diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 24a5a18c5d2..14232dcc2e2 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -9,7 +9,6 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @module @ui5/builder/tasks/jsdoc/executeJsdocSdkTransformation */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Task to transform the api.json file as created by the * [generateJsdoc]{@link @ui5/builder/tasks/jsdoc/generateJsdoc} task into a pre-processed api.json @@ -19,67 +18,73 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return { + requiresDependencies: true, + task + }; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {object} parameters.options Options * @param {string|Array} parameters.options.dotLibraryPattern Pattern to locate the .library resource to be processed * @param {string} parameters.options.projectName Project name - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the transformed api.json has been written */ -export default function() { - return {requiresDependencies: true, task: async function( - {workspace, dependencies, options: {projectName, dotLibraryPattern}} = {} - ) { - if (!projectName || !dotLibraryPattern) { - throw new Error("[executeJsdocSdkTransformation]: One or more mandatory options not provided"); - } +async function task({workspace, dependencies, options: {projectName, dotLibraryPattern}} = {}) { + if (!projectName || !dotLibraryPattern) { + throw new Error("[executeJsdocSdkTransformation]: One or more mandatory options not provided"); + } - const [apiJsons, dotLibraries, depApiJsons] = await Promise.all([ - workspace.byGlob("/test-resources/**/designtime/api.json"), - workspace.byGlob(dotLibraryPattern), - dependencies.byGlob("/test-resources/**/designtime/api.json") - ]); - if (!apiJsons.length) { - log.info(`Failed to locate api.json resource for project ${projectName}. ` + - `Skipping SDK Transformation...`); - return; - } else if (apiJsons.length > 1) { - throw new Error(`[executeJsdocSdkTransformation]: Found more than one api.json resources for project ` + - `${projectName}.`); - } - if (!dotLibraries.length) { - throw new Error(`[executeJsdocSdkTransformation]: Failed to locate .library resource for project ` + - `${projectName}.`); - } else if (dotLibraries.length > 1) { - throw new Error(`[executeJsdocSdkTransformation]: Found more than one .library resources for project ` + - `${projectName}.`); - } + const [apiJsons, dotLibraries, depApiJsons] = await Promise.all([ + workspace.byGlob("/test-resources/**/designtime/api.json"), + workspace.byGlob(dotLibraryPattern), + dependencies.byGlob("/test-resources/**/designtime/api.json") + ]); + if (!apiJsons.length) { + log.info(`Failed to locate api.json resource for project ${projectName}. ` + + `Skipping SDK Transformation...`); + return; + } else if (apiJsons.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one api.json resources for project ` + + `${projectName}.`); + } + if (!dotLibraries.length) { + throw new Error(`[executeJsdocSdkTransformation]: Failed to locate .library resource for project ` + + `${projectName}.`); + } else if (dotLibraries.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one .library resources for project ` + + `${projectName}.`); + } - const combo = new ReaderCollectionPrioritized({ - name: `executeJsdocSdkTransformation - custom workspace + dependencies FS: ${projectName}`, - readers: [workspace, dependencies] - }); + const combo = new ReaderCollectionPrioritized({ + name: `executeJsdocSdkTransformation - custom workspace + dependencies FS: ${projectName}`, + readers: [workspace, dependencies] + }); - const apiJsonPath = apiJsons[0].getPath(); - const dotLibraryPath = dotLibraries[0].getPath(); - const dependencyApiJsonPaths = depApiJsons.map((res) => { - return res.getPath(); - }); + const apiJsonPath = apiJsons[0].getPath(); + const dotLibraryPath = dotLibraries[0].getPath(); + const dependencyApiJsonPaths = depApiJsons.map((res) => { + return res.getPath(); + }); - // Target path is typically "/test-resources/${options.namespace}/designtime/apiref/api.json" - const targetApiJsonPath = apiJsonPath.replace(/\/api\.json$/i, "/apiref/api.json"); + // Target path is typically "/test-resources/${options.namespace}/designtime/apiref/api.json" + const targetApiJsonPath = apiJsonPath.replace(/\/api\.json$/i, "/apiref/api.json"); - const createdResources = await sdkTransformer({ - apiJsonPath, - dotLibraryPath, - dependencyApiJsonPaths, - targetApiJsonPath, - fs: fsInterface(combo) - }); + const createdResources = await sdkTransformer({ + apiJsonPath, + dotLibraryPath, + dependencyApiJsonPaths, + targetApiJsonPath, + fs: fsInterface(combo) + }); - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); - }}; + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); } diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index 1528330b0ae..4926a850c8f 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -7,56 +7,56 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * @module @ui5/builder/tasks/jsdoc/generateApiIndex */ -/* eslint "jsdoc/check-param-names": "off" */ /** * Compiles an api-index.json resource from all available api.json resources as created by the * [executeJsdocSdkTransformation]{@link @ui5/builder/tasks/jsdoc/executeJsdocSdkTransformation} task. * The resulting api-index.json resource is mainly to be used in the SDK. * - * The default export is a factory returning the task descriptor. The parameters below document the - * task body that receives them at build time. - * * @public * @function default * @static * + * @returns {object} The task descriptor + */ +export default function() { + return { + requiresDependencies: true, + task + }; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the api-index.json resources have been written */ -export default function() { - return {requiresDependencies: true, task: async function({ - workspace, - dependencies, - options: {projectName} - }) { - const combo = new ReaderCollectionPrioritized({ - name: `generateApiIndex - workspace + dependencies: ${projectName}`, - readers: [workspace, dependencies] - }); +async function task({workspace, dependencies, options: {projectName}}) { + const combo = new ReaderCollectionPrioritized({ + name: `generateApiIndex - workspace + dependencies: ${projectName}`, + readers: [workspace, dependencies] + }); - const versionInfoPath = "/resources/sap-ui-version.json"; - const testResourcesRootPath = "/test-resources"; - const targetApiIndexPath = "/docs/api/api-index.json"; - const targetApiIndexDeprecatedPath = "/docs/api/api-index-deprecated.json"; - const targetApiIndexExperimentalPath = "/docs/api/api-index-experimental.json"; - const targetApiIndexSincePath = "/docs/api/api-index-since.json"; + const versionInfoPath = "/resources/sap-ui-version.json"; + const testResourcesRootPath = "/test-resources"; + const targetApiIndexPath = "/docs/api/api-index.json"; + const targetApiIndexDeprecatedPath = "/docs/api/api-index-deprecated.json"; + const targetApiIndexExperimentalPath = "/docs/api/api-index-experimental.json"; + const targetApiIndexSincePath = "/docs/api/api-index-since.json"; - const createdResources = await apiIndexGenerator({ - versionInfoPath, - testResourcesRootPath, - targetApiIndexPath, - targetApiIndexDeprecatedPath, - targetApiIndexExperimentalPath, - targetApiIndexSincePath, - fs: fsInterface(combo), - }); + const createdResources = await apiIndexGenerator({ + versionInfoPath, + testResourcesRootPath, + targetApiIndexPath, + targetApiIndexDeprecatedPath, + targetApiIndexExperimentalPath, + targetApiIndexSincePath, + fs: fsInterface(combo), + }); - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); - }}; + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); } diff --git a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js index f8506bfbab9..92d17502ad9 100644 --- a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js @@ -49,60 +49,58 @@ import {createAdapter} from "@ui5/fs/resourceFactory"; export default function generateJsdoc(options) { const {version, namespace, jsdocExcludes = []} = options; const patterns = ["/resources/**/*.js", ...jsdocExcludes.map((p) => `!/resources/${p}`)]; - return { - requiresDependencies: true, - determineBuildSignature: async () => version, - task: async function({ - taskUtil, - workspace, - dependencies, - options = {} - }) { - const {projectName} = options; - - if (!projectName || !namespace || !version || !patterns.length) { - throw new Error("[generateJsdoc]: One or more mandatory options not provided"); - } - const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} = - await utils.createTmpDirs(projectName); - - taskUtil?.registerCleanupTask(cleanup); - - const [writtenResourcesCount] = await Promise.all([ - utils.writeResourcesToDir({ - workspace, - pattern: patterns, - targetPath: resourcePath - }), - utils.writeDependencyApisToDir({ - dependencies, - targetPath: path.join(tmpPath, "dependency-apis") - }) - ]); - - if (writtenResourcesCount === 0) { - log.info(`Failed to find any input resources for project ${projectName} using pattern ` + - `${patterns}. Skipping JSDoc generation...`); - return; - } + const task = async function({taskUtil, workspace, dependencies, options = {}}) { + const {projectName} = options; - const createdResources = await jsdocGenerator({ - sourcePath: resourcePath, - targetPath, - tmpPath, - options: { - projectName, - namespace, - version, - variants: ["apijson"] - } - }); - - await Promise.all(createdResources.map((resource) => { - return workspace.write(resource); - })); + if (!projectName || !namespace || !version || !patterns.length) { + throw new Error("[generateJsdoc]: One or more mandatory options not provided"); } + + const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} = + await utils.createTmpDirs(projectName); + + taskUtil?.registerCleanupTask(cleanup); + + const [writtenResourcesCount] = await Promise.all([ + utils.writeResourcesToDir({ + workspace, + pattern: patterns, + targetPath: resourcePath + }), + utils.writeDependencyApisToDir({ + dependencies, + targetPath: path.join(tmpPath, "dependency-apis") + }) + ]); + + if (writtenResourcesCount === 0) { + log.info(`Failed to find any input resources for project ${projectName} using pattern ` + + `${patterns}. Skipping JSDoc generation...`); + return; + } + + const createdResources = await jsdocGenerator({ + sourcePath: resourcePath, + targetPath, + tmpPath, + options: { + projectName, + namespace, + version, + variants: ["apijson"] + } + }); + + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); + }; + + return { + requiresDependencies: true, + determineBuildSignature: async () => version, + task }; } diff --git a/packages/builder/lib/tasks/transformBootstrapHtml.js b/packages/builder/lib/tasks/transformBootstrapHtml.js index 53c64d99c4e..bc6e610da53 100644 --- a/packages/builder/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/lib/tasks/transformBootstrapHtml.js @@ -2,40 +2,41 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("builder:tasks:transformBootstrapHtml"); import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js"; -/* eslint "jsdoc/check-param-names": "off" */ /** * Task for transforming the application bootstrap HTML file. * * @module builder/tasks/transformBootstrapHtml + * @returns {object} The task descriptor + */ +export default function() { + return {task}; +} + +/** * @param {object} parameters Parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files * @param {object} parameters.options Options * @param {string} parameters.options.projectName Project name * @param {string} [parameters.options.projectNamespace] Project namespace - * @returns {object} The task descriptor + * @returns {Promise} Promise resolving once the bootstrap HTML has been transformed */ -export default function() { - return {task: async function({workspace, options}) { - const {projectName} = options; - const namespace = options.projectNamespace; - - let indexPath; - if (namespace) { - indexPath = `/resources/${namespace}/index.html`; - } else { - indexPath = "/index.html"; - } - const resource = await workspace.byPath(indexPath); - if (!resource) { - log.warn(`Skipping bootstrap transformation due to missing index.html in project "${projectName}".`); - return; +async function task({workspace, options: {projectName, projectNamespace}}) { + let indexPath; + if (projectNamespace) { + indexPath = `/resources/${projectNamespace}/index.html`; + } else { + indexPath = "/index.html"; + } + const resource = await workspace.byPath(indexPath); + if (!resource) { + log.warn(`Skipping bootstrap transformation due to missing index.html in project "${projectName}".`); + return; + } + const processedResources = await bootstrapHtmlTransformer({ + resources: [resource], + options: { + src: "resources/sap-ui-custom.js" } - const processedResources = await bootstrapHtmlTransformer({ - resources: [resource], - options: { - src: "resources/sap-ui-custom.js" - } - }); - await Promise.all(processedResources.map((resource) => workspace.write(resource))); - }}; + }); + await Promise.all(processedResources.map((resource) => workspace.write(resource))); } From 384955265eb1ad1920ad73138d24caa0a7a2771e Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 14 Oct 2026 09:47:43 +0200 Subject: [PATCH 3/8] refactor(builder): Rename task factory to defineTask The default export of a task module is a factory `(options) => descriptor`. `build` was misleading, since the factory builds nothing: it declares the task and returns its descriptor. Rename it to `defineTask`, matching the plugin-API convention (Vite's `defineConfig`) and the descriptor's own `task`/`steps` fields. Give the factories that were anonymous (`export default function()`) the same name, so every task module reads the same way. The utility `createModuleNameMapping` keeps its own default export; it is not a task. --- packages/builder/lib/tasks/buildThemes.js | 2 +- packages/builder/lib/tasks/bundlers/generateBundle.js | 2 +- .../builder/lib/tasks/bundlers/generateComponentPreload.js | 2 +- .../builder/lib/tasks/bundlers/generateFlexChangesBundle.js | 2 +- packages/builder/lib/tasks/bundlers/generateLibraryPreload.js | 2 +- .../builder/lib/tasks/bundlers/generateStandaloneAppBundle.js | 2 +- packages/builder/lib/tasks/enhanceManifest.js | 2 +- packages/builder/lib/tasks/escapeNonAsciiCharacters.js | 2 +- packages/builder/lib/tasks/generateCachebusterInfo.js | 2 +- packages/builder/lib/tasks/generateLibraryManifest.js | 2 +- packages/builder/lib/tasks/generateResourcesJson.js | 2 +- packages/builder/lib/tasks/generateThemeDesignerResources.js | 2 +- packages/builder/lib/tasks/generateVersionInfo.js | 2 +- .../builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js | 2 +- packages/builder/lib/tasks/jsdoc/generateApiIndex.js | 2 +- packages/builder/lib/tasks/jsdoc/generateJsdoc.js | 4 ++-- packages/builder/lib/tasks/minify.js | 2 +- packages/builder/lib/tasks/replaceBuildtime.js | 2 +- packages/builder/lib/tasks/replaceCopyright.js | 2 +- packages/builder/lib/tasks/replaceVersion.js | 2 +- packages/builder/lib/tasks/transformBootstrapHtml.js | 2 +- 21 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index 72cb390d1b5..49ac8136eed 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -182,7 +182,7 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP * @param {boolean} [options.compress=true] * @returns {object} The task descriptor with its build steps */ -export default function build({projectName, inputPattern, librariesPattern, themesPattern, compress}) { +export default function defineTask({projectName, inputPattern, librariesPattern, themesPattern, compress}) { compress = compress === undefined ? true : compress; const buildThemesStep = { diff --git a/packages/builder/lib/tasks/bundlers/generateBundle.js b/packages/builder/lib/tasks/bundlers/generateBundle.js index f55f7210a3a..f30412f972a 100644 --- a/packages/builder/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateBundle.js @@ -29,7 +29,7 @@ import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; * @returns {object} The task descriptor */ /* eslint-enable max-len */ -export default function() { +export default function defineTask() { return {requiresDependencies: true, task: async function({ workspace, dependencies, taskUtil, options: {projectName, bundleDefinition, bundleOptions} }) { diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index d09c58a177f..01f05aa6b66 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -19,7 +19,7 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index 85f0344a711..987e5c1672a 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -22,7 +22,7 @@ import semver from "semver"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index 05f3071b78b..ca17a69d29b 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -323,7 +323,7 @@ function getSapUiCoreBunDef(name, filters, preload) { * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index bf558d8a01a..327603c12f8 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -74,7 +74,7 @@ function getBundleDefinition(config) { * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/enhanceManifest.js b/packages/builder/lib/tasks/enhanceManifest.js index 4bc389b5af4..1092e347983 100644 --- a/packages/builder/lib/tasks/enhanceManifest.js +++ b/packages/builder/lib/tasks/enhanceManifest.js @@ -24,7 +24,7 @@ import fsInterface from "@ui5/fs/fsInterface"; * @param {string} options.projectNamespace Namespace of the application * @returns {object} The task descriptor with its build steps */ -export default function build({projectNamespace}) { +export default function defineTask({projectNamespace}) { return {steps: [{ name: "enhanceManifest", // One key per manifest.json. The i18n bundle reads made by manifestEnhancer go through the step's diff --git a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js index fe3aa1f1791..6cf1a07d087 100644 --- a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js @@ -22,7 +22,7 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js"; * @param {string} options.encoding source file encoding either "UTF-8" or "ISO-8859-1" * @returns {object} The task descriptor with its build steps */ -export default function build({pattern, encoding}) { +export default function defineTask({pattern, encoding}) { if (!encoding) { throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided"); } diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index bb32694882a..4b78a54c523 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -43,7 +43,7 @@ function getSigner(type) { * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index 7e88c5e2430..59d300d0539 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -17,7 +17,7 @@ import manifestCreator from "../processors/manifestCreator.js"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index 020ddf17cff..214e1fba77d 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -105,7 +105,7 @@ function getCreatorOptions(projectName) { * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/generateThemeDesignerResources.js b/packages/builder/lib/tasks/generateThemeDesignerResources.js index a899e564067..d031c3d10e6 100644 --- a/packages/builder/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/lib/tasks/generateThemeDesignerResources.js @@ -114,7 +114,7 @@ async function generateThemeDotTheming({workspace, combo, themeFolder}) { * namespace. Omit for type theme-library * @returns {object} The task descriptor with its build steps */ -export default function build(options) { +export default function defineTask(options) { const {projectName, version} = options; const namespace = options.projectNamespace; diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index cd413a31dbf..8eb6050a110 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -16,7 +16,7 @@ const MANIFEST_JSON = "manifest.json"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 14232dcc2e2..6a5b814798e 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -20,7 +20,7 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index 4926a850c8f..783e8329b13 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -18,7 +18,7 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js index 92d17502ad9..5bb346aee41 100644 --- a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js @@ -46,7 +46,7 @@ import {createAdapter} from "@ui5/fs/resourceFactory"; * @param {string[]} [options.jsdocExcludes=[]] List of exclude patterns, relative to /resources/ * @returns {object} The task descriptor */ -export default function generateJsdoc(options) { +export default function defineTask(options) { const {version, namespace, jsdocExcludes = []} = options; const patterns = ["/resources/**/*.js", ...jsdocExcludes.map((p) => `!/resources/${p}`)]; @@ -204,5 +204,5 @@ const utils = { // Export utils for testing only /* istanbul ignore else */ if (process.env.NODE_ENV === "test") { - generateJsdoc._utils = utils; + defineTask._utils = utils; } diff --git a/packages/builder/lib/tasks/minify.js b/packages/builder/lib/tasks/minify.js index 275d96bab0a..9d892321657 100644 --- a/packages/builder/lib/tasks/minify.js +++ b/packages/builder/lib/tasks/minify.js @@ -30,7 +30,7 @@ const log = getLogger("builder:tasks:minify"); * source files, such as TypeScript files, in the generated source map. * @returns {object} The task descriptor with its build steps */ -export default function build({pattern, omitSourceMapResources = false, useInputSourceMaps = true}) { +export default function defineTask({pattern, omitSourceMapResources = false, useInputSourceMaps = true}) { const minifierOptions = { addSourceMappingUrl: !omitSourceMapResources, readSourceMappingUrl: !!useInputSourceMaps, diff --git a/packages/builder/lib/tasks/replaceBuildtime.js b/packages/builder/lib/tasks/replaceBuildtime.js index 53f9bb2d57b..11e8b06eff6 100644 --- a/packages/builder/lib/tasks/replaceBuildtime.js +++ b/packages/builder/lib/tasks/replaceBuildtime.js @@ -36,7 +36,7 @@ function formatTimestamp(date) { * @param {string} options.pattern Pattern to locate the files to be processed * @returns {object} The task descriptor with its build steps */ -export default function build({pattern}) { +export default function defineTask({pattern}) { return {steps: [{ name: "replaceBuildtime", // One key per matched resource, so a delta build re-processes only the resources that changed. diff --git a/packages/builder/lib/tasks/replaceCopyright.js b/packages/builder/lib/tasks/replaceCopyright.js index 7a5731c4a6d..7e6647b254c 100644 --- a/packages/builder/lib/tasks/replaceCopyright.js +++ b/packages/builder/lib/tasks/replaceCopyright.js @@ -33,7 +33,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {string} options.pattern Pattern to locate the files to be processed * @returns {object} The task descriptor with its build steps */ -export default function build({copyright, pattern}) { +export default function defineTask({copyright, pattern}) { if (!copyright) { return {steps: []}; } diff --git a/packages/builder/lib/tasks/replaceVersion.js b/packages/builder/lib/tasks/replaceVersion.js index 8c1216d227f..df0dd1a199c 100644 --- a/packages/builder/lib/tasks/replaceVersion.js +++ b/packages/builder/lib/tasks/replaceVersion.js @@ -22,7 +22,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {string} options.version Replacement version * @returns {object} The task descriptor with its build steps */ -export default function build({pattern, version}) { +export default function defineTask({pattern, version}) { const replacerOptions = { pattern: /\$\{(?:project\.)?version\}/g, replacement: version diff --git a/packages/builder/lib/tasks/transformBootstrapHtml.js b/packages/builder/lib/tasks/transformBootstrapHtml.js index bc6e610da53..aa8735f0643 100644 --- a/packages/builder/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/lib/tasks/transformBootstrapHtml.js @@ -8,7 +8,7 @@ import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js" * @module builder/tasks/transformBootstrapHtml * @returns {object} The task descriptor */ -export default function() { +export default function defineTask() { return {task}; } From e56d74295c9583fb3058989fa946c48a495cb26f Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Fri, 16 Oct 2026 15:02:20 +0200 Subject: [PATCH 4/8] docs(builder): Document task factory options on the public default export The specVersion 5.0 refactor moved each legacy `task`-based standard task's body (and its parameter JSDoc) onto a module-scoped `task` function, leaving the public default export (the factory) with no parameter documentation. The generated API reference then showed `default() => {object}` with no Parameters section, so a consumer could no longer tell which options a task accepts to run it standalone. `generateBundle` had the opposite problem: the body's `parameters.{workspace,dependencies,taskUtil,options}` block stayed on the factory, documenting a signature the factory does not have. Document the factory's `options` on every `task`-based standard task, matching the convention the step-based tasks already use. Each factory now names its `options` parameter so the documented names match the signature, and the `generateBundle` factory block drops the reader parameters it never receives. The module-scoped `task` functions keep their own parameter JSDoc for maintainers. Also finish the `build` -> `defineTask` rename: update the stale factory naming in the `runSteps` helper JSDoc and in the custom-task documentation examples (the examples now use an anonymous default export, matching the others). No behavior change: the only runtime change is an unused `options` parameter on the factories. Builder lint and unit tests pass. --- .../docs/pages/extensibility/CustomTasks.md | 4 ++-- .../lib/tasks/bundlers/generateBundle.js | 19 ++++++++----------- .../bundlers/generateComponentPreload.js | 18 ++++++++++++++++-- .../bundlers/generateFlexChangesBundle.js | 4 +++- .../tasks/bundlers/generateLibraryPreload.js | 11 ++++++++++- .../bundlers/generateStandaloneAppBundle.js | 5 ++++- .../lib/tasks/generateCachebusterInfo.js | 5 ++++- .../lib/tasks/generateLibraryManifest.js | 4 +++- .../lib/tasks/generateResourcesJson.js | 4 +++- .../builder/lib/tasks/generateVersionInfo.js | 5 ++++- .../jsdoc/executeJsdocSdkTransformation.js | 5 ++++- .../lib/tasks/jsdoc/generateApiIndex.js | 4 +++- packages/builder/lib/tasks/runSteps.js | 8 ++++---- 13 files changed, 68 insertions(+), 28 deletions(-) diff --git a/internal/documentation/docs/pages/extensibility/CustomTasks.md b/internal/documentation/docs/pages/extensibility/CustomTasks.md index 205e2c92f37..d7505e29c68 100644 --- a/internal/documentation/docs/pages/extensibility/CustomTasks.md +++ b/internal/documentation/docs/pages/extensibility/CustomTasks.md @@ -393,7 +393,7 @@ const log = getLogger("builder:tasks:renderMarkdownFiles"); /* * Render all .md (Markdown) files in the project to HTML */ -export default function build(options) { +export default function(options) { return {steps: [{ name: "render", // One cached invocation per Markdown file, so a delta build re-renders only the files that changed. @@ -429,7 +429,7 @@ const log = getLogger("builder:tasks:renderMarkdownFiles"); /* * Render all .md (Markdown) files in the project to HTML */ -module.exports = function build(options) { +module.exports = function(options) { return {steps: [{ name: "render", // One cached invocation per Markdown file, so a delta build re-renders only the files that changed. diff --git a/packages/builder/lib/tasks/bundlers/generateBundle.js b/packages/builder/lib/tasks/bundlers/generateBundle.js index f30412f972a..401da3b5fff 100644 --- a/packages/builder/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateBundle.js @@ -8,28 +8,25 @@ import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; * @module @ui5/builder/tasks/bundlers/generateBundle */ -/* eslint-disable max-len */ /** * Generates a bundle based on the given bundle definition * + * The default export is the task factory. It receives the task options and returns the task + * descriptor. The same options are passed to the task body at execution time. + * * @public * @function default * @static * - * @param {object} parameters Parameters - * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files - * @param {@ui5/fs/ReaderCollection} parameters.dependencies Collection to read dependency files - * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil - * @param {object} parameters.options Options - * @param {string} parameters.options.projectName Project name - * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} parameters.options.bundleDefinition Module + * @param {object} options Options + * @param {string} options.projectName Project name + * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} options.bundleDefinition Module * bundle definition - * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [parameters.options.bundleOptions] Module + * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [options.bundleOptions] Module * bundle options * @returns {object} The task descriptor */ -/* eslint-enable max-len */ -export default function defineTask() { +export default function defineTask(options) { return {requiresDependencies: true, task: async function({ workspace, dependencies, taskUtil, options: {projectName, bundleDefinition, bundleOptions} }) { diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index 01f05aa6b66..58514b172c3 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -11,15 +11,29 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; */ /** - * Task to for application bundling. + * Task for Component preload bundling. + * + * The default export is the task factory. It receives the task options and returns the task + * descriptor. The same options are passed to the task body at execution time. * * @public * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name + * @param {string[]} [options.excludes=[]] List of modules declared as glob patterns (resource name patterns) + * that should be excluded. + * A pattern ending with a slash '/' will, similarly to the use of a single '*' or double '**' asterisk, + * denote an arbitrary number of characters or folder names. + * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later + * inclusion overrides an earlier exclusion, and vice versa. + * @param {string[]} [options.paths] Array of paths (or glob patterns) for component files + * @param {string[]} [options.namespaces] Array of component namespaces + * @param {string[]} [options.skipBundles] Names of bundles that should not be created * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index 987e5c1672a..9c20d0d03dd 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -20,9 +20,11 @@ import semver from "semver"; * @function default * @static * + * @param {object} [options] Options + * @param {string} [options.projectNamespace] Project Namespace * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index ca17a69d29b..10a8d01aa5c 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -321,9 +321,18 @@ function getSapUiCoreBunDef(name, filters, preload) { * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name + * @param {string[]} [options.skipBundles] Names of bundles that should not be created + * @param {string[]} [options.excludes=[]] List of modules declared as glob patterns (resource name patterns) + * that should be excluded from the library-preload.js bundle. + * A pattern ending with a slash '/' will, similarly to the use of a single '*' or double '**' asterisk, + * denote an arbitrary number of characters or folder names. + * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later + * inclusion overrides an earlier exclusion, and vice versa. * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return {task}; } diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index 327603c12f8..9c7cee1894e 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -72,9 +72,12 @@ function getBundleDefinition(config) { * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name + * @param {string} [options.projectNamespace] Project namespace * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index 4b78a54c523..12ae1c0950a 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -41,9 +41,12 @@ function getSigner(type) { * @function default * @static * + * @param {object} options Options + * @param {string} options.projectNamespace Namespace of the application + * @param {string} [options.signatureType='time'] Type of signature to be used ('time' or 'hash') * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return {task}; } diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index 59d300d0539..7f8482adafd 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -15,9 +15,11 @@ import manifestCreator from "../processors/manifestCreator.js"; * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return {task}; } diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index 214e1fba77d..578d93aa402 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -103,9 +103,11 @@ function getCreatorOptions(projectName) { * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index 8eb6050a110..e2f302a42dc 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -14,9 +14,12 @@ const MANIFEST_JSON = "manifest.json"; * @function default * @static * + * @param {object} options Options + * @param {string} options.pattern Glob pattern for .library resources + * @param {object} options.rootProject Root project (used for its name and version) * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 6a5b814798e..7313af97cec 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -18,9 +18,12 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @function default * @static * + * @param {object} options Options + * @param {string|Array} options.dotLibraryPattern Pattern to locate the .library resource to be processed + * @param {string} options.projectName Project name * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index 783e8329b13..b6d501a2b56 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -16,9 +16,11 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * @function default * @static * + * @param {object} options Options + * @param {string} options.projectName Project name * @returns {object} The task descriptor */ -export default function defineTask() { +export default function defineTask(options) { return { requiresDependencies: true, task diff --git a/packages/builder/lib/tasks/runSteps.js b/packages/builder/lib/tasks/runSteps.js index 4420413b4c6..17d99e0827b 100644 --- a/packages/builder/lib/tasks/runSteps.js +++ b/packages/builder/lib/tasks/runSteps.js @@ -58,7 +58,7 @@ class BufferedWriter extends AbstractReaderWriter { /** * Runs a step-based task's steps without a build cache. * - * A step-based task default-exports a factory build(options) => descriptor whose descriptor + * A step-based task default-exports a factory defineTask(options) => descriptor whose descriptor * carries a steps array. This runner is the no-cache counterpart to the cached step runner in * @ui5/project (which @ui5/builder cannot import, the dependency direction being * @ui5/cli -> @ui5/project -> @ui5/builder): it runs every step in order, fans out every @@ -71,7 +71,7 @@ class BufferedWriter extends AbstractReaderWriter { * * @public * @module @ui5/builder/tasks/runSteps - * @param {Function} build Task factory build(options) => descriptor whose descriptor + * @param {Function} defineTask Task factory defineTask(options) => descriptor whose descriptor * carries a steps array * @param {object} parameters * @param {@ui5/fs/DuplexCollection} parameters.workspace Workspace to read and write files @@ -80,8 +80,8 @@ class BufferedWriter extends AbstractReaderWriter { * @param {object} [parameters.options] Task options, passed to the factory and each step * @returns {Promise} Resolves once all steps have run and their writes are flushed */ -export default async function runSteps(build, {workspace, dependencies, taskUtil, options} = {}) { - const {steps} = await build(options); +export default async function runSteps(defineTask, {workspace, dependencies, taskUtil, options} = {}) { + const {steps} = await defineTask(options); if (!steps) { throw new Error( "runSteps requires a step-based task whose factory returns a descriptor with a 'steps' array"); From 02b78bfe100d210ad5fee72db9dd4e21f01939c9 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Fri, 16 Oct 2026 15:52:10 +0200 Subject: [PATCH 5/8] docs(builder): Document the task descriptor shape in the API reference Add a shared public typedef TaskDescriptor on the @ui5/builder/tasks/runSteps module and point every built-in task's default export at it, so the generated API reference shows the descriptor's shape (steps, task, requiresDependencies, determineRequiredDependencies, determineBuildSignature, specVersion) instead of a bare "object". Split the runSteps module comment into a standalone module, the typedef, and the default function doclet, so the typedef attaches to the module and renders under "Type Definitions". Add the missing requiresDependencies to the custom tasks guide. --- .../docs/pages/extensibility/CustomTasks.md | 1 + packages/builder/lib/tasks/buildThemes.js | 2 +- .../lib/tasks/bundlers/generateBundle.js | 2 +- .../bundlers/generateComponentPreload.js | 2 +- .../bundlers/generateFlexChangesBundle.js | 2 +- .../tasks/bundlers/generateLibraryPreload.js | 2 +- .../bundlers/generateStandaloneAppBundle.js | 2 +- packages/builder/lib/tasks/enhanceManifest.js | 2 +- .../lib/tasks/escapeNonAsciiCharacters.js | 2 +- .../lib/tasks/generateCachebusterInfo.js | 2 +- .../lib/tasks/generateLibraryManifest.js | 2 +- .../lib/tasks/generateResourcesJson.js | 2 +- .../tasks/generateThemeDesignerResources.js | 2 +- .../builder/lib/tasks/generateVersionInfo.js | 2 +- .../jsdoc/executeJsdocSdkTransformation.js | 2 +- .../lib/tasks/jsdoc/generateApiIndex.js | 2 +- .../builder/lib/tasks/jsdoc/generateJsdoc.js | 2 +- packages/builder/lib/tasks/minify.js | 2 +- .../builder/lib/tasks/replaceBuildtime.js | 2 +- .../builder/lib/tasks/replaceCopyright.js | 2 +- packages/builder/lib/tasks/replaceVersion.js | 2 +- packages/builder/lib/tasks/runSteps.js | 36 +++++++++++++++++++ .../lib/tasks/transformBootstrapHtml.js | 2 +- 23 files changed, 58 insertions(+), 21 deletions(-) diff --git a/internal/documentation/docs/pages/extensibility/CustomTasks.md b/internal/documentation/docs/pages/extensibility/CustomTasks.md index d7505e29c68..dec1ee3c74e 100644 --- a/internal/documentation/docs/pages/extensibility/CustomTasks.md +++ b/internal/documentation/docs/pages/extensibility/CustomTasks.md @@ -109,6 +109,7 @@ From [Specification Version](../Configuration.md#specification-versions) 5.0, a * `task` — a traditional task body (see the signature below), **or** * `steps` — an array of steps for a [cache-aware task](#cache-aware-tasks) (exactly one of `task` or `steps` must be defined) +* `requiresDependencies` — optional; whether the task needs access to the resources of the project's dependencies. When `false` (the default), the `dependencies` reader is not provided * `determineRequiredDependencies` — optional, see [Required Dependencies](#required-dependencies) * `determineBuildSignature` — optional callback contributing to the project's build signature * `specVersion` — optional; if set, it must equal the task extension's configured Specification Version diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index 49ac8136eed..202ca1c5e23 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -180,7 +180,7 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP * @param {string} [options.librariesPattern] Search pattern for .library files * @param {string} [options.themesPattern] Search pattern for sap.ui.core theme folders * @param {boolean} [options.compress=true] - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({projectName, inputPattern, librariesPattern, themesPattern, compress}) { compress = compress === undefined ? true : compress; diff --git a/packages/builder/lib/tasks/bundlers/generateBundle.js b/packages/builder/lib/tasks/bundlers/generateBundle.js index 401da3b5fff..902bac1075c 100644 --- a/packages/builder/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateBundle.js @@ -24,7 +24,7 @@ import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; * bundle definition * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [options.bundleOptions] Module * bundle options - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {requiresDependencies: true, task: async function({ diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index 58514b172c3..0c6f40546c2 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -31,7 +31,7 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @param {string[]} [options.paths] Array of paths (or glob patterns) for component files * @param {string[]} [options.namespaces] Array of component namespaces * @param {string[]} [options.skipBundles] Names of bundles that should not be created - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index 9c20d0d03dd..cd975968856 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -22,7 +22,7 @@ import semver from "semver"; * * @param {object} [options] Options * @param {string} [options.projectNamespace] Project Namespace - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index 10a8d01aa5c..80aec39997c 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -330,7 +330,7 @@ function getSapUiCoreBunDef(name, filters, preload) { * denote an arbitrary number of characters or folder names. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later * inclusion overrides an earlier exclusion, and vice versa. - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index 9c7cee1894e..63956e3a958 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -75,7 +75,7 @@ function getBundleDefinition(config) { * @param {object} options Options * @param {string} options.projectName Project name * @param {string} [options.projectNamespace] Project namespace - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/enhanceManifest.js b/packages/builder/lib/tasks/enhanceManifest.js index 1092e347983..1babf9077db 100644 --- a/packages/builder/lib/tasks/enhanceManifest.js +++ b/packages/builder/lib/tasks/enhanceManifest.js @@ -22,7 +22,7 @@ import fsInterface from "@ui5/fs/fsInterface"; * * @param {object} options Options * @param {string} options.projectNamespace Namespace of the application - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({projectNamespace}) { return {steps: [{ diff --git a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js index 6cf1a07d087..600d96a5857 100644 --- a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js @@ -20,7 +20,7 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js"; * @param {object} options Options * @param {string} options.pattern Glob pattern to locate the files to be processed * @param {string} options.encoding source file encoding either "UTF-8" or "ISO-8859-1" - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, encoding}) { if (!encoding) { diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index 12ae1c0950a..3a4ab3f6779 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -44,7 +44,7 @@ function getSigner(type) { * @param {object} options Options * @param {string} options.projectNamespace Namespace of the application * @param {string} [options.signatureType='time'] Type of signature to be used ('time' or 'hash') - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index 7f8482adafd..2c89dbaac9c 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -17,7 +17,7 @@ import manifestCreator from "../processors/manifestCreator.js"; * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index 578d93aa402..2cc23df0d9f 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -105,7 +105,7 @@ function getCreatorOptions(projectName) { * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/generateThemeDesignerResources.js b/packages/builder/lib/tasks/generateThemeDesignerResources.js index d031c3d10e6..b052d827cff 100644 --- a/packages/builder/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/lib/tasks/generateThemeDesignerResources.js @@ -112,7 +112,7 @@ async function generateThemeDotTheming({workspace, combo, themeFolder}) { * @param {string} options.version Project version * @param {string} [options.projectNamespace] If the project is of type library, provide its * namespace. Omit for type theme-library - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask(options) { const {projectName, version} = options; diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index e2f302a42dc..cde8cd9bd09 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -17,7 +17,7 @@ const MANIFEST_JSON = "manifest.json"; * @param {object} options Options * @param {string} options.pattern Glob pattern for .library resources * @param {object} options.rootProject Root project (used for its name and version) - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 7313af97cec..9392ef14094 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -21,7 +21,7 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @param {object} options Options * @param {string|Array} options.dotLibraryPattern Pattern to locate the .library resource to be processed * @param {string} options.projectName Project name - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index b6d501a2b56..f8b9ec39d75 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -18,7 +18,7 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js index 5bb346aee41..04d23f81bd6 100644 --- a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js @@ -44,7 +44,7 @@ import {createAdapter} from "@ui5/fs/resourceFactory"; * @param {string} options.namespace Namespace to build (e.g. some/project/name) * @param {string} options.version Project version * @param {string[]} [options.jsdocExcludes=[]] List of exclude patterns, relative to /resources/ - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask(options) { const {version, namespace, jsdocExcludes = []} = options; diff --git a/packages/builder/lib/tasks/minify.js b/packages/builder/lib/tasks/minify.js index 9d892321657..56248f50681 100644 --- a/packages/builder/lib/tasks/minify.js +++ b/packages/builder/lib/tasks/minify.js @@ -28,7 +28,7 @@ const log = getLogger("builder:tasks:minify"); * @param {boolean} [options.useInputSourceMaps=true] Whether to make use of any existing source * maps referenced in the resources to be minified. Use this option to preserve reference to the original * source files, such as TypeScript files, in the generated source map. - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, omitSourceMapResources = false, useInputSourceMaps = true}) { const minifierOptions = { diff --git a/packages/builder/lib/tasks/replaceBuildtime.js b/packages/builder/lib/tasks/replaceBuildtime.js index 11e8b06eff6..e484069d292 100644 --- a/packages/builder/lib/tasks/replaceBuildtime.js +++ b/packages/builder/lib/tasks/replaceBuildtime.js @@ -34,7 +34,7 @@ function formatTimestamp(date) { * * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern}) { return {steps: [{ diff --git a/packages/builder/lib/tasks/replaceCopyright.js b/packages/builder/lib/tasks/replaceCopyright.js index 7e6647b254c..6877b8408ed 100644 --- a/packages/builder/lib/tasks/replaceCopyright.js +++ b/packages/builder/lib/tasks/replaceCopyright.js @@ -31,7 +31,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.copyright Replacement copyright * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({copyright, pattern}) { if (!copyright) { diff --git a/packages/builder/lib/tasks/replaceVersion.js b/packages/builder/lib/tasks/replaceVersion.js index df0dd1a199c..10f77a80da3 100644 --- a/packages/builder/lib/tasks/replaceVersion.js +++ b/packages/builder/lib/tasks/replaceVersion.js @@ -20,7 +20,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed * @param {string} options.version Replacement version - * @returns {object} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, version}) { const replacerOptions = { diff --git a/packages/builder/lib/tasks/runSteps.js b/packages/builder/lib/tasks/runSteps.js index 17d99e0827b..b000cca5a54 100644 --- a/packages/builder/lib/tasks/runSteps.js +++ b/packages/builder/lib/tasks/runSteps.js @@ -71,6 +71,42 @@ class BufferedWriter extends AbstractReaderWriter { * * @public * @module @ui5/builder/tasks/runSteps + */ + +/** + * The descriptor a task factory returns from its default export, from + * [Specification Version](https://ui5.github.io/cli/stable/pages/Configuration/#specification-versions) 5.0. + * It carries the whole task in one object: the task body, its optional callbacks, and an optional declared + * Specification Version. + * + * The factory must stay pure and cheap. UI5 CLI calls it once per build, early also for the build signature, + * so it must not read or write resources, and it must not import heavy processors. The work happens in the + * task body or in the step callbacks. + * + * @public + * @typedef {object} TaskDescriptor + * @property {object[]} [steps] The build steps of a cache-aware, step-based task. Give exactly one of + * steps or task. A step is a scalar step {name, needs?, run} or a + * map step {name, needs?, sequential?, keys, each}; the build cache re-runs only the steps, + * and within a map step only the keys, whose inputs changed. + * @property {Function} [task] The task body async (parameters) => Promise<undefined> of a + * traditional task. Give exactly one of steps or task. + * @property {boolean} [requiresDependencies=false] Whether the task needs access to the resources of the + * project's dependencies. When false, the dependencies reader is not provided. + * @property {Function} [determineRequiredDependencies] Optional callback that reduces the set of dependencies + * made available through the dependencies reader. + * @property {Function} [determineBuildSignature] Optional callback that contributes to the project's build + * signature, so a change outside the project's own resources can invalidate the task's cached output. + * @property {string} [specVersion] Optional Specification Version. When set, it must be equal to the task + * extension's configured Specification Version. + */ + +/** + * Runs a step-based task's steps without a build cache. + * + * @public + * @function default + * @static * @param {Function} defineTask Task factory defineTask(options) => descriptor whose descriptor * carries a steps array * @param {object} parameters diff --git a/packages/builder/lib/tasks/transformBootstrapHtml.js b/packages/builder/lib/tasks/transformBootstrapHtml.js index aa8735f0643..19e2e0fb22b 100644 --- a/packages/builder/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/lib/tasks/transformBootstrapHtml.js @@ -6,7 +6,7 @@ import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js" * Task for transforming the application bootstrap HTML file. * * @module builder/tasks/transformBootstrapHtml - * @returns {object} The task descriptor + * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor */ export default function defineTask() { return {task}; From b7a5386de19afc6045ad7e181d618546c657344d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 21 Oct 2026 16:11:33 +0200 Subject: [PATCH 6/8] refactor(builder): Rename standalone task runner --- .../skills/incremental-build/architecture.md | 6 ++--- packages/builder/lib/tasks/buildThemes.js | 4 +-- .../lib/tasks/bundlers/generateBundle.js | 2 +- .../bundlers/generateComponentPreload.js | 2 +- .../bundlers/generateFlexChangesBundle.js | 2 +- .../tasks/bundlers/generateLibraryPreload.js | 2 +- .../bundlers/generateStandaloneAppBundle.js | 2 +- packages/builder/lib/tasks/enhanceManifest.js | 2 +- .../lib/tasks/escapeNonAsciiCharacters.js | 2 +- .../lib/tasks/generateCachebusterInfo.js | 2 +- .../lib/tasks/generateLibraryManifest.js | 2 +- .../lib/tasks/generateResourcesJson.js | 2 +- .../tasks/generateThemeDesignerResources.js | 4 +-- .../builder/lib/tasks/generateVersionInfo.js | 2 +- .../jsdoc/executeJsdocSdkTransformation.js | 2 +- .../lib/tasks/jsdoc/generateApiIndex.js | 2 +- .../builder/lib/tasks/jsdoc/generateJsdoc.js | 2 +- packages/builder/lib/tasks/minify.js | 2 +- .../builder/lib/tasks/replaceBuildtime.js | 2 +- .../builder/lib/tasks/replaceCopyright.js | 2 +- packages/builder/lib/tasks/replaceVersion.js | 2 +- .../lib/tasks/{runSteps.js => runTask.js} | 6 ++--- .../lib/tasks/transformBootstrapHtml.js | 2 +- packages/builder/test/lib/package-exports.js | 1 + .../test/lib/tasks/buildThemes.integration.js | 6 ++--- .../builder/test/lib/tasks/buildThemes.js | 16 ++++++------ .../builder/test/lib/tasks/enhanceManifest.js | 10 +++---- .../lib/tasks/escapeNonAsciiCharacters.js | 6 ++--- .../tasks/generateThemeDesignerResources.js | 22 ++++++++-------- .../test/lib/tasks/minify.integration.js | 18 ++++++------- packages/builder/test/lib/tasks/minify.js | 10 +++---- .../test/lib/tasks/replaceBuildtime.js | 6 ++--- .../test/lib/tasks/replaceCopyright.js | 8 +++--- .../builder/test/lib/tasks/replaceVersion.js | 4 +-- .../lib/tasks/{runSteps.js => runTask.js} | 26 +++++++++---------- packages/fs/lib/internal/stepWriteBuffer.js | 2 +- .../project/lib/build/helpers/StepRunner.js | 4 +-- 37 files changed, 99 insertions(+), 98 deletions(-) rename packages/builder/lib/tasks/{runSteps.js => runTask.js} (97%) rename packages/builder/test/lib/tasks/{runSteps.js => runTask.js} (91%) diff --git a/.agents/skills/incremental-build/architecture.md b/.agents/skills/incremental-build/architecture.md index 92ee5e68c4c..98d21812f0c 100644 --- a/.agents/skills/incremental-build/architecture.md +++ b/.agents/skills/incremental-build/architecture.md @@ -316,9 +316,9 @@ Two step shapes, run in array order by `StepRunner.runSteps()`: A scalar step is a one-key group (a single implicit invocation); a map step is a multi-key group. Both go through the same per-key machinery within their stage, so delta selection, stale-output derivation, and per-stage request/input folding are uniform across shapes. -**`needs` wiring.** A step lists earlier step names in `needs`; those steps' returns arrive as `needs.` in its context (both `keys` and `each` for a map step). A step may reference only earlier steps, so array order is always a valid execution order. One `needs` object is shared by a step's `keys` enumerator and all of its invocations, and is frozen (shallowly): an invocation assigning to `needs.` would otherwise leak into its siblings and into the `needsInputs` recorded for whichever invocation ran next, making a delta build's per-invocation selection depend on execution order. `@ui5/builder`'s standalone `runSteps` freezes it too, so a step behaves the same under both runners. Because steps share `needs` state in memory, `StepRunner` remains the per-task driver that runs all of a task's steps in order — it just drives one stage per step (see below) instead of folding them into one. +**`needs` wiring.** A step lists earlier step names in `needs`; those steps' returns arrive as `needs.` in its context (both `keys` and `each` for a map step). A step may reference only earlier steps, so array order is always a valid execution order. One `needs` object is shared by a step's `keys` enumerator and all of its invocations, and is frozen (shallowly): an invocation assigning to `needs.` would otherwise leak into its siblings and into the `needsInputs` recorded for whichever invocation ran next, making a delta build's per-invocation selection depend on execution order. `@ui5/builder`'s standalone `runTask` freezes it too, so a step behaves the same under both runners. Because steps share `needs` state in memory, `StepRunner` remains the per-task driver that runs all of a task's steps in order — it just drives one stage per step (see below) instead of folding them into one. -**Declaration validation.** The whole step list is validated **before any stage is created**, by `validateSteps(steps, {taskName?})` (exported from `StepRunner.js`). `TaskRunner` calls it at discovery, right after the factory returns and before it derives the step names for `setTasks`: a stage id is `task/{taskName}::step/{stepName}`, so a duplicate step name would create two stages sharing one id — the error must be raised before `setTasks`, not when the step later runs. `StepRunner.runSteps()` calls it again at its entry, so a directly or standalone instantiated runner validates identically. The rules are static over the descriptors (no reader, taskUtil, options or cache state): the factory must return an array; each element must be an object with a non-empty string `name`; names must be unique; a step is either scalar (`run`) or map (`keys` **and** `each`), never both nor neither (a half-defined map names the missing half); `needs`, when present, must be an array of strings naming only **earlier** steps (so a forward or self reference throws, which is what makes a `needs` cycle impossible). Scope is `@ui5/project`; `@ui5/builder`'s standalone `runSteps` does not yet share this validator. +**Declaration validation.** The whole step list is validated **before any stage is created**, by `validateSteps(steps, {taskName?})` (exported from `StepRunner.js`). `TaskRunner` calls it at discovery, right after the factory returns and before it derives the step names for `setTasks`: a stage id is `task/{taskName}::step/{stepName}`, so a duplicate step name would create two stages sharing one id — the error must be raised before `setTasks`, not when the step later runs. `StepRunner.runSteps()` calls it again at its entry, so a directly or standalone instantiated runner validates identically. The rules are static over the descriptors (no reader, taskUtil, options or cache state): the factory must return an array; each element must be an object with a non-empty string `name`; names must be unique; a step is either scalar (`run`) or map (`keys` **and** `each`), never both nor neither (a half-defined map names the missing half); `needs`, when present, must be an array of strings naming only **earlier** steps (so a forward or self reference throws, which is what makes a `needs` cycle impossible). Scope is `@ui5/project`; `@ui5/builder`'s standalone `runTask` does not yet share this validator. **Authoring a step-based task (the DSL).** A task module default-exports the `build(options)` factory and returns the step array. The reference shape is `generateThemeDesignerResources` (`packages/builder/lib/tasks/generateThemeDesignerResources.js`), a scalar producer feeding a scalar consumer and a map step: @@ -373,7 +373,7 @@ Two DSL rules make the per-step recording honest, and both are visible here. The **Driving one stage per step.** Before the build, `TaskRunner.runTasks` reads each step-based task's `descriptor.steps` (already resolved by `TaskDefinitions`) and hands `ProjectBuildCache.setTasks` a `[{taskName, stepNames?}]` list, which creates one stage per step in step order. The step array is kept on the task (frozen, so a custom task cannot mutate the shared value) and reused when the step runs. One memoized descriptor is the single source of truth: discovery and execution cannot drive different stages, so no runtime check is needed to reconcile two factory calls. The `options` object is completed at registration time (`TaskDefinitions`/`_addTask` fill in `projectName`, `projectNamespace`) and is fixed for a `TaskRunner`'s lifetime: a changed `ui5.yaml` task configuration rebuilds the whole serving stack through `Supervisor.reinitialize`, giving a fresh `TaskRunner`, while a plain source-change rebuild reuses the `TaskRunner` and re-reads the frozen step array. For each step, the `StepRunner` drives that step's own stage via hooks the `TaskRunner` supplies (`#createStepStageHooks`): `prepareStage(stepName)` switches the project to the step's stage and returns its cache verdict (`prepareStageExecutionAndValidateCache(taskName, stepName)`); `createStageContext()` builds fresh monitored `workspace`/`dependencies` readers and a `MonitoredTaskUtil` bound to that stage (so the reads reflect the cumulative output of all earlier stages with this stage's writer on top); `recordStage(stepName, …)` records the step's stage (`recordStageResult({taskName, …, stepName})`). A fully-cached stage (`prepareStage` returns `true`) does not run: its return is rebuilt from the persisted per-key data (in key order) so later steps' `needs` resolve, and its tag operations are replayed. A `needs` return is excluded from the stage signature, so a full-hit consumer whose producer re-ran with a changed return would otherwise serve stale output. The `StepRunner` checks this on the full-hit path (`#needsReturnChanged`) and, when it fires, reopens the stage with a fresh writer and re-runs it as a full execution (`reopenStage` returns a falsy verdict, not a delta). Reopen installs a fresh empty writable stage, so pruning the re-run to only the affected units would need re-seeding it with the cached outputs first, so the whole stage re-runs instead. No shipped multi-step task pays for this: only `generateThemeDesignerResources` wires `needs`, and a changed `scan` return regenerates its themes anyway. The verdict (`true` / delta object / falsy) is read three ways in `runSteps`. An explicit verdict shape would read more clearly but is deferred, since that shape is shared with the stage-signature code. A task is reported to the `ProjectBuildLogger` as skipped (`skipTask`) when every step was served from cache, else as executed. Its skip verdict is only known once every stage has been driven, so it cannot be announced up front like a legacy task: the `StepRunner` instead calls `notifyStepExecution` from the first stage that stops being a pure cache hit, before that stage runs anything, and `TaskRunner.#createTaskExecutionReport` turns that into `startTask`. `task-start` therefore precedes the work it announces, and the matching `endTask` (with the union of the stages' written paths) follows it, so a `project-build-status` consumer sees the task running while it runs. A step-based task that returns **no** steps still has one stage (`task/{taskName}`) that is prepared and recorded (empty), so its empty result caches and a later build reports it skipped. -**Standalone (uncached) runner.** `@ui5/builder` exposes `runSteps(factory, {workspace, dependencies, taskUtil, options})` (`lib/tasks/runSteps.js`) for standalone task invocation with no build cache: it calls the factory, reads `descriptor.steps` (throwing if the descriptor has no `steps` array), runs every step in order, fans out each `keys` set, threads `needs` in memory, and buffers map writes in key order, without delta selection, persistence, or tag replay. It is an independent implementation (it does not use the `@ui5/project` `StepRunner`); the full cached engine stays in `@ui5/project` because `@ui5/builder` cannot import it. The two runners stay separate but share one definition of the write-buffer contract: the same-path guard (with its user-visible error message) and the key-order flush live in `@ui5/fs/internal/stepWriteBuffer` (`assertDistinctWrite`, `flushWriteBuffer`), reached by both because both already depend on `@ui5/fs`. The two writers differ only in which method they override: `@ui5/project`'s `RecordingReaderWriter` overrides `_write` (it also records writes for the cache, and the base `write` has already defaulted options), while `@ui5/builder`'s `BufferedWriter` overrides the public `write` to preserve the caller's exact arguments; both buffer `{resource, args, index}` entries and flush them identically. +**Standalone (uncached) runner.** `@ui5/builder` exposes `runTask(factory, {workspace, dependencies, taskUtil, options})` (`lib/tasks/runTask.js`) for standalone task invocation with no build cache: it calls the factory, reads `descriptor.steps` (throwing if the descriptor has no `steps` array), runs every step in order, fans out each `keys` set, threads `needs` in memory, and buffers map writes in key order, without delta selection, persistence, or tag replay. It is an independent implementation (it does not use the `@ui5/project` `StepRunner`); the full cached engine stays in `@ui5/project` because `@ui5/builder` cannot import it. The two runners stay separate but share one definition of the write-buffer contract: the same-path guard (with its user-visible error message) and the key-order flush live in `@ui5/fs/internal/stepWriteBuffer` (`assertDistinctWrite`, `flushWriteBuffer`), reached by both because both already depend on `@ui5/fs`. The two writers differ only in which method they override: `@ui5/project`'s `RecordingReaderWriter` overrides `_write` (it also records writes for the cache, and the base `write` has already defaulted options), while `@ui5/builder`'s `BufferedWriter` overrides the public `write` to preserve the caller's exact arguments; both buffer `{resource, args, index}` entries and flush them identically. Per invocation within a stage, the callback receives its own recording readers (`{workspace, dependencies}`) that delegate to the stage's monitored readers and additionally attribute each read to the invocation, plus a per-invocation `MonitoredTaskUtil` wrapping the stage's one. The per-invocation monitor attributes the invocation's non-resource inputs (`getEnv`, `getTime`, `getProject(name).getVersion()`, `isRootProject`, `getDependencies`) and its `getTag`/`setTag`/`clearTag` operations to the invocation, while reads still delegate through the stage monitor so a full build's stage-level recording stays the union that keys the stage. Key identity (`StepRunner.#keyId`) is content-based: a resource key by its path and a content discriminator (path distinguishes resources that share content but produce different output; the discriminator makes a content change a new key, so the invocation re-runs and its previous output is dropped rather than served stale), a string key by its value. Compound keys are the caller's responsibility to express as a stable string. The discriminator is **tiered like `isResourceUnchanged`** (see §"HashTree") rather than always the SSRI integrity: `lastModified` + `size` when both are statically available (`getLastModified()` returns a number and `hasSize()` is true, so no content read), falling back to the integrity when either is missing — a memory-backed or generated resource with no `lastModified`, or one whose size is not statically known. A resource restored from a stage cache carries its `integrity`, so the fallback `getIntegrity()` resolves without reading content and that path stays cheap. This avoids hashing every enumerated key's full content before delta selection; it is why `#resolveEntries` is cheap for a broad `workspace.byGlob` step (`replaceCopyright`, `replaceVersion`) on a stale-cache build (see performance-investigation.md §12). The residual staleness risk is identical to `isResourceUnchanged`'s and accepted for the same reason: a content change that preserves **both** `lastModified` and `size` keeps the same key and does not re-run the invocation — a real edit moves mtime, so the gap is only for mtime-preserving replacements (`cp -p`, `tar -x`, atomic rename) that also hold size constant, and the changed-path delta does not independently cover this case because it is derived through the same tiered comparison. Because the stat tier and the integrity tier use distinct prefixes (`\0m…\0s…` vs `\0i…`), a key never aliases across the two tiers. diff --git a/packages/builder/lib/tasks/buildThemes.js b/packages/builder/lib/tasks/buildThemes.js index 202ca1c5e23..7b9a1b64378 100644 --- a/packages/builder/lib/tasks/buildThemes.js +++ b/packages/builder/lib/tasks/buildThemes.js @@ -168,7 +168,7 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP * A step-based task: the default export is a factory returning one map step with a key per theme's * library.source.less. A step probes only the gating marker and imports of its own theme, * so a delta build rebuilds only the affected theme and leaves the others served from cache. Standalone - * invocation runs every theme through @ui5/builder's runSteps. + * invocation runs every theme through @ui5/builder's runTask. * * @public * @function default @@ -180,7 +180,7 @@ async function isThemeAvailable(themeResource, combo, {librariesPattern, themesP * @param {string} [options.librariesPattern] Search pattern for .library files * @param {string} [options.themesPattern] Search pattern for sap.ui.core theme folders * @param {boolean} [options.compress=true] - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({projectName, inputPattern, librariesPattern, themesPattern, compress}) { compress = compress === undefined ? true : compress; diff --git a/packages/builder/lib/tasks/bundlers/generateBundle.js b/packages/builder/lib/tasks/bundlers/generateBundle.js index 902bac1075c..081787a1540 100644 --- a/packages/builder/lib/tasks/bundlers/generateBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateBundle.js @@ -24,7 +24,7 @@ import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; * bundle definition * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [options.bundleOptions] Module * bundle options - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {requiresDependencies: true, task: async function({ diff --git a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js index 0c6f40546c2..8660a954758 100644 --- a/packages/builder/lib/tasks/bundlers/generateComponentPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateComponentPreload.js @@ -31,7 +31,7 @@ import {negateFilters} from "../../lbt/resources/ResourceFilterList.js"; * @param {string[]} [options.paths] Array of paths (or glob patterns) for component files * @param {string[]} [options.namespaces] Array of component namespaces * @param {string[]} [options.skipBundles] Names of bundles that should not be created - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js index cd975968856..338a0568e32 100644 --- a/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateFlexChangesBundle.js @@ -22,7 +22,7 @@ import semver from "semver"; * * @param {object} [options] Options * @param {string} [options.projectNamespace] Project Namespace - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js index 80aec39997c..7f318ae22d2 100644 --- a/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js +++ b/packages/builder/lib/tasks/bundlers/generateLibraryPreload.js @@ -330,7 +330,7 @@ function getSapUiCoreBunDef(name, filters, preload) { * denote an arbitrary number of characters or folder names. * Re-includes should be marked with a leading exclamation mark '!'. The order of filters is relevant; a later * inclusion overrides an earlier exclusion, and vice versa. - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js index 63956e3a958..34d83ea36ab 100644 --- a/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js +++ b/packages/builder/lib/tasks/bundlers/generateStandaloneAppBundle.js @@ -75,7 +75,7 @@ function getBundleDefinition(config) { * @param {object} options Options * @param {string} options.projectName Project name * @param {string} [options.projectNamespace] Project namespace - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/enhanceManifest.js b/packages/builder/lib/tasks/enhanceManifest.js index 1babf9077db..05d3530e826 100644 --- a/packages/builder/lib/tasks/enhanceManifest.js +++ b/packages/builder/lib/tasks/enhanceManifest.js @@ -22,7 +22,7 @@ import fsInterface from "@ui5/fs/fsInterface"; * * @param {object} options Options * @param {string} options.projectNamespace Namespace of the application - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({projectNamespace}) { return {steps: [{ diff --git a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js index 600d96a5857..6791f2334dd 100644 --- a/packages/builder/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/lib/tasks/escapeNonAsciiCharacters.js @@ -20,7 +20,7 @@ import nonAsciiEscaper from "../processors/nonAsciiEscaper.js"; * @param {object} options Options * @param {string} options.pattern Glob pattern to locate the files to be processed * @param {string} options.encoding source file encoding either "UTF-8" or "ISO-8859-1" - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, encoding}) { if (!encoding) { diff --git a/packages/builder/lib/tasks/generateCachebusterInfo.js b/packages/builder/lib/tasks/generateCachebusterInfo.js index 3a4ab3f6779..3902d7a0c5d 100644 --- a/packages/builder/lib/tasks/generateCachebusterInfo.js +++ b/packages/builder/lib/tasks/generateCachebusterInfo.js @@ -44,7 +44,7 @@ function getSigner(type) { * @param {object} options Options * @param {string} options.projectNamespace Namespace of the application * @param {string} [options.signatureType='time'] Type of signature to be used ('time' or 'hash') - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/generateLibraryManifest.js b/packages/builder/lib/tasks/generateLibraryManifest.js index 2c89dbaac9c..06e4b1dd60a 100644 --- a/packages/builder/lib/tasks/generateLibraryManifest.js +++ b/packages/builder/lib/tasks/generateLibraryManifest.js @@ -17,7 +17,7 @@ import manifestCreator from "../processors/manifestCreator.js"; * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return {task}; diff --git a/packages/builder/lib/tasks/generateResourcesJson.js b/packages/builder/lib/tasks/generateResourcesJson.js index 2cc23df0d9f..de22875fad5 100644 --- a/packages/builder/lib/tasks/generateResourcesJson.js +++ b/packages/builder/lib/tasks/generateResourcesJson.js @@ -105,7 +105,7 @@ function getCreatorOptions(projectName) { * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/generateThemeDesignerResources.js b/packages/builder/lib/tasks/generateThemeDesignerResources.js index b052d827cff..1fc2a8ecda0 100644 --- a/packages/builder/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/lib/tasks/generateThemeDesignerResources.js @@ -101,7 +101,7 @@ async function generateThemeDotTheming({workspace, combo, themeFolder}) { * library.less, reading the core .theming and the less imports through its own * combo so those reads are recorded per step. A delta build regenerates only the affected theme. The * later steps consume the scan result through needs, so they stay cached while a library has - * themes even as individual themes change. Standalone invocation runs every step through runSteps. + * themes even as individual themes change. Standalone invocation runs every step through runTask. * * @public * @function default @@ -112,7 +112,7 @@ async function generateThemeDotTheming({workspace, combo, themeFolder}) { * @param {string} options.version Project version * @param {string} [options.projectNamespace] If the project is of type library, provide its * namespace. Omit for type theme-library - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask(options) { const {projectName, version} = options; diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index cde8cd9bd09..827dd4493ec 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -17,7 +17,7 @@ const MANIFEST_JSON = "manifest.json"; * @param {object} options Options * @param {string} options.pattern Glob pattern for .library resources * @param {object} options.rootProject Root project (used for its name and version) - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js index 9392ef14094..464ddb7b713 100644 --- a/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js +++ b/packages/builder/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -21,7 +21,7 @@ import sdkTransformer from "../../processors/jsdoc/sdkTransformer.js"; * @param {object} options Options * @param {string|Array} options.dotLibraryPattern Pattern to locate the .library resource to be processed * @param {string} options.projectName Project name - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js index f8b9ec39d75..e9c8a675730 100644 --- a/packages/builder/lib/tasks/jsdoc/generateApiIndex.js +++ b/packages/builder/lib/tasks/jsdoc/generateApiIndex.js @@ -18,7 +18,7 @@ import apiIndexGenerator from "../../processors/jsdoc/apiIndexGenerator.js"; * * @param {object} options Options * @param {string} options.projectName Project name - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { return { diff --git a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js index 04d23f81bd6..108df0b81b3 100644 --- a/packages/builder/lib/tasks/jsdoc/generateJsdoc.js +++ b/packages/builder/lib/tasks/jsdoc/generateJsdoc.js @@ -44,7 +44,7 @@ import {createAdapter} from "@ui5/fs/resourceFactory"; * @param {string} options.namespace Namespace to build (e.g. some/project/name) * @param {string} options.version Project version * @param {string[]} [options.jsdocExcludes=[]] List of exclude patterns, relative to /resources/ - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask(options) { const {version, namespace, jsdocExcludes = []} = options; diff --git a/packages/builder/lib/tasks/minify.js b/packages/builder/lib/tasks/minify.js index 56248f50681..e3167940d36 100644 --- a/packages/builder/lib/tasks/minify.js +++ b/packages/builder/lib/tasks/minify.js @@ -28,7 +28,7 @@ const log = getLogger("builder:tasks:minify"); * @param {boolean} [options.useInputSourceMaps=true] Whether to make use of any existing source * maps referenced in the resources to be minified. Use this option to preserve reference to the original * source files, such as TypeScript files, in the generated source map. - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, omitSourceMapResources = false, useInputSourceMaps = true}) { const minifierOptions = { diff --git a/packages/builder/lib/tasks/replaceBuildtime.js b/packages/builder/lib/tasks/replaceBuildtime.js index e484069d292..66e573f6381 100644 --- a/packages/builder/lib/tasks/replaceBuildtime.js +++ b/packages/builder/lib/tasks/replaceBuildtime.js @@ -34,7 +34,7 @@ function formatTimestamp(date) { * * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern}) { return {steps: [{ diff --git a/packages/builder/lib/tasks/replaceCopyright.js b/packages/builder/lib/tasks/replaceCopyright.js index 6877b8408ed..11017bd9291 100644 --- a/packages/builder/lib/tasks/replaceCopyright.js +++ b/packages/builder/lib/tasks/replaceCopyright.js @@ -31,7 +31,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.copyright Replacement copyright * @param {string} options.pattern Pattern to locate the files to be processed - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({copyright, pattern}) { if (!copyright) { diff --git a/packages/builder/lib/tasks/replaceVersion.js b/packages/builder/lib/tasks/replaceVersion.js index 10f77a80da3..fecbcbc256f 100644 --- a/packages/builder/lib/tasks/replaceVersion.js +++ b/packages/builder/lib/tasks/replaceVersion.js @@ -20,7 +20,7 @@ import stringReplacer from "../processors/stringReplacer.js"; * @param {object} options Options * @param {string} options.pattern Pattern to locate the files to be processed * @param {string} options.version Replacement version - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor with its build steps + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor with its build steps */ export default function defineTask({pattern, version}) { const replacerOptions = { diff --git a/packages/builder/lib/tasks/runSteps.js b/packages/builder/lib/tasks/runTask.js similarity index 97% rename from packages/builder/lib/tasks/runSteps.js rename to packages/builder/lib/tasks/runTask.js index b000cca5a54..952c7c05064 100644 --- a/packages/builder/lib/tasks/runSteps.js +++ b/packages/builder/lib/tasks/runTask.js @@ -70,7 +70,7 @@ class BufferedWriter extends AbstractReaderWriter { * absent here; every step runs. * * @public - * @module @ui5/builder/tasks/runSteps + * @module @ui5/builder/tasks/runTask */ /** @@ -116,11 +116,11 @@ class BufferedWriter extends AbstractReaderWriter { * @param {object} [parameters.options] Task options, passed to the factory and each step * @returns {Promise} Resolves once all steps have run and their writes are flushed */ -export default async function runSteps(defineTask, {workspace, dependencies, taskUtil, options} = {}) { +export default async function runTask(defineTask, {workspace, dependencies, taskUtil, options} = {}) { const {steps} = await defineTask(options); if (!steps) { throw new Error( - "runSteps requires a step-based task whose factory returns a descriptor with a 'steps' array"); + "runTask requires a step-based task whose factory returns a descriptor with a 'steps' array"); } // Each step's return, so a later step's needs can consume it. A scalar step's return is its single // value; a map step's is the array of its per-key returns in key order. diff --git a/packages/builder/lib/tasks/transformBootstrapHtml.js b/packages/builder/lib/tasks/transformBootstrapHtml.js index 19e2e0fb22b..d8a0b01d043 100644 --- a/packages/builder/lib/tasks/transformBootstrapHtml.js +++ b/packages/builder/lib/tasks/transformBootstrapHtml.js @@ -6,7 +6,7 @@ import bootstrapHtmlTransformer from "../processors/bootstrapHtmlTransformer.js" * Task for transforming the application bootstrap HTML file. * * @module builder/tasks/transformBootstrapHtml - * @returns {module:@ui5/builder/tasks/runSteps~TaskDescriptor} The task descriptor + * @returns {module:@ui5/builder/tasks/runTask~TaskDescriptor} The task descriptor */ export default function defineTask() { return {task}; diff --git a/packages/builder/test/lib/package-exports.js b/packages/builder/test/lib/package-exports.js index 3587a38aa7c..a721f495c40 100644 --- a/packages/builder/test/lib/package-exports.js +++ b/packages/builder/test/lib/package-exports.js @@ -48,6 +48,7 @@ test("check number of exports", (t) => { "tasks/replaceCopyright", "tasks/replaceVersion", "tasks/replaceBuildtime", + "tasks/runTask", "tasks/transformBootstrapHtml", "tasks/enhanceManifest", diff --git a/packages/builder/test/lib/tasks/buildThemes.integration.js b/packages/builder/test/lib/tasks/buildThemes.integration.js index 765c91d0f7f..6ac2b5645c7 100644 --- a/packages/builder/test/lib/tasks/buildThemes.integration.js +++ b/packages/builder/test/lib/tasks/buildThemes.integration.js @@ -1,6 +1,6 @@ import test from "ava"; import buildThemes from "../../../lib/tasks/buildThemes.js"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; import DuplexCollection from "@ui5/fs/DuplexCollection"; @@ -46,7 +46,7 @@ test("integration: simple", async (t) => { string: content }); await reader.write(resource); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace: duplexCollection, dependencies: dependencies, options: { @@ -127,7 +127,7 @@ test("integration: imports", async (t) => { return reader.write(resource); })); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace: duplexCollection, dependencies: dependencies, options: { diff --git a/packages/builder/test/lib/tasks/buildThemes.js b/packages/builder/test/lib/tasks/buildThemes.js index f85cb49d4d6..4ef29dc91b3 100644 --- a/packages/builder/test/lib/tasks/buildThemes.js +++ b/packages/builder/test/lib/tasks/buildThemes.js @@ -2,7 +2,7 @@ import test from "ava"; import sinon from "sinon"; import esmock from "esmock"; import {deserializeResources} from "../../../lib/processors/themeBuilderWorker.js"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; let buildThemes; test.before(async () => { @@ -63,7 +63,7 @@ test.serial("buildThemes", async (t) => { jsonParametersResource ]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, options: { projectName: "sap.ui.demo.app", @@ -116,7 +116,7 @@ test.serial("buildThemes (compress = false)", async (t) => { jsonParametersResource ]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, options: { projectName: "sap.ui.demo.app", @@ -191,7 +191,7 @@ test.serial("buildThemes (filtering libraries)", async (t) => { // One step per surviving theme; a fresh result per call so concurrent writes stay independent. t.context.themeBuilderStub.callsFake(() => [{}]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, options: { projectName: "sap.ui.test.lib1", @@ -267,7 +267,7 @@ test.serial("buildThemes (filtering themes)", async (t) => { // One step per surviving theme; a fresh result per call so concurrent writes stay independent. t.context.themeBuilderStub.callsFake(() => [{}]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, options: { projectName: "sap.ui.test.lib1", @@ -381,7 +381,7 @@ test.serial("buildThemes (filtering libraries + themes)", async (t) => { // One step per surviving theme; a fresh result per call so concurrent writes stay independent. t.context.themeBuilderStub.callsFake(() => [{}]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, options: { projectName: "sap.ui.test.lib1", @@ -452,7 +452,7 @@ test.serial("buildThemes (useWorkers = true)", async (t) => { jsonParametersResource ]); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, taskUtil: taskUtilMock, options: { @@ -523,7 +523,7 @@ test.serial("buildThemes with taskUtil and unexpected termination of the workerp mkdir: (...args) => args[args.length - 1](null, {}), }); - await runSteps(buildThemes, { + await runTask(buildThemes, { workspace, taskUtil: taskUtilMock, options: { diff --git a/packages/builder/test/lib/tasks/enhanceManifest.js b/packages/builder/test/lib/tasks/enhanceManifest.js index 18c6edb6bb1..19f397885e2 100644 --- a/packages/builder/test/lib/tasks/enhanceManifest.js +++ b/packages/builder/test/lib/tasks/enhanceManifest.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import sinonGlobal from "sinon"; import esmock from "esmock"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; @@ -72,7 +72,7 @@ test.serial("Transforms single manifest.json resource", async (t) => { t.context.manifestEnhancerStub.returns([resource]); - await runSteps(enhanceManifest, { + await runTask(enhanceManifest, { workspace, options: { projectNamespace: "sap/ui/demo/app" @@ -179,7 +179,7 @@ test.serial("Transforms all manifest.json resources", async (t) => { t.context.manifestEnhancerStub.callsFake(({resources}) => resources[0] === resourceReuseComp2 ? [] : [resources[0]]); - await runSteps(enhanceManifest, { + await runTask(enhanceManifest, { workspace, options: { projectNamespace: "sap/ui/demo/lib" @@ -279,7 +279,7 @@ test.serial("Transforms multiple manifest.json resources", async (t) => { // One step per manifest.json: each changed manifest is written back at its own path. t.context.manifestEnhancerStub.callsFake(({resources}) => [resources[0]]); - await runSteps(enhanceManifest, { + await runTask(enhanceManifest, { workspace, options: { projectNamespace: "sap/ui/demo/lib" @@ -323,7 +323,7 @@ test.serial("Should not rewrite the manifest.json if no changes were made", asyn t.context.manifestEnhancerStub.returns([]); - await runSteps(enhanceManifest, { + await runTask(enhanceManifest, { workspace, options: { projectNamespace: "sap/ui/demo/app" diff --git a/packages/builder/test/lib/tasks/escapeNonAsciiCharacters.js b/packages/builder/test/lib/tasks/escapeNonAsciiCharacters.js index 63453884a36..1197c1dbe69 100644 --- a/packages/builder/test/lib/tasks/escapeNonAsciiCharacters.js +++ b/packages/builder/test/lib/tasks/escapeNonAsciiCharacters.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import escapeNonAsciiCharacters from "../../../lib/tasks/escapeNonAsciiCharacters.js"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; import DuplexCollection from "@ui5/fs/DuplexCollection"; @@ -33,7 +33,7 @@ city=Ort:`; }); await workspace.write(resource); - await runSteps(escapeNonAsciiCharacters, { + await runTask(escapeNonAsciiCharacters, { workspace, options: { encoding: "UTF-8", @@ -80,7 +80,7 @@ city=Ort:`; }); await workspace.write(resource); - await runSteps(escapeNonAsciiCharacters, { + await runTask(escapeNonAsciiCharacters, { workspace, options: { encoding: "ISO-8859-1", diff --git a/packages/builder/test/lib/tasks/generateThemeDesignerResources.js b/packages/builder/test/lib/tasks/generateThemeDesignerResources.js index 646b84dbdca..3cd73ff8b75 100644 --- a/packages/builder/test/lib/tasks/generateThemeDesignerResources.js +++ b/packages/builder/test/lib/tasks/generateThemeDesignerResources.js @@ -1,7 +1,7 @@ import test from "ava"; import sinonGlobal from "sinon"; import esmock from "esmock"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; test.beforeEach(async (t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); @@ -83,7 +83,7 @@ test.serial("generateThemeDesignerResources: Library", async (t) => { ]); libraryLessGeneratorStub.callsFake(async ({resources}) => [lessByTheme.get(resources[0])]); - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -188,7 +188,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core", async (t) => libraryLessGeneratorStub.resolves([libraryLessResource]); - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -287,7 +287,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core with existing l libraryLessGeneratorStub.resolves([libraryLessResource]); - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -378,7 +378,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core without themes, libraryLessGeneratorStub.resolves([libraryLessResource]); - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -447,7 +447,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.core with existing i libraryLessGeneratorStub.resolves([libraryLessResource]); - await t.throwsAsync(runSteps(generateThemeDesignerResources, { + await t.throwsAsync(runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -480,7 +480,7 @@ test.serial("generateThemeDesignerResources: Library sap.ui.documentation is ski write: sinon.stub() }; - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace: {}, dependencies: {}, options: { @@ -508,7 +508,7 @@ test.serial("generateThemeDesignerResources: Library without themes", async (t) write: sinon.stub() }; - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies: {}, options: { @@ -566,7 +566,7 @@ test.serial("generateThemeDesignerResources: Theme-Library", async (t) => { libraryLessGeneratorStub.resolves([libraryLessResource]); - await runSteps(generateThemeDesignerResources, { + await runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -644,7 +644,7 @@ test.serial("generateThemeDesignerResources: .theming file missing in sap.ui.cor libraryLessGeneratorStub.resolves([libraryLessResource]); - await t.throwsAsync(runSteps(generateThemeDesignerResources, { + await t.throwsAsync(runTask(generateThemeDesignerResources, { workspace, dependencies, options: { @@ -699,7 +699,7 @@ test.serial("generateThemeDesignerResources: Failed to extract library name from }; const dependencies = {}; - await t.throwsAsync(runSteps(generateThemeDesignerResources, { + await t.throwsAsync(runTask(generateThemeDesignerResources, { workspace, dependencies, options: { diff --git a/packages/builder/test/lib/tasks/minify.integration.js b/packages/builder/test/lib/tasks/minify.integration.js index 48cd0196b50..f034a8ad93a 100644 --- a/packages/builder/test/lib/tasks/minify.integration.js +++ b/packages/builder/test/lib/tasks/minify.integration.js @@ -1,7 +1,7 @@ import test from "ava"; import sinonGlobal from "sinon"; import minify from "../../../lib/tasks/minify.js"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import * as resourceFactory from "@ui5/fs/resourceFactory"; import DuplexCollection from "@ui5/fs/DuplexCollection"; @@ -55,7 +55,7 @@ test();`; }); await reader.write(testResource); - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { @@ -135,7 +135,7 @@ test();`; }); await reader.write(testResource); - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { @@ -200,7 +200,7 @@ test();`; }); await reader.write(testResource); - await runSteps(minify, { + await runTask(minify, { workspace, options: { pattern: "/test.js", @@ -247,7 +247,7 @@ test();`; }); await reader.write(testResource); - await runSteps(minify, { + await runTask(minify, { workspace, options: { pattern: "/test.js", @@ -304,7 +304,7 @@ return;`; await reader.write(testResource); await t.throwsAsync(() => { - return runSteps(minify, { + return runTask(minify, { workspace, taskUtil, options: { @@ -336,7 +336,7 @@ return;`; await reader.write(testResource); await t.throwsAsync(() => { - return runSteps(minify, { + return runTask(minify, { workspace, options: { pattern: "/resources/my/namespace/test.js", @@ -388,7 +388,7 @@ test.serial("integration: minify with taskUtil and resources tagged with OmitFro registerCleanupTask: t.context.sinon.stub() }; - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { @@ -464,7 +464,7 @@ test.serial("integration: minify skips a .js with an existing -dbg.js sibling an string: content })); - await runSteps(minify, { + await runTask(minify, { workspace, options: { pattern: "/resources/**/*.js" diff --git a/packages/builder/test/lib/tasks/minify.js b/packages/builder/test/lib/tasks/minify.js index cf524dae391..89f14fd05ea 100644 --- a/packages/builder/test/lib/tasks/minify.js +++ b/packages/builder/test/lib/tasks/minify.js @@ -1,7 +1,7 @@ import test from "ava"; import sinonGlobal from "sinon"; import esmock from "esmock"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; test.beforeEach(async (t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); @@ -68,7 +68,7 @@ test("minify: Default params", async (t) => { }] ]); minifierStub.callsFake(({resources}) => Promise.resolve([processedByInput.get(resources[0])])); - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { @@ -134,7 +134,7 @@ test("minify: omitSourceMapResources: true, useInputSourceMaps: false", async (t }] ]); minifierStub.callsFake(({resources}) => Promise.resolve([processedByInput.get(resources[0])])); - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { @@ -214,7 +214,7 @@ test("minify: No taskUtil", async (t) => { }] ]); minifierStub.callsFake(({resources}) => Promise.resolve([processedByInput.get(resources[0])])); - await runSteps(minify, { + await runTask(minify, { workspace, options: { pattern: "**" @@ -259,7 +259,7 @@ test("minify: Skips and warns for an existing -dbg.js next to its .js", async (t ]); minifierStub.callsFake(({resources}) => Promise.resolve([processedByInput.get(resources[0])])); - await runSteps(minify, { + await runTask(minify, { workspace, taskUtil, options: { diff --git a/packages/builder/test/lib/tasks/replaceBuildtime.js b/packages/builder/test/lib/tasks/replaceBuildtime.js index 40e772f52c7..4cbefbe14ca 100644 --- a/packages/builder/test/lib/tasks/replaceBuildtime.js +++ b/packages/builder/test/lib/tasks/replaceBuildtime.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import replaceBuildtime from "../../../lib/tasks/replaceBuildtime.js"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; import DuplexCollection from "@ui5/fs/DuplexCollection"; @@ -23,7 +23,7 @@ test("integration: replace version", async (t) => { const workspace = new DuplexCollection({reader, writer}); await reader.write(resource); - await runSteps(replaceBuildtime, { + await runTask(replaceBuildtime, { workspace, options: { pattern: "/test.js" @@ -64,7 +64,7 @@ test("integration: buildtime is sourced from taskUtil.getBuildTime", async (t) = }); await reader.write(resource); - await runSteps(replaceBuildtime, { + await runTask(replaceBuildtime, { workspace, taskUtil: {getBuildTime: () => buildTime}, options: { diff --git a/packages/builder/test/lib/tasks/replaceCopyright.js b/packages/builder/test/lib/tasks/replaceCopyright.js index d4503ef7748..fcb48a1a847 100644 --- a/packages/builder/test/lib/tasks/replaceCopyright.js +++ b/packages/builder/test/lib/tasks/replaceCopyright.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import sinon from "sinon"; import replaceCopyright from "../../../lib/tasks/replaceCopyright.js"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; @@ -40,7 +40,7 @@ console.log('HelloWorld');`; await workspace.write(resource); - await runSteps(replaceCopyright, { + await runTask(replaceCopyright, { workspace, options: { copyright: copyright, @@ -91,7 +91,7 @@ console.log('HelloWorld');`; await workspace.write(resource); - await runSteps(replaceCopyright, { + await runTask(replaceCopyright, { workspace, taskUtil: {getTime}, options: { @@ -139,7 +139,7 @@ test("test.xml: replace @copyright@", async (t) => { }); await reader.write(resource); - await runSteps(replaceCopyright, { + await runTask(replaceCopyright, { workspace, options: { pattern: "/**/*.xml", diff --git a/packages/builder/test/lib/tasks/replaceVersion.js b/packages/builder/test/lib/tasks/replaceVersion.js index 58ebf224340..2286b336b2b 100644 --- a/packages/builder/test/lib/tasks/replaceVersion.js +++ b/packages/builder/test/lib/tasks/replaceVersion.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; import replaceVersion from "../../../lib/tasks/replaceVersion.js"; import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; import DuplexCollection from "@ui5/fs/DuplexCollection"; @@ -22,7 +22,7 @@ test("integration: replace version", async (t) => { const workspace = new DuplexCollection({reader, writer}); await reader.write(resource); - await runSteps(replaceVersion, { + await runTask(replaceVersion, { workspace, options: { pattern: "/test.js", diff --git a/packages/builder/test/lib/tasks/runSteps.js b/packages/builder/test/lib/tasks/runTask.js similarity index 91% rename from packages/builder/test/lib/tasks/runSteps.js rename to packages/builder/test/lib/tasks/runTask.js index 48bdecf734f..89d04ca8a59 100644 --- a/packages/builder/test/lib/tasks/runSteps.js +++ b/packages/builder/test/lib/tasks/runTask.js @@ -1,5 +1,5 @@ import test from "ava"; -import runSteps from "../../../lib/tasks/runSteps.js"; +import runTask from "../../../lib/tasks/runTask.js"; function createResource(resourcePath, content = resourcePath) { return { @@ -34,7 +34,7 @@ function createWorkspace(initial = []) { test("Runs a scalar step and threads its return into a consumer via needs", async (t) => { const workspace = createWorkspace(); let consumed; - await runSteps((options) => ({steps: [ + await runTask((options) => ({steps: [ {name: "scan", run: async () => ({flag: options.flag})}, {name: "use", needs: ["scan"], run: async ({needs, workspace}) => { consumed = needs.scan; @@ -49,7 +49,7 @@ test("Runs a scalar step and threads its return into a consumer via needs", asyn test("Fans out a map step's keys and writes each", async (t) => { const workspace = createWorkspace(); const ran = []; - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "m", keys: async () => ["a", "b", "c"], each: async (key, {workspace}) => { ran.push(key); await workspace.write(createResource(`/out/${key}`)); @@ -64,7 +64,7 @@ test("Fans out a map step's keys and writes each", async (t) => { test("A producer return reaches a map step's keys and each", async (t) => { const workspace = createWorkspace(); const eachSaw = []; - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "scan", run: async () => ({wanted: ["x", "y"]})}, {name: "build", needs: ["scan"], keys: async ({needs}) => needs.scan.wanted, each: async (key, {needs}) => { @@ -77,7 +77,7 @@ test("A producer return reaches a map step's keys and each", async (t) => { test("A concurrent map step flushes its writes in key order", async (t) => { const workspace = createWorkspace(); - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "m", keys: async () => ["a", "b", "c"], each: async (key, {workspace}) => { // Reverse the natural completion order so the key-order flush is observable. if (key === "a") { @@ -94,7 +94,7 @@ test("A concurrent map step flushes its writes in key order", async (t) => { test("A sequential map step makes an earlier key's write visible to a later key", async (t) => { const workspace = createWorkspace(); let secondSawFirst = false; - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "m", sequential: true, keys: async () => ["first", "second"], each: async (key, {workspace}) => { if (key === "first") { await workspace.write(createResource("/shared")); @@ -111,7 +111,7 @@ test("Concurrent map-step keys writing the same path throw", async (t) => { const workspace = createWorkspace(); const inputA = createResource("/resources/first.js"); const inputB = createResource("/resources/second.js"); - const err = await t.throwsAsync(runSteps(() => ({steps: [ + const err = await t.throwsAsync(runTask(() => ({steps: [ {name: "m", keys: async () => [inputA, inputB], each: async (key, {workspace}) => { await workspace.write(createResource("/same")); }}, @@ -125,7 +125,7 @@ test("Concurrent map-step keys writing the same path throw", async (t) => { test("A concurrent map step preserves each key's write arguments through the flush", async (t) => { const workspace = createWorkspace(); - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "m", keys: async () => ["with", "without"], each: async (key, {workspace}) => { if (key === "with") { await workspace.write(createResource("/with"), {drain: true}); @@ -144,7 +144,7 @@ test("A concurrent map step preserves each key's write arguments through the flu test("A later step sees an earlier step's write", async (t) => { const workspace = createWorkspace(); let laterSaw = false; - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "first", run: async ({workspace}) => { await workspace.write(createResource("/from-first")); }}, @@ -161,7 +161,7 @@ test("taskUtil and dependencies are passed through to steps", async (t) => { const taskUtil = {marker: "taskUtil"}; const dependencies = {marker: "dependencies"}; let seen; - await runSteps(() => ({steps: [ + await runTask(() => ({steps: [ {name: "s", run: async (ctx) => { seen = {taskUtil: ctx.taskUtil, dependencies: ctx.dependencies}; }}, @@ -173,14 +173,14 @@ test("taskUtil and dependencies are passed through to steps", async (t) => { test("An empty step list is a no-op", async (t) => { const workspace = createWorkspace(); - await runSteps(() => ({steps: []}), {workspace}); + await runTask(() => ({steps: []}), {workspace}); t.is(workspace.store.size, 0, "Nothing written"); }); test("A descriptor without a steps array throws", async (t) => { const workspace = createWorkspace(); - const err = await t.throwsAsync(runSteps(() => ({task: async () => {}}), {workspace})); + const err = await t.throwsAsync(runTask(() => ({task: async () => {}}), {workspace})); t.is(err.message, - "runSteps requires a step-based task whose factory returns a descriptor with a 'steps' array", + "runTask requires a step-based task whose factory returns a descriptor with a 'steps' array", "A non-step descriptor surfaces a clear error"); }); diff --git a/packages/fs/lib/internal/stepWriteBuffer.js b/packages/fs/lib/internal/stepWriteBuffer.js index 304c8db16f6..5136fc01dce 100644 --- a/packages/fs/lib/internal/stepWriteBuffer.js +++ b/packages/fs/lib/internal/stepWriteBuffer.js @@ -9,7 +9,7 @@ * * Two runners implement this: the cache-aware runner in @ui5/project * (build/helpers/StepRunner) and the cache-free runner in @ui5/builder - * (tasks/runSteps). @ui5/builder cannot import @ui5/project + * (tasks/runTask). @ui5/builder cannot import @ui5/project * (the dependency direction is @ui5/cli -> @ui5/project -> @ui5/builder), so the two runners * stay separate, but the same-path guard, its user-visible error message and the key-order flush live here * so both runners share one definition. diff --git a/packages/project/lib/build/helpers/StepRunner.js b/packages/project/lib/build/helpers/StepRunner.js index d0292feaec3..bc10a910e6a 100644 --- a/packages/project/lib/build/helpers/StepRunner.js +++ b/packages/project/lib/build/helpers/StepRunner.js @@ -273,11 +273,11 @@ class RecordingReaderWriter extends AbstractReaderWriter { return this.#workspace.byPath(virPath, options); } - // Overrides _write rather than the public write (unlike @ui5/builder's runSteps BufferedWriter) because + // Overrides _write rather than the public write (unlike @ui5/builder's runTask BufferedWriter) because // this writer also records every write for the build cache, and AbstractReaderWriter.write has already // defaulted options by the time it delegates here. The buffered entry therefore stores the defaulted // options as the single replay argument, which flushWriteBuffer replays via write(resource, ...args). - // The same-path guard, its error message and the key-order flush are shared with runSteps through + // The same-path guard, its error message and the key-order flush are shared with runTask through // @ui5/fs/internal/stepWriteBuffer so the two runners cannot drift. async _write(resource, options) { const resourcePath = resource.getPath(); From e166dac3bb18541fe60e894de875d38211541fcf Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 21 Oct 2026 17:24:13 +0200 Subject: [PATCH 7/8] refactor(project): Preserve legacy dependency callbacks Specification Version 3.x and 4.x task extensions can export `determineRequiredDependencies` at the top level. TaskRunner read the callback from `descriptor`, which is undefined below Version 5, so it ignored the export and omitted the dependency reader. `Task#getRequiredDependenciesCallback` already resolves the top-level export for legacy extensions and the descriptor property for Version 5 extensions. Use this version-aware accessor for dependency selection. --- packages/project/lib/build/TaskRunner.js | 9 ++- packages/project/test/lib/build/TaskRunner.js | 73 +++++++++++-------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js index 0e3817813c7..f36154d2734 100644 --- a/packages/project/lib/build/TaskRunner.js +++ b/packages/project/lib/build/TaskRunner.js @@ -457,9 +457,9 @@ class TaskRunner { const specVersion = task.getSpecVersion(); // From Specification Version 5.0 the default export is a factory returning a descriptor. Below 5.0 it - // is the legacy task body invoked directly (any value it returns is ignored), so there is no descriptor - // and none of its callbacks apply. The factory options match those used for the build signature in - // TaskDefinitions, so Task#getDescriptor memoizes one descriptor shared by both phases. + // is the legacy task body invoked directly (any value it returns is ignored), so there is no descriptor. + // The factory options match those used for the build signature in TaskDefinitions, so Task#getDescriptor + // memoizes one descriptor shared by both phases. const isV5 = specVersion.gte("5.0"); const descriptorOptions = { projectName: project.getName(), @@ -470,7 +470,8 @@ class TaskRunner { const descriptor = isV5 ? await task.getDescriptor(taskName, descriptorOptions) : undefined; // Tasks can provide an optional callback to tell build process which dependencies they require - const requiredDependenciesCallback = descriptor?.determineRequiredDependencies; + const requiredDependenciesCallback = + await task.getRequiredDependenciesCallback(taskName, descriptorOptions); let requiredDependencies; // Always provide a dependencies-reader, even if empty. Unless the task is specVersion >=3.0 diff --git a/packages/project/test/lib/build/TaskRunner.js b/packages/project/test/lib/build/TaskRunner.js index 3847c8f4436..a5a3434d75b 100644 --- a/packages/project/test/lib/build/TaskRunner.js +++ b/packages/project/test/lib/build/TaskRunner.js @@ -817,10 +817,7 @@ test("Custom task with legacy spec version", async (t) => { "taskUtil#getInterface got called with correct argument"); }); -// Below specVersion 5.0 the descriptor path is not taken, so the task extension's -// getRequiredDependenciesCallback is never invoked by the runner. All direct dependencies -// are provided by default when the task's specVersion is also below 3.0. -test("Custom task with legacy spec version: requiredDependenciesCallback is not honored", async (t) => { +test("Custom task with legacy spec version and requiredDependenciesCallback", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); const specVersionGteStub = sinon.stub().returns(false); @@ -828,8 +825,8 @@ test("Custom task with legacy spec version: requiredDependenciesCallback is not toString: () => "1.0", gte: specVersionGteStub }; - // The old getRequiredDependenciesCallback is no longer called below specVersion 5.0. - const getRequiredDependenciesCallbackStub = sinon.stub().resolves(sinon.stub().resolves(new Set(["dep.b"]))); + const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(requiredDependenciesCallbackStub); graph.getExtension.returns({ getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, @@ -846,11 +843,19 @@ test("Custom task with legacy spec version: requiredDependenciesCallback is not await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); - // The callback is not honored below 5.0: the task defaults to all direct dependencies. - t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), - "Below specVersion 5.0, all direct dependencies are provided (callback not honored)"); - t.is(getRequiredDependenciesCallbackStub.callCount, 0, - "getRequiredDependenciesCallback is not invoked below specVersion 5.0"); + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), + "The callback selects the required dependency"); + t.is(getRequiredDependenciesCallbackStub.callCount, 1, + "getRequiredDependenciesCallback is invoked below Specification Version 5.0"); + t.deepEqual(getRequiredDependenciesCallbackStub.firstCall.args, [ + "myTask", + { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "configuration", + taskName: "myTask" + } + ], "getRequiredDependenciesCallback receives the runtime task name and options"); const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") .resolves({getName: () => "dependencies"}); @@ -860,14 +865,14 @@ test("Custom task with legacy spec version: requiredDependenciesCallback is not t.is(specVersionGteStub.getCall(0).args[0], "5.0", "SpecificationVersion#gte first call checks 5.0"); t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte second call checks 3.0 (no-callback branch)"); + "SpecificationVersion#gte second call checks 3.0 for callback parameters"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", "SpecificationVersion#gte third call checks 3.0 (task execution)"); t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); t.deepEqual(createDependencyReaderStub.getCall(0).args[0], - new Set(["dep.a", "dep.b"]), - "All direct dependencies provided"); + new Set(["dep.b"]), + "The dependency reader contains the callback selection"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); @@ -883,9 +888,6 @@ test("Custom task with legacy spec version: requiredDependenciesCallback is not "taskUtil#getInterface got called with correct argument"); }); -// Below specVersion 5.0 there is no descriptor, so determineRequiredDependencies is not -// available via the descriptor. A task at specVersion 3.0-4.x gets no dependencies by -// default (gte("3.0") = true with no descriptor callback → empty set, no reader). test("Custom task with specVersion 3.0", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); @@ -896,8 +898,8 @@ test("Custom task with specVersion 3.0", async (t) => { gte: specVersionGteStub }; - // Old getRequiredDependenciesCallback API is not called below V5. - const getRequiredDependenciesCallbackStub = sinon.stub().resolves(sinon.stub().resolves(new Set(["dep.b"]))); + const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(requiredDependenciesCallbackStub); graph.getExtension.returns({ getName: () => "custom task name", @@ -916,11 +918,10 @@ test("Custom task with specVersion 3.0", async (t) => { await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); - // Below V5, no descriptor callback; gte("3.0") = true → empty set (no reader). - t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), - "Below V5, a V3 task with no descriptor callback gets no dependencies"); - t.is(getRequiredDependenciesCallbackStub.callCount, 0, - "getRequiredDependenciesCallback is not called below V5"); + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]), + "The legacy callback selects the required dependency"); + t.is(getRequiredDependenciesCallbackStub.callCount, 1, + "getRequiredDependenciesCallback is invoked below Specification Version 5.0"); const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") .resolves({getName: () => "dependencies"}); @@ -930,15 +931,19 @@ test("Custom task with specVersion 3.0", async (t) => { t.is(specVersionGteStub.getCall(0).args[0], "5.0", "SpecificationVersion#gte first call checks 5.0 (isV5)"); t.is(specVersionGteStub.getCall(1).args[0], "3.0", - "SpecificationVersion#gte second call checks 3.0 (no-callback branch)"); + "SpecificationVersion#gte second call checks 3.0 for callback parameters"); t.is(specVersionGteStub.getCall(2).args[0], "3.0", "SpecificationVersion#gte third call checks 3.0 (task execution)"); - t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once"); + t.is(taskUtil.getInterface.callCount, 2, "taskUtil#getInterface got called twice"); t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion, "taskUtil#getInterface got called with correct argument on first call"); + t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersion, + "taskUtil#getInterface got called with correct argument on second call"); - t.is(createDependencyReaderStub.callCount, 0, "getDependenciesReader did not get called"); + t.is(createDependencyReaderStub.callCount, 1, "getDependenciesReader got called once"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], new Set(["dep.b"]), + "The dependency reader contains the callback selection"); t.is(taskStub.callCount, 1, "Task got called once"); t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); @@ -955,8 +960,7 @@ test("Custom task with specVersion 3.0", async (t) => { test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", async (t) => { const {sinon, graph, taskUtil} = t.context; const taskStub = sinon.stub(); - // All gte calls return true; this is treated as >= 5.0 and takes the descriptor path. - const specVersionGteStub = sinon.stub().returns(true); + const specVersionGteStub = sinon.stub().callsFake((v) => v === "3.0"); const mockSpecVersion = { toString: () => "3.0", gte: specVersionGteStub @@ -966,8 +970,7 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy getName: () => "custom task name", getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, - // Descriptor carries no determineRequiredDependencies callback, so the task gets no deps. - getDescriptor: sinon.stub().resolves({task: taskStub}), + getRequiredDependenciesCallback: sinon.stub().resolves(undefined), getStepBased: async () => false, }); @@ -980,7 +983,7 @@ test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", asy await taskRunner._initTasks(); t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); - // Descriptor has no determineRequiredDependencies → gte("3.0") check → no reader. + // A task at Specification Version 3.0 or later gets no reader when the legacy callback is absent. t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), "Custom tasks requires no dependencies by default"); const createDependencyReaderStub = sinon.stub(taskRunner, "getDependenciesReader") @@ -1043,12 +1046,14 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { getName: () => "Task Name A", getTask: () => taskStubA, getSpecVersion: () => mockSpecVersionA, + getRequiredDependenciesCallback: sinon.stub().resolves(undefined), getStepBased: async () => false, }); graph.getExtension.onSecondCall().returns({ getName: () => "Task Name B", getTask: () => taskStubB, getSpecVersion: () => mockSpecVersionB, + getRequiredDependenciesCallback: sinon.stub().resolves(undefined), getStepBased: async () => false, }); graph.getExtension.onThirdCall().returns({ @@ -1057,6 +1062,7 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { getSpecVersion: () => mockSpecVersionC, // V5 descriptor with no determineRequiredDependencies → no deps. getDescriptor: sinon.stub().resolves({task: taskStubC}), + getRequiredDependenciesCallback: sinon.stub().resolves(undefined), getStepBased: async () => false, }); graph.getExtension.onCall(3).returns({ @@ -1065,6 +1071,7 @@ test("Multiple custom tasks with same name are called correctly", async (t) => { getSpecVersion: () => mockSpecVersionD, // V5 descriptor with no determineRequiredDependencies → no deps. getDescriptor: sinon.stub().resolves({task: taskStubD}), + getRequiredDependenciesCallback: sinon.stub().resolves(undefined), getStepBased: async () => false, }); const project = getMockProject("module"); @@ -1213,6 +1220,7 @@ test("Custom task: requiredDependenciesCallback returns unknown dependency", asy getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, getDescriptor: getDescriptorStub, + getRequiredDependenciesCallback: sinon.stub().resolves(requiredDependenciesCallbackStub), getStepBased: async () => false, }); @@ -1252,6 +1260,7 @@ test("Custom task: requiredDependenciesCallback returns Array instead of Set", a getTask: () => taskStub, getSpecVersion: () => mockSpecVersion, getDescriptor: getDescriptorStub, + getRequiredDependenciesCallback: sinon.stub().resolves(requiredDependenciesCallbackStub), getStepBased: async () => false, }); From 554be5fbebc55671d748dab3a9e2eb255b035173 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 21 Oct 2026 17:35:49 +0200 Subject: [PATCH 8/8] refactor(project): Scope task descriptors by project A task extension instance is shared across all projects in a graph. Memoizing its descriptor only by runtime task name could reuse the first project's descriptor when another project configured the same task name. Scope descriptor promises by project name and runtime task name. This keeps signature calculation and execution on one descriptor while preserving each project's namespace, configuration, task body, steps, and signature callback. Add regression coverage for cross-project isolation and repeated task configurations. Update the incremental-build architecture reference to describe the cache scope. --- .../skills/incremental-build/architecture.md | 2 +- .../lib/specifications/extensions/Task.js | 20 ++++++---- .../lib/specifications/extensions/Task.js | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/.agents/skills/incremental-build/architecture.md b/.agents/skills/incremental-build/architecture.md index 98d21812f0c..c8347ac857d 100644 --- a/.agents/skills/incremental-build/architecture.md +++ b/.agents/skills/incremental-build/architecture.md @@ -369,7 +369,7 @@ Two DSL rules make the per-step recording honest, and both are visible here. The **The task descriptor (step-based vs legacy).** From Specification Version 5.0 (and for every built-in task), a task module's default export is a factory `(options) => descriptor`. The descriptor carries the whole task contract in one object: exactly one of `steps` (the step-based path) or `task` (a legacy single-stage body), plus the optional `determineRequiredDependencies` / `determineBuildSignature` callbacks and an optional declared `specVersion`. A task is step-based when its descriptor has a `steps` array. This replaced the former scattered opt-in (a static `stepBased` export on a custom task module, or a `stepBased: true` entry in `definitions/*.js`) and the former top-level named-export callbacks. The descriptor is validated by `validateDescriptor` (exported from `StepRunner.js`, next to `validateSteps`): exactly one of `steps`/`task`, callbacks must be functions, a declared `specVersion` must equal the task extension's configured Specification Version. For a **custom** task extension below 5.0 the gate is a hard version boundary with no shape-sniffing: the default export is the legacy task body invoked directly (any value it returns is ignored), so there is no descriptor and none of its callbacks apply. **This `specVersion` is the custom task extension's own (from its `ui5.yaml`), unrelated to the Specification Version of the project being built; built-in tasks carry the descriptor shape unconditionally and their project-specVersion gating stays in `definitions/*.js`.** Note the cache-internal `stepBased` flag (the persisted `stages` array `[stageId, stepBased?1:0]`, `BuildStageCache`), which means "this stage ran the step runner", is a different thing and is **retained** — only the task-API opt-in was removed. -**One factory call, memoized on `TaskDefinitions`.** The descriptor is resolved once per build and shared by the two phases that need it. `TaskDefinitions.getBuildSignatures()` (which needs `determineBuildSignature`) runs inside `ProjectBuildContext.create()`, before the `ProjectBuildCache` and `TaskRunner` exist; `TaskRunner` (which needs `steps`/`task`/`determineRequiredDependencies`) runs later off the **same** `TaskDefinitions` instance. For a built-in, `TaskDefinitions.#resolveStandardDescriptor` calls the factory from `taskRepository.getTask(name)` and caches it on the standard-task map entry (`getStandardDescriptor` exposes it to `TaskRunner`); a skipped (`skip:true`) or `taskFunction`-overridden task has no descriptor and falls back to hashing its options. For a custom task, `Task#getDescriptor(taskName, options)` memoizes per runtime `taskName` (so a `--N`-suffixed second configuration of the same extension instance gets its own descriptor). `TaskDefinitions` completes `projectName`/`projectNamespace` into each standard task's options **before** the first factory call, so a factory that branches on `projectNamespace` at factory time (`generateThemeDesignerResources` emits its `libraryTheming` step only for a namespace) produces the same descriptor at signature time and at execution. +**One factory call, memoized on `TaskDefinitions`.** The descriptor is resolved once per build and shared by the two phases that need it. `TaskDefinitions.getBuildSignatures()` (which needs `determineBuildSignature`) runs inside `ProjectBuildContext.create()`, before the `ProjectBuildCache` and `TaskRunner` exist; `TaskRunner` (which needs `steps`/`task`/`determineRequiredDependencies`) runs later off the **same** `TaskDefinitions` instance. For a built-in, `TaskDefinitions.#resolveStandardDescriptor` calls the factory from `taskRepository.getTask(name)` and caches it on the standard-task map entry (`getStandardDescriptor` exposes it to `TaskRunner`); a skipped (`skip:true`) or `taskFunction`-overridden task has no descriptor and falls back to hashing its options. For a custom task, `Task#getDescriptor(taskName, options)` memoizes per project and runtime `taskName`: two projects that use the same shared extension instance receive descriptors created with their own options, while a `--N`-suffixed second configuration within one project also gets its own descriptor. `TaskDefinitions` completes `projectName`/`projectNamespace` into each standard task's options **before** the first factory call, so a factory that branches on `projectNamespace` at factory time (`generateThemeDesignerResources` emits its `libraryTheming` step only for a namespace) produces the same descriptor at signature time and at execution. **Driving one stage per step.** Before the build, `TaskRunner.runTasks` reads each step-based task's `descriptor.steps` (already resolved by `TaskDefinitions`) and hands `ProjectBuildCache.setTasks` a `[{taskName, stepNames?}]` list, which creates one stage per step in step order. The step array is kept on the task (frozen, so a custom task cannot mutate the shared value) and reused when the step runs. One memoized descriptor is the single source of truth: discovery and execution cannot drive different stages, so no runtime check is needed to reconcile two factory calls. The `options` object is completed at registration time (`TaskDefinitions`/`_addTask` fill in `projectName`, `projectNamespace`) and is fixed for a `TaskRunner`'s lifetime: a changed `ui5.yaml` task configuration rebuilds the whole serving stack through `Supervisor.reinitialize`, giving a fresh `TaskRunner`, while a plain source-change rebuild reuses the `TaskRunner` and re-reads the frozen step array. For each step, the `StepRunner` drives that step's own stage via hooks the `TaskRunner` supplies (`#createStepStageHooks`): `prepareStage(stepName)` switches the project to the step's stage and returns its cache verdict (`prepareStageExecutionAndValidateCache(taskName, stepName)`); `createStageContext()` builds fresh monitored `workspace`/`dependencies` readers and a `MonitoredTaskUtil` bound to that stage (so the reads reflect the cumulative output of all earlier stages with this stage's writer on top); `recordStage(stepName, …)` records the step's stage (`recordStageResult({taskName, …, stepName})`). A fully-cached stage (`prepareStage` returns `true`) does not run: its return is rebuilt from the persisted per-key data (in key order) so later steps' `needs` resolve, and its tag operations are replayed. A `needs` return is excluded from the stage signature, so a full-hit consumer whose producer re-ran with a changed return would otherwise serve stale output. The `StepRunner` checks this on the full-hit path (`#needsReturnChanged`) and, when it fires, reopens the stage with a fresh writer and re-runs it as a full execution (`reopenStage` returns a falsy verdict, not a delta). Reopen installs a fresh empty writable stage, so pruning the re-run to only the affected units would need re-seeding it with the cached outputs first, so the whole stage re-runs instead. No shipped multi-step task pays for this: only `generateThemeDesignerResources` wires `needs`, and a changed `scan` return regenerates its themes anyway. The verdict (`true` / delta object / falsy) is read three ways in `runSteps`. An explicit verdict shape would read more clearly but is deferred, since that shape is shared with the stage-signature code. A task is reported to the `ProjectBuildLogger` as skipped (`skipTask`) when every step was served from cache, else as executed. Its skip verdict is only known once every stage has been driven, so it cannot be announced up front like a legacy task: the `StepRunner` instead calls `notifyStepExecution` from the first stage that stops being a pure cache hit, before that stage runs anything, and `TaskRunner.#createTaskExecutionReport` turns that into `startTask`. `task-start` therefore precedes the work it announces, and the matching `endTask` (with the union of the stages' written paths) follows it, so a `project-build-status` consumer sees the task running while it runs. A step-based task that returns **no** steps still has one stage (`task/{taskName}`) that is prepared and recorded (empty), so its empty result caches and a later build reports it skipped. diff --git a/packages/project/lib/specifications/extensions/Task.js b/packages/project/lib/specifications/extensions/Task.js index ae18266342a..88e2658f191 100644 --- a/packages/project/lib/specifications/extensions/Task.js +++ b/packages/project/lib/specifications/extensions/Task.js @@ -13,7 +13,7 @@ import {validateDescriptor} from "../../build/helpers/StepRunner.js"; * @hideconstructor */ class Task extends Extension { - #descriptorPromises = new Map(); + #descriptorPromisesByProject = new Map(); constructor(parameters) { super(parameters); @@ -35,11 +35,10 @@ class Task extends Extension { /** * Resolves and validates the task descriptor by calling the task factory (the default export) once with - * the given options. The result is memoized per runtime taskName, so a factory that branches - * on options (for example on projectNamespace) is called exactly once per build - * and the same descriptor is shared by the build-signature phase and task execution. Keyed by - * taskName rather than once per instance because one task extension can back more than one - * runtime task (a repeated configuration gets a --<number> suffix) with different options. + * the given options. The result is memoized per project and runtime taskName, so a factory that + * branches on options (for example on projectNamespace) is called once for each + * project/task pair. The build-signature phase and task execution share that descriptor. The runtime task + * name keeps repeated configurations (which get a --<number> suffix) separate. * * Only valid from Specification Version 5.0. If the descriptor declares a specVersion, it must * be equal to this task extension's configured Specification Version (the task extension's own @@ -51,7 +50,12 @@ class Task extends Extension { * @returns {Promise} The validated descriptor */ async getDescriptor(taskName, options) { - let descriptorPromise = this.#descriptorPromises.get(taskName); + let descriptorPromises = this.#descriptorPromisesByProject.get(options.projectName); + if (!descriptorPromises) { + descriptorPromises = new Map(); + this.#descriptorPromisesByProject.set(options.projectName, descriptorPromises); + } + let descriptorPromise = descriptorPromises.get(taskName); if (!descriptorPromise) { descriptorPromise = (async () => { const factory = (await this._getImplementation()).default; @@ -66,7 +70,7 @@ class Task extends Extension { } return descriptor; })(); - this.#descriptorPromises.set(taskName, descriptorPromise); + descriptorPromises.set(taskName, descriptorPromise); } return descriptorPromise; } diff --git a/packages/project/test/lib/specifications/extensions/Task.js b/packages/project/test/lib/specifications/extensions/Task.js index 059468e28d6..2a7c9393048 100644 --- a/packages/project/test/lib/specifications/extensions/Task.js +++ b/packages/project/test/lib/specifications/extensions/Task.js @@ -124,6 +124,46 @@ test("getExpectedOutputCallback (ESM)", async (t) => { t.is(callback, undefined, "Returns undefined when not exported"); }); +test.serial("getDescriptor memoizes descriptors per project and runtime task name", async (t) => { + const taskInput = clone(basicEsmTaskInput); + taskInput.configuration.specVersion = "5.0"; + const extension = await Specification.create(taskInput); + const factory = sinon.stub().callsFake((options) => ({ + task: async () => options.projectName, + determineBuildSignature: async () => options.projectName + })); + sinon.stub(extension, "_getImplementation").resolves({default: factory}); + + const projectAOptions = { + projectName: "project.a", + projectNamespace: "project/a", + configuration: {value: "a"} + }; + const projectBOptions = { + projectName: "project.b", + projectNamespace: "project/b", + configuration: {value: "b"} + }; + + const projectASignature = + await extension.getDetermineBuildSignatureCallback("task-a", projectAOptions); + const projectBSignature = + await extension.getDetermineBuildSignatureCallback("task-a", projectBOptions); + const projectADescriptor = await extension.getDescriptor("task-a", projectAOptions); + const projectASecondCall = await extension.getDescriptor("task-a", {...projectAOptions}); + const projectBDescriptor = await extension.getDescriptor("task-a", projectBOptions); + const projectASecondTask = await extension.getDescriptor("task-a--2", projectAOptions); + + t.is(projectASecondCall, projectADescriptor, "Reused the descriptor within one project"); + t.not(projectBDescriptor, projectADescriptor, "Created a separate descriptor for another project"); + t.not(projectASecondTask, projectADescriptor, "Created a separate descriptor for another runtime task"); + t.is(await projectASignature(), "project.a", "The first signature callback captured project A's options"); + t.is(await projectBSignature(), "project.b", "The second signature callback captured project B's options"); + t.is(await projectADescriptor.task(), "project.a", "The first descriptor captured project A's options"); + t.is(await projectBDescriptor.task(), "project.b", "The second descriptor captured project B's options"); + t.is(factory.callCount, 3, "Called the factory once per project and runtime task"); +}); + test("Extension abstract class cannot be directly instantiated", (t) => { t.throws(() => new Extension({configuration: {}}), { instanceOf: TypeError,