diff --git a/eslint.common.config.js b/eslint.common.config.js index 7484e443eb8..6be4e9ee442 100644 --- a/eslint.common.config.js +++ b/eslint.common.config.js @@ -12,6 +12,7 @@ export default [{ "test/fixtures/", "**/docs/", "**/jsdocs/", + "packages/" ], }, js.configs.recommended, google, ava.configs["flat/recommended"], { name: "Common ESLint config used for all UI5 CLI repos", diff --git a/packages/project/.chglog/CHANGELOG.tpl.md b/packages/project/.chglog/CHANGELOG.tpl.md new file mode 100755 index 00000000000..71e25e25192 --- /dev/null +++ b/packages/project/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,606 @@ +# Changelog +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +{{ if .Versions -}} +A list of unreleased changes can be found [here]({{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD). +{{ end -}} + +{{ range .Versions }} + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} +{{ range .CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [`{{ .Hash.Short }}`]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }}) +{{ end }} +{{ end -}} + +{{- if .RevertCommits -}} +### Reverts +{{ range .RevertCommits -}} +- {{ .Revert.Header }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + + +## [v3.9.2] - 2024-06-24 +### Dependency Updates +- Bump pacote from 17.0.7 to 18.0.6 [`9b6d580`](https://github.com/SAP/ui5-project/commit/9b6d58085bb74e4a2dfc1dccf528434db217e868) + + + +## [v3.9.1] - 2024-03-27 + + +## [v3.9.0] - 2023-12-12 +### Features +- **ProjectBuilder:** Add `outputStyle` option to request flat build output ([#624](https://github.com/SAP/ui5-project/issues/624)) [`79312fc`](https://github.com/SAP/ui5-project/commit/79312fcefea1ea97c1f3d403ac4470f890069809) +- **specVersion 3.2:** depCache bundling mode ([#673](https://github.com/SAP/ui5-project/issues/673)) [`68c5278`](https://github.com/SAP/ui5-project/commit/68c52782afbb617ddf110aca02d96f34a39ad5f7) + + + +## [v3.8.0] - 2023-11-20 +### Bug Fixes +- **application:** Improve error message for missing manifest.json [`016a846`](https://github.com/SAP/ui5-project/commit/016a84692aa1645f2e4267673d99495457c28458) + +### Features +- **TaskUtil:** Add 'force' flag to cleanup task callback ([#677](https://github.com/SAP/ui5-project/issues/677)) [`a0a21b7`](https://github.com/SAP/ui5-project/commit/a0a21b7ecd2805ff3d8d78ba9a453df64012556a) + + + +## [v3.7.3] - 2023-10-20 +### Bug Fixes +- ProjectBuilder now can be executed in parallel ([#669](https://github.com/SAP/ui5-project/issues/669)) [`f652461`](https://github.com/SAP/ui5-project/commit/f652461455a28718835cc66c7265f628be1e13b9) + + + +## [v3.7.2] - 2023-10-11 +### Dependency Updates +- Bump make-fetch-happen from 11.1.1 to 13.0.0 [`f2e264e`](https://github.com/SAP/ui5-project/commit/f2e264e87dfef1d5a132a1a0bf35043a789f8e84) +- Bump pacote from 15.2.0 to 17.0.4 [`f071399`](https://github.com/SAP/ui5-project/commit/f071399d994963b415c8ea35a629c465ae539f23) +- Bump [@npmcli](https://github.com/npmcli)/config from 6.4.0 to 8.0.0 [`c9f5218`](https://github.com/SAP/ui5-project/commit/c9f521815bab022bc8c0e8a3c27658266f01c655) + + + +## [v3.7.1] - 2023-10-02 +### Bug Fixes +- Allow usage of after/before task assignment for all standard tasks ([#628](https://github.com/SAP/ui5-project/issues/628)) [`1a272d2`](https://github.com/SAP/ui5-project/commit/1a272d2bd2700fa849ebb46bf9bd98806fa17fb2) + + + +## [v3.7.0] - 2023-09-06 +### Bug Fixes +- Ensure usage of provided UI5 data dir [`1e0503a`](https://github.com/SAP/ui5-project/commit/1e0503a32dae06202b62408558d5ef85bb49daf1) +- **NodePackageDependencies:** Implement validation for missing package.json attributes [`b070972`](https://github.com/SAP/ui5-project/commit/b0709725b373441fd62fe9e33cc0440b6df17401) +- **ProjectGraph:** Improve error message when adding duplicate projects or extensions [`2b4a49e`](https://github.com/SAP/ui5-project/commit/2b4a49e2b6dc4004bf078d259c1a8f54ccc0ae2c) +- **pacote:** Use npm cache within UI5 data dir [`f1e2178`](https://github.com/SAP/ui5-project/commit/f1e217803d0c455f61135084b00a7daf42fb9094) + +### Features +- **Resolvers:** Allow ranges / npm tags for version resolution [`2841004`](https://github.com/SAP/ui5-project/commit/28410044f9d4abd348dc3e0697048543eb7796d9) +- **Resolvers:** Use npm tags for determining 'latest' [`5cde95a`](https://github.com/SAP/ui5-project/commit/5cde95a04f2f040fffd0798822058f9692761cc4) + + + +## [v3.6.0] - 2023-08-22 +### Features +- Add specVersion 3.1 and builder resource excludes for modules ([#639](https://github.com/SAP/ui5-project/issues/639)) [`2ac053e`](https://github.com/SAP/ui5-project/commit/2ac053ef299bbaf02e73e12e2876f301d2b07d1b) +- **AbstractResolver:** Resolve version ranges specifying major version only [`1f8cfdf`](https://github.com/SAP/ui5-project/commit/1f8cfdf3c72745904fbdceab049ae5d2cbf86b06) + + + +## [v3.5.1] - 2023-08-18 +### Bug Fixes +- Resolve UI5 data directory relative to project ([#642](https://github.com/SAP/ui5-project/issues/642)) [`228b14c`](https://github.com/SAP/ui5-project/commit/228b14c63fbd736962c513fdd1656a7983f51bbc) + + + +## [v3.5.0] - 2023-08-09 +### Features +- Allow to configure location of UI5 home directory ([#635](https://github.com/SAP/ui5-project/issues/635)) [`8c86083`](https://github.com/SAP/ui5-project/commit/8c860839d94abdaedaf878614a9121a89b85f116) + + + +## [v3.4.2] - 2023-07-13 +### Bug Fixes +- **Application:** Fallback to manifest.appdescr_variant if manifest.json is not found ([#631](https://github.com/SAP/ui5-project/issues/631)) [`43c6b22`](https://github.com/SAP/ui5-project/commit/43c6b224cf7ecad39a060baf8c6922f919e6dd59) + +### Dependency Updates +- Bump read-pkg-up from 9.1.0 to 10.0.0 [`557cb36`](https://github.com/SAP/ui5-project/commit/557cb36790ba53aa43a15cf7211560461dabb9e5) + + + +## [v3.4.1] - 2023-07-03 +### Bug Fixes +- Migrate from libnpmconfig to [@npmcli](https://github.com/npmcli)/config ([#618](https://github.com/SAP/ui5-project/issues/618)) [`13d019b`](https://github.com/SAP/ui5-project/commit/13d019bb4d8eda05c0a1564c6a2b96fa4eb05ab1) + + + +## [v3.4.0] - 2023-06-21 +### Bug Fixes +- **maven/Registry:** Prevent socket timeouts when installing framework libraries [`3de767f`](https://github.com/SAP/ui5-project/commit/3de767fb7cc9278bf984ff88064a16e593db6db0) + +### Features +- **Sapui5MavenSnapshotResolver:** Use npm-dist.zip artifact for 1.116.0 and later ([#622](https://github.com/SAP/ui5-project/issues/622)) [`45dcee0`](https://github.com/SAP/ui5-project/commit/45dcee00f141b6632d5a1217affbd212f6faf1f4) + + + +## [v3.3.2] - 2023-06-06 +### Bug Fixes +- **ui5Framework:** Treat 'optional' dependencies of root project as non-optional [`f3318f0`](https://github.com/SAP/ui5-project/commit/f3318f0daff617e12ac97050e19d41a16ecbc748) +- **ui5Framework:** Choose correct resolver for snapshot framework version overrides [`ba860de`](https://github.com/SAP/ui5-project/commit/ba860de97bc1674fa8381706cc09bd68ee08df38) + +### Dependency Updates +- Bump xml2js from 0.5.0 to 0.6.0 [`aa7d853`](https://github.com/SAP/ui5-project/commit/aa7d853f4a719006a6aaf4e51cc5c12fd00d2aa1) + + + +## [v3.3.1] - 2023-05-23 +### Bug Fixes +- **Workspace:** Ignore empty npm workspace modules ([#614](https://github.com/SAP/ui5-project/issues/614)) [`66e82a3`](https://github.com/SAP/ui5-project/commit/66e82a37f8c559eb7219fad0329a4d77fd3a6481) +- **projectGraphBuilder:** Add module cache invalidation ([#612](https://github.com/SAP/ui5-project/issues/612)) [`65496ea`](https://github.com/SAP/ui5-project/commit/65496eabeaafc50348dfc276d19d135eb035b261) + + + +## [v3.3.0] - 2023-05-05 +### Bug Fixes +- Resolve properly package.json dependency aliases ([#608](https://github.com/SAP/ui5-project/issues/608)) [`f8753e5`](https://github.com/SAP/ui5-project/commit/f8753e53c6bc7f89bb19107073fb52db0a725cb9) + +### Features +- **Sapui5MavenSnapshotResolver:** Expose cacheMode parameter through all APIs ([#607](https://github.com/SAP/ui5-project/issues/607)) [`78eb482`](https://github.com/SAP/ui5-project/commit/78eb4825ecab9534426f517e764451f53d232fed) + + + +## [v3.2.2] - 2023-04-27 +### Bug Fixes +- **ui5Framework:** Respect npm proxy configuration to fetch libraries [`5e3da0c`](https://github.com/SAP/ui5-project/commit/5e3da0c552593ff521c8e27cdbb4aeb849f56aa4) + + + +## [v3.2.1] - 2023-04-21 +### Bug Fixes +- **Configuration:** Rename toJSON => toJson [`4dfbf28`](https://github.com/SAP/ui5-project/commit/4dfbf28a20d67ce8d482c9d8ca18331d7fa69629) + + + +## [v3.2.0] - 2023-04-21 +### Dependency Updates +- Bump rimraf from 4.4.1 to 5.0.0 ([#597](https://github.com/SAP/ui5-project/issues/597)) [`1da76bc`](https://github.com/SAP/ui5-project/commit/1da76bc21c218b154b1a6014808f8d3a4d101b69) + +### Features +- Add Configuration ([#575](https://github.com/SAP/ui5-project/issues/575)) [`fd37cef`](https://github.com/SAP/ui5-project/commit/fd37cefffdc22b4a4bbc3fcbde20581848d937fa) +- Enable snapshot consumption from Maven repository ([#570](https://github.com/SAP/ui5-project/issues/570)) [`ade2c49`](https://github.com/SAP/ui5-project/commit/ade2c49d66ebba229b62c6614c8bbdfed10bc6b0) + + + +## [v3.1.1] - 2023-04-12 +### Dependency Updates +- Bump xml2js from 0.4.23 to 0.5.0 [`d6d86c9`](https://github.com/SAP/ui5-project/commit/d6d86c93db5c4d288161aa11b72bb6537c4f4cf4) +- Bump read-pkg from 7.1.0 to 8.0.0 [`9800c06`](https://github.com/SAP/ui5-project/commit/9800c06004e44a4af8b86492b0f15cab465be0c0) + + + +## [v3.1.0] - 2023-03-31 +### Bug Fixes +- **Taskrunner:** pass new taskutil options to determineRequiredDependencies hook [`94bcd99`](https://github.com/SAP/ui5-project/commit/94bcd9931d6709170b78a92e7372bbd0de44ae03) +- **ui5Framework:** Prevent install of libraries within workspace ([#589](https://github.com/SAP/ui5-project/issues/589)) [`8ffc676`](https://github.com/SAP/ui5-project/commit/8ffc676434defd320c70b615960efc9182a29de9) + +### Features +- **Specification:** Add getId method [`7bdb47a`](https://github.com/SAP/ui5-project/commit/7bdb47a2925c0936ee33faf23f51f6c6ab396369) +- **Workspace:** Add getModules method [`1e2aa0e`](https://github.com/SAP/ui5-project/commit/1e2aa0e48bb2d895728f3d5f4cb74d55fbc8ec34) + + + +## [v3.0.4] - 2023-03-10 +### Bug Fixes +- Resolve properly absolute path for ui5HomeDir ([#588](https://github.com/SAP/ui5-project/issues/588)) [`9b414a7`](https://github.com/SAP/ui5-project/commit/9b414a77a1d86f6a3560231ae04db407e2f022c5) + + + +## [v3.0.3] - 2023-03-01 +### Bug Fixes +- **jsdoc:** enable generateVersionInfo task [`a58e5eb`](https://github.com/SAP/ui5-project/commit/a58e5eb0769a9ba63a0b0aa267675ef2f9c08769) + + + +## [v3.0.2] - 2023-02-17 +### Bug Fixes +- **ComponentProject#getWorkspace:** Apply builder resource excludes [`5257e59`](https://github.com/SAP/ui5-project/commit/5257e5977c4e92e2aca5b0ce4b2ed55688a66646) + + + +## [v3.0.1] - 2023-02-16 +### Bug Fixes +- Prevent socket timeouts when installing framework libraries [`a198356`](https://github.com/SAP/ui5-project/commit/a198356c9c5f39dd94fb8cf7542d9059ee628f3b) +- **Library:** Do not throw for missing .library file [`1163821`](https://github.com/SAP/ui5-project/commit/11638210994fd9511b2ab5ee3da40e3ccf294e58) +- **Project#getReader:** Do not apply builder resource excludes for style 'runtime' [`1cd94f7`](https://github.com/SAP/ui5-project/commit/1cd94f7f15ed07283e198238edb546517ee25691) +- **TaskUtil:** Provide framework configuration getters to custom tasks ([#580](https://github.com/SAP/ui5-project/issues/580)) [`6a40927`](https://github.com/SAP/ui5-project/commit/6a409278285252da59ea4d42fcf154814518661d) +- **graph:** Always resolve rootConfigPath to CWD [`ef3e569`](https://github.com/SAP/ui5-project/commit/ef3e56996111233aaa04410c95f11b1c3495a9b2) +- **projectGraphBuilder:** Apply extensions of the same module only once [`6d753a8`](https://github.com/SAP/ui5-project/commit/6d753a850f2a4ca34a50f64a404472bf0081054e) +- **ui5Framework:** Improve error handling for duplicate lib declaration [`fb1db6d`](https://github.com/SAP/ui5-project/commit/fb1db6d7cb74dee9c4754ffb62a2a970cb0e2fbe) + + + +## [v3.0.0] - 2023-02-09 +### Breaking Changes +- Implement Project Graph, build execution [`161f462`](https://github.com/SAP/ui5-project/commit/161f462cf6a9955337fff512007125128c6c39dd) +- Run 'generateThemeDesignerResources' only on framework libs [`e4bb108`](https://github.com/SAP/ui5-project/commit/e4bb1084df3e0ae906df27aba4a674d187ff8069) + +### BREAKING CHANGE +Support for older Node.js and npm releases has been dropped for all UI5 Tooling modules. +Only Node.js versions v16.18.0, v18.12.0 or higher as well as npm v8 or higher are supported. + +All packages have been transformed to ES Modules. Therefore modules are no longer provides a CommonJS exports. +If your project uses CommonJS, it needs to be converted to ESM or use a dynamic import for consuming UI5 Tooling modules. + +For more information see also: +- https://sap.github.io/ui5-tooling/updates/migrate-v3/ +- https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +- normalizer and projectTree APIs have been removed. Use generateProjectGraph instead +- Going forward only specification versions 2.0 and higher are supported + - In case a legacy specification version is detected, an automatic, transparent migration is attempted. +- Build: + - The "dev" build mode has been removed + - The task "generateVersionInfo" is no longer executed for application projects by default. You may enable it again using the includedTasks parameter + +### Features +- specVersion 3.0 ([#522](https://github.com/SAP/ui5-project/issues/522)) [`c5070e5`](https://github.com/SAP/ui5-project/commit/c5070e55d92ced4326cd7611caf3ec9a3da9e7ed) +- Introduce SpecificationVersion class ([#431](https://github.com/SAP/ui5-project/issues/431)) [`e57842b`](https://github.com/SAP/ui5-project/commit/e57842b06397a5b36e6373df97f7b7bb91f09741) +- **TaskRunner:** Provide taskName and logger instance to custom tasks [`36cd2d8`](https://github.com/SAP/ui5-project/commit/36cd2d83f9a6a92cbd28619d8a25c0ba3f732117) +- **TaskUtil:** Add resourceFactory API to v3 interface [`2e863cf`](https://github.com/SAP/ui5-project/commit/2e863cfaf9f8924d0c87fe9dfe01568c1fd979c8) +- **TaskUtil:** Add getProject/getDependencies API to interface [`51f2949`](https://github.com/SAP/ui5-project/commit/51f29493f57f094396776bb2686c8a74e8901a7f) + +### Bug Fixes +- **npm/Installer:** Do not wrap promise provided by rimraf v4 [`2d1ccda`](https://github.com/SAP/ui5-project/commit/2d1ccda54edd29dabadcb7bad9136bff09da8eac) +- **ProjectBuilder:** Fix verbose logging for already built projects [`f04ffd2`](https://github.com/SAP/ui5-project/commit/f04ffd2c0ab0270df697c20258474ff536811476) +- **ProjectBuilder:** Skip build for projects that do not require to be built [`ac5f1f8`](https://github.com/SAP/ui5-project/commit/ac5f1f891255b56597e51d121329f03786338d4a) +- **Specification:** Fix migration for legacy projects that are not applications or libraries [`d89d804`](https://github.com/SAP/ui5-project/commit/d89d8047519ca8f162dc7a225f138ae304871ecb) +- Fix build manifest creation [`b1459eb`](https://github.com/SAP/ui5-project/commit/b1459eb26aa8a4b18ad84a369c122c114d64b64b) + +### Dependency Updates +- Bump rimraf from 3.0.2 to 4.1.1 ([#550](https://github.com/SAP/ui5-project/issues/550)) [`99876ae`](https://github.com/SAP/ui5-project/commit/99876ae35e9d8f5c725e2e87bd3be37d7ed4363c) + + + +## [v2.6.0] - 2021-10-19 +### Bug Fixes +- **ui5Framework:** Skip processing of framework libs ([#424](https://github.com/SAP/ui5-project/issues/424)) [`539d953`](https://github.com/SAP/ui5-project/commit/539d9539a5d2aaa6d01c4f539e3c86d8269788f2) + +### Features +- specVersion 2.6 [`9bd921a`](https://github.com/SAP/ui5-project/commit/9bd921a05bd5c0d8b6c6d94a864e60e4e181ad63) + + + +## [v2.5.0] - 2021-07-23 +### Features +- specVersion 2.5 [`3008dac`](https://github.com/SAP/ui5-project/commit/3008dace09109ba0fac49f0ddfc79255038f192c) + + + +## [v2.4.0] - 2021-06-01 +### Features +- specVersion 2.4 [`69ffc6c`](https://github.com/SAP/ui5-project/commit/69ffc6c34e387bcaaaf7b703559181b78fd33d54) + + + +## [v2.3.1] - 2021-03-04 +### Bug Fixes +- **ui5Framework:** Don't access metadata of deduped projects [`0255f8f`](https://github.com/SAP/ui5-project/commit/0255f8f628281ecb3cbbdb50192d2d4721bccea2) + +### Dependency Updates +- Bump js-yaml from 3.14.1 to 4.0.0 ([#380](https://github.com/SAP/ui5-project/issues/380)) [`a862186`](https://github.com/SAP/ui5-project/commit/a86218657703a5b607ebd09f8f71dd7ea810c6be) + + + +## [v2.3.0] - 2021-02-09 +### Features +- specVersion 2.3 ([#388](https://github.com/SAP/ui5-project/issues/388)) [`3e28026`](https://github.com/SAP/ui5-project/commit/3e280267b60a9a72183d5ab0905d838b6fcfaf33) + + + +## [v2.2.6] - 2021-01-28 +### Bug Fixes +- **ui5Framework.Installer:** Ensure target directory does not exist before rename ([#390](https://github.com/SAP/ui5-project/issues/390)) [`f107cdf`](https://github.com/SAP/ui5-project/commit/f107cdf2b1703791c153009150a5e1713e123b73) + + + +## [v2.2.5] - 2021-01-26 +### Bug Fixes +- **ui5Framework.Installer:** Ensure atomic install process [`72568a9`](https://github.com/SAP/ui5-project/commit/72568a990620cee69ffaf2470c684a7ba02c200c) + + + +## [v2.2.4] - 2020-11-06 +### Performance Improvements +- Reduce install size by removing 'string.prototype.matchall' dependency [`b69d75e`](https://github.com/SAP/ui5-project/commit/b69d75e740bfc594668ea73273bb03fdd40a4ce2) +- **validator:** Lazy load dependencies [`609346b`](https://github.com/SAP/ui5-project/commit/609346b2b1bb0417fde36a35ec43e9970c68504f) + + + +## [v2.2.3] - 2020-10-22 +### Bug Fixes +- **Schema:** Add missing bundle section "name" [`ba2d601`](https://github.com/SAP/ui5-project/commit/ba2d6015b6a04af92edb8f1b779a229fe73b705a) + + + +## [v2.2.2] - 2020-09-15 +### Bug Fixes +- **ui5Framework.mergeTrees:** Do not abort merge if a project has already been processed [`264c353`](https://github.com/SAP/ui5-project/commit/264c353b6973bade57164aded4f10a668986482d) + + + +## [v2.2.1] - 2020-09-02 + + +## [v2.2.0] - 2020-08-11 +### Features +- specVersion 2.2 ([#341](https://github.com/SAP/ui5-project/issues/341)) [`f44d14e`](https://github.com/SAP/ui5-project/commit/f44d14e136a4163d59dd8fd8c0be0ea2b59930be) + + + +## [v2.1.5] - 2020-07-14 +### Bug Fixes +- **Node.js API:** TypeScript type definition support ([#335](https://github.com/SAP/ui5-project/issues/335)) [`c610305`](https://github.com/SAP/ui5-project/commit/c610305e8fb869461a8dd5ba876270c7f7b71a22) + + + +## [v2.1.4] - 2020-05-29 +### Bug Fixes +- **ui5Framework:** Allow providing exact prerelease versions ([#326](https://github.com/SAP/ui5-project/issues/326)) [`6ce985c`](https://github.com/SAP/ui5-project/commit/6ce985c8feab26e6a97ca4570b3931f507773666) + + + +## [v2.1.3] - 2020-05-14 + + +## [v2.1.2] - 2020-05-11 +### Bug Fixes +- **framework t8r:** Allow use of specVersion 2.1 [`961847d`](https://github.com/SAP/ui5-project/commit/961847d113e6f594526201ab9ecccb898d2497e2) + + + +## [v2.1.1] - 2020-05-11 +### Bug Fixes +- Allow the use of specVersion 2.1 for projects [`a42172f`](https://github.com/SAP/ui5-project/commit/a42172fc341666b8d9a9b6049c365b28c55c76f0) + + + +## [v2.1.0] - 2020-05-05 +### Features +- **specVersion 2.1:** Add support for "customConfiguration" ([#308](https://github.com/SAP/ui5-project/issues/308)) [`201aaab`](https://github.com/SAP/ui5-project/commit/201aaab6beb8ad86fefdf371ae20c971970f6547) + + + +## [v2.0.4] - 2020-04-30 +### Bug Fixes +- Workaround missing dependency info for OpenUI5 packages in version 1.77.x [`3dfb812`](https://github.com/SAP/ui5-project/commit/3dfb8126e347fd1e7f6cc87e20318298e19eaf70) +- Namespaces in API Reference (JSDoc) [`3174d9f`](https://github.com/SAP/ui5-project/commit/3174d9f21f471252d2a39b8cb085eeeb5debe0a6) + + + +## [v2.0.3] - 2020-04-02 +### Bug Fixes +- **Schema:** Add missing metadata properties [`16894e1`](https://github.com/SAP/ui5-project/commit/16894e11c5c21a77a405431dfaf5d8642accfc1d) +- **package.json:** Downgrade pacote from 11.1.4 to 9.5.12 [`c76fb49`](https://github.com/SAP/ui5-project/commit/c76fb49e64b5905a3cd592d94fc0076cecc909b5) + + + +## [v2.0.2] - 2020-04-01 +### Bug Fixes +- **ui5Framework t8r:** Resolve versionOverride string [`4fffabe`](https://github.com/SAP/ui5-project/commit/4fffabe2a417b1ea46a47546c6269ac0ffbc3931) + + + +## [v2.0.1] - 2020-04-01 +### Bug Fixes +- **ui5Framework.mergeTrees:** Do not process the same project multiple times [`1377ec2`](https://github.com/SAP/ui5-project/commit/1377ec2ecea71a2470a9ea9b1e0698e466154838) + + + +## [v2.0.0] - 2020-03-31 +### Breaking Changes +- Require Node.js >= 10 [`f21e704`](https://github.com/SAP/ui5-project/commit/f21e704f85297e3fa774c59bf5d4e8282b947b41) + +### Features +- Add Configuration Schema ([#274](https://github.com/SAP/ui5-project/issues/274)) [`eb961c3`](https://github.com/SAP/ui5-project/commit/eb961c3377d42d3c93f7b7db5033f4e6716ddc71) +- Support for spec version 2.0 ([#277](https://github.com/SAP/ui5-project/issues/277)) [`770a56f`](https://github.com/SAP/ui5-project/commit/770a56feed331a3157c9f9fad486a4674dc12c87) +- Add ui5Framework translator and resolvers ([#265](https://github.com/SAP/ui5-project/issues/265)) [`5183e5c`](https://github.com/SAP/ui5-project/commit/5183e5cf99ac8cae6e4ccc8030d94214bce0563c) +- **projectPreprocessor:** Log warning when using a deprecated or restricted dependency ([#268](https://github.com/SAP/ui5-project/issues/268)) [`b776a4f`](https://github.com/SAP/ui5-project/commit/b776a4fcc4604f3ecb0d3fc1e6418ed190c11756) + +### BREAKING CHANGE + +Support for older Node.js releases has been dropped. +Only Node.js v10 or higher is supported. + + + +## [v1.2.0] - 2020-01-13 +### Features +- Add specification version 1.1 ([#252](https://github.com/SAP/ui5-project/issues/252)) [`5a83308`](https://github.com/SAP/ui5-project/commit/5a833086ccd415c5557c2bc3bbb705c18ac54314) + + + +## [v1.1.1] - 2019-11-07 + + +## [v1.1.0] - 2019-07-11 +### Features +- **projectPreprocessor:** Add handling for server-middleware extensions [`2ce964c`](https://github.com/SAP/ui5-project/commit/2ce964cd9feb6c1da39cd783ad45e0030c46b81a) + + + +## [v1.0.3] - 2019-06-25 +### Bug Fixes +- **projectPreprocessor:** Do not remove already removed dependencies ([#189](https://github.com/SAP/ui5-project/issues/189)) [`4600d63`](https://github.com/SAP/ui5-project/commit/4600d63cf323d3e143072c6c3416b5a48e90bb71) + + + +## [v1.0.2] - 2019-04-12 +### Bug Fixes +- **ProjectPreprocessor:** Fix dependency resolution [`0671a8b`](https://github.com/SAP/ui5-project/commit/0671a8bf2de9ca24823df6f041a77e7c8e46f6f0) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 1.0.2 to 1.0.3 ([#154](https://github.com/SAP/ui5-project/issues/154)) [`cf86764`](https://github.com/SAP/ui5-project/commit/cf867643b8b621019a5d5b0f5d3117ebcdd1cd44) + + + +## [v1.0.1] - 2019-02-14 +### Bug Fixes +- **npm translator:** Remove deduped optional dependencies from tree [`3481154`](https://github.com/SAP/ui5-project/commit/348115426f03bd3a5bb823ac54a6b15475a84657) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 1.0.0 to 1.0.1 ([#113](https://github.com/SAP/ui5-project/issues/113)) [`96a3d6a`](https://github.com/SAP/ui5-project/commit/96a3d6a2a54cb1eab190ba89f9da686e8aae2d84) + + + +## [v1.0.0] - 2019-01-10 +### Breaking Changes +- **normalizer:** Rename optional parameter "translator" [`92321e0`](https://github.com/SAP/ui5-project/commit/92321e08e43175611b8417047fc957792d539b10) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 0.2.9 to 1.0.0 ([#99](https://github.com/SAP/ui5-project/issues/99)) [`7dd5d5c`](https://github.com/SAP/ui5-project/commit/7dd5d5cda909e3a109821315cc5a5a80f05cd5d3) +- Bump [@ui5](https://github.com/ui5)/logger from 0.2.2 to 1.0.0 ([#98](https://github.com/SAP/ui5-project/issues/98)) [`8068a76`](https://github.com/SAP/ui5-project/commit/8068a76dc43701f5c8b0467933a83d777ccdee01) + +### Features +- Add specification version 1.0 [`b0c02f6`](https://github.com/SAP/ui5-project/commit/b0c02f67296f6251a7ef4fe5c61146bb169a6705) + +### BREAKING CHANGE + +Renamed parameter "translator" of functions generateDependencyTree and generateProjectTree to "translatorName" + + + +## [v0.2.5] - 2018-12-19 +### Bug Fixes +- **npm translator:** Deduplicate subtrees of pending dependencies [`7e55ae3`](https://github.com/SAP/ui5-project/commit/7e55ae3d88280746f5800bffc7bbd13e1495ba07) +- **npm translator:** Fix handling of indirect dependency cycles [`c99d6d3`](https://github.com/SAP/ui5-project/commit/c99d6d3a19fbb6c197b449dfd6cb8acc48837dba) + + + +## [v0.2.4] - 2018-12-17 +### Bug Fixes +- **npm t8r:** Add deduplication of npm dependencies [`2717088`](https://github.com/SAP/ui5-project/commit/2717088532d415b6922f290b58d9227b946a965f) +- **projectPreprocessor:** Ignore deduped modules [`84f7b25`](https://github.com/SAP/ui5-project/commit/84f7b25a9e45df3bc55a7957e4f61db580e68509) + + + +## [v0.2.3] - 2018-11-20 +### Bug Fixes +- **npm t8r:** Again, handle npm optionalDependencies correctly [`9fd78dc`](https://github.com/SAP/ui5-project/commit/9fd78dca4d836f9a37036fd151a78e9295b28aa1) + + + +## [v0.2.2] - 2018-11-17 +### Bug Fixes +- **npm t8r:** Handle npm optionalDependencies correctly [`da707d7`](https://github.com/SAP/ui5-project/commit/da707d73b5c75b489e2e499de2b4f54924018844) + +### Features +- **projectPreprocessor:** Add handling for task extensions [`0722865`](https://github.com/SAP/ui5-project/commit/072286591ae3b20cca8e418030c3f2bc048352c5) +- **projectPreprocessor:** Allow application project dependency on non-root level [`b8a59d5`](https://github.com/SAP/ui5-project/commit/b8a59d56c8b5cf4c330fe99cb2162c1701aa51ca) + + + +## [v0.2.1] - 2018-10-29 +### Features +- Add shim extension [`93c9b39`](https://github.com/SAP/ui5-project/commit/93c9b3960ca36f240c5f8453a89f72792a01fe92) +- Add "extension" projects [`476b785`](https://github.com/SAP/ui5-project/commit/476b785810d6993d2a3e21707ffa67e568e67eac) + + + +## [v0.2.0] - 2018-07-11 + + +## [v0.1.0] - 2018-06-26 +### Bug Fixes +- Fix some typos in log messages ([#17](https://github.com/SAP/ui5-project/issues/17)) [`1f2f2fd`](https://github.com/SAP/ui5-project/commit/1f2f2fd164abaf449cc5e7d94ec792f469710207) +- **npm translator:** Fix endless loop in case of dependency cycles ([#15](https://github.com/SAP/ui5-project/issues/15)) [`cf31112`](https://github.com/SAP/ui5-project/commit/cf3111288278e8dd36a09b549bd2b254e86af041) + + + +## v0.0.1 - 2018-06-06 +### Bug Fixes +- **npm t8r:** Fix collection fallback with missing package.json [`578466f`](https://github.com/SAP/ui5-project/commit/578466fdedf871091874c93d1a9305859e34e3ed) + + +{{- if .Versions }} +{{ range .Versions -}} +{{ if and .Tag.Previous (ne .Tag.Name "v3.0.0") -}} +[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} +{{ end -}} +{{ end -}} +{{ end -}} +[v3.9.2]: https://github.com/SAP/ui5-project/compare/v3.9.1...v3.9.2 +[v3.9.1]: https://github.com/SAP/ui5-project/compare/v3.9.0...v3.9.1 +[v3.9.0]: https://github.com/SAP/ui5-project/compare/v3.8.0...v3.9.0 +[v3.8.0]: https://github.com/SAP/ui5-project/compare/v3.7.3...v3.8.0 +[v3.7.3]: https://github.com/SAP/ui5-project/compare/v3.7.2...v3.7.3 +[v3.7.2]: https://github.com/SAP/ui5-project/compare/v3.7.1...v3.7.2 +[v3.7.1]: https://github.com/SAP/ui5-project/compare/v3.7.0...v3.7.1 +[v3.7.0]: https://github.com/SAP/ui5-project/compare/v3.6.0...v3.7.0 +[v3.6.0]: https://github.com/SAP/ui5-project/compare/v3.5.1...v3.6.0 +[v3.5.1]: https://github.com/SAP/ui5-project/compare/v3.5.0...v3.5.1 +[v3.5.0]: https://github.com/SAP/ui5-project/compare/v3.4.2...v3.5.0 +[v3.4.2]: https://github.com/SAP/ui5-project/compare/v3.4.1...v3.4.2 +[v3.4.1]: https://github.com/SAP/ui5-project/compare/v3.4.0...v3.4.1 +[v3.4.0]: https://github.com/SAP/ui5-project/compare/v3.3.2...v3.4.0 +[v3.3.2]: https://github.com/SAP/ui5-project/compare/v3.3.1...v3.3.2 +[v3.3.1]: https://github.com/SAP/ui5-project/compare/v3.3.0...v3.3.1 +[v3.3.0]: https://github.com/SAP/ui5-project/compare/v3.2.2...v3.3.0 +[v3.2.2]: https://github.com/SAP/ui5-project/compare/v3.2.1...v3.2.2 +[v3.2.1]: https://github.com/SAP/ui5-project/compare/v3.2.0...v3.2.1 +[v3.2.0]: https://github.com/SAP/ui5-project/compare/v3.1.1...v3.2.0 +[v3.1.1]: https://github.com/SAP/ui5-project/compare/v3.1.0...v3.1.1 +[v3.1.0]: https://github.com/SAP/ui5-project/compare/v3.0.4...v3.1.0 +[v3.0.4]: https://github.com/SAP/ui5-project/compare/v3.0.3...v3.0.4 +[v3.0.3]: https://github.com/SAP/ui5-project/compare/v3.0.2...v3.0.3 +[v3.0.2]: https://github.com/SAP/ui5-project/compare/v3.0.1...v3.0.2 +[v3.0.1]: https://github.com/SAP/ui5-project/compare/v3.0.0...v3.0.1 +[v3.0.0]: https://github.com/SAP/ui5-project/compare/v2.6.0...v3.0.0 +[v2.6.0]: https://github.com/SAP/ui5-project/compare/v2.5.0...v2.6.0 +[v2.5.0]: https://github.com/SAP/ui5-project/compare/v2.4.0...v2.5.0 +[v2.4.0]: https://github.com/SAP/ui5-project/compare/v2.3.1...v2.4.0 +[v2.3.1]: https://github.com/SAP/ui5-project/compare/v2.3.0...v2.3.1 +[v2.3.0]: https://github.com/SAP/ui5-project/compare/v2.2.6...v2.3.0 +[v2.2.6]: https://github.com/SAP/ui5-project/compare/v2.2.5...v2.2.6 +[v2.2.5]: https://github.com/SAP/ui5-project/compare/v2.2.4...v2.2.5 +[v2.2.4]: https://github.com/SAP/ui5-project/compare/v2.2.3...v2.2.4 +[v2.2.3]: https://github.com/SAP/ui5-project/compare/v2.2.2...v2.2.3 +[v2.2.2]: https://github.com/SAP/ui5-project/compare/v2.2.1...v2.2.2 +[v2.2.1]: https://github.com/SAP/ui5-project/compare/v2.2.0...v2.2.1 +[v2.2.0]: https://github.com/SAP/ui5-project/compare/v2.1.5...v2.2.0 +[v2.1.5]: https://github.com/SAP/ui5-project/compare/v2.1.4...v2.1.5 +[v2.1.4]: https://github.com/SAP/ui5-project/compare/v2.1.3...v2.1.4 +[v2.1.3]: https://github.com/SAP/ui5-project/compare/v2.1.2...v2.1.3 +[v2.1.2]: https://github.com/SAP/ui5-project/compare/v2.1.1...v2.1.2 +[v2.1.1]: https://github.com/SAP/ui5-project/compare/v2.1.0...v2.1.1 +[v2.1.0]: https://github.com/SAP/ui5-project/compare/v2.0.4...v2.1.0 +[v2.0.4]: https://github.com/SAP/ui5-project/compare/v2.0.3...v2.0.4 +[v2.0.3]: https://github.com/SAP/ui5-project/compare/v2.0.2...v2.0.3 +[v2.0.2]: https://github.com/SAP/ui5-project/compare/v2.0.1...v2.0.2 +[v2.0.1]: https://github.com/SAP/ui5-project/compare/v2.0.0...v2.0.1 +[v2.0.0]: https://github.com/SAP/ui5-project/compare/v1.2.0...v2.0.0 +[v1.2.0]: https://github.com/SAP/ui5-project/compare/v1.1.1...v1.2.0 +[v1.1.1]: https://github.com/SAP/ui5-project/compare/v1.1.0...v1.1.1 +[v1.1.0]: https://github.com/SAP/ui5-project/compare/v1.0.3...v1.1.0 +[v1.0.3]: https://github.com/SAP/ui5-project/compare/v1.0.2...v1.0.3 +[v1.0.2]: https://github.com/SAP/ui5-project/compare/v1.0.1...v1.0.2 +[v1.0.1]: https://github.com/SAP/ui5-project/compare/v1.0.0...v1.0.1 +[v1.0.0]: https://github.com/SAP/ui5-project/compare/v0.2.5...v1.0.0 +[v0.2.5]: https://github.com/SAP/ui5-project/compare/v0.2.4...v0.2.5 +[v0.2.4]: https://github.com/SAP/ui5-project/compare/v0.2.3...v0.2.4 +[v0.2.3]: https://github.com/SAP/ui5-project/compare/v0.2.2...v0.2.3 +[v0.2.2]: https://github.com/SAP/ui5-project/compare/v0.2.1...v0.2.2 +[v0.2.1]: https://github.com/SAP/ui5-project/compare/v0.2.0...v0.2.1 +[v0.2.0]: https://github.com/SAP/ui5-project/compare/v0.1.0...v0.2.0 +[v0.1.0]: https://github.com/SAP/ui5-project/compare/v0.0.1...v0.1.0 diff --git a/packages/project/.chglog/RELEASE.tpl.md b/packages/project/.chglog/RELEASE.tpl.md new file mode 100755 index 00000000000..efc482c385d --- /dev/null +++ b/packages/project/.chglog/RELEASE.tpl.md @@ -0,0 +1,33 @@ +{{ range .Versions }} +{{ range .CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [`{{ .Hash.Short }}`]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }}) +{{ end }} +{{ end -}} + +{{- if .RevertCommits -}} +### Reverts +{{ range .RevertCommits -}} +- {{ .Revert.Header }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} + +{{ if .Tag.Previous }} +### All changes +[`{{ .Tag.Previous.Name }}...{{ .Tag.Name }}`] +{{ end }} + +{{ if .Tag.Previous -}} +[`{{ .Tag.Previous.Name }}...{{ .Tag.Name }}`]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} +{{ end -}} +{{ end -}} diff --git a/packages/project/.chglog/config.yml b/packages/project/.chglog/config.yml new file mode 100755 index 00000000000..de0a569389b --- /dev/null +++ b/packages/project/.chglog/config.yml @@ -0,0 +1,33 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/SAP/ui5-project +options: + commits: + filters: + Type: + - FEATURE + - FIX + - PERF + - DEPENDENCY + - BREAKING + commit_groups: + title_maps: + FEATURE: Features + FIX: Bug Fixes + PERF: Performance Improvements + DEPENDENCY: Dependency Updates + BREAKING: Breaking Changes + header: + pattern: "^\\[(\\w*)\\]\\s(?:([^\\:]*)\\:\\s)?(.*)$" + pattern_maps: + - Type + - Scope + - Subject + issues: + prefix: + - "#" + notes: + keywords: + - BREAKING CHANGE diff --git a/packages/project/.chglog/release-config.yml b/packages/project/.chglog/release-config.yml new file mode 100755 index 00000000000..14c3b27d514 --- /dev/null +++ b/packages/project/.chglog/release-config.yml @@ -0,0 +1,33 @@ +style: github +template: RELEASE.tpl.md +info: + repository_url: https://github.com/SAP/ui5-project +options: + tag_filter_pattern: '^v[^0123]' # For release notes ignore versions below v4 to that we always compare the _last v4+_ tag with the current release + commits: + filters: + Type: + - FEATURE + - FIX + - PERF + - DEPENDENCY + - BREAKING + commit_groups: + title_maps: + FEATURE: Features + FIX: Bug Fixes + PERF: Performance Improvements + DEPENDENCY: Dependency Updates + BREAKING: Breaking Changes + header: + pattern: "^\\[(\\w*)\\]\\s(?:([^\\:]*)\\:\\s)?(.*)$" + pattern_maps: + - Type + - Scope + - Subject + issues: + prefix: + - "#" + notes: + keywords: + - BREAKING CHANGE diff --git a/packages/project/.editorconfig b/packages/project/.editorconfig new file mode 100644 index 00000000000..b432804f7fc --- /dev/null +++ b/packages/project/.editorconfig @@ -0,0 +1,20 @@ +# see http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = tab + +[*.{css,html,js,cjs,mjs,jsx,ts,tsx,less,txt,json,yml,md}] +trim_trailing_whitespace = true +end_of_line = lf +indent_size = 4 +insert_final_newline = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/packages/project/.gitattributes b/packages/project/.gitattributes new file mode 100644 index 00000000000..6fe78884f2b --- /dev/null +++ b/packages/project/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +build/** -linguist-generated=false +lib/build/** linguist-generated=false +lib/** linguist-vendored=false +lib/** linguist-generated=false \ No newline at end of file diff --git a/packages/project/.github/ISSUE_TEMPLATE.md b/packages/project/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..6ddeb2221a8 --- /dev/null +++ b/packages/project/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ +## 🚨 Issues Have Been Transferred to UI5 CLI Repository + +Please create new issues in the UI5 CLI repository: https://github.com/UI5/cli/issues/new/choose diff --git a/packages/project/.github/ISSUE_TEMPLATE/config.yml b/packages/project/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..34df09809c9 --- /dev/null +++ b/packages/project/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Report UI5 CLI Issues or Request a Feature + url: https://github.com/UI5/cli/issues/new/choose + about: Please create new issues in the UI5 CLI repository diff --git a/packages/project/.github/PULL_REQUEST_TEMPLATE.md b/packages/project/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..2f043b5c5cc --- /dev/null +++ b/packages/project/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +**Thank you for your contribution!** 🙌 + +To get it merged faster, kindly review the checklist below: + +## Pull Request Checklist +- [ ] Reviewed the [Contributing Guidelines](https://github.com/UI5/cli/blob/main/CONTRIBUTING.md#-contributing-code) + + Especially the [How to Contribute](https://github.com/UI5/cli/blob/main/CONTRIBUTING.md#how-to-contribute) section +- [ ] [No merge commits](https://github.com/UI5/cli/blob/main/docs/Guidelines.md#no-merge-commits) +- [ ] [Correct commit message style](https://github.com/UI5/cli/blob/main/docs/Guidelines.md#commit-message-style) diff --git a/packages/project/.github/dependabot.yml b/packages/project/.github/dependabot.yml new file mode 100644 index 00000000000..ebf6ca750f8 --- /dev/null +++ b/packages/project/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + day: sunday + time: "10:00" + timezone: Etc/UCT + reviewers: + - "SAP/ui5-foundation" + versioning-strategy: increase + commit-message: + prefix: "[DEPENDENCY] " + prefix-development: "[INTERNAL] " diff --git a/packages/project/.github/in-solidarity.yml b/packages/project/.github/in-solidarity.yml new file mode 100644 index 00000000000..4ce829a6be3 --- /dev/null +++ b/packages/project/.github/in-solidarity.yml @@ -0,0 +1 @@ +_extends: ietf/terminology diff --git a/packages/project/.github/workflows/dependabot-auto-merge.yml b/packages/project/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000000..43d92c94fd6 --- /dev/null +++ b/packages/project/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,28 @@ +name: Dependabot auto-merge +on: + pull_request: + branches: + - v4 + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' && github.event.pull_request.auto_merge == null }} + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Approve and auto-merge PRs for minor/patch updates of github-actions + if: | + steps.metadata.outputs.package-ecosystem == 'github_actions' && + contains(fromJSON('["version-update:semver-minor", "version-update:semver-patch"]'), steps.metadata.outputs.update-type) + run: gh pr review --approve "$PR_URL" && gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/packages/project/.github/workflows/github-ci.yml b/packages/project/.github/workflows/github-ci.yml new file mode 100644 index 00000000000..f649c335d83 --- /dev/null +++ b/packages/project/.github/workflows/github-ci.yml @@ -0,0 +1,34 @@ +name: GitHub CI + +on: + push: + branches: + - v4 + pull_request: + branches: + - v4 + +# No permissions are required for this workflow +permissions: {} + +jobs: + test: + name: General checks, tests and coverage reporting + runs-on: ubuntu-24.04 + steps: + + - uses: actions/checkout@v5 + + - name: Use Node.js LTS 20.11.0 + uses: actions/setup-node@v5.0.0 + with: + node-version: 20.11.0 + + - name: Install dependencies + run: npm ci + + - name: Perform checks and tests + run: npm test + + - name: Send report to Coveralls + uses: coverallsapp/github-action@v2.3.6 diff --git a/packages/project/.github/workflows/reuse-compliance.yml b/packages/project/.github/workflows/reuse-compliance.yml new file mode 100644 index 00000000000..ee696a44f1b --- /dev/null +++ b/packages/project/.github/workflows/reuse-compliance.yml @@ -0,0 +1,21 @@ +name: REUSE + +on: + push: + branches: + - v4 + pull_request: + branches: + - v4 + +# No permissions are required for this workflow +permissions: {} + +jobs: + compliance-check: + name: Compliance Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Execute REUSE Compliance Check + uses: fsfe/reuse-action@v5 diff --git a/packages/project/.gitignore b/packages/project/.gitignore new file mode 100644 index 00000000000..8ba81815932 --- /dev/null +++ b/packages/project/.gitignore @@ -0,0 +1,63 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# CI (Azure Pipelines) xUnit test results +test-results.xml + +# IDEs +.vscode/ +*.~vsdx +.idea/ + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Misc +yarn.lock +.DS_Store + +# Don't include private SSH key for deployment via Travis CI +deploy_key + +# Custom directories +!test/fixtures/**/node_modules +test/tmp/ +jsdocs/ diff --git a/packages/project/.npmrc b/packages/project/.npmrc new file mode 100644 index 00000000000..93ec4f76ba6 --- /dev/null +++ b/packages/project/.npmrc @@ -0,0 +1,3 @@ +# Enforce public npm registry +registry=https://registry.npmjs.org/ +lockfile-version=3 diff --git a/packages/project/CHANGELOG.md b/packages/project/CHANGELOG.md new file mode 100644 index 00000000000..7207b3adc42 --- /dev/null +++ b/packages/project/CHANGELOG.md @@ -0,0 +1,654 @@ +# Changelog +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +A list of unreleased changes can be found [here](https://github.com/SAP/ui5-project/compare/v4.0.6...HEAD). + + +## [v4.0.6] - 2025-09-12 + + +## [v4.0.5] - 2025-09-11 +### Bug Fixes +- Rename project to UI5 CLI [`425ed2f`](https://github.com/SAP/ui5-project/commit/425ed2fdc7686cd6b631d2f01790ff91c970c604) + +### Dependency Updates +- Bump make-fetch-happen from 14.0.3 to 15.0.0 ([#812](https://github.com/SAP/ui5-project/issues/812)) [`442f20b`](https://github.com/SAP/ui5-project/commit/442f20b3c282016ade7b689a780de82c0c793dbd) +- Bump pacote from 19.0.1 to 21.0.0 ([#782](https://github.com/SAP/ui5-project/issues/782)) [`ffd87e5`](https://github.com/SAP/ui5-project/commit/ffd87e5b4aceba9b71231c15b4e5df8077fac1f4) + + + +## [v4.0.4] - 2024-11-29 +### Dependency Updates +- Switch from "rimraf" to native "fs.rm" ([#780](https://github.com/SAP/ui5-project/issues/780)) [`1998257`](https://github.com/SAP/ui5-project/commit/1998257295be7038dd4222f1b241848c1fd0a05f) +- Bump [@npmcli](https://github.com/npmcli)/config from 8.3.4 to 9.0.0 ([#773](https://github.com/SAP/ui5-project/issues/773)) [`ad38e17`](https://github.com/SAP/ui5-project/commit/ad38e1788c2b4b4407ea7250c04d74e4faf3a1d6) +- Bump pacote from 18.0.6 to 19.0.0 ([#772](https://github.com/SAP/ui5-project/issues/772)) [`df4bbfe`](https://github.com/SAP/ui5-project/commit/df4bbfef98923c48d43c742b13feefd640cc9529) +- Bump make-fetch-happen from 13.0.1 to 14.0.0 ([#771](https://github.com/SAP/ui5-project/issues/771)) [`d52255c`](https://github.com/SAP/ui5-project/commit/d52255c3c1ca50ee255cb28c9bbef24007b16d51) + + + +## [v4.0.3] - 2024-08-27 + + +## [v4.0.2] - 2024-08-01 +### Bug Fixes +- decorateBootstrapModule should default to "false" [`b2a420a`](https://github.com/SAP/ui5-project/commit/b2a420a13e8ad025672ef616ff2e56e0371f5439) + + + +## [v4.0.1] - 2024-07-31 +### Dependency Updates +- Fix [@ui5](https://github.com/ui5)/builder peerDependency range [`71a4d6e`](https://github.com/SAP/ui5-project/commit/71a4d6e7bbe10a7f5a735dcef2e9022a4ecbdb97) + + + +## [v4.0.0] - 2024-07-23 +### Breaking Changes +- Drop node v21 support [`b017633`](https://github.com/SAP/ui5-project/commit/b01763338ceff80f9df459b246f12b06c77891d0) +- Make '[@ui5](https://github.com/ui5)/builder' an optional peerDependency [`cb2e99d`](https://github.com/SAP/ui5-project/commit/cb2e99dbc7804fed4d8e10a2e95063b6357e963d) +- Rename ui5HomeDir to ui5DataDir in APIs ([#707](https://github.com/SAP/ui5-project/issues/707)) [`5103c3e`](https://github.com/SAP/ui5-project/commit/5103c3ee63bc9c5b5fa6db136badec78e89ee28d) +- Set default workspaceName to "default" for API usage ([#706](https://github.com/SAP/ui5-project/issues/706)) [`a2d8f9d`](https://github.com/SAP/ui5-project/commit/a2d8f9d03154cfc330ccbeaf0c0aaa10032c2337) +- Require Node.js 20.11.x/>=21.2.0 and npm >=10 [`6a444a0`](https://github.com/SAP/ui5-project/commit/6a444a077166451ada16334ef62f1357e2c15bd7) + +### Dependency Updates +- Bump rimraf from 5.0.9 to 6.0.1 [`9c3c70f`](https://github.com/SAP/ui5-project/commit/9c3c70f71963ba24734f02380b0f468c505e6482) +- Bump pacote from 17.0.7 to 18.0.6 [`c6b17c4`](https://github.com/SAP/ui5-project/commit/c6b17c48a1021226b0a51826c889a549ae459983) +- Bump read-pkg-up from 10.1.0 to 11.0.0 [`83e93aa`](https://github.com/SAP/ui5-project/commit/83e93aab5c90dfb8133921eaf5785b06eb51b6cc) +- Bump read-pkg from 8.1.0 to 9.0.1 [`0279ac9`](https://github.com/SAP/ui5-project/commit/0279ac9dd248e7d2215c44ed5e3a7ebc19894de2) +- Bump globby from 13.2.2 to 14.0.1 [`eb9d6d8`](https://github.com/SAP/ui5-project/commit/eb9d6d8501123cd48b1e7b8375d2c9fb70ad7334) + +### Features +- Apply specVersion defaults from ui5.yaml.json schema ([#733](https://github.com/SAP/ui5-project/issues/733)) [`e3e8f85`](https://github.com/SAP/ui5-project/commit/e3e8f855506c23cc0bac3e57cbca0ab6779956de) +- **Schema:** Introduce specVersion 4.0 ([#731](https://github.com/SAP/ui5-project/issues/731)) [`c5a9fde`](https://github.com/SAP/ui5-project/commit/c5a9fde02b53b36af25f7691cadd79a434ddd0aa) +- **manifest.json:** Auto-fill supportedLocales ([#683](https://github.com/SAP/ui5-project/issues/683)) [`c905d4f`](https://github.com/SAP/ui5-project/commit/c905d4f7ee022a60596c6d867abf588275f8f1d2) + +### BREAKING CHANGE + +Consumers of the Node.js API that make use of the ProjectGraph#build + +Installers and Resolvers' argument `ui5HomeDir` is now renamed to +`ui5DataDir` + +JIRA: CPOUI5FOUNDATION-802 +Relates to: https://github.com/SAP/ui5-tooling/issues/701 + +Set default workspaceName to "default" for API usage +(https://github.com/SAP/ui5-project/pull/586) + +JIRA: CPOUI5FOUNDATION-802 +Relates to: https://github.com/SAP/ui5-tooling/issues/701 + +--------- + +Support for older Node.js and npm releases has been dropped. +Only Node.js 20.11.x and >=21.2.0 as well as npm v10 or higher are supported. + + +## [v3.9.2] - 2024-06-24 +### Dependency Updates +- Bump pacote from 17.0.7 to 18.0.6 [`9b6d580`](https://github.com/SAP/ui5-project/commit/9b6d58085bb74e4a2dfc1dccf528434db217e868) + + + +## [v3.9.1] - 2024-03-27 + + +## [v3.9.0] - 2023-12-12 +### Features +- **ProjectBuilder:** Add `outputStyle` option to request flat build output ([#624](https://github.com/SAP/ui5-project/issues/624)) [`79312fc`](https://github.com/SAP/ui5-project/commit/79312fcefea1ea97c1f3d403ac4470f890069809) +- **specVersion 3.2:** depCache bundling mode ([#673](https://github.com/SAP/ui5-project/issues/673)) [`68c5278`](https://github.com/SAP/ui5-project/commit/68c52782afbb617ddf110aca02d96f34a39ad5f7) + + + +## [v3.8.0] - 2023-11-20 +### Bug Fixes +- **application:** Improve error message for missing manifest.json [`016a846`](https://github.com/SAP/ui5-project/commit/016a84692aa1645f2e4267673d99495457c28458) + +### Features +- **TaskUtil:** Add 'force' flag to cleanup task callback ([#677](https://github.com/SAP/ui5-project/issues/677)) [`a0a21b7`](https://github.com/SAP/ui5-project/commit/a0a21b7ecd2805ff3d8d78ba9a453df64012556a) + + + +## [v3.7.3] - 2023-10-20 +### Bug Fixes +- ProjectBuilder now can be executed in parallel ([#669](https://github.com/SAP/ui5-project/issues/669)) [`f652461`](https://github.com/SAP/ui5-project/commit/f652461455a28718835cc66c7265f628be1e13b9) + + + +## [v3.7.2] - 2023-10-11 +### Dependency Updates +- Bump make-fetch-happen from 11.1.1 to 13.0.0 [`f2e264e`](https://github.com/SAP/ui5-project/commit/f2e264e87dfef1d5a132a1a0bf35043a789f8e84) +- Bump pacote from 15.2.0 to 17.0.4 [`f071399`](https://github.com/SAP/ui5-project/commit/f071399d994963b415c8ea35a629c465ae539f23) +- Bump [@npmcli](https://github.com/npmcli)/config from 6.4.0 to 8.0.0 [`c9f5218`](https://github.com/SAP/ui5-project/commit/c9f521815bab022bc8c0e8a3c27658266f01c655) + + + +## [v3.7.1] - 2023-10-02 +### Bug Fixes +- Allow usage of after/before task assignment for all standard tasks ([#628](https://github.com/SAP/ui5-project/issues/628)) [`1a272d2`](https://github.com/SAP/ui5-project/commit/1a272d2bd2700fa849ebb46bf9bd98806fa17fb2) + + + +## [v3.7.0] - 2023-09-06 +### Bug Fixes +- Ensure usage of provided UI5 data dir [`1e0503a`](https://github.com/SAP/ui5-project/commit/1e0503a32dae06202b62408558d5ef85bb49daf1) +- **NodePackageDependencies:** Implement validation for missing package.json attributes [`b070972`](https://github.com/SAP/ui5-project/commit/b0709725b373441fd62fe9e33cc0440b6df17401) +- **ProjectGraph:** Improve error message when adding duplicate projects or extensions [`2b4a49e`](https://github.com/SAP/ui5-project/commit/2b4a49e2b6dc4004bf078d259c1a8f54ccc0ae2c) +- **pacote:** Use npm cache within UI5 data dir [`f1e2178`](https://github.com/SAP/ui5-project/commit/f1e217803d0c455f61135084b00a7daf42fb9094) + +### Features +- **Resolvers:** Allow ranges / npm tags for version resolution [`2841004`](https://github.com/SAP/ui5-project/commit/28410044f9d4abd348dc3e0697048543eb7796d9) +- **Resolvers:** Use npm tags for determining 'latest' [`5cde95a`](https://github.com/SAP/ui5-project/commit/5cde95a04f2f040fffd0798822058f9692761cc4) + + + +## [v3.6.0] - 2023-08-22 +### Features +- Add specVersion 3.1 and builder resource excludes for modules ([#639](https://github.com/SAP/ui5-project/issues/639)) [`2ac053e`](https://github.com/SAP/ui5-project/commit/2ac053ef299bbaf02e73e12e2876f301d2b07d1b) +- **AbstractResolver:** Resolve version ranges specifying major version only [`1f8cfdf`](https://github.com/SAP/ui5-project/commit/1f8cfdf3c72745904fbdceab049ae5d2cbf86b06) + + + +## [v3.5.1] - 2023-08-18 +### Bug Fixes +- Resolve UI5 data directory relative to project ([#642](https://github.com/SAP/ui5-project/issues/642)) [`228b14c`](https://github.com/SAP/ui5-project/commit/228b14c63fbd736962c513fdd1656a7983f51bbc) + + + +## [v3.5.0] - 2023-08-09 +### Features +- Allow to configure location of UI5 home directory ([#635](https://github.com/SAP/ui5-project/issues/635)) [`8c86083`](https://github.com/SAP/ui5-project/commit/8c860839d94abdaedaf878614a9121a89b85f116) + + + +## [v3.4.2] - 2023-07-13 +### Bug Fixes +- **Application:** Fallback to manifest.appdescr_variant if manifest.json is not found ([#631](https://github.com/SAP/ui5-project/issues/631)) [`43c6b22`](https://github.com/SAP/ui5-project/commit/43c6b224cf7ecad39a060baf8c6922f919e6dd59) + +### Dependency Updates +- Bump read-pkg-up from 9.1.0 to 10.0.0 [`557cb36`](https://github.com/SAP/ui5-project/commit/557cb36790ba53aa43a15cf7211560461dabb9e5) + + + +## [v3.4.1] - 2023-07-03 +### Bug Fixes +- Migrate from libnpmconfig to [@npmcli](https://github.com/npmcli)/config ([#618](https://github.com/SAP/ui5-project/issues/618)) [`13d019b`](https://github.com/SAP/ui5-project/commit/13d019bb4d8eda05c0a1564c6a2b96fa4eb05ab1) + + + +## [v3.4.0] - 2023-06-21 +### Bug Fixes +- **maven/Registry:** Prevent socket timeouts when installing framework libraries [`3de767f`](https://github.com/SAP/ui5-project/commit/3de767fb7cc9278bf984ff88064a16e593db6db0) + +### Features +- **Sapui5MavenSnapshotResolver:** Use npm-dist.zip artifact for 1.116.0 and later ([#622](https://github.com/SAP/ui5-project/issues/622)) [`45dcee0`](https://github.com/SAP/ui5-project/commit/45dcee00f141b6632d5a1217affbd212f6faf1f4) + + + +## [v3.3.2] - 2023-06-06 +### Bug Fixes +- **ui5Framework:** Treat 'optional' dependencies of root project as non-optional [`f3318f0`](https://github.com/SAP/ui5-project/commit/f3318f0daff617e12ac97050e19d41a16ecbc748) +- **ui5Framework:** Choose correct resolver for snapshot framework version overrides [`ba860de`](https://github.com/SAP/ui5-project/commit/ba860de97bc1674fa8381706cc09bd68ee08df38) + +### Dependency Updates +- Bump xml2js from 0.5.0 to 0.6.0 [`aa7d853`](https://github.com/SAP/ui5-project/commit/aa7d853f4a719006a6aaf4e51cc5c12fd00d2aa1) + + + +## [v3.3.1] - 2023-05-23 +### Bug Fixes +- **Workspace:** Ignore empty npm workspace modules ([#614](https://github.com/SAP/ui5-project/issues/614)) [`66e82a3`](https://github.com/SAP/ui5-project/commit/66e82a37f8c559eb7219fad0329a4d77fd3a6481) +- **projectGraphBuilder:** Add module cache invalidation ([#612](https://github.com/SAP/ui5-project/issues/612)) [`65496ea`](https://github.com/SAP/ui5-project/commit/65496eabeaafc50348dfc276d19d135eb035b261) + + + +## [v3.3.0] - 2023-05-05 +### Bug Fixes +- Resolve properly package.json dependency aliases ([#608](https://github.com/SAP/ui5-project/issues/608)) [`f8753e5`](https://github.com/SAP/ui5-project/commit/f8753e53c6bc7f89bb19107073fb52db0a725cb9) + +### Features +- **Sapui5MavenSnapshotResolver:** Expose cacheMode parameter through all APIs ([#607](https://github.com/SAP/ui5-project/issues/607)) [`78eb482`](https://github.com/SAP/ui5-project/commit/78eb4825ecab9534426f517e764451f53d232fed) + + + +## [v3.2.2] - 2023-04-27 +### Bug Fixes +- **ui5Framework:** Respect npm proxy configuration to fetch libraries [`5e3da0c`](https://github.com/SAP/ui5-project/commit/5e3da0c552593ff521c8e27cdbb4aeb849f56aa4) + + + +## [v3.2.1] - 2023-04-21 +### Bug Fixes +- **Configuration:** Rename toJSON => toJson [`4dfbf28`](https://github.com/SAP/ui5-project/commit/4dfbf28a20d67ce8d482c9d8ca18331d7fa69629) + + + +## [v3.2.0] - 2023-04-21 +### Dependency Updates +- Bump rimraf from 4.4.1 to 5.0.0 ([#597](https://github.com/SAP/ui5-project/issues/597)) [`1da76bc`](https://github.com/SAP/ui5-project/commit/1da76bc21c218b154b1a6014808f8d3a4d101b69) + +### Features +- Add Configuration ([#575](https://github.com/SAP/ui5-project/issues/575)) [`fd37cef`](https://github.com/SAP/ui5-project/commit/fd37cefffdc22b4a4bbc3fcbde20581848d937fa) +- Enable snapshot consumption from Maven repository ([#570](https://github.com/SAP/ui5-project/issues/570)) [`ade2c49`](https://github.com/SAP/ui5-project/commit/ade2c49d66ebba229b62c6614c8bbdfed10bc6b0) + + + +## [v3.1.1] - 2023-04-12 +### Dependency Updates +- Bump xml2js from 0.4.23 to 0.5.0 [`d6d86c9`](https://github.com/SAP/ui5-project/commit/d6d86c93db5c4d288161aa11b72bb6537c4f4cf4) +- Bump read-pkg from 7.1.0 to 8.0.0 [`9800c06`](https://github.com/SAP/ui5-project/commit/9800c06004e44a4af8b86492b0f15cab465be0c0) + + + +## [v3.1.0] - 2023-03-31 +### Bug Fixes +- **Taskrunner:** pass new taskutil options to determineRequiredDependencies hook [`94bcd99`](https://github.com/SAP/ui5-project/commit/94bcd9931d6709170b78a92e7372bbd0de44ae03) +- **ui5Framework:** Prevent install of libraries within workspace ([#589](https://github.com/SAP/ui5-project/issues/589)) [`8ffc676`](https://github.com/SAP/ui5-project/commit/8ffc676434defd320c70b615960efc9182a29de9) + +### Features +- **Specification:** Add getId method [`7bdb47a`](https://github.com/SAP/ui5-project/commit/7bdb47a2925c0936ee33faf23f51f6c6ab396369) +- **Workspace:** Add getModules method [`1e2aa0e`](https://github.com/SAP/ui5-project/commit/1e2aa0e48bb2d895728f3d5f4cb74d55fbc8ec34) + + + +## [v3.0.4] - 2023-03-10 +### Bug Fixes +- Resolve properly absolute path for ui5HomeDir ([#588](https://github.com/SAP/ui5-project/issues/588)) [`9b414a7`](https://github.com/SAP/ui5-project/commit/9b414a77a1d86f6a3560231ae04db407e2f022c5) + + + +## [v3.0.3] - 2023-03-01 +### Bug Fixes +- **jsdoc:** enable generateVersionInfo task [`a58e5eb`](https://github.com/SAP/ui5-project/commit/a58e5eb0769a9ba63a0b0aa267675ef2f9c08769) + + + +## [v3.0.2] - 2023-02-17 +### Bug Fixes +- **ComponentProject#getWorkspace:** Apply builder resource excludes [`5257e59`](https://github.com/SAP/ui5-project/commit/5257e5977c4e92e2aca5b0ce4b2ed55688a66646) + + + +## [v3.0.1] - 2023-02-16 +### Bug Fixes +- Prevent socket timeouts when installing framework libraries [`a198356`](https://github.com/SAP/ui5-project/commit/a198356c9c5f39dd94fb8cf7542d9059ee628f3b) +- **Library:** Do not throw for missing .library file [`1163821`](https://github.com/SAP/ui5-project/commit/11638210994fd9511b2ab5ee3da40e3ccf294e58) +- **Project#getReader:** Do not apply builder resource excludes for style 'runtime' [`1cd94f7`](https://github.com/SAP/ui5-project/commit/1cd94f7f15ed07283e198238edb546517ee25691) +- **TaskUtil:** Provide framework configuration getters to custom tasks ([#580](https://github.com/SAP/ui5-project/issues/580)) [`6a40927`](https://github.com/SAP/ui5-project/commit/6a409278285252da59ea4d42fcf154814518661d) +- **graph:** Always resolve rootConfigPath to CWD [`ef3e569`](https://github.com/SAP/ui5-project/commit/ef3e56996111233aaa04410c95f11b1c3495a9b2) +- **projectGraphBuilder:** Apply extensions of the same module only once [`6d753a8`](https://github.com/SAP/ui5-project/commit/6d753a850f2a4ca34a50f64a404472bf0081054e) +- **ui5Framework:** Improve error handling for duplicate lib declaration [`fb1db6d`](https://github.com/SAP/ui5-project/commit/fb1db6d7cb74dee9c4754ffb62a2a970cb0e2fbe) + + + +## [v3.0.0] - 2023-02-09 +### Breaking Changes +- Implement Project Graph, build execution [`161f462`](https://github.com/SAP/ui5-project/commit/161f462cf6a9955337fff512007125128c6c39dd) +- Run 'generateThemeDesignerResources' only on framework libs [`e4bb108`](https://github.com/SAP/ui5-project/commit/e4bb1084df3e0ae906df27aba4a674d187ff8069) + +### BREAKING CHANGE +Support for older Node.js and npm releases has been dropped for all UI5 Tooling modules. +Only Node.js versions v16.18.0, v18.12.0 or higher as well as npm v8 or higher are supported. + +All packages have been transformed to ES Modules. Therefore modules are no longer provides a CommonJS exports. +If your project uses CommonJS, it needs to be converted to ESM or use a dynamic import for consuming UI5 Tooling modules. + +For more information see also: +- https://sap.github.io/ui5-tooling/updates/migrate-v3/ +- https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +- normalizer and projectTree APIs have been removed. Use generateProjectGraph instead +- Going forward only specification versions 2.0 and higher are supported + - In case a legacy specification version is detected, an automatic, transparent migration is attempted. +- Build: + - The "dev" build mode has been removed + - The task "generateVersionInfo" is no longer executed for application projects by default. You may enable it again using the includedTasks parameter + +### Features +- specVersion 3.0 ([#522](https://github.com/SAP/ui5-project/issues/522)) [`c5070e5`](https://github.com/SAP/ui5-project/commit/c5070e55d92ced4326cd7611caf3ec9a3da9e7ed) +- Introduce SpecificationVersion class ([#431](https://github.com/SAP/ui5-project/issues/431)) [`e57842b`](https://github.com/SAP/ui5-project/commit/e57842b06397a5b36e6373df97f7b7bb91f09741) +- **TaskRunner:** Provide taskName and logger instance to custom tasks [`36cd2d8`](https://github.com/SAP/ui5-project/commit/36cd2d83f9a6a92cbd28619d8a25c0ba3f732117) +- **TaskUtil:** Add resourceFactory API to v3 interface [`2e863cf`](https://github.com/SAP/ui5-project/commit/2e863cfaf9f8924d0c87fe9dfe01568c1fd979c8) +- **TaskUtil:** Add getProject/getDependencies API to interface [`51f2949`](https://github.com/SAP/ui5-project/commit/51f29493f57f094396776bb2686c8a74e8901a7f) + +### Bug Fixes +- **npm/Installer:** Do not wrap promise provided by rimraf v4 [`2d1ccda`](https://github.com/SAP/ui5-project/commit/2d1ccda54edd29dabadcb7bad9136bff09da8eac) +- **ProjectBuilder:** Fix verbose logging for already built projects [`f04ffd2`](https://github.com/SAP/ui5-project/commit/f04ffd2c0ab0270df697c20258474ff536811476) +- **ProjectBuilder:** Skip build for projects that do not require to be built [`ac5f1f8`](https://github.com/SAP/ui5-project/commit/ac5f1f891255b56597e51d121329f03786338d4a) +- **Specification:** Fix migration for legacy projects that are not applications or libraries [`d89d804`](https://github.com/SAP/ui5-project/commit/d89d8047519ca8f162dc7a225f138ae304871ecb) +- Fix build manifest creation [`b1459eb`](https://github.com/SAP/ui5-project/commit/b1459eb26aa8a4b18ad84a369c122c114d64b64b) + +### Dependency Updates +- Bump rimraf from 3.0.2 to 4.1.1 ([#550](https://github.com/SAP/ui5-project/issues/550)) [`99876ae`](https://github.com/SAP/ui5-project/commit/99876ae35e9d8f5c725e2e87bd3be37d7ed4363c) + + + +## [v2.6.0] - 2021-10-19 +### Bug Fixes +- **ui5Framework:** Skip processing of framework libs ([#424](https://github.com/SAP/ui5-project/issues/424)) [`539d953`](https://github.com/SAP/ui5-project/commit/539d9539a5d2aaa6d01c4f539e3c86d8269788f2) + +### Features +- specVersion 2.6 [`9bd921a`](https://github.com/SAP/ui5-project/commit/9bd921a05bd5c0d8b6c6d94a864e60e4e181ad63) + + + +## [v2.5.0] - 2021-07-23 +### Features +- specVersion 2.5 [`3008dac`](https://github.com/SAP/ui5-project/commit/3008dace09109ba0fac49f0ddfc79255038f192c) + + + +## [v2.4.0] - 2021-06-01 +### Features +- specVersion 2.4 [`69ffc6c`](https://github.com/SAP/ui5-project/commit/69ffc6c34e387bcaaaf7b703559181b78fd33d54) + + + +## [v2.3.1] - 2021-03-04 +### Bug Fixes +- **ui5Framework:** Don't access metadata of deduped projects [`0255f8f`](https://github.com/SAP/ui5-project/commit/0255f8f628281ecb3cbbdb50192d2d4721bccea2) + +### Dependency Updates +- Bump js-yaml from 3.14.1 to 4.0.0 ([#380](https://github.com/SAP/ui5-project/issues/380)) [`a862186`](https://github.com/SAP/ui5-project/commit/a86218657703a5b607ebd09f8f71dd7ea810c6be) + + + +## [v2.3.0] - 2021-02-09 +### Features +- specVersion 2.3 ([#388](https://github.com/SAP/ui5-project/issues/388)) [`3e28026`](https://github.com/SAP/ui5-project/commit/3e280267b60a9a72183d5ab0905d838b6fcfaf33) + + + +## [v2.2.6] - 2021-01-28 +### Bug Fixes +- **ui5Framework.Installer:** Ensure target directory does not exist before rename ([#390](https://github.com/SAP/ui5-project/issues/390)) [`f107cdf`](https://github.com/SAP/ui5-project/commit/f107cdf2b1703791c153009150a5e1713e123b73) + + + +## [v2.2.5] - 2021-01-26 +### Bug Fixes +- **ui5Framework.Installer:** Ensure atomic install process [`72568a9`](https://github.com/SAP/ui5-project/commit/72568a990620cee69ffaf2470c684a7ba02c200c) + + + +## [v2.2.4] - 2020-11-06 +### Performance Improvements +- Reduce install size by removing 'string.prototype.matchall' dependency [`b69d75e`](https://github.com/SAP/ui5-project/commit/b69d75e740bfc594668ea73273bb03fdd40a4ce2) +- **validator:** Lazy load dependencies [`609346b`](https://github.com/SAP/ui5-project/commit/609346b2b1bb0417fde36a35ec43e9970c68504f) + + + +## [v2.2.3] - 2020-10-22 +### Bug Fixes +- **Schema:** Add missing bundle section "name" [`ba2d601`](https://github.com/SAP/ui5-project/commit/ba2d6015b6a04af92edb8f1b779a229fe73b705a) + + + +## [v2.2.2] - 2020-09-15 +### Bug Fixes +- **ui5Framework.mergeTrees:** Do not abort merge if a project has already been processed [`264c353`](https://github.com/SAP/ui5-project/commit/264c353b6973bade57164aded4f10a668986482d) + + + +## [v2.2.1] - 2020-09-02 + + +## [v2.2.0] - 2020-08-11 +### Features +- specVersion 2.2 ([#341](https://github.com/SAP/ui5-project/issues/341)) [`f44d14e`](https://github.com/SAP/ui5-project/commit/f44d14e136a4163d59dd8fd8c0be0ea2b59930be) + + + +## [v2.1.5] - 2020-07-14 +### Bug Fixes +- **Node.js API:** TypeScript type definition support ([#335](https://github.com/SAP/ui5-project/issues/335)) [`c610305`](https://github.com/SAP/ui5-project/commit/c610305e8fb869461a8dd5ba876270c7f7b71a22) + + + +## [v2.1.4] - 2020-05-29 +### Bug Fixes +- **ui5Framework:** Allow providing exact prerelease versions ([#326](https://github.com/SAP/ui5-project/issues/326)) [`6ce985c`](https://github.com/SAP/ui5-project/commit/6ce985c8feab26e6a97ca4570b3931f507773666) + + + +## [v2.1.3] - 2020-05-14 + + +## [v2.1.2] - 2020-05-11 +### Bug Fixes +- **framework t8r:** Allow use of specVersion 2.1 [`961847d`](https://github.com/SAP/ui5-project/commit/961847d113e6f594526201ab9ecccb898d2497e2) + + + +## [v2.1.1] - 2020-05-11 +### Bug Fixes +- Allow the use of specVersion 2.1 for projects [`a42172f`](https://github.com/SAP/ui5-project/commit/a42172fc341666b8d9a9b6049c365b28c55c76f0) + + + +## [v2.1.0] - 2020-05-05 +### Features +- **specVersion 2.1:** Add support for "customConfiguration" ([#308](https://github.com/SAP/ui5-project/issues/308)) [`201aaab`](https://github.com/SAP/ui5-project/commit/201aaab6beb8ad86fefdf371ae20c971970f6547) + + + +## [v2.0.4] - 2020-04-30 +### Bug Fixes +- Workaround missing dependency info for OpenUI5 packages in version 1.77.x [`3dfb812`](https://github.com/SAP/ui5-project/commit/3dfb8126e347fd1e7f6cc87e20318298e19eaf70) +- Namespaces in API Reference (JSDoc) [`3174d9f`](https://github.com/SAP/ui5-project/commit/3174d9f21f471252d2a39b8cb085eeeb5debe0a6) + + + +## [v2.0.3] - 2020-04-02 +### Bug Fixes +- **Schema:** Add missing metadata properties [`16894e1`](https://github.com/SAP/ui5-project/commit/16894e11c5c21a77a405431dfaf5d8642accfc1d) +- **package.json:** Downgrade pacote from 11.1.4 to 9.5.12 [`c76fb49`](https://github.com/SAP/ui5-project/commit/c76fb49e64b5905a3cd592d94fc0076cecc909b5) + + + +## [v2.0.2] - 2020-04-01 +### Bug Fixes +- **ui5Framework t8r:** Resolve versionOverride string [`4fffabe`](https://github.com/SAP/ui5-project/commit/4fffabe2a417b1ea46a47546c6269ac0ffbc3931) + + + +## [v2.0.1] - 2020-04-01 +### Bug Fixes +- **ui5Framework.mergeTrees:** Do not process the same project multiple times [`1377ec2`](https://github.com/SAP/ui5-project/commit/1377ec2ecea71a2470a9ea9b1e0698e466154838) + + + +## [v2.0.0] - 2020-03-31 +### Breaking Changes +- Require Node.js >= 10 [`f21e704`](https://github.com/SAP/ui5-project/commit/f21e704f85297e3fa774c59bf5d4e8282b947b41) + +### Features +- Add Configuration Schema ([#274](https://github.com/SAP/ui5-project/issues/274)) [`eb961c3`](https://github.com/SAP/ui5-project/commit/eb961c3377d42d3c93f7b7db5033f4e6716ddc71) +- Support for spec version 2.0 ([#277](https://github.com/SAP/ui5-project/issues/277)) [`770a56f`](https://github.com/SAP/ui5-project/commit/770a56feed331a3157c9f9fad486a4674dc12c87) +- Add ui5Framework translator and resolvers ([#265](https://github.com/SAP/ui5-project/issues/265)) [`5183e5c`](https://github.com/SAP/ui5-project/commit/5183e5cf99ac8cae6e4ccc8030d94214bce0563c) +- **projectPreprocessor:** Log warning when using a deprecated or restricted dependency ([#268](https://github.com/SAP/ui5-project/issues/268)) [`b776a4f`](https://github.com/SAP/ui5-project/commit/b776a4fcc4604f3ecb0d3fc1e6418ed190c11756) + +### BREAKING CHANGE + +Support for older Node.js releases has been dropped. +Only Node.js v10 or higher is supported. + + + +## [v1.2.0] - 2020-01-13 +### Features +- Add specification version 1.1 ([#252](https://github.com/SAP/ui5-project/issues/252)) [`5a83308`](https://github.com/SAP/ui5-project/commit/5a833086ccd415c5557c2bc3bbb705c18ac54314) + + + +## [v1.1.1] - 2019-11-07 + + +## [v1.1.0] - 2019-07-11 +### Features +- **projectPreprocessor:** Add handling for server-middleware extensions [`2ce964c`](https://github.com/SAP/ui5-project/commit/2ce964cd9feb6c1da39cd783ad45e0030c46b81a) + + + +## [v1.0.3] - 2019-06-25 +### Bug Fixes +- **projectPreprocessor:** Do not remove already removed dependencies ([#189](https://github.com/SAP/ui5-project/issues/189)) [`4600d63`](https://github.com/SAP/ui5-project/commit/4600d63cf323d3e143072c6c3416b5a48e90bb71) + + + +## [v1.0.2] - 2019-04-12 +### Bug Fixes +- **ProjectPreprocessor:** Fix dependency resolution [`0671a8b`](https://github.com/SAP/ui5-project/commit/0671a8bf2de9ca24823df6f041a77e7c8e46f6f0) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 1.0.2 to 1.0.3 ([#154](https://github.com/SAP/ui5-project/issues/154)) [`cf86764`](https://github.com/SAP/ui5-project/commit/cf867643b8b621019a5d5b0f5d3117ebcdd1cd44) + + + +## [v1.0.1] - 2019-02-14 +### Bug Fixes +- **npm translator:** Remove deduped optional dependencies from tree [`3481154`](https://github.com/SAP/ui5-project/commit/348115426f03bd3a5bb823ac54a6b15475a84657) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 1.0.0 to 1.0.1 ([#113](https://github.com/SAP/ui5-project/issues/113)) [`96a3d6a`](https://github.com/SAP/ui5-project/commit/96a3d6a2a54cb1eab190ba89f9da686e8aae2d84) + + + +## [v1.0.0] - 2019-01-10 +### Breaking Changes +- **normalizer:** Rename optional parameter "translator" [`92321e0`](https://github.com/SAP/ui5-project/commit/92321e08e43175611b8417047fc957792d539b10) + +### Dependency Updates +- Bump [@ui5](https://github.com/ui5)/builder from 0.2.9 to 1.0.0 ([#99](https://github.com/SAP/ui5-project/issues/99)) [`7dd5d5c`](https://github.com/SAP/ui5-project/commit/7dd5d5cda909e3a109821315cc5a5a80f05cd5d3) +- Bump [@ui5](https://github.com/ui5)/logger from 0.2.2 to 1.0.0 ([#98](https://github.com/SAP/ui5-project/issues/98)) [`8068a76`](https://github.com/SAP/ui5-project/commit/8068a76dc43701f5c8b0467933a83d777ccdee01) + +### Features +- Add specification version 1.0 [`b0c02f6`](https://github.com/SAP/ui5-project/commit/b0c02f67296f6251a7ef4fe5c61146bb169a6705) + +### BREAKING CHANGE + +Renamed parameter "translator" of functions generateDependencyTree and generateProjectTree to "translatorName" + + + +## [v0.2.5] - 2018-12-19 +### Bug Fixes +- **npm translator:** Deduplicate subtrees of pending dependencies [`7e55ae3`](https://github.com/SAP/ui5-project/commit/7e55ae3d88280746f5800bffc7bbd13e1495ba07) +- **npm translator:** Fix handling of indirect dependency cycles [`c99d6d3`](https://github.com/SAP/ui5-project/commit/c99d6d3a19fbb6c197b449dfd6cb8acc48837dba) + + + +## [v0.2.4] - 2018-12-17 +### Bug Fixes +- **npm t8r:** Add deduplication of npm dependencies [`2717088`](https://github.com/SAP/ui5-project/commit/2717088532d415b6922f290b58d9227b946a965f) +- **projectPreprocessor:** Ignore deduped modules [`84f7b25`](https://github.com/SAP/ui5-project/commit/84f7b25a9e45df3bc55a7957e4f61db580e68509) + + + +## [v0.2.3] - 2018-11-20 +### Bug Fixes +- **npm t8r:** Again, handle npm optionalDependencies correctly [`9fd78dc`](https://github.com/SAP/ui5-project/commit/9fd78dca4d836f9a37036fd151a78e9295b28aa1) + + + +## [v0.2.2] - 2018-11-17 +### Bug Fixes +- **npm t8r:** Handle npm optionalDependencies correctly [`da707d7`](https://github.com/SAP/ui5-project/commit/da707d73b5c75b489e2e499de2b4f54924018844) + +### Features +- **projectPreprocessor:** Add handling for task extensions [`0722865`](https://github.com/SAP/ui5-project/commit/072286591ae3b20cca8e418030c3f2bc048352c5) +- **projectPreprocessor:** Allow application project dependency on non-root level [`b8a59d5`](https://github.com/SAP/ui5-project/commit/b8a59d56c8b5cf4c330fe99cb2162c1701aa51ca) + + + +## [v0.2.1] - 2018-10-29 +### Features +- Add shim extension [`93c9b39`](https://github.com/SAP/ui5-project/commit/93c9b3960ca36f240c5f8453a89f72792a01fe92) +- Add "extension" projects [`476b785`](https://github.com/SAP/ui5-project/commit/476b785810d6993d2a3e21707ffa67e568e67eac) + + + +## [v0.2.0] - 2018-07-11 + + +## [v0.1.0] - 2018-06-26 +### Bug Fixes +- Fix some typos in log messages ([#17](https://github.com/SAP/ui5-project/issues/17)) [`1f2f2fd`](https://github.com/SAP/ui5-project/commit/1f2f2fd164abaf449cc5e7d94ec792f469710207) +- **npm translator:** Fix endless loop in case of dependency cycles ([#15](https://github.com/SAP/ui5-project/issues/15)) [`cf31112`](https://github.com/SAP/ui5-project/commit/cf3111288278e8dd36a09b549bd2b254e86af041) + + + +## v0.0.1 - 2018-06-06 +### Bug Fixes +- **npm t8r:** Fix collection fallback with missing package.json [`578466f`](https://github.com/SAP/ui5-project/commit/578466fdedf871091874c93d1a9305859e34e3ed) +[v4.0.6]: https://github.com/SAP/ui5-project/compare/v4.0.5...v4.0.6 +[v4.0.5]: https://github.com/SAP/ui5-project/compare/v4.0.4...v4.0.5 +[v4.0.4]: https://github.com/SAP/ui5-project/compare/v4.0.3...v4.0.4 +[v4.0.3]: https://github.com/SAP/ui5-project/compare/v4.0.2...v4.0.3 +[v4.0.2]: https://github.com/SAP/ui5-project/compare/v4.0.1...v4.0.2 +[v4.0.1]: https://github.com/SAP/ui5-project/compare/v4.0.0...v4.0.1 +[v4.0.0]: https://github.com/SAP/ui5-project/compare/v3.9.0...v4.0.0 +[v3.9.2]: https://github.com/SAP/ui5-project/compare/v3.9.1...v3.9.2 +[v3.9.1]: https://github.com/SAP/ui5-project/compare/v3.9.0...v3.9.1 +[v3.9.0]: https://github.com/SAP/ui5-project/compare/v3.8.0...v3.9.0 +[v3.8.0]: https://github.com/SAP/ui5-project/compare/v3.7.3...v3.8.0 +[v3.7.3]: https://github.com/SAP/ui5-project/compare/v3.7.2...v3.7.3 +[v3.7.2]: https://github.com/SAP/ui5-project/compare/v3.7.1...v3.7.2 +[v3.7.1]: https://github.com/SAP/ui5-project/compare/v3.7.0...v3.7.1 +[v3.7.0]: https://github.com/SAP/ui5-project/compare/v3.6.0...v3.7.0 +[v3.6.0]: https://github.com/SAP/ui5-project/compare/v3.5.1...v3.6.0 +[v3.5.1]: https://github.com/SAP/ui5-project/compare/v3.5.0...v3.5.1 +[v3.5.0]: https://github.com/SAP/ui5-project/compare/v3.4.2...v3.5.0 +[v3.4.2]: https://github.com/SAP/ui5-project/compare/v3.4.1...v3.4.2 +[v3.4.1]: https://github.com/SAP/ui5-project/compare/v3.4.0...v3.4.1 +[v3.4.0]: https://github.com/SAP/ui5-project/compare/v3.3.2...v3.4.0 +[v3.3.2]: https://github.com/SAP/ui5-project/compare/v3.3.1...v3.3.2 +[v3.3.1]: https://github.com/SAP/ui5-project/compare/v3.3.0...v3.3.1 +[v3.3.0]: https://github.com/SAP/ui5-project/compare/v3.2.2...v3.3.0 +[v3.2.2]: https://github.com/SAP/ui5-project/compare/v3.2.1...v3.2.2 +[v3.2.1]: https://github.com/SAP/ui5-project/compare/v3.2.0...v3.2.1 +[v3.2.0]: https://github.com/SAP/ui5-project/compare/v3.1.1...v3.2.0 +[v3.1.1]: https://github.com/SAP/ui5-project/compare/v3.1.0...v3.1.1 +[v3.1.0]: https://github.com/SAP/ui5-project/compare/v3.0.4...v3.1.0 +[v3.0.4]: https://github.com/SAP/ui5-project/compare/v3.0.3...v3.0.4 +[v3.0.3]: https://github.com/SAP/ui5-project/compare/v3.0.2...v3.0.3 +[v3.0.2]: https://github.com/SAP/ui5-project/compare/v3.0.1...v3.0.2 +[v3.0.1]: https://github.com/SAP/ui5-project/compare/v3.0.0...v3.0.1 +[v3.0.0]: https://github.com/SAP/ui5-project/compare/v2.6.0...v3.0.0 +[v2.6.0]: https://github.com/SAP/ui5-project/compare/v2.5.0...v2.6.0 +[v2.5.0]: https://github.com/SAP/ui5-project/compare/v2.4.0...v2.5.0 +[v2.4.0]: https://github.com/SAP/ui5-project/compare/v2.3.1...v2.4.0 +[v2.3.1]: https://github.com/SAP/ui5-project/compare/v2.3.0...v2.3.1 +[v2.3.0]: https://github.com/SAP/ui5-project/compare/v2.2.6...v2.3.0 +[v2.2.6]: https://github.com/SAP/ui5-project/compare/v2.2.5...v2.2.6 +[v2.2.5]: https://github.com/SAP/ui5-project/compare/v2.2.4...v2.2.5 +[v2.2.4]: https://github.com/SAP/ui5-project/compare/v2.2.3...v2.2.4 +[v2.2.3]: https://github.com/SAP/ui5-project/compare/v2.2.2...v2.2.3 +[v2.2.2]: https://github.com/SAP/ui5-project/compare/v2.2.1...v2.2.2 +[v2.2.1]: https://github.com/SAP/ui5-project/compare/v2.2.0...v2.2.1 +[v2.2.0]: https://github.com/SAP/ui5-project/compare/v2.1.5...v2.2.0 +[v2.1.5]: https://github.com/SAP/ui5-project/compare/v2.1.4...v2.1.5 +[v2.1.4]: https://github.com/SAP/ui5-project/compare/v2.1.3...v2.1.4 +[v2.1.3]: https://github.com/SAP/ui5-project/compare/v2.1.2...v2.1.3 +[v2.1.2]: https://github.com/SAP/ui5-project/compare/v2.1.1...v2.1.2 +[v2.1.1]: https://github.com/SAP/ui5-project/compare/v2.1.0...v2.1.1 +[v2.1.0]: https://github.com/SAP/ui5-project/compare/v2.0.4...v2.1.0 +[v2.0.4]: https://github.com/SAP/ui5-project/compare/v2.0.3...v2.0.4 +[v2.0.3]: https://github.com/SAP/ui5-project/compare/v2.0.2...v2.0.3 +[v2.0.2]: https://github.com/SAP/ui5-project/compare/v2.0.1...v2.0.2 +[v2.0.1]: https://github.com/SAP/ui5-project/compare/v2.0.0...v2.0.1 +[v2.0.0]: https://github.com/SAP/ui5-project/compare/v1.2.0...v2.0.0 +[v1.2.0]: https://github.com/SAP/ui5-project/compare/v1.1.1...v1.2.0 +[v1.1.1]: https://github.com/SAP/ui5-project/compare/v1.1.0...v1.1.1 +[v1.1.0]: https://github.com/SAP/ui5-project/compare/v1.0.3...v1.1.0 +[v1.0.3]: https://github.com/SAP/ui5-project/compare/v1.0.2...v1.0.3 +[v1.0.2]: https://github.com/SAP/ui5-project/compare/v1.0.1...v1.0.2 +[v1.0.1]: https://github.com/SAP/ui5-project/compare/v1.0.0...v1.0.1 +[v1.0.0]: https://github.com/SAP/ui5-project/compare/v0.2.5...v1.0.0 +[v0.2.5]: https://github.com/SAP/ui5-project/compare/v0.2.4...v0.2.5 +[v0.2.4]: https://github.com/SAP/ui5-project/compare/v0.2.3...v0.2.4 +[v0.2.3]: https://github.com/SAP/ui5-project/compare/v0.2.2...v0.2.3 +[v0.2.2]: https://github.com/SAP/ui5-project/compare/v0.2.1...v0.2.2 +[v0.2.1]: https://github.com/SAP/ui5-project/compare/v0.2.0...v0.2.1 +[v0.2.0]: https://github.com/SAP/ui5-project/compare/v0.1.0...v0.2.0 +[v0.1.0]: https://github.com/SAP/ui5-project/compare/v0.0.1...v0.1.0 diff --git a/packages/project/CONTRIBUTING.md b/packages/project/CONTRIBUTING.md new file mode 100644 index 00000000000..2ddb6276ed5 --- /dev/null +++ b/packages/project/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to the UI5 CLI + +See CONTRIBUTING.md in the [UI5/cli](https://github.com/UI5/cli/blob/main/CONTRIBUTING.md) repository. diff --git a/packages/project/LICENSE.txt b/packages/project/LICENSE.txt new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/project/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/project/LICENSES/Apache-2.0.txt b/packages/project/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000000..4ed90b95224 --- /dev/null +++ b/packages/project/LICENSES/Apache-2.0.txt @@ -0,0 +1,208 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, +AND DISTRIBUTION + + 1. Definitions. + + + +"License" shall mean the terms and conditions for use, reproduction, and distribution +as defined by Sections 1 through 9 of this document. + + + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + + + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct +or indirect, to cause the direction or management of such entity, whether +by contract or otherwise, or (ii) ownership of fifty percent (50%) or more +of the outstanding shares, or (iii) beneficial ownership of such entity. + + + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions +granted by this License. + + + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + + + +"Object" form shall mean any form resulting from mechanical transformation +or translation of a Source form, including but not limited to compiled object +code, generated documentation, and conversions to other media types. + + + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that +is included in or attached to the work (an example is provided in the Appendix +below). + + + +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative +Works shall not include works that remain separable from, or merely link (or +bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative +Works thereof, that is intentionally submitted to Licensor for inclusion in +the Work by the copyright owner or by an individual or Legal Entity authorized +to submit on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication +sent to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor +for the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + + + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute +the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their Contribution(s) +alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against +any entity (including a cross-claim or counterclaim in a lawsuit) alleging +that the Work or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses granted to You +under this License for that Work shall terminate as of the date such litigation +is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and +in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy +of this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source +form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, +then any Derivative Works that You distribute must include a readable copy +of the attribution notices contained within such NOTICE file, excluding those +notices that do not pertain to any part of the Derivative Works, in at least +one of the following places: within a NOTICE text file distributed as part +of the Derivative Works; within the Source form or documentation, if provided +along with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works +that You distribute, alongside or as an addendum to the NOTICE text from the +Work, provided that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, +or distribution of Your modifications, or for any such Derivative Works as +a whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without +any additional terms or conditions. Notwithstanding the above, nothing herein +shall supersede or modify the terms of any separate license agreement you +may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to +in writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR +A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness +of using or redistributing the Work and assume any risks associated with Your +exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether +in tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to +in writing, shall any Contributor be liable to You for damages, including +any direct, indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or inability +to use the Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all other commercial +damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work +or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, not on +behalf of any other Contributor, and only if You agree to indemnify, defend, +and hold each Contributor harmless for any liability incurred by, or claims +asserted against, such Contributor by reason of your accepting any such warranty +or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own identifying +information. (Don't include the brackets!) The text should be enclosed in +the appropriate comment syntax for the file format. We also recommend that +a file or class name and description of purpose be included on the same "printed +page" as the copyright notice for easier identification within third-party +archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. diff --git a/packages/project/README.md b/packages/project/README.md new file mode 100644 index 00000000000..6f37c30b9b0 --- /dev/null +++ b/packages/project/README.md @@ -0,0 +1,26 @@ +![UI5 icon](https://raw.githubusercontent.com/UI5/cli/main/docs/images/UI5_logo_wide.png) + +# ui5-project +> Modules for building a projects dependency tree, including UI5 specific configuration. +> Part of the [UI5 CLI](https://github.com/UI5/cli) + +[![REUSE status](https://api.reuse.software/badge/github.com/SAP/ui5-project)](https://api.reuse.software/info/github.com/SAP/ui5-project) +[![Build Status](https://dev.azure.com/sap/opensource/_apis/build/status/SAP.ui5-project?branchName=v4)](https://dev.azure.com/sap/opensource/_build/latest?definitionId=35&branchName=v4) +[![npm Package Version](https://badge.fury.io/js/%40ui5%2Fproject.svg)](https://www.npmjs.com/package/@ui5/project) +[![Coverage Status](https://coveralls.io/repos/github/SAP/ui5-project/badge.svg)](https://coveralls.io/github/SAP/ui5-project) + +## Documentation +UI5 Project documentation can be found here: [ui5.github.io/cli](https://ui5.github.io/cli/v4/pages/Project/) + +The UI5 Project API Reference can be found here: [`@ui5/project`](https://ui5.github.io/cli/v4/api/) + +## Contributing +Please check our [Contribution Guidelines](https://github.com/UI5/cli/blob/main/CONTRIBUTING.md). + +## Support +Please follow our [Contribution Guidelines](https://github.com/UI5/cli/blob/main/CONTRIBUTING.md#report-an-issue) on how to report an issue. + +Please report issues in the main [UI5 CLI](https://github.com/UI5/cli) repository. + +## Release History +See [CHANGELOG.md](CHANGELOG.md). diff --git a/packages/project/REUSE.toml b/packages/project/REUSE.toml new file mode 100644 index 00000000000..16764782f8b --- /dev/null +++ b/packages/project/REUSE.toml @@ -0,0 +1,11 @@ +version = 1 +SPDX-PackageName = "ui5-project" +SPDX-PackageSupplier = "SAP OpenUI5 " +SPDX-PackageDownloadLocation = "https://github.com/SAP/ui5-project" +SPDX-PackageComment = "The code in this project may include calls to APIs (“API Calls”) of\n SAP or third-party products or services developed outside of this project\n (“External Products”).\n “APIs” means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project’s code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls." + +[[annotations]] +path = "**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 SAP SE or an SAP affiliate company and UI5 CLI contributors" +SPDX-License-Identifier = "Apache-2.0" diff --git a/packages/project/azure-pipelines.yml b/packages/project/azure-pipelines.yml new file mode 100644 index 00000000000..acbe7250177 --- /dev/null +++ b/packages/project/azure-pipelines.yml @@ -0,0 +1,72 @@ +# Node.js +# Build a general Node.js project with npm. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript + +trigger: +- v4 + +variables: + CI: true + +strategy: + matrix: + linux_node_lts_20_min_version: + imageName: 'ubuntu-24.04' + node_version: 20.11.0 + linux_node_22_min_version: + imageName: 'ubuntu-24.04' + node_version: 22.1.0 + linux_node_lts_20: + imageName: 'ubuntu-24.04' + node_version: 20.x + mac_node_lts_20: + imageName: 'macos-13' + node_version: 20.x + windows_node_lts_20: + imageName: 'windows-2022' + node_version: 20.x + linux_node_22: + imageName: 'ubuntu-24.04' + node_version: 22.x + mac_node_22: + imageName: 'macos-13' + node_version: 22.x + windows_node_22: + imageName: 'windows-2022' + node_version: 22.x + +pool: + vmImage: $(imageName) + +steps: +- task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: Install Node.js + +- script: npm ci + displayName: Install Dependencies + +- script: npm ls --prod + displayName: Check for missing / extraneous Dependencies + +- script: npm run test-azure + displayName: Run Tests + +- task: PublishTestResults@2 + displayName: Publish Test Results + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(System.DefaultWorkingDirectory)/test-results.xml' + +- task: PublishCodeCoverageResults@2 + displayName: Publish Test Coverage Results + condition: succeededOrFailed() + inputs: + summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml' + +- script: npm run coverage + displayName: Run Test Natively in Case of Failures + condition: failed() diff --git a/packages/project/docs/BuildExtensibility.md b/packages/project/docs/BuildExtensibility.md new file mode 100644 index 00000000000..eaff71e7cf6 --- /dev/null +++ b/packages/project/docs/BuildExtensibility.md @@ -0,0 +1,2 @@ +# UI5 Build Extensibility +This documentation moved to a new place: [ui5.github.io/cli/pages/extensibility/CustomTasks/](https://ui5.github.io/cli/pages/extensibility/CustomTasks/) diff --git a/packages/project/docs/Configuration.md b/packages/project/docs/Configuration.md new file mode 100644 index 00000000000..bfc79050496 --- /dev/null +++ b/packages/project/docs/Configuration.md @@ -0,0 +1,4 @@ +# Configuration +This documentation moved to a new place: [ui5.github.io/cli/pages/Configuration/](https://ui5.github.io/cli/pages/Configuration/). + +The **Specification Version Documentation** can be found here: [ui5.github.io/cli/pages/Configuration/#specification-versions](https://ui5.github.io/cli/pages/Configuration/#specification-versions) diff --git a/packages/project/eslint.common.config.js b/packages/project/eslint.common.config.js new file mode 100644 index 00000000000..07876d526f0 --- /dev/null +++ b/packages/project/eslint.common.config.js @@ -0,0 +1,99 @@ +import jsdoc from "eslint-plugin-jsdoc"; +import ava from "eslint-plugin-ava"; +import globals from "globals"; +import js from "@eslint/js"; +import google from "eslint-config-google"; + +export default [{ + ignores: [ // Common ignore patterns across all tooling repos + "**/coverage/", + "test/tmp/", + "test/expected/", + "test/fixtures/", + "**/docs/", + "**/jsdocs/", + ], +}, js.configs.recommended, google, ava.configs["flat/recommended"], { + name: "Common ESLint config used for all tooling repos", + + plugins: { + jsdoc, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2023, + sourceType: "module", + }, + + settings: { + jsdoc: { + mode: "jsdoc", + + tagNamePreference: { + return: "returns", + augments: "extends", + }, + }, + }, + + rules: { + "indent": ["error", "tab"], + "linebreak-style": ["error", "unix"], + + "quotes": ["error", "double", { + allowTemplateLiterals: true, + }], + + "semi": ["error", "always"], + "no-negated-condition": "off", + "require-jsdoc": "off", + "no-mixed-requires": "off", + + "max-len": ["error", { + code: 120, + ignoreUrls: true, + ignoreRegExpLiterals: true, + }], + + "no-implicit-coercion": [2, { + allow: ["!!"], + }], + + "comma-dangle": "off", + "no-tabs": "off", + "no-console": 2, // Disallow console.log() + "no-eval": 2, + // The following rule must be disabled as of ESLint 9. + // It's removed and causes issues when present + // https://eslint.org/docs/latest/rules/valid-jsdoc + "valid-jsdoc": 0, + "jsdoc/check-examples": 0, + "jsdoc/check-param-names": 2, + "jsdoc/check-tag-names": 2, + "jsdoc/check-types": 2, + "jsdoc/no-undefined-types": 0, + "jsdoc/require-description": 0, + "jsdoc/require-description-complete-sentence": 0, + "jsdoc/require-example": 0, + "jsdoc/require-hyphen-before-param-description": 0, + "jsdoc/require-param": 2, + "jsdoc/require-param-description": 0, + "jsdoc/require-param-name": 2, + "jsdoc/require-param-type": 2, + "jsdoc/require-returns": 0, + "jsdoc/require-returns-description": 0, + "jsdoc/require-returns-type": 2, + + "jsdoc/tag-lines": [2, "any", { + startLines: 1, + }], + + "jsdoc/valid-types": 0, + "ava/assertion-arguments": 0, + }, +} +]; diff --git a/packages/project/eslint.config.js b/packages/project/eslint.config.js new file mode 100644 index 00000000000..74539640b1d --- /dev/null +++ b/packages/project/eslint.config.js @@ -0,0 +1,9 @@ +import eslintCommonConfig from "./eslint.common.config.js"; + +export default [ + ...eslintCommonConfig, // Load common ESLint config + { + // Add project-specific ESLint config rules here + // in order to override common config + } +]; diff --git a/packages/project/jsdoc-plugin.cjs b/packages/project/jsdoc-plugin.cjs new file mode 100644 index 00000000000..cd7ef446d0f --- /dev/null +++ b/packages/project/jsdoc-plugin.cjs @@ -0,0 +1,9 @@ +/* + * This plugin fixes unexpected JSDoc behavior that prevents us from using types that start with an at-sign (@). + * JSDoc doesn't see "{@" as a valid type expression, probably as there's also {@link ...}. + */ +exports.handlers = { + jsdocCommentFound: function(e) { + e.comment = e.comment.replace(/{@ui5\//g, "{ @ui5/"); + } +}; diff --git a/packages/project/jsdoc.json b/packages/project/jsdoc.json new file mode 100644 index 00000000000..7fc7a4c6dd0 --- /dev/null +++ b/packages/project/jsdoc.json @@ -0,0 +1,61 @@ +{ + "tags": { + "allowUnknownTags": false + }, + "source": { + "include": ["README.md"], + "includePattern": ".+\\.js$", + "excludePattern": "(node_modules(\\\\|/))" + }, + "plugins": [ + "./jsdoc-plugin.cjs" + ], + "opts": { + "encoding": "utf8", + "destination": "jsdocs/", + "recurse": true, + "verbose": true, + "access": "public" + }, + "templates": { + "cleverLinks": false, + "monospaceLinks": false, + "default": { + "useLongnameInNav": true + } + }, + "openGraph": { + "title": "UI5 CLI - API Reference", + "type": "website", + "image": "https://ui5.github.io/cli/v4/images/UI5_logo_wide.png", + "site_name": "UI5 CLI - API Reference", + "url": "https://ui5.github.io/cli/" + }, + "docdash": { + "sectionOrder": [ + "Modules", + "Namespaces", + "Classes", + "Externals", + "Events", + "Mixins", + "Tutorials", + "Interfaces" + ], + "meta": { + "title": "UI5 CLI - API Reference - UI5 Project", + "description": "UI5 CLI - API Reference - UI5 Project", + "keyword": "openui5 sapui5 ui5 build development tool api reference" + }, + "search": true, + "wrap": true, + "menu": { + "GitHub": { + "href": "https://github.com/SAP/ui5-project", + "target": "_blank", + "class": "menu-item", + "id": "github_link" + } + } + } +} diff --git a/packages/project/lib/build/ProjectBuilder.js b/packages/project/lib/build/ProjectBuilder.js new file mode 100644 index 00000000000..af1804b6af1 --- /dev/null +++ b/packages/project/lib/build/ProjectBuilder.js @@ -0,0 +1,512 @@ +import {rmrf} from "../utils/fs.js"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; +import BuildLogger from "@ui5/logger/internal/loggers/Build"; +import composeProjectList from "./helpers/composeProjectList.js"; +import BuildContext from "./helpers/BuildContext.js"; +import prettyHrtime from "pretty-hrtime"; +import OutputStyleEnum from "./helpers/ProjectBuilderOutputStyle.js"; + +/** + * @public + * @class + * @alias @ui5/project/build/ProjectBuilder + */ +class ProjectBuilder { + #log; + /** + * Build Configuration + * + * @public + * @typedef {object} @ui5/project/build/ProjectBuilder~BuildConfiguration + * @property {boolean} [selfContained=false] Flag to activate self contained build + * @property {boolean} [cssVariables=false] Flag to activate CSS variables generation + * @property {boolean} [jsdoc=false] Flag to activate JSDoc build + * @property {boolean} [createBuildManifest=false] + * Whether to create a build manifest file for the root project. + * This is currently only supported for projects of type 'library' and 'theme-library' + * No other dependencies can be included in the build result. + * @property {module:@ui5/project/build/ProjectBuilderOutputStyle} [outputStyle=Default] + * Processes build results into a specific directory structure. + * @property {Array.} [includedTasks=[]] List of tasks to be included + * @property {Array.} [excludedTasks=[]] List of tasks to be excluded. + * If the wildcard '*' is provided, only the included tasks will be executed. + */ + + /** + * As an alternative to providing plain lists of names of dependencies to include and exclude, you can provide a + * more complex "Dependency Includes" object to define which dependencies should be part of the build result. + *
+ * This information is then used to compile lists of includedDependencies and + * excludedDependencies, which are applied during the build process. + *

+ * Regular expression-parameters are directly applied to a list of all project dependencies + * so that they don't need to be evaluated in later processing steps. + *

+ * Generally, includes are handled with a higher priority than excludes. Additionally, operations for processing + * transitive dependencies are handled with a lower priority than explicitly mentioned dependencies. The "default" + * dependency-includes are appended at the end. + *

+ * The priority of the various dependency lists is applied in the following order. + * Note that a later exclude can't overrule an earlier include. + *
+ *
    + *
  1. includeDependency, includeDependencyRegExp
  2. + *
  3. excludeDependency, excludeDependencyRegExp
  4. + *
  5. includeDependencyTree
  6. + *
  7. excludeDependencyTree
  8. + *
  9. defaultIncludeDependency, defaultIncludeDependencyRegExp, + * defaultIncludeDependencyTree
  10. + *
+ * + * @public + * @typedef {object} @ui5/project/build/ProjectBuilder~DependencyIncludes + * @property {boolean} includeAllDependencies + * Whether all dependencies should be part of the build result + * This parameter has the lowest priority and basically includes all remaining (not excluded) projects as include + * @property {string[]} includeDependency + * The dependencies to be considered in includedDependencies; the + * * character can be used as wildcard for all dependencies and + * is an alias for the CLI option --all + * @property {string[]} includeDependencyRegExp + * Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in includedDependencies + * @property {string[]} includeDependencyTree + * The dependencies to be considered in includedDependencies; + * transitive dependencies are also appended + * @property {string[]} excludeDependency + * The dependencies to be considered in excludedDependencies + * @property {string[]} excludeDependencyRegExp + * Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in excludedDependencies + * @property {string[]} excludeDependencyTree + * The dependencies to be considered in excludedDependencies; + * transitive dependencies are also appended + * @property {string[]} defaultIncludeDependency + * Same as includeDependency parameter; + * typically used in project build settings + * @property {string[]} defaultIncludeDependencyRegExp + * Same as includeDependencyRegExp parameter; + * typically used in project build settings + * @property {string[]} defaultIncludeDependencyTree + * Same as includeDependencyTree parameter; + * typically used in project build settings + */ + + /** + * Executes a project build, including all necessary or requested dependencies + * + * @public + * @param {object} parameters + * @param {@ui5/project/graph/ProjectGraph} parameters.graph Project graph + * @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} [parameters.buildConfig] Build configuration + * @param {@ui5/builder/tasks/taskRepository} parameters.taskRepository Task Repository module to use + */ + constructor({graph, buildConfig, taskRepository}) { + if (!graph) { + throw new Error(`Missing parameter 'graph'`); + } + if (!taskRepository) { + throw new Error(`Missing parameter 'taskRepository'`); + } + if (!graph.isSealed()) { + throw new Error( + `Can not build project graph with root node ${graph.getRoot().getName()}: Graph is not sealed`); + } + + this._graph = graph; + this._buildContext = new BuildContext(graph, taskRepository, buildConfig); + this.#log = new BuildLogger("ProjectBuilder"); + } + + /** + * Executes a project build, including all necessary or requested dependencies + * + * @public + * @param {object} parameters Parameters + * @param {string} parameters.destPath Target path + * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build + * @param {Array.} [parameters.includedDependencies=[]] + * List of names of projects to include in the build result + * If the wildcard '*' is provided, all dependencies will be included in the build result. + * @param {Array.} [parameters.excludedDependencies=[]] + * List of names of projects to exclude from the build result. + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} [parameters.dependencyIncludes] + * Alternative to the includedDependencies and excludedDependencies parameters. + * Allows for a more sophisticated configuration for defining which dependencies should be + * part of the build result. If this is provided, the other mentioned parameters are ignored. + * @returns {Promise} Promise resolving once the build has finished + */ + async build({ + destPath, cleanDest = false, + includedDependencies = [], excludedDependencies = [], + dependencyIncludes + }) { + if (!destPath) { + throw new Error(`Missing parameter 'destPath'`); + } + if (dependencyIncludes) { + if (includedDependencies.length || excludedDependencies.length) { + throw new Error( + "Parameter 'dependencyIncludes' can't be used in conjunction " + + "with parameters 'includedDependencies' or 'excludedDependencies"); + } + } + const rootProjectName = this._graph.getRoot().getName(); + this.#log.info(`Preparing build for project ${rootProjectName}`); + this.#log.info(` Target directory: ${destPath}`); + + // Get project filter function based on include/exclude params + // (also logs some info to console) + const filterProject = await this._getProjectFilter({ + explicitIncludes: includedDependencies, + explicitExcludes: excludedDependencies, + dependencyIncludes + }); + + // Count total number of projects to build based on input + const requestedProjects = this._graph.getProjectNames().filter(function(projectName) { + return filterProject(projectName); + }); + + if (requestedProjects.length > 1) { + const {createBuildManifest} = this._buildContext.getBuildConfig(); + if (createBuildManifest) { + throw new Error( + `It is currently not supported to request the creation of a build manifest ` + + `while including any dependencies into the build result`); + } + } + + const projectBuildContexts = await this._createRequiredBuildContexts(requestedProjects); + const cleanupSigHooks = this._registerCleanupSigHooks(); + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: destPath, + virBasePath: "/" + }); + + const queue = []; + const alreadyBuilt = []; + + // Create build queue based on graph depth-first search to ensure correct build order + await this._graph.traverseDepthFirst(async ({project}) => { + const projectName = project.getName(); + const projectBuildContext = projectBuildContexts.get(projectName); + if (projectBuildContext) { + // Build context exists + // => This project needs to be built or, in case it has already + // been built, it's build result needs to be written out (if requested) + queue.push(projectBuildContext); + if (!projectBuildContext.requiresBuild()) { + alreadyBuilt.push(projectName); + } + } + }); + + this.#log.setProjects(queue.map((projectBuildContext) => { + return projectBuildContext.getProject().getName(); + })); + if (queue.length > 1) { // Do not log if only the root project is being built + this.#log.info(`Processing ${queue.length} projects`); + if (alreadyBuilt.length) { + this.#log.info(` Reusing build results of ${alreadyBuilt.length} projects`); + this.#log.info(` Building ${queue.length - alreadyBuilt.length} projects`); + } + + if (this.#log.isLevelEnabled("verbose")) { + this.#log.verbose(` Required projects:`); + this.#log.verbose(` ${queue + .map((projectBuildContext) => { + const projectName = projectBuildContext.getProject().getName(); + let msg; + if (alreadyBuilt.includes(projectName)) { + const buildMetadata = projectBuildContext.getBuildMetadata(); + const ts = new Date(buildMetadata.timestamp).toUTCString(); + msg = `*> ${projectName} /// already built at ${ts}`; + } else { + msg = `=> ${projectName}`; + } + return msg; + }) + .join("\n ")}`); + } + } + + if (cleanDest) { + this.#log.info(`Cleaning target directory...`); + await rmrf(destPath); + } + const startTime = process.hrtime(); + try { + const pWrites = []; + for (const projectBuildContext of queue) { + const projectName = projectBuildContext.getProject().getName(); + const projectType = projectBuildContext.getProject().getType(); + this.#log.verbose(`Processing project ${projectName}...`); + + // Only build projects that are not already build (i.e. provide a matching build manifest) + if (alreadyBuilt.includes(projectName)) { + this.#log.skipProjectBuild(projectName, projectType); + } else { + this.#log.startProjectBuild(projectName, projectType); + await projectBuildContext.getTaskRunner().runTasks(); + this.#log.endProjectBuild(projectName, projectType); + } + if (!requestedProjects.includes(projectName)) { + // Project has not been requested + // => Its resources shall not be part of the build result + continue; + } + + this.#log.verbose(`Writing out files...`); + pWrites.push(this._writeResults(projectBuildContext, fsTarget)); + } + await Promise.all(pWrites); + this.#log.info(`Build succeeded in ${this._getElapsedTime(startTime)}`); + } catch (err) { + this.#log.error(`Build failed in ${this._getElapsedTime(startTime)}`); + throw err; + } finally { + this._deregisterCleanupSigHooks(cleanupSigHooks); + await this._executeCleanupTasks(); + } + } + + async _createRequiredBuildContexts(requestedProjects) { + const requiredProjects = new Set(this._graph.getProjectNames().filter((projectName) => { + return requestedProjects.includes(projectName); + })); + + const projectBuildContexts = new Map(); + + for (const projectName of requiredProjects) { + this.#log.verbose(`Creating build context for project ${projectName}...`); + const projectBuildContext = this._buildContext.createProjectContext({ + project: this._graph.getProject(projectName) + }); + + projectBuildContexts.set(projectName, projectBuildContext); + + if (projectBuildContext.requiresBuild()) { + const taskRunner = projectBuildContext.getTaskRunner(); + const requiredDependencies = await taskRunner.getRequiredDependencies(); + + if (requiredDependencies.size === 0) { + continue; + } + // This project needs to be built and required dependencies to be built as well + this._graph.getDependencies(projectName).forEach((depName) => { + if (projectBuildContexts.has(depName)) { + // Build context already exists + // => Dependency will be built + return; + } + if (!requiredDependencies.has(depName)) { + return; + } + // Add dependency to list of projects to build + requiredProjects.add(depName); + }); + } + } + + return projectBuildContexts; + } + + async _getProjectFilter({ + dependencyIncludes, + explicitIncludes, + explicitExcludes + }) { + const {includedDependencies, excludedDependencies} = await composeProjectList( + this._graph, + dependencyIncludes || { + includeDependencyTree: explicitIncludes, + excludeDependencyTree: explicitExcludes + } + ); + + if (includedDependencies.length) { + if (includedDependencies.length === this._graph.getSize() - 1) { + this.#log.info(` Including all dependencies`); + } else { + this.#log.info(` Requested dependencies:`); + this.#log.info(` + ${includedDependencies.join("\n + ")}`); + } + } + if (excludedDependencies.length) { + this.#log.info(` Excluded dependencies:`); + this.#log.info(` - ${excludedDependencies.join("\n + ")}`); + } + + const rootProjectName = this._graph.getRoot().getName(); + return function projectFilter(projectName) { + function projectMatchesAny(deps) { + return deps.some((dep) => dep instanceof RegExp ? + dep.test(projectName) : dep === projectName); + } + + if (projectName === rootProjectName) { + // Always include the root project + return true; + } + + if (projectMatchesAny(excludedDependencies)) { + return false; + } + + if (includedDependencies.includes("*") || projectMatchesAny(includedDependencies)) { + return true; + } + + return false; + }; + } + + async _writeResults(projectBuildContext, target) { + const project = projectBuildContext.getProject(); + const taskUtil = projectBuildContext.getTaskUtil(); + const buildConfig = this._buildContext.getBuildConfig(); + const {createBuildManifest, outputStyle} = buildConfig; + // Output styles are applied only for the root project + const isRootProject = taskUtil.isRootProject(); + + let readerStyle = "dist"; + if (createBuildManifest || + (isRootProject && outputStyle === OutputStyleEnum.Namespace && project.getType() === "application")) { + // Ensure buildtime (=namespaced) style when writing with a build manifest or when explicitly requested + readerStyle = "buildtime"; + } else if (isRootProject && outputStyle === OutputStyleEnum.Flat) { + readerStyle = "flat"; + } + + const reader = project.getReader({ + style: readerStyle + }); + const resources = await reader.byGlob("/**/*"); + + if (createBuildManifest) { + // Create and write a build manifest metadata file + const { + default: createBuildManifest + } = await import("./helpers/createBuildManifest.js"); + const metadata = await createBuildManifest(project, buildConfig, this._buildContext.getTaskRepository()); + await target.write(resourceFactory.createResource({ + path: `/.ui5/build-manifest.json`, + string: JSON.stringify(metadata, null, "\t") + })); + } + + await Promise.all(resources.map((resource) => { + if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { + this.#log.silly(`Skipping write of resource tagged as "OmitFromBuildResult": ` + + resource.getPath()); + return; // Skip target write for this resource + } + return target.write(resource); + })); + + if (isRootProject && + outputStyle === OutputStyleEnum.Flat && + project.getType() !== "application" /* application type is with a default flat build output structure */) { + const namespace = project.getNamespace(); + const libraryResourcesPrefix = `/resources/${namespace}/`; + const testResourcesPrefix = "/test-resources/"; + const namespacedRegex = new RegExp(`/(resources|test-resources)/${namespace}`); + const processedResourcesSet = resources.reduce((acc, resource) => acc.add(resource.getPath()), new Set()); + + // If outputStyle === "Flat", then the FlatReader would have filtered + // some resources. We now need to get all of the available resources and + // do an intersection with the processed/bundled ones. + const defaultReader = project.getReader(); + const defaultResources = await defaultReader.byGlob("/**/*"); + const flatDefaultResources = defaultResources.map((resource) => ({ + flatResource: resource.getPath().replace(namespacedRegex, ""), + originalPath: resource.getPath(), + })); + + const skippedResources = flatDefaultResources.filter((resource) => { + return processedResourcesSet.has(resource.flatResource) === false; + }); + + skippedResources.forEach((resource) => { + if (resource.originalPath.startsWith(testResourcesPrefix)) { + this.#log.verbose( + `Omitting ${resource.originalPath} from build result. File is part of ${testResourcesPrefix}.` + ); + } else if (!resource.originalPath.startsWith(libraryResourcesPrefix)) { + this.#log.warn( + `Omitting ${resource.originalPath} from build result. ` + + `File is not within project namespace '${namespace}'.` + ); + } + }); + } + } + + async _executeCleanupTasks(force) { + this.#log.info("Executing cleanup tasks..."); + + await this._buildContext.executeCleanupTasks(force); + } + + _registerCleanupSigHooks() { + const that = this; + function createListener(exitCode) { + return function() { + // Asynchronously cleanup resources, then exit + that._executeCleanupTasks(true).then(() => { + process.exit(exitCode); + }); + }; + } + + const processSignals = { + "SIGHUP": createListener(128 + 1), + "SIGINT": createListener(128 + 2), + "SIGTERM": createListener(128 + 15), + "SIGBREAK": createListener(128 + 21) + }; + + for (const signal of Object.keys(processSignals)) { + process.on(signal, processSignals[signal]); + } + + // TODO: Also cleanup for unhandled rejections and exceptions? + // Add additional events like signals since they are registered on the process + // event emitter in a similar fashion + // processSignals["unhandledRejection"] = createListener(1); + // process.once("unhandledRejection", processSignals["unhandledRejection"]); + // processSignals["uncaughtException"] = function(err, origin) { + // const fs = require("fs"); + // fs.writeSync( + // process.stderr.fd, + // `Caught exception: ${err}\n` + + // `Exception origin: ${origin}` + // ); + // createListener(1)(); + // }; + // process.once("uncaughtException", processSignals["uncaughtException"]); + + return processSignals; + } + + _deregisterCleanupSigHooks(signals) { + for (const signal of Object.keys(signals)) { + process.removeListener(signal, signals[signal]); + } + } + + /** + * Calculates the elapsed build time and returns a prettified output + * + * @private + * @param {Array} startTime Array provided by process.hrtime() + * @returns {string} Difference between now and the provided time array as formatted string + */ + _getElapsedTime(startTime) { + const timeDiff = process.hrtime(startTime); + return prettyHrtime(timeDiff); + } +} + +export default ProjectBuilder; diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js new file mode 100644 index 00000000000..70e7bf32ae4 --- /dev/null +++ b/packages/project/lib/build/TaskRunner.js @@ -0,0 +1,487 @@ +import {getLogger} from "@ui5/logger"; +import composeTaskList from "./helpers/composeTaskList.js"; +import {createReaderCollection} from "@ui5/fs/resourceFactory"; + +/** + * TaskRunner + * + * @private + * @hideconstructor + */ +class TaskRunner { + /** + * Constructor + * + * @param {object} parameters + * @param {object} parameters.graph + * @param {object} parameters.project + * @param {@ui5/logger/loggers/ProjectBuild} parameters.log Logger to use + * @param {@ui5/project/build/helpers/TaskUtil} parameters.taskUtil TaskUtil instance + * @param {@ui5/builder/tasks/taskRepository} parameters.taskRepository Task repository + * @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} parameters.buildConfig + * Build configuration + */ + constructor({graph, project, log, taskUtil, taskRepository, buildConfig}) { + if (!graph || !project || !log || !taskUtil || !taskRepository || !buildConfig) { + throw new Error("TaskRunner: One or more mandatory parameters not provided"); + } + this._project = project; + this._graph = graph; + this._taskUtil = taskUtil; + this._taskRepository = taskRepository; + this._buildConfig = buildConfig; + this._log = log; + + this._directDependencies = new Set(this._taskUtil.getDependencies()); + } + + async _initTasks() { + if (this._tasks) { + return; + } + + this._tasks = Object.create(null); + this._taskExecutionOrder = []; + + const project = this._project; + let buildDefinition; + + switch (project.getType()) { + case "application": + buildDefinition = "./definitions/application.js"; + break; + case "library": + buildDefinition = "./definitions/library.js"; + break; + case "module": + buildDefinition = "./definitions/module.js"; + break; + case "theme-library": + buildDefinition = "./definitions/themeLibrary.js"; + break; + default: + throw new Error(`Unknown project type ${project.getType()}`); + } + + const {default: getStandardTasks} = await import(buildDefinition); + + const standardTasks = getStandardTasks({ + project, + taskUtil: this._taskUtil, + getTask: this._taskRepository.getTask + }); + + for (const [taskName, params] of standardTasks) { + this._addTask(taskName, params); + } + + await this._addCustomTasks(); + + // Create readers for *all* dependencies + const depReaders = []; + await this._graph.traverseBreadthFirst(project.getName(), async function({project: dep}) { + if (dep.getName() === project.getName()) { + // Ignore project itself + return; + } + depReaders.push(dep.getReader()); + }); + + this._allDependenciesReader = createReaderCollection({ + name: `Dependency reader collection of project ${project.getName()}`, + readers: depReaders + }); + } + + /** + * Takes a list of tasks which should be executed from the available task list of the current builder + * + * @returns {Promise} Returns promise resolving once all tasks have been executed + */ + async runTasks() { + await this._initTasks(); + const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig); + const allTasks = this._taskExecutionOrder.filter((taskName) => { + // There might be a numeric suffix in case a custom task is configured multiple times. + // The suffix needs to be removed in order to check against the list of tasks to run. + // + // Note: The 'tasksToRun' parameter only allows to specify the custom task name + // (without suffix), so it executes either all or nothing. + // It's currently not possible to just execute some occurrences of a custom task. + // This would require a more robust contract to identify task executions + // (e.g. via an 'id' that can be assigned to a specific execution in the configuration). + const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); + return tasksToRun.includes(taskWithoutSuffixCounter) && + // Task can be explicitly excluded by making its taskFunction = null + this._tasks[taskName].task !== null; + }); + + this._log.setTasks(allTasks); + for (const taskName of allTasks) { + const taskFunction = this._tasks[taskName].task; + + if (typeof taskFunction === "function") { + await this._executeTask(taskName, taskFunction); + } + } + } + + /** + * First compiles a list of all tasks that will be executed, then a list of all direct project + * dependencies that those tasks require access to. + * + * @returns {Set} Returns a set containing the names of all required direct project dependencies + */ + async getRequiredDependencies() { + await this._initTasks(); + const tasksToRun = composeTaskList(Object.keys(this._tasks), this._buildConfig); + const allTasks = this._taskExecutionOrder.filter((taskName) => { + // There might be a numeric suffix in case a custom task is configured multiple times. + // The suffix needs to be removed in order to check against the list of tasks to run. + // + // Note: The 'tasksToRun' parameter only allows to specify the custom task name + // (without suffix), so it executes either all or nothing. + // It's currently not possible to just execute some occurrences of a custom task. + // This would require a more robust contract to identify task executions + // (e.g. via an 'id' that can be assigned to a specific execution in the configuration). + const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); + return tasksToRun.includes(taskWithoutSuffixCounter); + }); + return allTasks.reduce((requiredDependencies, taskName) => { + if (this._tasks[taskName].requiredDependencies.size) { + this._log.verbose(`Task ${taskName} for project ${this._project.getName()} requires dependencies`); + } + for (const depName of this._tasks[taskName].requiredDependencies) { + requiredDependencies.add(depName); + } + return requiredDependencies; + }, new Set()); + } + + /** + * Adds an executable task to the builder + * + * The order this function is being called defines the build order. FIFO. + * + * @param {string} taskName Name of the task which should be in the list availableTasks. + * @param {object} [parameters] + * @param {boolean} [parameters.requiresDependencies] + * @param {object} [parameters.options] + * @param {Function} [parameters.taskFunction] + */ + _addTask(taskName, {requiresDependencies = false, options = {}, taskFunction} = {}) { + if (this._tasks[taskName]) { + throw new Error(`Failed to add duplicate task ${taskName} for project ${this._project.getName()}`); + } + if (this._taskExecutionOrder.includes(taskName)) { + throw new Error(`Failed to add task ${taskName} for project ${this._project.getName()}. ` + + `It has already been scheduled for execution`); + } + + let task; + if (taskFunction === null) { + this._log.verbose(`Task ${taskName} is set to be explicitly skipped in definitions.`); + task = null; + } else { + task = async (log) => { + options.projectName = this._project.getName(); + options.projectNamespace = this._project.getNamespace(); + + const params = { + workspace: this._project.getWorkspace(), + taskUtil: this._taskUtil, + options + }; + + if (requiresDependencies) { + params.dependencies = this._allDependenciesReader; + } + + if (!taskFunction) { + taskFunction = (await this._taskRepository.getTask(taskName)).task; + } + return taskFunction(params); + }; + } + this._tasks[taskName] = { + task, + requiredDependencies: requiresDependencies ? this._directDependencies : new Set() + }; + this._taskExecutionOrder.push(taskName); + } + + /** + * + * @private + */ + async _addCustomTasks() { + const projectCustomTasks = this._project.getCustomTasks(); + if (!projectCustomTasks || projectCustomTasks.length === 0) { + return; // No custom tasks defined + } + for (let i = 0; i < projectCustomTasks.length; i++) { + // Add tasks one-by-one to keep order as defined in project configuration + await this._addCustomTask(projectCustomTasks[i]); + } + } + /** + * Adds custom tasks to execute + * + * @private + * @param {object} taskDef + */ + async _addCustomTask(taskDef) { + const project = this._project; + const graph = this._graph; + const taskUtil = this._taskUtil; + + if (!taskDef.name) { + throw new Error(`Missing name for custom task in configuration of project ${project.getName()}`); + } + if (taskDef.beforeTask && taskDef.afterTask) { + throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` + + `defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`); + } + if (this._taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) { + // Iff there are tasks configured, beforeTask or afterTask must be given + throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` + + `defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`); + } + const standardTasks = this._taskRepository.getAllTaskNames(); + if (standardTasks.includes(taskDef.name)) { + throw new Error( + `Custom task configuration of project ${project.getName()} ` + + `references standard task ${taskDef.name}. Only custom tasks must be provided here.`); + } + + let newTaskName = taskDef.name; + if (this._tasks[newTaskName]) { + // Task is already known + // => add a suffix to allow for multiple configurations of the same task + let suffixCounter = 1; + while (this._tasks[newTaskName]) { + suffixCounter++; // Start at 2 + newTaskName = `${taskDef.name}--${suffixCounter}`; + } + } + const task = graph.getExtension(taskDef.name); + if (!task) { + throw new Error( + `Could not find custom task ${taskDef.name}, referenced by project ${project.getName()} ` + + `in project graph with root node ${graph.getRoot().getName()}`); + } + + // Tasks can provide an optional callback to tell build process which dependencies they require + const requiredDependenciesCallback = await task.getRequiredDependenciesCallback(); + const specVersion = task.getSpecVersion(); + let requiredDependencies; + + // Always provide a dependencies-reader, even if empty. Unless the task is specVersion >=3.0 + // and did not define the respective callback. + // This is to distinguish between tasks semi-intentionally not requesting any dependencies, + // because none are available (i.e. because the project does not have any) and tasks that + // intentionally do not request any dependencies, by not providing a dependency-determination callback function + let provideDependenciesReader = true; + if (!requiredDependenciesCallback) { + if (specVersion.gte("3.0")) { + // Default for new spec versions: Provide no dependencies if no callback is provided + this._log.verbose( + `Custom task ${task.getName()} of project ${this._project.getName()} ` + + `does not provide a callback for determining its required dependencies. ` + + `Defaulting to not providing any dependencies to the task`); + requiredDependencies = new Set(); + + // Ensure that no reader is provided, in order to produce an exception if + // access is still attempted + provideDependenciesReader = false; + } else { + // Default for old spec versions: Assume all dependencies are required + requiredDependencies = this._directDependencies; + } + } else { + const dependencyDeterminationParams = { + availableDependencies: new Set(this._directDependencies) + }; + + if (specVersion.gte("3.0")) { + // Add getProjects, getDependencies and options to parameters + const taskUtilInterface = taskUtil.getInterface(specVersion); + dependencyDeterminationParams.getProject = + taskUtilInterface.getProject.bind(taskUtilInterface); + dependencyDeterminationParams.getDependencies = + taskUtilInterface.getDependencies.bind(taskUtilInterface); + } + + dependencyDeterminationParams.options = { + projectName: project.getName(), + projectNamespace: project.getNamespace(), + configuration: taskDef.configuration, + taskName: newTaskName + }; + + requiredDependencies = await requiredDependenciesCallback(dependencyDeterminationParams); + if (!(requiredDependencies instanceof Set)) { + throw new Error( + `'determineRequiredDependencies' callback function of custom task ${task.getName()} of ` + + `project ${project.getName()} must resolve with Set.`); + } + requiredDependencies.forEach((depName) => { + // Returned requiredDependencies must be a subset of all direct dependencies of the project + if (!this._directDependencies.has(depName)) { + throw new Error( + `'determineRequiredDependencies' callback function of custom task ${task.getName()} ` + + `of project ${project.getName()} must resolve with a subset of the the direct ` + + `dependencies of the project. ${depName} is not a direct dependency of the project.`); + } + }); + } + + this._tasks[newTaskName] = { + task: this._createCustomTaskWrapper({ + task, + project, + taskUtil, + taskName: newTaskName, + taskConfiguration: taskDef.configuration, + provideDependenciesReader, + getDependenciesReader: () => { + // Create the dependencies reader on-demand + return this._createDependenciesReader(requiredDependencies); + }, + }), + requiredDependencies + }; + + if (this._taskExecutionOrder.length) { + // There is at least one task configured. Use before- and afterTask to add the custom task + const refTaskName = taskDef.beforeTask || taskDef.afterTask; + let refTaskIdx = this._taskExecutionOrder.indexOf(refTaskName); + if (refTaskIdx === -1) { + if (this._taskRepository.getRemovedTaskNames().includes(refTaskName)) { + throw new Error( + `Standard task ${refTaskName}, referenced by custom task ${newTaskName} ` + + `in project ${project.getName()}, ` + + `has been removed in this version of UI5 CLI and can't be referenced anymore. ` + + `Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`); + } + throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` + + `to be scheduled for project ${project.getName()}`); + } + if (taskDef.afterTask) { + // Insert after index of referenced task + refTaskIdx++; + } + this._taskExecutionOrder.splice(refTaskIdx, 0, newTaskName); + } else { + // There is no task configured so far. Just add the custom task + this._taskExecutionOrder.push(newTaskName); + } + } + + _createCustomTaskWrapper({ + project, taskUtil, getDependenciesReader, provideDependenciesReader, task, taskName, taskConfiguration + }) { + return async function() { + /* Custom Task Interface + Parameters: + {Object} parameters Parameters + {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files + {@ui5/fs/AbstractReader} parameters.dependencies + Reader or Collection to read dependency files + {@ui5/project/build/helpers/TaskUtil} parameters.taskUtil Specification Version-dependent + interface of a [TaskUtil]{@link @ui5/project/build/helpers/TaskUtil} instance + {@ui5/logger/Logger} [parameters.log] Logger instance to use by the custom task. + This parameter is only available to custom task extensions defining + Specification Version 3.0 and above. + {Object} parameters.options Options + {string} parameters.options.projectName Project name + {string|null} parameters.options.projectNamespace Project namespace if available + {string} [parameters.options.taskName] Runtime name of the task. + If a task is executed multiple times, a suffix is added to distinguish the executions. + This attribute is only available to custom task extensions defining + Specification Version 3.0 and above. + {string} [parameters.options.configuration] Task configuration if given in ui5.yaml + Returns: + {Promise} Promise resolving with undefined once data has been written + */ + const params = { + workspace: project.getWorkspace(), + options: { + projectName: project.getName(), + projectNamespace: project.getNamespace(), + configuration: taskConfiguration, + } + }; + const specVersion = task.getSpecVersion(); + const taskUtilInterface = taskUtil.getInterface(specVersion); + // Interface is undefined if specVersion does not support taskUtil + if (taskUtilInterface) { + params.taskUtil = taskUtilInterface; + } + const taskFunction = await task.getTask(); + + if (specVersion.gte("3.0")) { + params.options.taskName = taskName; + params.log = getLogger(`builder:custom-task:${taskName}`); + } + + if (provideDependenciesReader) { + params.dependencies = await getDependenciesReader(); + } + return taskFunction(params); + }; + } + + /** + * Adds progress related functionality to task function. + * + * @private + * @param {string} taskName Name of the task + * @param {Function} taskFunction Function which executed the task + * @param {object} taskParams Base parameters for all tasks + * @returns {Promise} Resolves when task has finished + */ + async _executeTask(taskName, taskFunction, taskParams) { + this._log.startTask(taskName); + this._taskStart = performance.now(); + await taskFunction(taskParams, this._log); + if (this._log.isLevelEnabled("perf")) { + this._log.perf(`Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`); + } + this._log.endTask(taskName); + } + + async _createDependenciesReader(requiredDirectDependencies) { + if (requiredDirectDependencies.size === this._directDependencies.size) { + // Shortcut: If all direct dependencies are required, just return the already created reader + return this._allDependenciesReader; + } + const rootProject = this._project; + + // Collect readers for all requested dependencies + const readers = []; + + // Add transitive dependencies to set of required dependencies + const requiredDependencies = new Set(requiredDirectDependencies); + for (const projectName of requiredDirectDependencies) { + this._graph.getTransitiveDependencies(projectName).forEach((depName) => { + requiredDependencies.add(depName); + }); + } + + // Collect readers for all (transitive) dependencies + await this._graph.traverseBreadthFirst(rootProject.getName(), async ({project}) => { + if (requiredDependencies.has(project.getName())) { + readers.push(project.getReader()); + } + }); + + // Create a reader collection for that + return createReaderCollection({ + name: `Reduced dependency reader collection of project ${rootProject.getName()}`, + readers + }); + } +} + +export default TaskRunner; diff --git a/packages/project/lib/build/definitions/_utils.js b/packages/project/lib/build/definitions/_utils.js new file mode 100644 index 00000000000..3c12953cb7c --- /dev/null +++ b/packages/project/lib/build/definitions/_utils.js @@ -0,0 +1,23 @@ + +/** + * Appends the list of 'excludes' to the list of 'patterns'. To harmonize both lists, the 'excludes' + * are negated and the 'patternPrefix' is added to make them absolute. + * + * @private + * @param {string[]} patterns + * List of absolute default patterns. + * @param {string[]} excludes + * List of relative patterns to be excluded. Excludes with a leading "!" are meant to be re-included. + * @param {string} patternPrefix + * Prefix to be added to the excludes to make them absolute. The prefix must have a leading and a + * trailing "/". + */ +export function enhancePatternWithExcludes(patterns, excludes, patternPrefix) { + excludes.forEach((exclude) => { + if (exclude.startsWith("!")) { + patterns.push(`${patternPrefix}${exclude.slice(1)}`); + } else { + patterns.push(`!${patternPrefix}${exclude}`); + } + }); +} diff --git a/packages/project/lib/build/definitions/application.js b/packages/project/lib/build/definitions/application.js new file mode 100644 index 00000000000..c546ee9d6bf --- /dev/null +++ b/packages/project/lib/build/definitions/application.js @@ -0,0 +1,134 @@ +import {enhancePatternWithExcludes} from "./_utils.js"; +import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; + +/** + * Get tasks and their configuration for a given application project + * + * @private + * @param {object} parameters + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {Function} parameters.getTask + */ +export default function({project, taskUtil, getTask}) { + const tasks = new Map(); + tasks.set("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + + tasks.set("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,json}" + } + }); + + tasks.set("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json}" + } + }); + + // Support rules should not be minified to have readable code in the Support Assistant + const minificationPattern = ["/**/*.js", "!**/*.support.js"]; + if (project.getSpecVersion().gte("2.6")) { + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + } + tasks.set("minify", { + options: { + pattern: minificationPattern + } + }); + + tasks.set("enhanceManifest", {}); + + tasks.set("generateFlexChangesBundle", {}); + + const bundles = project.getBundles(); + const existingBundleDefinitionNames = + bundles.map(({bundleDefinition}) => bundleDefinition.name).filter(Boolean); + + const componentPreloadPaths = project.getComponentPreloadPaths(); + const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadExcludes(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + tasks.set("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } else { + // Default component preload for application namespace + tasks.set("generateComponentPreload", { + options: { + namespaces: [project.getNamespace()], + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } + + tasks.set("generateStandaloneAppBundle", {requiresDependencies: true}); + + tasks.set("transformBootstrapHtml", {}); + + if (bundles.length) { + tasks.set("generateBundle", { + requiresDependencies: true, + taskFunction: async ({workspace, dependencies, taskUtil, options}) => { + const generateBundleTask = await getTask("generateBundle"); + // 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({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + }); + }, Promise.resolve()); + } + }); + } else { + // No bundles defined. Just set task so that it can be referenced by custom tasks + tasks.set("generateBundle", { + taskFunction: null + }); + } + + tasks.set("generateVersionInfo", { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }); + + tasks.set("generateCachebusterInfo", { + options: { + signatureType: project.getCachebusterSignatureType(), + } + }); + + tasks.set("generateApiIndex", {requiresDependencies: true}); + tasks.set("generateResourcesJson", {requiresDependencies: true}); + + return tasks; +} diff --git a/packages/project/lib/build/definitions/library.js b/packages/project/lib/build/definitions/library.js new file mode 100644 index 00000000000..9b92177d1cd --- /dev/null +++ b/packages/project/lib/build/definitions/library.js @@ -0,0 +1,175 @@ +import {enhancePatternWithExcludes} from "./_utils.js"; +import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; + +/** + * Get tasks and their configuration for a given application project + * + * @private + * @param {object} parameters + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {Function} parameters.getTask + */ +export default function({project, taskUtil, getTask}) { + const tasks = new Map(); + tasks.set("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + + tasks.set("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }); + + tasks.set("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }); + + tasks.set("replaceBuildtime", { + options: { + pattern: "/resources/sap/ui/{Global,core/Core}.js" + } + }); + + tasks.set("generateJsdoc", { + requiresDependencies: true, + 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 + } + }); + } + }); + + tasks.set("executeJsdocSdkTransformation", { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library", + } + }); + + // Support rules should not be minified to have readable code in the Support Assistant + const minificationPattern = ["/resources/**/*.js", "!**/*.support.js"]; + if (project.getSpecVersion().gte("2.6")) { + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + } + + tasks.set("minify", { + options: { + pattern: minificationPattern + } + }); + + tasks.set("generateLibraryManifest", {}); + + tasks.set("enhanceManifest", {}); + + const bundles = project.getBundles(); + const existingBundleDefinitionNames = + bundles.map(({bundleDefinition}) => bundleDefinition.name).filter(Boolean); + const componentPreloadPaths = project.getComponentPreloadPaths(); + const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadExcludes(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + tasks.set("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } else { + tasks.set("generateComponentPreload", {taskFunction: null}); + } + + tasks.set("generateLibraryPreload", { + options: { + excludes: project.getLibraryPreloadExcludes(), + skipBundles: existingBundleDefinitionNames + } + }); + + if (bundles.length) { + tasks.set("generateBundle", { + requiresDependencies: true, + taskFunction: async ({workspace, dependencies, taskUtil, options}) => { + const generateBundleTask = await getTask("generateBundle"); + // 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({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + }); + }, Promise.resolve()); + } + }); + } else { + tasks.set("generateBundle", {taskFunction: null}); + } + + tasks.set("buildThemes", { + requiresDependencies: true, + options: { + projectName: project.getName(), + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern: `/resources/${project.getNamespace()}/themes/*/library.source.less`, + cssVariables: taskUtil.getBuildOption("cssVariables") + } + }); + + if (project.isFrameworkProject()) { + tasks.set("generateThemeDesignerResources", { + requiresDependencies: true, + options: { + version: project.getVersion() + } + }); + } else { + tasks.set("generateThemeDesignerResources", {taskFunction: null}); + } + + tasks.set("generateResourcesJson", { + requiresDependencies: true + }); + + return tasks; +} diff --git a/packages/project/lib/build/definitions/module.js b/packages/project/lib/build/definitions/module.js new file mode 100644 index 00000000000..0db873673dc --- /dev/null +++ b/packages/project/lib/build/definitions/module.js @@ -0,0 +1,12 @@ +/** + * Get tasks and their configuration for a given application project + * + * @private + * @param {object} parameters + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {Function} parameters.getTask + */ +export default function({project, taskUtil, getTask}) { + return new Map(); +} diff --git a/packages/project/lib/build/definitions/themeLibrary.js b/packages/project/lib/build/definitions/themeLibrary.js new file mode 100644 index 00000000000..2acf0392768 --- /dev/null +++ b/packages/project/lib/build/definitions/themeLibrary.js @@ -0,0 +1,51 @@ + +/** + * Get tasks and their configuration for a given application project + * + * @private + * @param {object} parameters + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {Function} parameters.getTask + */ +export default function({project, taskUtil, getTask}) { + const tasks = new Map(); + tasks.set("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/resources/**/*.{less,theme}" + } + }); + + tasks.set("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/resources/**/*.{less,theme}" + } + }); + + tasks.set("buildThemes", { + requiresDependencies: true, + options: { + projectName: project.getName(), + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: taskUtil.getBuildOption("cssVariables") + } + }); + + if (project.isFrameworkProject()) { + tasks.set("generateThemeDesignerResources", { + requiresDependencies: true, + options: { + version: project.getVersion() + } + }); + } else { + tasks.set("generateThemeDesignerResources", {taskFunction: null}); + } + + tasks.set("generateResourcesJson", {requiresDependencies: true}); + return tasks; +} diff --git a/packages/project/lib/build/helpers/BuildContext.js b/packages/project/lib/build/helpers/BuildContext.js new file mode 100644 index 00000000000..8d8d1e1a329 --- /dev/null +++ b/packages/project/lib/build/helpers/BuildContext.js @@ -0,0 +1,116 @@ +import ProjectBuildContext from "./ProjectBuildContext.js"; +import OutputStyleEnum from "./ProjectBuilderOutputStyle.js"; + +/** + * Context of a build process + * + * @private + * @memberof @ui5/project/build/helpers + */ +class BuildContext { + constructor(graph, taskRepository, { // buildConfig + selfContained = false, + cssVariables = false, + jsdoc = false, + createBuildManifest = false, + outputStyle = OutputStyleEnum.Default, + includedTasks = [], excludedTasks = [], + } = {}) { + if (!graph) { + throw new Error(`Missing parameter 'graph'`); + } + if (!taskRepository) { + throw new Error(`Missing parameter 'taskRepository'`); + } + + const rootProjectType = graph.getRoot().getType(); + + if (createBuildManifest && !["library", "theme-library"].includes(rootProjectType)) { + throw new Error( + `Build manifest creation is currently not supported for projects of type ` + + rootProjectType); + } + + if (createBuildManifest && selfContained) { + throw new Error( + `Build manifest creation is currently not supported for ` + + `self-contained builds`); + } + + if (createBuildManifest && outputStyle === OutputStyleEnum.Flat) { + throw new Error( + `Build manifest creation is not supported in conjunction with flat build output`); + } + if (outputStyle !== OutputStyleEnum.Default) { + if (rootProjectType === "theme-library") { + throw new Error( + `${outputStyle} build output style is currently not supported for projects of type` + + `theme-library since they commonly have more than one namespace. ` + + `Currently only the Default output style is supported for this project type.` + ); + } + if (rootProjectType === "module") { + throw new Error( + `${outputStyle} build output style is currently not supported for projects of type` + + `module. Their path mappings configuration can't be mapped to any namespace.` + + `Currently only the Default output style is supported for this project type.` + ); + } + } + + this._graph = graph; + this._buildConfig = { + selfContained, + cssVariables, + jsdoc, + createBuildManifest, + outputStyle, + includedTasks, + excludedTasks, + }; + + this._taskRepository = taskRepository; + + this._options = { + cssVariables: cssVariables + }; + this._projectBuildContexts = []; + } + + getRootProject() { + return this._graph.getRoot(); + } + + getOption(key) { + return this._options[key]; + } + + getBuildConfig() { + return this._buildConfig; + } + + getTaskRepository() { + return this._taskRepository; + } + + getGraph() { + return this._graph; + } + + createProjectContext({project}) { + const projectBuildContext = new ProjectBuildContext({ + buildContext: this, + project + }); + this._projectBuildContexts.push(projectBuildContext); + return projectBuildContext; + } + + async executeCleanupTasks(force = false) { + await Promise.all(this._projectBuildContexts.map((ctx) => { + return ctx.executeCleanupTasks(force); + })); + } +} + +export default BuildContext; diff --git a/packages/project/lib/build/helpers/ProjectBuildContext.js b/packages/project/lib/build/helpers/ProjectBuildContext.js new file mode 100644 index 00000000000..10eb2a67a83 --- /dev/null +++ b/packages/project/lib/build/helpers/ProjectBuildContext.js @@ -0,0 +1,148 @@ +import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; +import ProjectBuildLogger from "@ui5/logger/internal/loggers/ProjectBuild"; +import TaskUtil from "./TaskUtil.js"; +import TaskRunner from "../TaskRunner.js"; + +/** + * Build context of a single project. Always part of an overall + * [Build Context]{@link @ui5/project/build/helpers/BuildContext} + * + * @private + * @memberof @ui5/project/build/helpers + */ +class ProjectBuildContext { + constructor({buildContext, project}) { + if (!buildContext) { + throw new Error(`Missing parameter 'buildContext'`); + } + if (!project) { + throw new Error(`Missing parameter 'project'`); + } + this._buildContext = buildContext; + this._project = project; + this._log = new ProjectBuildLogger({ + moduleName: "Build", + projectName: project.getName(), + projectType: project.getType() + }); + this._queues = { + cleanup: [] + }; + + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"], + allowedNamespaces: ["build"] + }); + } + + isRootProject() { + return this._project === this._buildContext.getRootProject(); + } + + getOption(key) { + return this._buildContext.getOption(key); + } + + registerCleanupTask(callback) { + this._queues.cleanup.push(callback); + } + + async executeCleanupTasks(force) { + await Promise.all(this._queues.cleanup.map((callback) => { + return callback(force); + })); + } + + /** + * Retrieve a single project from the dependency graph + * + * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @returns {@ui5/project/specifications/Project|undefined} + * project instance or undefined if the project is unknown to the graph + */ + getProject(projectName) { + if (projectName) { + return this._buildContext.getGraph().getProject(projectName); + } + return this._project; + } + + /** + * Retrieve a list of direct dependencies of a given project from the dependency graph + * + * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @returns {string[]} Names of all direct dependencies + * @throws {Error} If the requested project is unknown to the graph + */ + getDependencies(projectName) { + return this._buildContext.getGraph().getDependencies(projectName || this._project.getName()); + } + + getResourceTagCollection(resource, tag) { + if (!resource.hasProject()) { + this._log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`); + resource.setProject(this._project); + // throw new Error( + // `Unable to get tag collection for resource ${resource.getPath()}: ` + + // `Resource must be associated to a project`); + } + const projectCollection = resource.getProject().getResourceTagCollection(); + if (projectCollection.acceptsTag(tag)) { + return projectCollection; + } + if (this._resourceTagCollection.acceptsTag(tag)) { + return this._resourceTagCollection; + } + throw new Error(`Could not find collection for resource ${resource.getPath()} and tag ${tag}`); + } + + getTaskUtil() { + if (!this._taskUtil) { + this._taskUtil = new TaskUtil({ + projectBuildContext: this + }); + } + + return this._taskUtil; + } + + getTaskRunner() { + if (!this._taskRunner) { + this._taskRunner = new TaskRunner({ + project: this._project, + log: this._log, + taskUtil: this.getTaskUtil(), + graph: this._buildContext.getGraph(), + taskRepository: this._buildContext.getTaskRepository(), + buildConfig: this._buildContext.getBuildConfig() + }); + } + return this._taskRunner; + } + + /** + * Determine whether the project has to be built or is already built + * (typically indicated by the presence of a build manifest) + * + * @returns {boolean} True if the project needs to be built + */ + requiresBuild() { + return !this._project.getBuildManifest(); + } + + getBuildMetadata() { + const buildManifest = this._project.getBuildManifest(); + if (!buildManifest) { + return null; + } + const timeDiff = (new Date().getTime() - new Date(buildManifest.timestamp).getTime()); + + // TODO: Format age properly via a new @ui5/logger util module + return { + timestamp: buildManifest.timestamp, + age: timeDiff / 1000 + " seconds" + }; + } +} + +export default ProjectBuildContext; diff --git a/packages/project/lib/build/helpers/ProjectBuilderOutputStyle.js b/packages/project/lib/build/helpers/ProjectBuilderOutputStyle.js new file mode 100644 index 00000000000..62d0bbb674f --- /dev/null +++ b/packages/project/lib/build/helpers/ProjectBuilderOutputStyle.js @@ -0,0 +1,18 @@ +/** + * Processes build results into a specific directory structure. + * + * @public + * @readonly + * @enum {string} + * @property {string} Default The default directory structure for every project type. + * For applications this is identical to "Flat" and for libraries to "Namespace". + * Other types have a more distinct default output style. + * @property {string} Flat Omits the project namespace and the "resources" directory. + * @property {string} Namespace Respects project namespace and the "resources" directory. + * @module @ui5/project/build/ProjectBuilderOutputStyle + */ +export default { + Default: "Default", + Flat: "Flat", + Namespace: "Namespace" +}; diff --git a/packages/project/lib/build/helpers/TaskUtil.js b/packages/project/lib/build/helpers/TaskUtil.js new file mode 100644 index 00000000000..661dc04ae07 --- /dev/null +++ b/packages/project/lib/build/helpers/TaskUtil.js @@ -0,0 +1,359 @@ +import { + createReaderCollection, + createReaderCollectionPrioritized, + createResource, + createFilterReader, + createLinkReader, + createFlatReader +} from "@ui5/fs/resourceFactory"; + +/** + * Convenience functions for UI5 tasks. + * An instance of this class is passed to every standard UI5 task that requires it. + * + * Custom tasks that define a specification version >= 2.2 will receive an interface + * to an instance of this class when called. + * The set of available functions on that interface depends on the specification + * version defined for the extension. + * + * @public + * @class + * @alias @ui5/project/build/helpers/TaskUtil + * @hideconstructor + */ +class TaskUtil { + /** + * Standard Build Tags. See UI5 CLI + * [RFC 0008]{@link https://github.com/UI5/cli/blob/main/rfcs/0008-resource-tagging-during-build.md} + * for details. + * + * @public + * @typedef {object} @ui5/project/build/helpers/TaskUtil~StandardBuildTags + * @property {string} OmitFromBuildResult + * Setting this tag to true will prevent the resource from being written to the build target directory + * @property {string} IsBundle + * This tag identifies resources that contain (i.e. bundle) multiple other resources + * @property {string} IsDebugVariant + * This tag identifies resources that are a debug variant (typically named with a "-dbg" suffix) + * of another resource. This tag is part of the build manifest. + * @property {string} HasDebugVariant + * This tag identifies resources for which a debug variant has been created. + * This tag is part of the build manifest. + */ + + /** + * Since @ui5/project/build/helpers/ProjectBuildContext is a private class, TaskUtil must not be + * instantiated by modules other than @ui5/project itself. + * + * @param {object} parameters + * @param {@ui5/project/build/helpers/ProjectBuildContext} parameters.projectBuildContext ProjectBuildContext + * @public + */ + constructor({projectBuildContext}) { + this._projectBuildContext = projectBuildContext; + /** + * @member {@ui5/project/build/helpers/TaskUtil~StandardBuildTags} + * @public + */ + this.STANDARD_TAGS = Object.freeze({ + // "Project" tags: + // Will be stored on project instance and are hence part of the build manifest + IsDebugVariant: "ui5:IsDebugVariant", + HasDebugVariant: "ui5:HasDebugVariant", + + // "Build" tags: + // Will be stored on the project build context + // They are only available to the build tasks of a single project + OmitFromBuildResult: "ui5:OmitFromBuildResult", + IsBundle: "ui5:IsBundle" + }); + } + + /** + * Stores a tag with value for a given resource's path. Note that the tag is independent of the supplied + * resource instance. For two resource instances with the same path, the same tag value is returned. + * If the path of a resource is changed, any tag information previously stored for that resource is lost. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 2.2 and above. + * + * @param {@ui5/fs/Resource} resource Resource-instance the tag should be stored for + * @param {string} tag Name of the tag. Currently only the + * [STANDARD_TAGS]{@link @ui5/project/build/helpers/TaskUtil#STANDARD_TAGS} are allowed + * @param {string|boolean|integer} [value=true] Tag value. Must be primitive + * @public + */ + setTag(resource, tag, value) { + if (typeof resource === "string") { + throw new Error("Deprecated parameter: " + + "Since UI5 CLI 3.0, #setTag requires a resource instance. Strings are no longer accepted"); + } + + const collection = this._projectBuildContext.getResourceTagCollection(resource, tag); + return collection.setTag(resource, tag, value); + } + + /** + * Retrieves the value for a stored tag. If no value is stored, undefined is returned. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 2.2 and above. + * + * @param {@ui5/fs/Resource} resource Resource-instance the tag should be retrieved for + * @param {string} tag Name of the tag + * @returns {string|boolean|integer|undefined} Tag value for the given resource. + * undefined if no value is available + * @public + */ + getTag(resource, tag) { + if (typeof resource === "string") { + throw new Error("Deprecated parameter: " + + "Since UI5 CLI 3.0, #getTag requires a resource instance. Strings are no longer accepted"); + } + const collection = this._projectBuildContext.getResourceTagCollection(resource, tag); + return collection.getTag(resource, tag); + } + + /** + * Clears the value of a tag stored for the given resource's path. + * It's like the tag was never set for that resource. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 2.2 and above. + * + * @param {@ui5/fs/Resource} resource Resource-instance the tag should be cleared for + * @param {string} tag Tag + * @public + */ + clearTag(resource, tag) { + if (typeof resource === "string") { + throw new Error("Deprecated parameter: " + + "Since UI5 CLI 3.0, #clearTag requires a resource instance. Strings are no longer accepted"); + } + const collection = this._projectBuildContext.getResourceTagCollection(resource, tag); + return collection.clearTag(resource, tag); + } + + /** + * Check whether the project currently being built is the root project. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 2.2 and above. + * + * @returns {boolean} true if the currently built project is the root project + * @public + */ + isRootProject() { + return this._projectBuildContext.isRootProject(); + } + + /** + * Retrieves a build option defined by its keykey is stored, undefined is returned. + * + * @param {string} key The option key + * @returns {any|undefined} The build option (or undefined) + * @private + */ + getBuildOption(key) { + return this._projectBuildContext.getOption(key); + } + + /** + * Callback that is executed once the build has finished + * + * @public + * @callback @ui5/project/build/helpers/TaskUtil~cleanupTaskCallback + * @param {boolean} force Whether the cleanup callback should + * gracefully wait for certain jobs to be completed (false) + * or enforce immediate termination (true) + */ + + /** + * Register a function that must be executed once the build is finished. This can be used to, for example, + * clean up files temporarily created on the file system. If the callback returns a Promise, it will be waited for. + * It will also be executed in cases where the build has failed or has been aborted. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 2.2 and above. + * + * @param {@ui5/project/build/helpers/TaskUtil~cleanupTaskCallback} callback Callback to + * register; it will be waited for if it returns a Promise + * @public + */ + registerCleanupTask(callback) { + return this._projectBuildContext.registerCleanupTask(callback); + } + + /** + * Specification Version-dependent [Project]{@link @ui5/project/specifications/Project} interface. + * For details on individual functions, see [Project]{@link @ui5/project/specifications/Project} + * + * @public + * @typedef {object} @ui5/project/build/helpers/TaskUtil~ProjectInterface + * @property {Function} getType Get the project type + * @property {Function} getName Get the project name + * @property {Function} getVersion Get the project version + * @property {Function} getNamespace Get the project namespace + * @property {Function} getRootReader Get the project rootReader + * @property {Function} getReader Get the project reader + * @property {Function} getRootPath Get the local File System path of the project's root directory + * @property {Function} getSourcePath Get the local File System path of the project's source directory + * @property {Function} getCustomConfiguration Get the project Custom Configuration + * @property {Function} isFrameworkProject Check whether the project is a UI5-Framework project + * @property {Function} getFrameworkName Get the project's framework name configuration + * @property {Function} getFrameworkVersion Get the project's framework version configuration + * @property {Function} getFrameworkDependencies Get the project's framework dependencies configuration + */ + + /** + * Retrieve a single project from the dependency graph + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 3.0 and above. + * + * @param {string|@ui5/fs/Resource} [projectNameOrResource] + * Name of the project to retrieve or a Resource instance to retrieve the associated project for. + * Defaults to the name of the project currently being built + * @returns {@ui5/project/build/helpers/TaskUtil~ProjectInterface|undefined} + * Specification Version-dependent interface to the Project instance or undefined + * if the project name is unknown or the provided resource is not associated with any project. + * @public + */ + getProject(projectNameOrResource) { + if (projectNameOrResource) { + if (typeof projectNameOrResource === "string" || projectNameOrResource instanceof String) { + // A project name has been provided + return this._projectBuildContext.getProject(projectNameOrResource); + } else { + // A Resource instance has been provided + return projectNameOrResource.getProject(); + } + } + // No parameter has been provided, default to the project currently being built. + return this._projectBuildContext.getProject(); + } + + /** + * Retrieve a list of direct dependencies of a given project from the dependency graph. + * Note that this list does not include transitive dependencies. + * + *

+ * This method is only available to custom task extensions defining + * Specification Version 3.0 and above. + * + * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @returns {string[]} Names of all direct dependencies + * @throws {Error} If the requested project is unknown to the graph + * @public + */ + getDependencies(projectName) { + return this._projectBuildContext.getDependencies(projectName); + } + + /** + * Specification Version-dependent set of [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory} + * functions provided to tasks. + * For details on individual functions, see [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory} + * + * @public + * @typedef {object} @ui5/project/build/helpers/TaskUtil~resourceFactory + * @property {Function} createResource Creates a [Resource]{@link @ui5/fs/Resource}. + * Accepts the same parameters as the [Resource]{@link @ui5/fs/Resource} constructor. + * @property {Function} createReaderCollection Creates a reader collection: + * [ReaderCollection]{@link @ui5/fs/ReaderCollection} + * @property {Function} createReaderCollectionPrioritized Creates a prioritized reader collection: + * [ReaderCollectionPrioritized]{@link @ui5/fs/ReaderCollectionPrioritized} + * @property {Function} createFilterReader + * Create a [Filter-Reader]{@link @ui5/fs/readers/Filter} with the given reader. + * @property {Function} createLinkReader + * Create a [Link-Reader]{@link @ui5/fs/readers/Filter} with the given reader. + * @property {Function} createFlatReader Create a [Link-Reader]{@link @ui5/fs/readers/Link} + * where all requests are prefixed with /resources/. + */ + + /** + * Provides limited access to [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory} functions + * + *

+ * This attribute is only available to custom task extensions defining + * Specification Version 3.0 and above. + * + * @type {@ui5/project/build/helpers/TaskUtil~resourceFactory} + * @public + */ + resourceFactory = { + createResource, + createReaderCollection, + createReaderCollectionPrioritized, + createFilterReader, + createLinkReader, + createFlatReader, + }; + + /** + * Get an interface to an instance of this class that only provides those functions + * that are supported by the given custom task extension specification version. + * + * @param {@ui5/project/specifications/SpecificationVersion} specVersion + * SpecVersionComparator instance of the custom task + * @returns {object} An object with bound instance methods supported by the given specification version + */ + getInterface(specVersion) { + if (specVersion.lte("2.1")) { + // Tasks defining specVersion <= 2.1 do not have access to any TaskUtil APIs + return undefined; + } + + const baseInterface = { + STANDARD_TAGS: this.STANDARD_TAGS, + }; + bindFunctions(this, baseInterface, [ + "setTag", "clearTag", "getTag", "isRootProject", "registerCleanupTask" + ]); + + if (specVersion.gte("3.0")) { + // getProject function, returning an interfaced project instance + baseInterface.getProject = (projectName) => { + const project = this.getProject(projectName); + const baseProjectInterface = {}; + bindFunctions(project, baseProjectInterface, [ + "getType", "getName", "getVersion", "getNamespace", + "getRootReader", "getReader", "getRootPath", "getSourcePath", + "getCustomConfiguration", "isFrameworkProject", "getFrameworkName", + "getFrameworkVersion", "getFrameworkDependencies" + ]); + return baseProjectInterface; + }; + // getDependencies function, returning an array of project names + baseInterface.getDependencies = (projectName) => { + return this.getDependencies(projectName); + }; + + baseInterface.resourceFactory = Object.create(null); + [ + // Once new functions get added, extract this array into a variable + // and enhance based on spec version once new functions get added + "createResource", "createReaderCollection", "createReaderCollectionPrioritized", + "createFilterReader", "createLinkReader", "createFlatReader", + ].forEach((factoryFunction) => { + baseInterface.resourceFactory[factoryFunction] = this.resourceFactory[factoryFunction]; + }); + } + return baseInterface; + } +} + +function bindFunctions(sourceObject, targetObject, funcNames) { + funcNames.forEach((funcName) => { + targetObject[funcName] = sourceObject[funcName].bind(sourceObject); + }); +} + +export default TaskUtil; diff --git a/packages/project/lib/build/helpers/composeProjectList.js b/packages/project/lib/build/helpers/composeProjectList.js new file mode 100644 index 00000000000..d98ad17929e --- /dev/null +++ b/packages/project/lib/build/helpers/composeProjectList.js @@ -0,0 +1,163 @@ +import {getLogger} from "@ui5/logger"; +const log = getLogger("build:helpers:composeProjectList"); + +/** + * Creates an object containing the flattened project dependency tree. Each dependency is defined as an object key while + * its value is an array of all of its transitive dependencies. + * + * @param {@ui5/project/graph/ProjectGraph} graph + * @returns {Promise>} A promise resolving to an object with dependency names as + * key and each with an array of its transitive dependencies as value + */ +async function getFlattenedDependencyTree(graph) { + const dependencyMap = Object.create(null); + const rootName = graph.getRoot().getName(); + + await graph.traverseDepthFirst(({project, dependencies}) => { + if (project.getName() === rootName) { + // Skip root project + return; + } + const projectDeps = []; + dependencies.forEach((depName) => { + projectDeps.push(depName); + if (dependencyMap[depName]) { + projectDeps.push(...dependencyMap[depName]); + } + }); + dependencyMap[project.getName()] = projectDeps; + }); + return dependencyMap; +} + +/** + * Creates dependency lists for 'includedDependencies' and 'excludedDependencies'. + * + * See [ProjectBuilder~DependencyIncludes]{@link @ui5/project/build/ProjectBuilder~DependencyIncludes} + * for a detailed JSDoc. + * + * @param {@ui5/project/graph/ProjectGraph} graph + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} dependencyIncludes + * @returns {{includedDependencies:string[],excludedDependencies:string[]}} An object containing the + * 'includedDependencies' and 'excludedDependencies' + */ +async function createDependencyLists(graph, { + includeAllDependencies = false, + includeDependency = [], includeDependencyRegExp = [], includeDependencyTree = [], + excludeDependency = [], excludeDependencyRegExp = [], excludeDependencyTree = [], + defaultIncludeDependency = [], defaultIncludeDependencyRegExp = [], defaultIncludeDependencyTree = [] +}) { + if ( + !includeAllDependencies && + !includeDependency.length && !includeDependencyRegExp.length && !includeDependencyTree.length && + !excludeDependency.length && !excludeDependencyRegExp.length && !excludeDependencyTree.length && + !defaultIncludeDependency.length && !defaultIncludeDependencyRegExp.length && + !defaultIncludeDependencyTree.length + ) { + return {includedDependencies: [], excludedDependencies: []}; + } + + const flattenedDependencyTree = await getFlattenedDependencyTree(graph); + + function isExcluded(excludeList, depName) { + return excludeList && excludeList.has(depName); + } + function processDependencies({targetList, dependencies, dependenciesRegExp = [], excludeList, handleSubtree}) { + if (handleSubtree && dependenciesRegExp.length) { + throw new Error("dependenciesRegExp can't be combined with handleSubtree:true option"); + } + dependencies.forEach((depName) => { + if (depName === "*") { + targetList.add(depName); + } else if (flattenedDependencyTree[depName]) { + if (!isExcluded(excludeList, depName)) { + targetList.add(depName); + } + if (handleSubtree) { + flattenedDependencyTree[depName].forEach((dep) => { + if (!isExcluded(excludeList, dep)) { + targetList.add(dep); + } + }); + } + } else { + log.warn( + `Could not find dependency "${depName}" for project ${graph.getRoot().getName()}. ` + + `Dependency filter is ignored`); + } + }); + dependenciesRegExp.map((exp) => new RegExp(exp)).forEach((regExp) => { + for (const depName in flattenedDependencyTree) { + if (regExp.test(depName) && !isExcluded(excludeList, depName)) { + targetList.add(depName); + } + } + }); + } + + const includedDependencies = new Set(); + const excludedDependencies = new Set(); + + // add dependencies defined in includeDependency and includeDependencyRegExp to the list of includedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependency, + dependenciesRegExp: includeDependencyRegExp + }); + // add dependencies defined in excludeDependency and excludeDependencyRegExp to the list of excludedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependency, + dependenciesRegExp: excludeDependencyRegExp + }); + // add dependencies defined in includeDependencyTree with their transitive dependencies to the list of + // includedDependencies; due to prioritization only those dependencies are added which are not excluded + // by excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + // add dependencies defined in excludeDependencyTree with their transitive dependencies to the list of + // excludedDependencies; due to prioritization only those dependencies are added which are not excluded + // by includedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependencyTree, + excludeList: includedDependencies, + handleSubtree: true + }); + // due to the lower priority only add the dependencies defined in build settings if they are not excluded + // by any other dependency defined in excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependency, + dependenciesRegExp: defaultIncludeDependencyRegExp, + excludeList: excludedDependencies + }); + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + + if (includeAllDependencies) { + // If requested, add all dependencies not excluded to include set + Object.keys(flattenedDependencyTree).forEach((depName) => { + if (!isExcluded(excludedDependencies, depName)) { + includedDependencies.add(depName); + } + }); + } + + return { + includedDependencies: Array.from(includedDependencies), + excludedDependencies: Array.from(excludedDependencies) + }; +} + +createDependencyLists._getFlattenedDependencyTree = getFlattenedDependencyTree; + +export default createDependencyLists; diff --git a/packages/project/lib/build/helpers/composeTaskList.js b/packages/project/lib/build/helpers/composeTaskList.js new file mode 100644 index 00000000000..11a23d39dcb --- /dev/null +++ b/packages/project/lib/build/helpers/composeTaskList.js @@ -0,0 +1,96 @@ +/** + * Creates the list of tasks to be executed by the build process + * + * Sets specific tasks to be disabled by default, these tasks need to be included explicitly. + * Based on the selected build mode (selfContained|preload), different tasks are enabled. + * Tasks can be enabled or disabled. The wildcard * is also supported and affects all tasks. + * + * @private + * @param {string[]} allTasks + * @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} buildConfig + * Build configuration + * @returns {Array} List of tasks to be executed + */ +export default function composeTaskList(allTasks, {selfContained, jsdoc, includedTasks, excludedTasks}) { + let selectedTasks = allTasks.reduce((list, key) => { + list[key] = true; + return list; + }, {}); + + // Exclude non default tasks + selectedTasks.generateStandaloneAppBundle = false; + selectedTasks.transformBootstrapHtml = false; + selectedTasks.generateJsdoc = false; + selectedTasks.executeJsdocSdkTransformation = false; + selectedTasks.generateCachebusterInfo = false; + selectedTasks.generateApiIndex = false; + selectedTasks.generateThemeDesignerResources = false; + selectedTasks.generateVersionInfo = false; + + // Disable generateResourcesJson due to performance. + // When executed it analyzes each module's AST and therefore + // takes up much time (~10% more) + selectedTasks.generateResourcesJson = false; + + if (selfContained) { + // No preloads, bundle only + selectedTasks.generateComponentPreload = false; + selectedTasks.generateStandaloneAppBundle = true; + selectedTasks.transformBootstrapHtml = true; + selectedTasks.generateLibraryPreload = false; + } + + if (jsdoc) { + // Include JSDoc tasks + selectedTasks.generateJsdoc = true; + selectedTasks.executeJsdocSdkTransformation = true; + selectedTasks.generateApiIndex = true; + selectedTasks.generateVersionInfo = true; + + // Include theme build as required for SDK + selectedTasks.buildThemes = true; + + // Exclude all tasks not relevant to JSDoc generation + selectedTasks.replaceCopyright = false; + selectedTasks.replaceVersion = false; + selectedTasks.replaceBuildtime = false; + selectedTasks.generateComponentPreload = false; + selectedTasks.generateLibraryPreload = false; + selectedTasks.generateLibraryManifest = false; + selectedTasks.minify = false; + selectedTasks.generateFlexChangesBundle = false; + } + + // Exclude tasks + for (let i = 0; i < excludedTasks.length; i++) { + const taskName = excludedTasks[i]; + if (taskName === "*") { + Object.keys(selectedTasks).forEach((sKey) => { + selectedTasks[sKey] = false; + }); + break; + } + if (selectedTasks[taskName] === true) { + selectedTasks[taskName] = false; + } + } + + // Include tasks + for (let i = 0; i < includedTasks.length; i++) { + const taskName = includedTasks[i]; + if (taskName === "*") { + Object.keys(selectedTasks).forEach((sKey) => { + selectedTasks[sKey] = true; + }); + break; + } + if (selectedTasks[taskName] === false) { + selectedTasks[taskName] = true; + } + } + + // Filter only for tasks that will be executed + selectedTasks = Object.keys(selectedTasks).filter((task) => selectedTasks[task]); + + return selectedTasks; +} diff --git a/packages/project/lib/build/helpers/createBuildManifest.js b/packages/project/lib/build/helpers/createBuildManifest.js new file mode 100644 index 00000000000..998935b3c05 --- /dev/null +++ b/packages/project/lib/build/helpers/createBuildManifest.js @@ -0,0 +1,85 @@ +import {createRequire} from "node:module"; + +// Using CommonsJS require since JSON module imports are still experimental +const require = createRequire(import.meta.url); + +async function getVersion(pkg) { + return require(`${pkg}/package.json`).version; +} + +function getSortedTags(project) { + const tags = project.getResourceTagCollection().getAllTags(); + const entities = Object.entries(tags); + entities.sort(([keyA], [keyB]) => { + return keyA.localeCompare(keyB); + }); + return Object.fromEntries(entities); +} + +export default async function(project, buildConfig, taskRepository) { + if (!project) { + throw new Error(`Missing parameter 'project'`); + } + if (!buildConfig) { + throw new Error(`Missing parameter 'buildConfig'`); + } + if (!taskRepository) { + throw new Error(`Missing parameter 'taskRepository'`); + } + const projectName = project.getName(); + const type = project.getType(); + + const pathMapping = Object.create(null); + switch (type) { + case "application": + pathMapping.webapp = `resources/${project.getNamespace()}`; + break; + case "library": + case "theme-library": + pathMapping.src = `resources`; + pathMapping.test = `test-resources`; + break; + default: + throw new Error( + `Unable to create archive metadata for project ${project.getName()}: ` + + `Project type ${type} is currently not supported`); + } + + const {builderVersion, fsVersion: builderFsVersion} = await taskRepository.getVersions(); + const metadata = { + project: { + specVersion: project.getSpecVersion().toString(), + type, + metadata: { + name: projectName, + }, + resources: { + configuration: { + paths: pathMapping + } + } + }, + buildManifest: { + manifestVersion: "0.2", + timestamp: new Date().toISOString(), + versions: { + builderVersion: builderVersion, + projectVersion: await getVersion("@ui5/project"), + fsVersion: await getVersion("@ui5/fs"), + }, + buildConfig, + version: project.getVersion(), + namespace: project.getNamespace(), + tags: getSortedTags(project) + } + }; + + if (metadata.buildManifest.versions.fsVersion !== builderFsVersion) { + // Added in manifestVersion 0.2: + // @ui5/project and @ui5/builder use different versions of @ui5/fs. + // This should be mentioned in the build manifest: + metadata.buildManifest.versions.builderFsVersion = builderFsVersion; + } + + return metadata; +} diff --git a/packages/project/lib/config/Configuration.js b/packages/project/lib/config/Configuration.js new file mode 100644 index 00000000000..c6961d3b4ce --- /dev/null +++ b/packages/project/lib/config/Configuration.js @@ -0,0 +1,137 @@ +import path from "node:path"; +import os from "node:os"; + +/** + * Provides basic configuration for @ui5/project. + * Reads/writes configuration from/to ~/.ui5rc + * + * @public + * @class + * @alias @ui5/project/config/Configuration + */ +class Configuration { + /** + * A list of all configuration options. + * + * @public + * @static + */ + static OPTIONS = [ + "mavenSnapshotEndpointUrl", + "ui5DataDir" + ]; + + #options = new Map(); + + /** + * @param {object} configuration + * @param {string} [configuration.mavenSnapshotEndpointUrl] + * @param {string} [configuration.ui5DataDir] + */ + constructor(configuration) { + // Initialize map with undefined values for every option so that they are + // returned via toJson() + Configuration.OPTIONS.forEach((key) => this.#options.set(key, undefined)); + + Object.entries(configuration).forEach(([key, value]) => { + if (!Configuration.OPTIONS.includes(key)) { + throw new Error(`Unknown configuration option '${key}'`); + } + this.#options.set(key, value); + }); + } + + /** + * Maven Repository Snapshot URL. + * Used to download artifacts and packages from Maven's build-snapshots URL. + * + * @public + * @returns {string} + */ + getMavenSnapshotEndpointUrl() { + return this.#options.get("mavenSnapshotEndpointUrl"); + } + + /** + * Configurable directory where the framework artefacts are stored. + * + * @public + * @returns {string} + */ + getUi5DataDir() { + return this.#options.get("ui5DataDir"); + } + + /** + * @public + * @returns {object} The configuration in a JSON format + */ + toJson() { + return Object.fromEntries(this.#options); + } + + /** + * Creates Configuration from a JSON file + * + * @public + * @static + * @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file + * @returns {Promise<@ui5/project/config/Configuration>} Configuration instance + */ + static async fromFile(filePath) { + filePath = filePath || path.resolve(path.join(os.homedir(), ".ui5rc")); + + const {default: fs} = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const readFile = promisify(fs.readFile); + let config; + try { + const fileContent = await readFile(filePath); + if (!fileContent.length) { + config = {}; + } else { + config = JSON.parse(fileContent); + } + } catch (err) { + if (err.code === "ENOENT") { + // "File or directory does not exist" + config = {}; + } else { + throw new Error( + `Failed to read UI5 CLI configuration from ${filePath}: ${err.message}`, { + cause: err + }); + } + } + + return new Configuration(config); + } + + /** + * Saves Configuration to a JSON file + * + * @public + * @static + * @param {@ui5/project/config/Configuration} config Configuration to save + * @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file + * @returns {Promise} + */ + static async toFile(config, filePath) { + filePath = filePath || path.resolve(path.join(os.homedir(), ".ui5rc")); + + const {default: fs} = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const writeFile = promisify(fs.writeFile); + + try { + return writeFile(filePath, JSON.stringify(config.toJson())); + } catch (err) { + throw new Error( + `Failed to write UI5 CLI configuration to ${filePath}: ${err.message}`, { + cause: err + }); + } + } +} + +export default Configuration; diff --git a/packages/project/lib/graph/Module.js b/packages/project/lib/graph/Module.js new file mode 100644 index 00000000000..9609a49d456 --- /dev/null +++ b/packages/project/lib/graph/Module.js @@ -0,0 +1,421 @@ +import fs from "graceful-fs"; +import path from "node:path"; +import {promisify} from "node:util"; +const readFile = promisify(fs.readFile); +import jsyaml from "js-yaml"; +import {createReader} from "@ui5/fs/resourceFactory"; +import Specification from "../specifications/Specification.js"; +import {validate} from "../validation/validator.js"; + +import {getLogger} from "@ui5/logger"; + +const log = getLogger("graph:Module"); + +const DEFAULT_CONFIG_PATH = "ui5.yaml"; +const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} +/** + * Raw representation of a UI5 Project. A module can contain zero to one projects and n extensions. + * This class is intended for private use by the + * [@ui5/project/graph/ProjectGraphBuilder]{@link @ui5/project/graph/ProjectGraphBuilder} module + * + * @private + * @class + * @alias @ui5/project/graph/Module + */ +class Module { + /** + * @param {object} parameters Module parameters + * @param {string} parameters.id Unique ID for the module + * @param {string} parameters.version Version of the module + * @param {string} parameters.modulePath Absolute File System path of the module + * @param {string} [parameters.configPath=ui5.yaml] + * Either a path relative to `modulePath` which will be resolved by @ui5/fs (default), + * or an absolute File System path to the configuration file. + * @param {object|object[]} [parameters.configuration] + * Configuration object or array of objects to use. If supplied, no configuration files + * will be read and the `configPath` option must not be provided. + * @param {@ui5/project/graph.ShimCollection} [parameters.shimCollection] + * Collection of shims that might be relevant for this module + */ + constructor({id, version, modulePath, configPath, configuration = [], shimCollection}) { + if (!id) { + throw new Error(`Could not create Module: Missing or empty parameter 'id'`); + } + if (!version) { + throw new Error(`Could not create Module: Missing or empty parameter 'version'`); + } + if (!modulePath) { + throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`); + } + if (!path.isAbsolute(modulePath)) { + throw new Error(`Could not create Module: Parameter 'modulePath' must contain an absolute path`); + } + if ( + ( + (Array.isArray(configuration) && configuration.length > 0) || + (!Array.isArray(configuration) && typeof configuration === "object") + ) && configPath + ) { + throw new Error( + `Could not create Module: 'configPath' must not be provided in combination with 'configuration'` + ); + } + + this._id = id; + this._version = version; + this._modulePath = modulePath; + this._configPath = configPath || DEFAULT_CONFIG_PATH; + this._dependencies = Object.create(null); + + if (!Array.isArray(configuration)) { + configuration = [configuration]; + } + this._suppliedConfigs = configuration; + + if (shimCollection) { + // Retrieve and clone shims in constructor + // Shims added to the collection at a later point in time should not be applied in this module + const shims = shimCollection.getProjectConfigurationShims(this.getId()); + if (shims && shims.length) { + this._projectConfigShims = clone(shims); + } + } + } + + getId() { + return this._id; + } + + getVersion() { + return this._version; + } + + getPath() { + return this._modulePath; + } + + /** + * Specifications found in the module + * + * @private + * @typedef {object} @ui5/project/graph/Module~SpecificationsResult + * @property {@ui5/project/specifications/Project|null} Project found in the module (if one is found) + * @property {@ui5/project/specifications/Extension[]} Array of extensions found in the module + */ + + /** + * Get any available project and extensions of the module + * + * @returns {@ui5/project/graph/Module~SpecificationsResult} Project and extensions found in the module + */ + async getSpecifications() { + if (this._pGetSpecifications) { + return this._pGetSpecifications; + } + + return this._pGetSpecifications = this._getSpecifications(); + } + + async _getSpecifications() { + // Retrieve all configurations available for this module + let configs = await this._getConfigurations(); + + // Edge case: + // Search for project-shims to check whether this module defines a collection for itself + const isCollection = configs.find((configuration) => { + if (configuration.kind === "extension" && configuration.type === "project-shim") { + // TODO create Specification instance and ask it for the configuration + if (configuration.shims && configuration.shims.collections && + configuration.shims.collections[this.getId()]) { + return true; + } + } + }); + + if (isCollection) { + // This module is configured as a collection + // For compatibility reasons with the behavior of projectPreprocessor, + // the project contained in this module must be ignored + configs = configs.filter((configuration) => { + return configuration.kind !== "project"; + }); + } else { + // Patch configs + configs.forEach((configuration) => { + if (configuration.kind === "project" && configuration.type === "library" && + configuration.metadata && configuration.metadata.name) { + const libraryName = configuration.metadata.name; + // Old theme-libraries where configured as type "library" + if (SAP_THEMES_NS_EXEMPTIONS.includes(libraryName)) { + configuration.type = "theme-library"; + } + } + }); + } + + const specs = await Promise.all(configs.map(async (configuration) => { + const buildManifest = configuration._buildManifest; + if (configuration._buildManifest) { + delete configuration._buildManifest; + } + const spec = await Specification.create({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration, + buildManifest + }); + + log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); + return spec; + })); + + const projects = specs.filter((spec) => { + return spec.getKind() === "project"; + }); + + const extensions = specs.filter((spec) => { + return spec.getKind() === "extension"; + }); + + if (projects.length > 1) { + throw new Error( + `Found ${projects.length} configurations of kind 'project' for ` + + `module ${this.getId()}. There must be only one project per module.`); + } + + return { + project: projects[0] || null, + extensions + }; + } + + /** + * Configuration + */ + async _getConfigurations() { + let configurations; + + configurations = await this._getSuppliedConfigurations(); + + if (!configurations || !configurations.length) { + configurations = await this._getBuildManifestConfigurations(); + } + if (!configurations || !configurations.length) { + configurations = await this._getYamlConfigurations(); + } + if (!configurations || !configurations.length) { + configurations = await this._getShimConfigurations(); + } + return configurations || []; + } + + _normalizeAndApplyShims(config) { + this._normalizeConfig(config); + + if (config.kind === "project") { + this._applyProjectShims(config); + } + return config; + } + + async _createConfigurationFromShim() { + const config = this._applyProjectShims(); + if (config) { + this._normalizeConfig(config); + return config; + } + } + + _applyProjectShims(config = {}) { + if (!this._projectConfigShims) { + return; + } + this._projectConfigShims.forEach(({name, shim}) => { + log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); + Object.assign(config, shim); + }); + return config; + } + + async _getSuppliedConfigurations() { + if (this._suppliedConfigs.length) { + log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); + return await Promise.all(this._suppliedConfigs.map(async (suppliedConfig) => { + let config = suppliedConfig; + + // If we got supplied with a build manifest object, we need to move the build manifest metadata + // into the project and only return the project + if (suppliedConfig.buildManifest) { + config = suppliedConfig.project; + config._buildManifest = suppliedConfig.buildManifest; + } + return this._normalizeAndApplyShims(config); + })); + } + } + + async _getShimConfigurations() { + // No project configuration found + // => Try to create one from shims + const shimConfiguration = await this._createConfigurationFromShim(); + if (shimConfiguration) { + log.verbose(`Created configuration from shim extensions for module ${this.getId()}`); + return [shimConfiguration]; + } + } + + async _getYamlConfigurations() { + const configs = await this._readConfigFile(); + + if (!configs || !configs.length) { + log.verbose(`Could not find a configuration file for module ${this.getId()}`); + return []; + } + + return await Promise.all(configs.map((config) => { + return this._normalizeAndApplyShims(config); + })); + } + + async _readConfigFile() { + const configPath = this._configPath; + let configFile; + if (path.isAbsolute(configPath)) { + // Handle absolute file paths with the native FS module + try { + configFile = await readFile(configPath, {encoding: "utf8"}); + } catch (err) { + // TODO: Caller might wants to ignore exceptions for ENOENT errors for non-root projects + // However, this decision should not be made here + throw new Error("Failed to read configuration for module " + + `${this.getId()} at '${configPath}'. Error: ${err.message}`); + } + } else { + // Handle relative file paths with the @ui5/fs (virtual) file system + const reader = this.getReader(); + let configResource; + try { + configResource = await reader.byPath(path.posix.join("/", configPath)); + } catch (err) { + throw new Error("Failed to read configuration for module " + + `${this.getId()} at "${configPath}". Error: ${err.message}`); + } + if (!configResource) { + if (configPath !== DEFAULT_CONFIG_PATH) { + throw new Error("Failed to read configuration for module " + + `${this.getId()}: Could not find configuration file in module at path '${configPath}'`); + } + return null; + } + configFile = await configResource.getString(); + } + + let configs; + + try { + // Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename". + // safeLoadAll doesn't handle its parameters properly. + // See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381 + configs = jsyaml.loadAll(configFile, undefined, { + filename: configPath, + schema: jsyaml.DEFAULT_SAFE_SCHEMA + }); + } catch (err) { + if (err.name === "YAMLException") { + throw new Error("Failed to parse configuration for project " + + `${this.getId()} at '${configPath}'\nError: ${err.message}`); + } else { + throw err; + } + } + + if (!configs || !configs.length) { + // No configs found => exit here + return configs; + } + + // Validate found configurations with schema + // Validation is done again in the Specification class. But here we can reference the YAML file + // which adds helpful information like the line number + const validationResults = await Promise.all( + configs.map(async (config, documentIndex) => { + // Catch validation errors to ensure proper order of rejections within Promise.all + try { + await validate({ + config, + project: { + id: this.getId() + }, + yaml: { + path: configPath, + source: configFile, + documentIndex + } + }); + } catch (error) { + return error; + } + }) + ); + + const validationErrors = validationResults.filter(($) => $); + + if (validationErrors.length > 0) { + // Throw any validation errors + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + + log.verbose(`Configuration for module ${this.getId()} is provided in YAML file at ${configPath}`); + return configs; + } + + async _getBuildManifestConfigurations() { + const buildManifestMetadata = await this._readBuildManifest(); + + if (!buildManifestMetadata) { + log.verbose(`Could not find any build manifest in module ${this.getId()}`); + return []; + } + log.verbose(`Configuration for module ${this.getId()} is provided in build manifest`); + + // This function is expected to return the configuration of a project, so we add the buildManifest metadata + // to a temporary attribute of the project configuration and retrieve it later for Specification creation + const config = buildManifestMetadata.project; + config._buildManifest = buildManifestMetadata.buildManifest; + return [this._normalizeAndApplyShims(config)]; + } + + async _readBuildManifest() { + const reader = this.getReader(); + const buildManifestResource = await reader.byPath("/.ui5/build-manifest.json"); + if (buildManifestResource) { + return JSON.parse(await buildManifestResource.getString()); + } + } + + _normalizeConfig(config) { + if (!config.kind) { + config.kind = "project"; // default + } + return config; + } + + /** + * Resource Access + */ + getReader() { + return createReader({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Reader for module ${this.getId()}` + }); + } +} + +export default Module; diff --git a/packages/project/lib/graph/ProjectGraph.js b/packages/project/lib/graph/ProjectGraph.js new file mode 100644 index 00000000000..ba6967154e6 --- /dev/null +++ b/packages/project/lib/graph/ProjectGraph.js @@ -0,0 +1,649 @@ +import OutputStyleEnum from "../build/helpers/ProjectBuilderOutputStyle.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("graph:ProjectGraph"); + + +/** + * A rooted, directed graph representing a UI5 project, its dependencies and available extensions. + *

+ * While it allows defining cyclic dependencies, both traversal functions will throw an error if they encounter cycles. + * + * @public + * @class + * @alias @ui5/project/graph/ProjectGraph + */ +class ProjectGraph { + /** + * @public + * @param {object} parameters Parameters + * @param {string} parameters.rootProjectName Root project name + */ + constructor({rootProjectName}) { + if (!rootProjectName) { + throw new Error(`Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'`); + } + this._rootProjectName = rootProjectName; + + this._projects = new Map(); // maps project name to instance (= nodes) + this._adjList = new Map(); // maps project name to dependencies (= edges) + this._optAdjList = new Map(); // maps project name to optional dependencies (= edges) + + this._extensions = new Map(); // maps extension name to instance + + this._sealed = false; + this._hasUnresolvedOptionalDependencies = false; // Performance optimization flag + this._taskRepository = null; + } + + /** + * Get the root project of the graph + * + * @public + * @returns {@ui5/project/specifications/Project} Root project + */ + getRoot() { + const rootProject = this._projects.get(this._rootProjectName); + if (!rootProject) { + throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`); + } + return rootProject; + } + + /** + * Add a project to the graph + * + * @public + * @param {@ui5/project/specifications/Project} project Project which should be added to the graph + */ + addProject(project) { + this._checkSealed(); + const projectName = project.getName(); + if (this._projects.has(projectName)) { + throw new Error( + `Failed to add project ${projectName} to graph: A project with that name has already been added. ` + + `This might be caused by multiple modules containing projects with the same name`); + } + if (!isNaN(projectName)) { + // Reject integer-like project names. They would take precedence when traversing object keys which + // could lead to unexpected behavior. We don't really expect anyone to use such names anyways + throw new Error( + `Failed to add project ${projectName} to graph: Project name must not be integer-like`); + } + log.verbose(`Adding project: ${projectName}`); + this._projects.set(projectName, project); + this._adjList.set(projectName, new Set()); + this._optAdjList.set(projectName, new Set()); + } + + /** + * Retrieve a single project from the dependency graph + * + * @public + * @param {string} projectName Name of the project to retrieve + * @returns {@ui5/project/specifications/Project|undefined} + * project instance or undefined if the project is unknown to the graph + */ + getProject(projectName) { + return this._projects.get(projectName); + } + + /** + * Get all projects in the graph + * + * @public + * @returns {Iterable.<@ui5/project/specifications/Project>} + */ + getProjects() { + return this._projects.values(); + } + + /** + * Get names of all projects in the graph + * + * @public + * @returns {string[]} Names of all projects + */ + getProjectNames() { + return Array.from(this._projects.keys()); + } + + /** + * Get the number of projects in the graph + * + * @public + * @returns {integer} Count of projects in the graph + */ + getSize() { + return this._projects.size; + } + + /** + * Add an extension to the graph + * + * @public + * @param {@ui5/project/specifications/Extension} extension Extension which should be available in the graph + */ + addExtension(extension) { + this._checkSealed(); + const extensionName = extension.getName(); + if (this._extensions.has(extensionName)) { + throw new Error( + `Failed to add extension ${extensionName} to graph: ` + + `An extension with that name has already been added. ` + + `This might be caused by multiple modules containing extensions with the same name`); + } + if (!isNaN(extensionName)) { + // Reject integer-like extension names. They would take precedence when traversing object keys which + // might lead to unexpected behavior in the future. We don't really expect anyone to use such names anyways + throw new Error( + `Failed to add extension ${extensionName} to graph: Extension name must not be integer-like`); + } + this._extensions.set(extensionName, extension); + } + + /** + * @public + * @param {string} extensionName Name of the extension to retrieve + * @returns {@ui5/project/specifications/Extension|undefined} + * Extension instance or undefined if the extension is unknown to the graph + */ + getExtension(extensionName) { + return this._extensions.get(extensionName); + } + + /** + * Get all extensions in the graph + * + * @public + * @returns {Iterable.<@ui5/project/specifications/Extension>} + */ + getExtensions() { + return this._extensions.values(); + } + + /** + * Get names of all extensions in the graph + * + * @public + * @returns {string[]} Names of all extensions + */ + getExtensionNames() { + return Array.from(this._extensions.keys()); + } + + /** + * Declare a dependency from one project in the graph to another + * + * @public + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ + declareDependency(fromProjectName, toProjectName) { + this._checkSealed(); + try { + log.verbose(`Declaring dependency: ${fromProjectName} depends on ${toProjectName}`); + this._declareDependency(this._adjList, fromProjectName, toProjectName); + } catch (err) { + throw new Error( + `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + + err.message); + } + } + + + /** + * Declare a dependency from one project in the graph to another + * + * @public + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ + declareOptionalDependency(fromProjectName, toProjectName) { + this._checkSealed(); + try { + log.verbose(`Declaring optional dependency: ${fromProjectName} depends on ${toProjectName}`); + this._declareDependency(this._optAdjList, fromProjectName, toProjectName); + this._hasUnresolvedOptionalDependencies = true; + } catch (err) { + throw new Error( + `Failed to declare optional dependency from project ${fromProjectName} to ${toProjectName}: ` + + err.message); + } + } + + /** + * Declare a dependency from one project in the graph to another + * + * @param {object} map Adjacency map to use + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ + _declareDependency(map, fromProjectName, toProjectName) { + if (!this._projects.has(fromProjectName)) { + throw new Error( + `Unable to find depending project with name ${fromProjectName} in project graph`); + } + if (!this._projects.has(toProjectName)) { + throw new Error( + `Unable to find dependency project with name ${toProjectName} in project graph`); + } + if (fromProjectName === toProjectName) { + throw new Error( + `A project can't depend on itself`); + } + const adjacencies = map.get(fromProjectName); + if (adjacencies.has(toProjectName)) { + log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); + } else { + adjacencies.add(toProjectName); + } + } + + /** + * Get all direct dependencies of a project as an array of project names + * + * @public + * @param {string} projectName Name of the project to retrieve the dependencies of + * @returns {string[]} Names of all direct dependencies + */ + getDependencies(projectName) { + const adjacencies = this._adjList.get(projectName); + if (!adjacencies) { + throw new Error( + `Failed to get dependencies for project ${projectName}: ` + + `Unable to find project in project graph`); + } + return Array.from(adjacencies); + } + + /** + * Get all (direct and transitive) dependencies of a project as an array of project names + * + * @public + * @param {string} projectName Name of the project to retrieve the dependencies of + * @returns {string[]} Names of all direct and transitive dependencies + */ + getTransitiveDependencies(projectName) { + const dependencies = new Set(); + if (!this._projects.has(projectName)) { + throw new Error( + `Failed to get transitive dependencies for project ${projectName}: ` + + `Unable to find project in project graph`); + } + + const processDependency = (depName) => { + const adjacencies = this._adjList.get(depName); + adjacencies.forEach((depName) => { + if (!dependencies.has(depName)) { + dependencies.add(depName); + processDependency(depName); + } + }); + }; + + processDependency(projectName); + return Array.from(dependencies); + } + /** + * Checks whether a dependency is optional or not. + * Currently only used in tests. + * + * @private + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + * @returns {boolean} True if the dependency is currently optional + */ + isOptionalDependency(fromProjectName, toProjectName) { + const adjacencies = this._adjList.get(fromProjectName); + if (!adjacencies) { + throw new Error( + `Failed to determine whether dependency from ${fromProjectName} to ${toProjectName} ` + + `is optional: ` + + `Unable to find project with name ${fromProjectName} in project graph`); + } + if (adjacencies.has(toProjectName)) { + return false; + } + const optAdjacencies = this._optAdjList.get(fromProjectName); + if (optAdjacencies.has(toProjectName)) { + return true; + } + return false; + } + + /** + * Transforms any optional dependencies declared in the graph to non-optional dependency, if the target + * can already be reached from the root project. + * + * @public + */ + async resolveOptionalDependencies() { + this._checkSealed(); + if (!this._hasUnresolvedOptionalDependencies) { + log.verbose(`Skipping resolution of optional dependencies since none have been declared`); + return; + } + log.verbose(`Resolving optional dependencies...`); + + // First collect all projects that are currently reachable from the root project (=all non-optional projects) + const resolvedProjects = new Set(); + await this.traverseBreadthFirst(({project}) => { + resolvedProjects.add(project.getName()); + }); + + let unresolvedOptDeps = false; + for (const [fromProjectName, optDependencies] of this._optAdjList) { + for (const toProjectName of optDependencies) { + if (resolvedProjects.has(toProjectName)) { + // Target node is already reachable in the graph + // => Resolve optional dependency + log.verbose(`Resolving optional dependency from ${fromProjectName} to ${toProjectName}...`); + + if (this._adjList.get(toProjectName).has(fromProjectName)) { + log.verbose( + ` Cyclic optional dependency detected: ${toProjectName} already has a non-optional ` + + `dependency to ${fromProjectName}`); + log.verbose( + ` Optional dependency from ${fromProjectName} to ${toProjectName} ` + + `will not be declared as it would introduce a cycle`); + unresolvedOptDeps = true; + } else { + this.declareDependency(fromProjectName, toProjectName); + // This optional dependency has now been resolved + // => Remove it from the list of optional dependencies + optDependencies.delete(toProjectName); + } + } else { + unresolvedOptDeps = true; + } + } + } + if (!unresolvedOptDeps) { + this._hasUnresolvedOptionalDependencies = false; + } + } + + /** + * Callback for graph traversal operations + * + * @public + * @async + * @callback @ui5/project/graph/ProjectGraph~traversalCallback + * @param {object} parameters Parameters passed to the callback + * @param {@ui5/project/specifications/Project} parameters.project + * Project that is currently visited + * @param {string[]} parameters.dependencies + * Array containing the names of all direct dependencies of the project + * @returns {Promise|undefined} If a promise is returned, + * graph traversal will wait and only continue once the promise has resolved. + */ + + /** + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @public + * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + * @param {@ui5/project/graph/ProjectGraph~traversalCallback} callback Will be called + */ + async traverseBreadthFirst(startName, callback) { + if (!callback) { + // Default optional first parameter + callback = startName; + startName = this._rootProjectName; + } + + if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); + } + + const queue = [{ + projectNames: [startName], + ancestors: [] + }]; + + const visited = Object.create(null); + + while (queue.length) { + const {projectNames, ancestors} = queue.shift(); // Get and remove first entry from queue + + await Promise.all(projectNames.map(async (projectName) => { + this._checkCycle(ancestors, projectName); + + if (visited[projectName]) { + return visited[projectName]; + } + + return visited[projectName] = (async () => { + const newAncestors = [...ancestors, projectName]; + const dependencies = this.getDependencies(projectName); + + queue.push({ + projectNames: dependencies, + ancestors: newAncestors + }); + + await callback({ + project: this.getProject(projectName), + dependencies + }); + })(); + })); + } + } + + /** + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @public + * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + * @param {@ui5/project/graph/ProjectGraph~traversalCallback} callback Will be called + */ + async traverseDepthFirst(startName, callback) { + if (!callback) { + // Default optional first parameter + callback = startName; + startName = this._rootProjectName; + } + + if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); + } + return this._traverseDepthFirst(startName, Object.create(null), [], callback); + } + + async _traverseDepthFirst(projectName, visited, ancestors, callback) { + this._checkCycle(ancestors, projectName); + + if (visited[projectName]) { + return visited[projectName]; + } + return visited[projectName] = (async () => { + const newAncestors = [...ancestors, projectName]; + const dependencies = this.getDependencies(projectName); + await Promise.all(dependencies.map((depName) => { + return this._traverseDepthFirst(depName, visited, newAncestors, callback); + })); + + await callback({ + project: this.getProject(projectName), + dependencies + }); + })(); + } + + /** + * Join another project graph into this one. + * Projects and extensions which already exist in this graph will cause an error to be thrown + * + * @public + * @param {@ui5/project/graph/ProjectGraph} projectGraph Project Graph to merge into this one + */ + join(projectGraph) { + try { + this._checkSealed(); + if (!projectGraph.isSealed()) { + // Seal input graph to prevent further modification + log.verbose( + `Sealing project graph with root project ${projectGraph._rootProjectName} ` + + `before joining it into project graph with root project ${this._rootProjectName}...`); + projectGraph.seal(); + } + mergeMap(this._projects, projectGraph._projects); + mergeMap(this._extensions, projectGraph._extensions); + mergeMap(this._adjList, projectGraph._adjList); + mergeMap(this._optAdjList, projectGraph._optAdjList); + + this._hasUnresolvedOptionalDependencies = + this._hasUnresolvedOptionalDependencies || projectGraph._hasUnresolvedOptionalDependencies; + } catch (err) { + throw new Error( + `Failed to join project graph with root project ${projectGraph._rootProjectName} into ` + + `project graph with root project ${this._rootProjectName}: ${err.message}`); + } + } + + // Only to be used by @ui5/builder tests to inject its version of the taskRepository + setTaskRepository(taskRepository) { + this._taskRepository = taskRepository; + } + + async _getTaskRepository() { + if (!this._taskRepository) { + try { + this._taskRepository = await import("@ui5/builder/internal/taskRepository"); + } catch (err) { + throw new Error( + `Failed to load task repository. Missing dependency to '@ui5/builder'? ` + + `Error: ${err.message}`); + } + } + return this._taskRepository; + } + + /** + * Executes a build on the graph + * + * @public + * @param {object} parameters Build parameters + * @param {string} parameters.destPath Target path + * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build + * @param {Array.} [parameters.includedDependencies=[]] + * List of names of projects to include in the build result + * If the wildcard '*' is provided, all dependencies will be included in the build result. + * @param {Array.} [parameters.excludedDependencies=[]] + * List of names of projects to exclude from the build result. + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} [parameters.dependencyIncludes] + * Alternative to the includedDependencies and excludedDependencies parameters. + * Allows for a more sophisticated configuration for defining which dependencies should be + * part of the build result. If this is provided, the other mentioned parameters will be ignored. + * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build + * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation + * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build + * @param {boolean} [parameters.createBuildManifest=false] + * Whether to create a build manifest file for the root project. + * This is currently only supported for projects of type 'library' and 'theme-library' + * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included + * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. + * @param {module:@ui5/project/build/ProjectBuilderOutputStyle} [parameters.outputStyle=Default] + * Processes build results into a specific directory structure. + * @returns {Promise} Promise resolving to undefined once build has finished + */ + async build({ + destPath, cleanDest = false, + includedDependencies = [], excludedDependencies = [], + dependencyIncludes, + selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, + includedTasks = [], excludedTasks = [], + outputStyle = OutputStyleEnum.Default + }) { + this.seal(); // Do not allow further changes to the graph + if (this._built) { + throw new Error( + `Project graph with root node ${this._rootProjectName} has already been built. ` + + `Each graph can only be built once`); + } + this._built = true; + const { + default: ProjectBuilder + } = await import("../build/ProjectBuilder.js"); + const builder = new ProjectBuilder({ + graph: this, + taskRepository: await this._getTaskRepository(), + buildConfig: { + selfContained, cssVariables, jsdoc, + createBuildManifest, + includedTasks, excludedTasks, outputStyle, + } + }); + await builder.build({ + destPath, cleanDest, + includedDependencies, excludedDependencies, + dependencyIncludes, + }); + } + + /** + * Seal the project graph so that no further changes can be made to it + * + * @public + */ + seal() { + this._sealed = true; + } + + /** + * Check whether the project graph has been sealed. + * This means the graph is read-only. Neither projects, nor dependencies between projects + * can be added or removed. + * + * @public + * @returns {boolean} True if the project graph has been sealed + */ + isSealed() { + return this._sealed; + } + + /** + * Helper function to check and throw in case the project graph has been sealed. + * Intended for use in any function that attempts to make changes to the graph. + * + * @throws Throws in case the project graph has been sealed + */ + _checkSealed() { + if (this._sealed) { + throw new Error(`Project graph with root node ${this._rootProjectName} has been sealed and is read-only`); + } + } + + _checkCycle(ancestors, projectName) { + if (ancestors.includes(projectName)) { + // "Back-edge" detected. Neither BFS nor DFS searches should continue + // Mark first and last occurrence in chain with an asterisk and throw an error detailing the + // problematic dependency chain + ancestors[ancestors.indexOf(projectName)] = `*${projectName}*`; + throw new Error(`Detected cyclic dependency chain: ${ancestors.join(" -> ")} -> *${projectName}*`); + } + } + + // TODO: introduce function to check for dangling nodes/consistency in general? +} + +function mergeMap(target, source) { + for (const [key, value] of source) { + if (target.has(key)) { + throw new Error(`Failed to merge map: Key '${key}' already present in target set`); + } + if (value instanceof Set) { + // Shallow-clone any Sets + target.set(key, new Set(value)); + } else { + target.set(key, value); + } + } +} + +export default ProjectGraph; diff --git a/packages/project/lib/graph/ShimCollection.js b/packages/project/lib/graph/ShimCollection.js new file mode 100644 index 00000000000..9947996b13d --- /dev/null +++ b/packages/project/lib/graph/ShimCollection.js @@ -0,0 +1,63 @@ +import {getLogger} from "@ui5/logger"; +const log = getLogger("graph:ShimCollection"); + +function addToMap(name, fromMap, toMap) { + /* Dynamically populate the given map "toMap" with the following structure: + : [{ + name: , + shim: + }, { + name: , + shim: + }, ...] + */ + for (const [moduleId, shim] of Object.entries(fromMap)) { + if (!toMap[moduleId]) { + toMap[moduleId] = []; + } + toMap[moduleId].push({ + name, + shim + }); + } +} + +class ShimCollection { + constructor() { + this._projectConfigShims = Object.create(null); + this._dependencyShims = Object.create(null); + this._collectionShims = Object.create(null); + } + + addProjectShim(shimExtension) { + const name = shimExtension.getName(); + log.verbose(`Adding new shim ${name}...`); + + const configurations = shimExtension.getConfigurationShims(); + if (configurations) { + addToMap(name, configurations, this._projectConfigShims); + } + const dependencies = shimExtension.getDependencyShims(); + if (dependencies) { + addToMap(name, dependencies, this._dependencyShims); + } + const collections = shimExtension.getCollectionShims(); + if (collections) { + addToMap(name, collections, this._collectionShims); + } + } + + getProjectConfigurationShims(moduleId) { + return this._projectConfigShims[moduleId]; + } + + getCollectionShims(moduleId) { + return this._collectionShims[moduleId]; + } + + getAllDependencyShims() { + return this._dependencyShims; + } +} + +export default ShimCollection; diff --git a/packages/project/lib/graph/Workspace.js b/packages/project/lib/graph/Workspace.js new file mode 100644 index 00000000000..770d52b1f75 --- /dev/null +++ b/packages/project/lib/graph/Workspace.js @@ -0,0 +1,278 @@ +import fs from "graceful-fs"; +import {globby, isDynamicPattern} from "globby"; +import path from "node:path"; +import {promisify} from "node:util"; +import {getLogger} from "@ui5/logger"; +import Module from "./Module.js"; +import {validateWorkspace} from "../validation/validator.js"; + +const readFile = promisify(fs.readFile); +const log = getLogger("graph:Workspace"); + + +/** + * Workspace configuration. For details, refer to the + * [UI5 Workspaces documentation]{@link https://ui5.github.io/cli/v4/pages/Workspace/#configuration} + * + * @public + * @typedef {object} @ui5/project/graph/Workspace~Configuration + * @property {string} node.specVersion Workspace Specification Version + * @property {object} node.metadata + * @property {string} node.metadata.name Name of the workspace configuration + * @property {object} node.dependencyManagement + * @property {@ui5/project/graph/Workspace~DependencyManagementResolutions[]} node.dependencyManagement.resolutions + */ + +/** + * A resolution entry for the dependency management section of the workspace configuration + * + * @public + * @typedef {object} @ui5/project/graph/Workspace~DependencyManagementResolution + * @property {string} path Relative path to use for the workspace resolution process + */ + +/** + * UI5 Workspace + * + * @public + * @class + * @alias @ui5/project/graph/Workspace + */ +class Workspace { + #visitedNodePaths = new Set(); + #configValidated = false; + #configuration; + #cwd; + + /** + * @public + * @param {object} options + * @param {string} options.cwd Path to use for resolving all paths of the workspace configuration from. + * This should contain platform-specific path separators (i.e. must not be POSIX on non-POSIX systems) + * @param {@ui5/project/graph/Workspace~Configuration} options.configuration + * Workspace configuration + */ + constructor({cwd, configuration}) { + if (!cwd) { + throw new Error(`Could not create Workspace: Missing or empty parameter 'cwd'`); + } + if (!configuration) { + throw new Error(`Could not create Workspace: Missing or empty parameter 'configuration'`); + } + + this.#cwd = cwd; + this.#configuration = configuration; + } + + /** + * Get the name of this workspace + * + * @public + * @returns {string} Name of this workspace configuration + */ + getName() { + return this.#configuration.metadata.name; + } + + /** + * Returns an array of [Module]{@ui5/project/graph/Module} instances found in the configured + * dependency-management resolution paths of this workspace, sorted by module ID. + * + * @public + * @returns {Promise<@ui5/project/graph/Module[]>} + * Array of Module instances sorted by module ID + */ + async getModules() { + const {moduleIdMap} = await this._getResolvedModules(); + const sortedMap = new Map([...moduleIdMap].sort((a, b) => String(a[0]).localeCompare(b[0]))); + return Array.from(sortedMap.values()); + } + + /** + * For a given project name (e.g. the value of the metadata.name property in a ui5.yaml), + * returns a [Module]{@ui5/project/graph/Module} instance or undefined depending on whether the project + * has been found in the configured dependency-management resolution paths of this workspace + * + * @public + * @param {string} projectName Name of the project + * @returns {Promise<@ui5/project/graph/Module|undefined>} + * Module instance, or undefined if none is found + */ + async getModuleByProjectName(projectName) { + const {projectNameMap} = await this._getResolvedModules(); + return projectNameMap.get(projectName); + } + + /** + * For a given node id (e.g. the value of the name property in a package.json), + * returns a [Module]{@ui5/project/graph/Module} instance or undefined depending on whether the module + * has been found in the configured dependency-management resolution paths of this workspace + * and contains at least one project or extension + * + * @public + * @param {string} nodeId Node ID of the module + * @returns {Promise<@ui5/project/graph/Module|undefined>} + * Module instance, or undefined if none is found + */ + async getModuleByNodeId(nodeId) { + const {moduleIdMap} = await this._getResolvedModules(); + return moduleIdMap.get(nodeId); + } + + _getResolvedModules() { + if (this._pResolvedModules) { + return this._pResolvedModules; + } + + return this._pResolvedModules = this._resolveModules(); + } + + async _resolveModules() { + await this._validateConfig(); + const resolutions = this.#configuration.dependencyManagement?.resolutions; + if (!resolutions?.length) { + return { + projectNameMap: new Map(), + moduleIdMap: new Map() + }; + } + + let resolvedModules = await Promise.all(resolutions.map(async (resolutionConfig) => { + if (!resolutionConfig.path) { + throw new Error( + `Missing property 'path' in dependency resolution configuration of workspace ${this.getName()}`); + } + return await this._getModulesFromPath( + this.#cwd, resolutionConfig.path); + })); + + // Flatten array since package-workspaces might have resolved to multiple modules for a single resolution + resolvedModules = Array.prototype.concat.apply([], resolvedModules); + + const projectNameMap = new Map(); + const moduleIdMap = new Map(); + await Promise.all(resolvedModules.map(async (module) => { + const {project, extensions} = await module.getSpecifications(); + if (project || extensions.length) { + moduleIdMap.set(module.getId(), module); + } else { + log.warn( + `Failed to create a project or extensions from module ${module.getId()} at ${module.getPath()}`); + } + if (project) { + projectNameMap.set(project.getName(), module); + log.verbose(`Module ${module.getId()} contains project ${project.getName()}`); + } + if (extensions.length) { + const extensionNames = extensions.map((e) => e.getName()).join(", "); + log.verbose(`Module ${module.getId()} contains extensions: ${extensionNames}`); + } + })); + return { + projectNameMap, + moduleIdMap + }; + } + + async _getModulesFromPath(cwd, relPath, failOnMissingFiles = true) { + const nodePath = path.join(cwd, relPath); + if (this.#visitedNodePaths.has(nodePath)) { + log.verbose(`Module located at ${nodePath} has already been visited`); + return []; + } + this.#visitedNodePaths.add(nodePath); + let pkg; + try { + pkg = await this._readPackageJson(nodePath); + if (!pkg?.name || !pkg?.version) { + throw new Error( + `package.json must contain fields 'name' and 'version'`); + } + } catch (err) { + if (!failOnMissingFiles && err.code === "ENOENT") { + // When resolving a dynamic workspace pattern (not a static path), ignore modules that + // are missing a package.json (this might simply indicate an empty directory) + log.verbose(`Ignoring module at path ${nodePath}: Directory does not contain a package.json`); + return []; + } + throw new Error( + `Failed to resolve workspace dependency resolution path ${relPath} to ${nodePath}: ${err.message}`); + } + + // If the package.json defines an npm "workspaces", or an equivalent "ui5.workspaces" configuration, + // resolve the workspace and only use the resulting modules. The root package is ignored. + const packageWorkspaceConfig = pkg.ui5?.workspaces || pkg.workspaces; + if (packageWorkspaceConfig?.length) { + log.verbose(`Module ${pkg.name} provides a package.json workspaces configuration. ` + + `Ignoring the module and resolving workspaces instead...`); + const staticPatterns = []; + // Split provided patterns into dynamic and static patterns + // This is necessary, since fast-glob currently behaves different from + // "glob" (used by @npmcli/map-workspaces) in that it does not match the + // base directory in case it is equal to the pattern (https://github.com/mrmlnc/fast-glob/issues/47) + // For example a pattern "package-a" would not match a directory called + // "package-a" in the root directory of the project. + // We therefore detect the static pattern and resolve it directly + const dynamicPatterns = packageWorkspaceConfig.filter((pattern) => { + if (isDynamicPattern(pattern)) { + return true; + } else { + staticPatterns.push(pattern); + return false; + } + }); + + let searchPaths = []; + if (dynamicPatterns.length) { + searchPaths = await globby(dynamicPatterns, { + cwd: nodePath, + followSymbolicLinks: false, + onlyDirectories: true, + }); + } + searchPaths.push(...staticPatterns); + + const resolvedModules = new Map(); + await Promise.all(searchPaths.map(async (pkgPath) => { + const modules = await this._getModulesFromPath(nodePath, pkgPath, staticPatterns.includes(pkgPath)); + modules.forEach((module) => { + const id = module.getId(); + if (!resolvedModules.get(id)) { + resolvedModules.set(id, module); + } + }); + })); + return Array.from(resolvedModules.values()); + } else { + return [new Module({ + id: pkg.name, + version: pkg.version, + modulePath: nodePath + })]; + } + } + + /** + * Reads the package.json file and returns its content + * + * @private + * @param {string} modulePath Path to the module containing the package.json + * @returns {object} Package json content + */ + async _readPackageJson(modulePath) { + const content = await readFile(path.join(modulePath, "package.json"), "utf8"); + return JSON.parse(content); + } + + async _validateConfig() { + if (this.#configValidated) { + return; + } + await validateWorkspace({ + config: this.#configuration + }); + this.#configValidated = true; + } +} + +export default Workspace; diff --git a/packages/project/lib/graph/graph.js b/packages/project/lib/graph/graph.js new file mode 100644 index 00000000000..0885265447f --- /dev/null +++ b/packages/project/lib/graph/graph.js @@ -0,0 +1,242 @@ +import path from "node:path"; +import projectGraphBuilder from "./projectGraphBuilder.js"; +import ui5Framework from "./helpers/ui5Framework.js"; +import createWorkspace from "./helpers/createWorkspace.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("generateProjectGraph"); + +/** + * Helper module to create a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph} + * from a directory + * + * @public + * @module @ui5/project/graph + */ + +/** + * Generates a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph} by resolving + * dependencies from package.json files and configuring projects from ui5.yaml files + * + * @public + * @static + * @param {object} [options] + * @param {string} [options.cwd=process.cwd()] Directory to start searching for the root module + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to + * cwd or an absolute path. In both case, platform-specific path segment separators must be used. + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph + * @param {string|null} [options.workspaceName=default] + * Name of the workspace configuration that should be used. "default" if not provided. + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] + * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {string} [options.workspaceConfigPath=ui5-workspace.yaml] + * Workspace configuration file to use if no object has been provided + * @param {@ui5/project/graph/Workspace~Configuration} [options.workspaceConfiguration] + * Workspace configuration object to use instead of reading from a configuration file. + * Parameter workspaceName can either be omitted or has to match with the given configuration name + * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance + */ +export async function graphFromPackageDependencies({ + cwd, rootConfiguration, rootConfigPath, + versionOverride, cacheMode, resolveFrameworkDependencies = true, + workspaceName="default", + workspaceConfiguration, workspaceConfigPath = "ui5-workspace.yaml" +}) { + log.verbose(`Creating project graph using npm provider...`); + const { + default: NpmProvider + } = await import("./providers/NodePackageDependencies.js"); + + cwd = cwd ? path.resolve(cwd) : process.cwd(); + rootConfigPath = utils.resolveConfigPath(cwd, rootConfigPath); + + let workspace; + if (workspaceName || workspaceConfiguration) { + workspace = await createWorkspace({ + cwd, + name: workspaceName, + configObject: workspaceConfiguration, + configPath: workspaceConfigPath + }); + } + + const provider = new NpmProvider({ + cwd, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(provider, workspace); + + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode, workspace}); + } + + return projectGraph; +} + +/** + * Generates a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph} from a + * YAML file following the structure of + * [@ui5/project/graph/providers/DependencyTree~TreeNode]{@link @ui5/project/graph/providers/DependencyTree~TreeNode}. + * + * Documentation: + * [Static Dependency Definition]{@link https://ui5.github.io/cli/stable/pages/Overview/#static-dependency-definition} + * + * @public + * @static + * @param {object} options + * @param {object} [options.filePath=projectDependencies.yaml] Path to the dependency configuration file + * @param {string} [options.cwd=process.cwd()] Directory to resolve relative paths to + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to + * cwd or an absolute path. In both case, platform-specific path segment separators must be used. + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] + * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph + * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance + */ +export async function graphFromStaticFile({ + filePath = "projectDependencies.yaml", cwd, + rootConfiguration, rootConfigPath, + versionOverride, cacheMode, resolveFrameworkDependencies = true +}) { + log.verbose(`Creating project graph using static file...`); + const { + default: DependencyTreeProvider + } = await import("./providers/DependencyTree.js"); + + cwd = cwd ? path.resolve(cwd) : process.cwd(); + rootConfigPath = utils.resolveConfigPath(cwd, rootConfigPath); + + const dependencyTree = await utils.readDependencyConfigFile(cwd, filePath); + + const provider = new DependencyTreeProvider({ + dependencyTree, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(provider); + + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + } + + return projectGraph; +} + +/** + * Generates a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph} from the + * given dependencyTree following the structure of + * [@ui5/project/graph/providers/DependencyTree~TreeNode]{@link @ui5/project/graph/providers/DependencyTree~TreeNode} + * + * @public + * @static + * @param {object} options + * @param {@ui5/project/graph/providers/DependencyTree~TreeNode} options.dependencyTree + * @param {string} [options.cwd=process.cwd()] Directory to resolve relative paths to + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to + * cwd or an absolute path. In both case, platform-specific path segment separators must be used. + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] + * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph + * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance +*/ +export async function graphFromObject({ + dependencyTree, cwd, + rootConfiguration, rootConfigPath, + versionOverride, cacheMode, resolveFrameworkDependencies = true +}) { + log.verbose(`Creating project graph using object...`); + const { + default: DependencyTreeProvider + } = await import("./providers/DependencyTree.js"); + + cwd = cwd ? path.resolve(cwd) : process.cwd(); + rootConfigPath = utils.resolveConfigPath(cwd, rootConfigPath); + + const dependencyTreeProvider = new DependencyTreeProvider({ + dependencyTree, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(dependencyTreeProvider); + + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + } + + return projectGraph; +} + +const utils = { + resolveConfigPath: function(cwd, configPath) { + if (configPath && !path.isAbsolute(configPath)) { + configPath = path.join(cwd, configPath); + } + return configPath; + }, + readDependencyConfigFile: async function(cwd, filePath) { + const { + default: fs + } = await import("graceful-fs"); + const {promisify} = await import("util"); + const readFile = promisify(fs.readFile); + const parseYaml =(await import("js-yaml")).load; + + filePath = utils.resolveConfigPath(cwd, filePath); + + let dependencyTree; + try { + const contents = await readFile(filePath, {encoding: "utf-8"}); + dependencyTree = parseYaml(contents, { + filename: filePath + }); + utils.resolveProjectPaths(cwd, dependencyTree); + } catch (err) { + throw new Error( + `Failed to load dependency tree configuration from path ${filePath}: ${err.message}`); + } + return dependencyTree; + }, + + resolveProjectPaths: function(cwd, project) { + if (!project.path) { + throw new Error(`Missing or empty attribute 'path' for project ${project.id}`); + } + project.path = path.resolve(cwd, project.path); + + if (!project.id) { + throw new Error(`Missing or empty attribute 'id' for project with path ${project.path}`); + } + if (!project.version) { + throw new Error(`Missing or empty attribute 'version' for project ${project.id}`); + } + + if (project.dependencies) { + project.dependencies.forEach((project) => utils.resolveProjectPaths(cwd, project)); + } + return project; + } +}; + +// Export function for testing only +/* istanbul ignore else */ +if (process.env.NODE_ENV === "test") { + graphFromStaticFile._utils = utils; +} diff --git a/packages/project/lib/graph/helpers/createWorkspace.js b/packages/project/lib/graph/helpers/createWorkspace.js new file mode 100644 index 00000000000..b284c6f5800 --- /dev/null +++ b/packages/project/lib/graph/helpers/createWorkspace.js @@ -0,0 +1,139 @@ +import path from "node:path"; +import Workspace from "../Workspace.js"; +import {validateWorkspace} from "../../validation/validator.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("generateProjectGraph"); + +const DEFAULT_WORKSPACE_CONFIG_PATH = "ui5-workspace.yaml"; +const DEFAULT_WORKSPACE_NAME = "default"; + +export default async function createWorkspace({ + cwd, name, configObject, configPath +}) { + if (!cwd || (!configObject && !configPath)) { + throw new Error(`createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'`); + } + if (configObject) { + if (!configObject?.metadata?.name) { + throw new Error(`Invalid workspace configuration: Missing or empty property 'metadata.name'`); + } + if (name && configObject.metadata.name !== name) { + throw new Error( + `The provided workspace name '${name}' does not match ` + + `the provided workspace configuration '${configObject.metadata.name}'`); + } else { + log.verbose(`Using provided workspace configuration ${configObject.metadata.name}...`); + return new Workspace({ + cwd, + configuration: configObject + }); + } + } else { + if (!name) { + throw new Error(`createWorkspace: Parameter 'configPath' implies parameter 'name', but it's empty`); + } + let filePath = configPath; + if (!path.isAbsolute(filePath)) { + filePath = path.join(cwd, configPath); + } + try { + const workspaceConfigs = await readWorkspaceConfigFile(filePath); + const configuration = workspaceConfigs.find((config) => { + return config.metadata.name === name; + }); + + if (configuration) { + log.verbose(`Using workspace configuration "${name}" from ${configPath}...`); + return new Workspace({ + cwd: path.dirname(filePath), + configuration + }); + } else if (name === DEFAULT_WORKSPACE_NAME) { + // Requested workspace not found + // Do not throw if the requested name is the default + return null; + } else { + throw new Error(`Could not find a workspace named '${name}' in ${configPath}`); + } + } catch (err) { + if (name === DEFAULT_WORKSPACE_NAME && configPath === DEFAULT_WORKSPACE_CONFIG_PATH && + err.cause?.code === "ENOENT") { + // Do not throw if the default workspace in the default file was requested but not found + log.verbose(`No workspace configuration file provided at ${filePath}`); + return null; + } else { + throw err; + } + } + } +} + +async function readWorkspaceConfigFile(filePath) { + const { + default: fs + } = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const readFile = promisify(fs.readFile); + const jsyaml = await import("js-yaml"); + + let fileContent; + try { + fileContent = await readFile(filePath, {encoding: "utf8"}); + } catch (err) { + throw new Error( + `Failed to load workspace configuration from path ${filePath}: ${err.message}`, { + cause: err + }); + } + let configs; + try { + configs = jsyaml.loadAll(fileContent, undefined, { + filename: filePath, + }); + } catch (err) { + throw new Error(`Failed to parse workspace configuration at ${filePath}\nError: ${err.message}`); + } + + if (!configs || !configs.length) { + // No configs found => exit here + log.verbose(`Found empty workspace configuration file at ${filePath}`); + return configs; + } + + // Validate found configurations with schema + // Validation is done again in the Workspace class. But here we can reference the YAML file + // which adds helpful information like the line number + const validationResults = await Promise.all( + configs.map(async (config, documentIndex) => { + // Catch validation errors to ensure proper order of rejections within Promise.all + try { + await validateWorkspace({ + config, + yaml: { + path: filePath, + source: fileContent, + documentIndex + } + }); + } catch (error) { + return error; + } + }) + ); + + const validationErrors = validationResults.filter(($) => $); + + if (validationErrors.length > 0) { + // Throw any validation errors + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + + return configs; +} + +// Export function for testing only +/* istanbul ignore else */ +if (process.env.NODE_ENV === "test") { + createWorkspace._readWorkspaceConfigFile = readWorkspaceConfigFile; +} diff --git a/packages/project/lib/graph/helpers/ui5Framework.js b/packages/project/lib/graph/helpers/ui5Framework.js new file mode 100644 index 00000000000..19737a4bd7b --- /dev/null +++ b/packages/project/lib/graph/helpers/ui5Framework.js @@ -0,0 +1,432 @@ +import Module from "../Module.js"; +import ProjectGraph from "../ProjectGraph.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("graph:helpers:ui5Framework"); +import Configuration from "../../config/Configuration.js"; +import path from "node:path"; + +class ProjectProcessor { + constructor({libraryMetadata, graph, workspace}) { + this._libraryMetadata = libraryMetadata; + this._graph = graph; + this._workspace = workspace; + this._projectGraphPromises = Object.create(null); + } + async addProjectToGraph(libName, ancestors) { + if (ancestors) { + this._checkCycle(ancestors, libName); + } + if (this._projectGraphPromises[libName]) { + return this._projectGraphPromises[libName]; + } + return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, ancestors); + } + async _addProjectToGraph(libName, ancestors = []) { + log.verbose(`Creating project for library ${libName}...`); + + if (!this._libraryMetadata[libName]) { + throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); + } + + const depMetadata = this._libraryMetadata[libName]; + const graph = this._graph; + + if (graph.getProject(libName)) { + // Already added + return; + } + + const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { + await this.addProjectToGraph(depName, [...ancestors, libName]); + return depName; + })); + + if (depMetadata.optionalDependencies) { + const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { + if (this._libraryMetadata[depName]) { + log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); + await this.addProjectToGraph(depName, [...ancestors, libName]); + return depName; + } + })); + + dependencies.push(...resolvedOptionals.filter(($)=>$)); + } + + let projectIsFromWorkspace = false; + let ui5Module; + if (this._workspace) { + ui5Module = await this._workspace.getModuleByProjectName(libName); + if (ui5Module) { + log.info(`Resolved project ${libName} via ${this._workspace.getName()} workspace ` + + `to version ${ui5Module.getVersion()}`); + log.verbose(` Resolved module ${libName} to path ${ui5Module.getPath()}`); + log.verbose(` Requested version was: ${depMetadata.version}`); + projectIsFromWorkspace = true; + } + } + + if (!ui5Module) { + ui5Module = new Module({ + id: depMetadata.id, + version: depMetadata.version, + modulePath: depMetadata.path + }); + } + const {project} = await ui5Module.getSpecifications(); + graph.addProject(project); + dependencies.forEach((dependency) => { + graph.declareDependency(libName, dependency); + }); + if (projectIsFromWorkspace) { + // Add any dependencies that are only declared in the workspace resolved project + // Do not remove superfluous dependencies (might be added later though) + await Promise.all(project.getFrameworkDependencies().map(async ({name, optional, development}) => { + // Only proceed with dependencies which are not "optional" or "development", + // and not already listed in the dependencies of the original node + if (optional || development || dependencies.includes(name)) { + return; + } + + if (!this._libraryMetadata[name]) { + throw new Error( + `Unable to find dependency ${name}, required by project ${project.getName()} ` + + `(resolved via ${this._workspace.getName()} workspace) in current set of libraries. ` + + `Try adding it temporarily to the root project's dependencies`); + } + + await this.addProjectToGraph(name, [...ancestors, libName]); + graph.declareDependency(libName, name); + })); + } + } + _checkCycle(ancestors, projectName) { + if (ancestors.includes(projectName)) { + // "Back-edge" detected. This would cause a deadlock + // Mark first and last occurrence in chain with an asterisk and throw an error detailing the + // problematic dependency chain + ancestors[ancestors.indexOf(projectName)] = `*${projectName}*`; + throw new Error( + `ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: ` + + `${ancestors.join(" -> ")} -> *${projectName}*`); + } + } +} + +const utils = { + shouldIncludeDependency({optional, development}, root) { + // Root project should include all dependencies + // Otherwise only non-optional and non-development dependencies should be included + return root || (optional !== true && development !== true); + }, + async getFrameworkLibrariesFromGraph(projectGraph) { + const ui5Dependencies = []; + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project !== rootProject && project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Specification API is >= 2.0 anyways + const frameworkDependencies = project.getFrameworkDependencies(); + if (!frameworkDependencies.length) { + log.verbose(`Project ${project.getName()} has no framework dependencies`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + frameworkDependencies.forEach((dependency) => { + if (!ui5Dependencies.includes(dependency.name) && + utils.shouldIncludeDependency(dependency, project === rootProject)) { + ui5Dependencies.push(dependency.name); + } + }); + }); + return ui5Dependencies; + }, + async declareFrameworkDependenciesInGraph(projectGraph) { + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project !== rootProject && project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Specification API is >= 2.0 anyways + const frameworkDependencies = project.getFrameworkDependencies(); + + if (!frameworkDependencies.length) { + log.verbose(`Project ${project.getName()} has no framework dependencies`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + const isRoot = project === rootProject; + frameworkDependencies.forEach((dependency) => { + if (isRoot || !dependency.development) { + // Root project should include all dependencies + // Otherwise all non-development dependencies should be considered + + if (isRoot) { + // Check for deprecated/internal dependencies of the root project + const depProject = projectGraph.getProject(dependency.name); + if (depProject && depProject.isDeprecated() && rootProject.getName() !== "testsuite") { + // No warning for testsuite projects + log.warn(`Dependency ${depProject.getName()} is deprecated ` + + `and should not be used for new projects!`); + } + if (depProject && depProject.isSapInternal() && !rootProject.getAllowSapInternal()) { + // Do not warn if project defines "allowSapInternal" + log.warn(`Dependency ${depProject.getName()} is restricted for use by ` + + `SAP internal projects only! ` + + `If the project ${rootProject.getName()} is an SAP internal project, ` + + `add the attribute "allowSapInternal: true" to its metadata configuration`); + } + } + if (!isRoot && dependency.optional) { + if (projectGraph.getProject(dependency.name)) { + projectGraph.declareOptionalDependency(project.getName(), dependency.name); + } + } else { + projectGraph.declareDependency(project.getName(), dependency.name); + } + } + }); + }); + await projectGraph.resolveOptionalDependencies(); + }, + checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph) { + // Check for duplicate framework libraries + const projectGraphProjectNames = projectGraph.getProjectNames(); + const duplicateFrameworkProjectNames = frameworkGraph.getProjectNames() + .filter((name) => projectGraphProjectNames.includes(name)); + + if (duplicateFrameworkProjectNames.length) { + throw new Error( + `Duplicate framework dependency definition(s) found for project ${projectGraph.getRoot().getName()}: ` + + `${duplicateFrameworkProjectNames.join(", ")}.\n` + + `Framework libraries should only be referenced via ui5.yaml configuration. ` + + `Neither the root project, nor any of its dependencies should include them as direct ` + + `dependencies (e.g. via package.json).` + ); + } + }, + /** + * This logic needs to stay in sync with the dependency definitions for the + * sapui5/distribution-metadata package. + * + * @param {@ui5/project/specifications/Project} project + */ + async getFrameworkLibraryDependencies(project) { + let dependencies = []; + let optionalDependencies = []; + + if (project.getId().startsWith("@sapui5/")) { + project.getFrameworkDependencies().forEach((dependency) => { + if (dependency.optional) { + // Add optional dependency to optionalDependencies + optionalDependencies.push(dependency.name); + } else if (!dependency.development) { + // Add non-development dependency to dependencies + dependencies.push(dependency.name); + } + }); + } else if (project.getId().startsWith("@openui5/")) { + const packageResource = await project.getRootReader().byPath("/package.json"); + const packageInfo = JSON.parse(await packageResource.getString()); + + dependencies = Object.keys( + packageInfo.dependencies || {} + ).map(($) => $.replace("@openui5/", "")); // @sapui5 dependencies must not be defined in package.json + optionalDependencies = Object.keys( + packageInfo.devDependencies || {} + ).map(($) => $.replace("@openui5/", "")); // @sapui5 dependencies must not be defined in package.json + } + + return {dependencies, optionalDependencies}; + }, + async getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph}) { + const libraryMetadata = Object.create(null); + const ui5Modules = await workspace.getModules(); + for (const ui5Module of ui5Modules) { + const {project} = await ui5Module.getSpecifications(); + // Only framework projects that are not already part of the projectGraph should be handled. + // Otherwise they would be available twice which is checked + // after installing via checkForDuplicateFrameworkProjects + if (project?.isFrameworkProject?.() && !projectGraph.getProject(project.getName())) { + const metadata = libraryMetadata[project.getName()] = Object.create(null); + metadata.id = project.getId(); + metadata.path = project.getRootPath(); + metadata.version = project.getVersion(); + const {dependencies, optionalDependencies} = await utils.getFrameworkLibraryDependencies(project); + metadata.dependencies = dependencies; + metadata.optionalDependencies = optionalDependencies; + } + } + return libraryMetadata; + }, + ProjectProcessor +}; + +/** + * + * + * @private + * @module @ui5/project/helpers/ui5Framework + */ +export default { + /** + * + * + * @public + * @param {@ui5/project/graph/ProjectGraph} projectGraph + * @param {object} [options] + * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework + * version + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] + * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {@ui5/project/graph/Workspace} [options.workspace] + * Optional workspace instance to use for overriding node resolutions + * @returns {Promise<@ui5/project/graph/ProjectGraph>} + * Promise resolving with the given graph instance to allow method chaining + */ + enrichProjectGraph: async function(projectGraph, options = {}) { + const {workspace, cacheMode} = options; + const rootProject = projectGraph.getRoot(); + const frameworkName = rootProject.getFrameworkName(); + const frameworkVersion = rootProject.getFrameworkVersion(); + const cwd = rootProject.getRootPath(); + + // It is allowed to omit the framework version in ui5.yaml and only provide one via the override + // This is a common use case for framework libraries, which generally should not define a + // framework version in their respective ui5.yaml + let version = options.versionOverride || frameworkVersion; + + if (rootProject.isFrameworkProject() && !version) { + // If the root project is a framework project and no framework version is defined, + // all framework dependencies must either be already part of the graph or part of the workspace. + // A mixed setup of framework deps within the graph AND from the workspace is currently not supported. + + const someDependencyMissing = rootProject.getFrameworkDependencies().some((dep) => { + return utils.shouldIncludeDependency(dep) && !projectGraph.getProject(dep.name); + }); + + // If all dependencies are available there is nothing else to do here. + // In case of a workspace setup, the resolver will be created below without a version and + // will succeed in case no library needs to be actually installed. + if (!someDependencyMissing) { + return projectGraph; + } + } + + + if (!frameworkName && !frameworkVersion) { + log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); + return projectGraph; + } + + if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { + throw new Error( + `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + + `Must be "OpenUI5" or "SAPUI5"` + ); + } + + const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); + if (!referencedLibraries.length) { + log.verbose( + `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + + `or in any of its dependencies`); + return projectGraph; + } + + let Resolver; + if (version && version.toLowerCase().endsWith("-snapshot")) { + Resolver = (await import("../../ui5Framework/Sapui5MavenSnapshotResolver.js")).default; + } else if (frameworkName === "OpenUI5") { + Resolver = (await import("../../ui5Framework/Openui5Resolver.js")).default; + } else if (frameworkName === "SAPUI5") { + Resolver = (await import("../../ui5Framework/Sapui5Resolver.js")).default; + } + + // ENV var should take precedence over the dataDir from the configuration. + let ui5DataDir = process.env.UI5_DATA_DIR; + if (!ui5DataDir) { + const config = await Configuration.fromFile(); + ui5DataDir = config.getUi5DataDir(); + } + if (ui5DataDir) { + ui5DataDir = path.resolve(cwd, ui5DataDir); + } + + if (options.versionOverride) { + version = await Resolver.resolveVersion(options.versionOverride, { + ui5DataDir, + cwd + }); + log.info( + `Overriding configured ${frameworkName} version ` + + `${frameworkVersion} with version ${version}` + ); + } + + if (version) { + log.info(`Using ${frameworkName} version: ${version}`); + } + + let providedLibraryMetadata; + if (workspace) { + providedLibraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({ + workspace, projectGraph + }); + } + + // Note: version might be undefined here and the Resolver will throw an error when calling + // #install and it can't be resolved via the provided library metadata + const resolver = new Resolver({ + cwd, + version, + providedLibraryMetadata, + cacheMode, + ui5DataDir + }); + + let startTime; + if (log.isLevelEnabled("verbose")) { + startTime = process.hrtime(); + } + + const {libraryMetadata} = await resolver.install(referencedLibraries); + + if (log.isLevelEnabled("verbose")) { + const timeDiff = process.hrtime(startTime); + const {default: prettyHrtime} = await import("pretty-hrtime"); + log.verbose( + `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + + `resolved in ${prettyHrtime(timeDiff)}`); + } + + const frameworkGraph = new ProjectGraph({ + rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` + }); + + const projectProcessor = new utils.ProjectProcessor({ + libraryMetadata, + graph: frameworkGraph, + workspace + }); + + await Promise.all(referencedLibraries.map(async (libName) => { + await projectProcessor.addProjectToGraph(libName); + })); + + utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph); + + log.verbose("Joining framework graph into project graph..."); + projectGraph.join(frameworkGraph); + await utils.declareFrameworkDependenciesInGraph(projectGraph); + return projectGraph; + }, + + // Export for testing only + _utils: process.env.NODE_ENV === "test" ? utils : /* istanbul ignore next */ undefined +}; diff --git a/packages/project/lib/graph/projectGraphBuilder.js b/packages/project/lib/graph/projectGraphBuilder.js new file mode 100644 index 00000000000..1376d3d224f --- /dev/null +++ b/packages/project/lib/graph/projectGraphBuilder.js @@ -0,0 +1,386 @@ +import path from "node:path"; +import Module from "./Module.js"; +import ProjectGraph from "./ProjectGraph.js"; +import ShimCollection from "./ShimCollection.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("graph:projectGraphBuilder"); + +function _handleExtensions(graph, shimCollection, extensions) { + extensions.forEach((extension) => { + const type = extension.getType(); + switch (type) { + case "project-shim": + shimCollection.addProjectShim(extension); + break; + case "task": + case "server-middleware": + graph.addExtension(extension); + break; + default: + throw new Error( + `Encountered unexpected extension of type ${type} ` + + `Supported types are 'project-shim', 'task' and 'middleware'`); + } + }); +} + +function validateNode(node) { + if (node.specVersion) { + throw new Error( + `Provided node with ID ${node.id} contains a top-level 'specVersion' property. ` + + `With UI5 CLI 3.0, project configuration needs to be provided in a dedicated ` + + `'configuration' object`); + } + if (node.metadata) { + throw new Error( + `Provided node with ID ${node.id} contains a top-level 'metadata' property. ` + + `With UI5 CLI 3.0, project configuration needs to be provided in a dedicated ` + + `'configuration' object`); + } +} + +/** + * @public + * @module @ui5/project/graph/ProjectGraphBuilder + */ + +/** + * Dependency graph node representing a module + * + * @public + * @typedef {object} @ui5/project/graph/ProjectGraphBuilder~Node + * @property {string} node.id Unique ID for the project + * @property {string} node.version Version of the project + * @property {string} node.path File System path to access the projects resources + * @property {object|object[]} [node.configuration] + * Configuration object or array of objects to use instead of reading from a configuration file + * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {boolean} [node.optional] + * Whether the node is an optional dependency of the parent it has been requested for + * @property {*} * Additional attributes are allowed but ignored. + * These can be used to pass information internally in the provider. + */ + +/** + * Node Provider interface + * + * @public + * @interface @ui5/project/graph/ProjectGraphBuilder~NodeProvider + */ + +/** + * Retrieve information on the root module + * + * @public + * @function + * @name @ui5/project/graph/ProjectGraphBuilder~NodeProvider#getRootNode + * @returns {Node} The root node of the dependency graph + */ + +/** + * Retrieve information on given a nodes dependencies + * + * @public + * @function + * @name @ui5/project/graph/ProjectGraphBuilder~NodeProvider#getDependencies + * @param {Node} node The root node of the dependency graph + * @param {@ui5/project/graph/Workspace} [workspace] workspace instance to use for overriding node resolution + * @returns {Node[]} Array of nodes which are direct dependencies of the given node + */ + +/** + * Generic helper module to create a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph}. + * For example from a dependency tree as returned by the legacy "translators". + * + * @public + * @function default + * @static + * @param {@ui5/project/graph/ProjectGraphBuilder~NodeProvider} nodeProvider + * Node provider instance to use for building the graph + * @param {@ui5/project/graph/Workspace} [workspace] + * Optional workspace instance to use for overriding project resolutions + * @returns {@ui5/project/graph/ProjectGraph} A new project graph instance + */ +async function projectGraphBuilder(nodeProvider, workspace) { + const shimCollection = new ShimCollection(); + const moduleCollection = Object.create(null); + const handledExtensions = new Set(); // Set containing the IDs of modules which' extensions have been handled + + const rootNode = await nodeProvider.getRootNode(); + validateNode(rootNode); + const rootModule = new Module({ + id: rootNode.id, + version: rootNode.version, + modulePath: rootNode.path, + configPath: rootNode.configPath, + configuration: rootNode.configuration + }); + const {project: rootProject, extensions: rootExtensions} = await rootModule.getSpecifications(); + if (!rootProject) { + throw new Error( + `Failed to create a UI5 project from module ${rootNode.id} at ${rootNode.path}. ` + + `Make sure the path is correct and a project configuration is present or supplied.`); + } + + moduleCollection[rootNode.id] = rootModule; + + const rootProjectName = rootProject.getName(); + + let qualifiedApplicationProject = null; + if (rootProject.getType() === "application") { + log.verbose(`Root project ${rootProjectName} qualified as application project for project graph`); + qualifiedApplicationProject = rootProject; + } + + + const projectGraph = new ProjectGraph({ + rootProjectName: rootProjectName + }); + projectGraph.addProject(rootProject); + + function handleExtensions(extensions) { + return _handleExtensions(projectGraph, shimCollection, extensions); + } + + handleExtensions(rootExtensions); + handledExtensions.add(rootNode.id); + + const queue = []; + + const rootDependencies = await nodeProvider.getDependencies(rootNode, workspace); + + if (rootDependencies && rootDependencies.length) { + queue.push({ + nodes: rootDependencies, + parentProject: rootProject + }); + } + + // Breadth-first search + while (queue.length) { + const {nodes, parentProject} = queue.shift(); // Get and remove first entry from queue + const res = await Promise.all(nodes.map(async (node) => { + let ui5Module = moduleCollection[node.id]; + + if (ui5Module) { + log.silly( + `Re-visiting module ${node.id} as a dependency of ${parentProject.getName()}`); + + const {project, extensions} = await ui5Module.getSpecifications(); + if (!project && !extensions.length) { + // Invalidate cache if the cached module is visited through another parent project and did not + // resolve to a project or extension(s) before. + // The module being visited now might be a different version containing for example + // UI5 CLI configuration, or one of the parent projects could have defined a + // relevant configuration shim meanwhile + log.silly( + `Cached module ${node.id} did not resolve to any projects or extensions. ` + + `Recreating module as a dependency of ${parentProject.getName()}...`); + ui5Module = null; + } + } + + if (!ui5Module) { + log.silly(`Visiting Module ${node.id} as a dependency of ${parentProject.getName()}`); + log.verbose(`Creating module ${node.id}...`); + validateNode(node); + ui5Module = moduleCollection[node.id] = new Module({ + id: node.id, + version: node.version, + modulePath: node.path, + configPath: node.configPath, + configuration: node.configuration, + shimCollection + }); + } else if (ui5Module.getPath() !== node.path) { + log.verbose( + `Warning - Dependency ${node.id} is available at multiple paths:` + + `\n Location of the already processed module (this one will be used): ${ui5Module.getPath()}` + + `\n Additional location (this one will be ignored): ${node.path}`); + } + + const {project, extensions} = await ui5Module.getSpecifications(); + return { + node, + project, + extensions + }; + })); + + // Keep this out of the async map function to ensure + // all projects and extensions are applied in a deterministic order + for (let i = 0; i < res.length; i++) { + const { + node, // Tree "raw" dependency tree node + project, // The project found for this node, if any + extensions // Any extensions found for this node + } = res[i]; + + if (extensions.length && (!node.optional || parentProject === rootProject)) { + // Only handle extensions in non-optional dependencies and any dependencies of the root project + if (handledExtensions.has(node.id)) { + // Do not handle extensions of the same module twice + log.verbose(`Extensions contained in module ${node.id} have already been handled`); + } else { + log.verbose(`Handling extensions for module ${node.id}...`); + // If a different module contains the same extension, we expect an error to be thrown by the graph + handleExtensions(extensions); + handledExtensions.add(node.id); + } + } + + // Check for collection shims + const collectionShims = shimCollection.getCollectionShims(node.id); + if (collectionShims && collectionShims.length) { + log.verbose( + `One or more module collection shims have been defined for module ${node.id}. ` + + `Therefore the module itself will not be resolved.`); + + const shimmedNodes = collectionShims.map(({name, shim}) => { + log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); + return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { + const shimModulePath = path.join(node.path, shimModuleRelPath); + log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); + return { + id: shimModuleId, + version: node.version, + path: shimModulePath + }; + }); + }); + + queue.push({ + nodes: Array.prototype.concat.apply([], shimmedNodes), + parentProject, + }); + // Skip collection node + continue; + } + + let skipDependencies = false; + if (project) { + const projectName = project.getName(); + if (project.getType() === "application") { + // Special handling of application projects of which there must be exactly *one* + // in the graph. Others shall be ignored. + if (!qualifiedApplicationProject) { + log.verbose(`Project ${projectName} qualified as application project for project graph`); + qualifiedApplicationProject = project; + } else if (qualifiedApplicationProject.getName() !== projectName) { + // Project is not a duplicate of an already qualified project (which should + // still be processed below), but a unique, additional application project + + // TODO: Should this rather be a verbose logging? + // projectPreprocessor handled this like any project that got ignored and did a + // (in this case misleading) general verbose logging: + // "Ignoring project with missing configuration" + log.info( + `Excluding additional application project ${projectName} from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); + continue; + } + } + if (projectGraph.getProject(projectName)) { + // Opposing to extensions, we are generally fine with the same project being contained in different + // modules. We simply ignore all but the first occurrence. + // This can happen for example if the same project is packaged in different ways/modules + // (e.g. one module containing the source and one containing the pre-built resources) + log.verbose( + `Project ${projectName} has already been added to the graph. ` + + `Skipping dependency resolution...`); + skipDependencies = true; + } else { + projectGraph.addProject(project); + } + + if (parentProject) { + if (node.optional) { + projectGraph.declareOptionalDependency(parentProject.getName(), projectName); + } else { + projectGraph.declareDependency(parentProject.getName(), projectName); + } + + if (project.isDeprecated() && parentProject === rootProject && + parentProject.getName() !== "testsuite") { + // Only warn for direct dependencies of the root project + // No warning for testsuite projects + log.warn( + `Dependency ${project.getName()} is deprecated and should not be used for new projects!`); + } + + if (project.isSapInternal() && parentProject === rootProject && + !parentProject.getAllowSapInternal()) { + // Only warn for direct dependencies of the root project, except it defines "allowSapInternal" + log.warn( + `Dependency ${project.getName()} is restricted for use by SAP internal projects only! ` + + `If the project ${parentProject.getName()} is an SAP internal project, add the attribute ` + + `"allowSapInternal: true" to its metadata configuration`); + } + } + } + + if (!project && !extensions.length) { + // Module provides neither a project nor an extension + // => Do not follow its dependencies + log.verbose( + `Module ${node.id} neither provides a project nor an extension. Skipping dependency resolution`); + skipDependencies = true; + } + + if (skipDependencies) { + continue; + } + + const nodeDependencies = await nodeProvider.getDependencies(node); + if (nodeDependencies && nodeDependencies.length) { + queue.push({ + // copy array, so that the queue is stable while ignored project dependencies are removed + nodes: [...nodeDependencies], + parentProject: project ? project : parentProject, + }); + } + } + } + + // Apply dependency shims + for (const [shimmedModuleId, moduleDepShims] of Object.entries(shimCollection.getAllDependencyShims())) { + const sourceModule = moduleCollection[shimmedModuleId]; + + for (let j = 0; j < moduleDepShims.length; j++) { + const depShim = moduleDepShims[j]; + if (!sourceModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Module ${shimmedModuleId} is unknown`); + continue; + } + const {project: sourceProject} = await sourceModule.getSpecifications(); + if (!sourceProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Source module ${shimmedModuleId} does not contain a project`); + continue; + } + for (let k = 0; k < depShim.shim.length; k++) { + const targetModuleId = depShim.shim[k]; + const targetModule = moduleCollection[targetModuleId]; + if (!targetModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module $${depShim} is unknown`); + continue; + } + const {project: targetProject} = await targetModule.getSpecifications(); + if (!targetProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module ${targetModuleId} does not contain a project`); + continue; + } + projectGraph.declareDependency(sourceProject.getName(), targetProject.getName()); + } + } + } + await projectGraph.resolveOptionalDependencies(); + + return projectGraph; +} + +export default projectGraphBuilder; diff --git a/packages/project/lib/graph/providers/DependencyTree.js b/packages/project/lib/graph/providers/DependencyTree.js new file mode 100644 index 00000000000..8d0f61548fb --- /dev/null +++ b/packages/project/lib/graph/providers/DependencyTree.js @@ -0,0 +1,59 @@ +/** + * Tree node + * + * @public + * @class + * @typedef {object} @ui5/project/graph/providers/DependencyTree~TreeNode + * @property {string} node.id Unique ID for the project + * @property {string} node.version Version of the project + * @property {string} node.path File System path to access the projects resources + * @property {object|object[]} [node.configuration] + * Configuration object or array of objects to use instead of reading from a configuration file + * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {@ui5/project/graph/providers/DependencyTree~TreeNode[]} dependencies + */ + +/** + * Helper module to create a [@ui5/project/graph/ProjectGraph]{@link @ui5/project/graph/ProjectGraph} + * from a dependency tree as returned by translators. + * + * @public + * @class + * @alias @ui5/project/graph/providers/DependencyTree + */ +class DependencyTree { + /** + * @param {object} options + * @param {@ui5/project/graph/providers/DependencyTree~TreeNode} options.dependencyTree + * Dependency tree as returned by a translator + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml + */ + constructor({dependencyTree, rootConfiguration, rootConfigPath}) { + if (!dependencyTree) { + throw new Error(`Failed to instantiate DependencyTree provider: Missing parameter 'dependencyTree'`); + } + this._tree = dependencyTree; + if (rootConfiguration) { + this._tree.configuration = rootConfiguration; + } + if (rootConfigPath) { + this._tree.configPath = rootConfigPath; + } + } + + async getRootNode() { + return this._tree; + } + + async getDependencies(node) { + if (node.deduped || !node.dependencies) { + return []; + } + return node.dependencies; + } +} + +export default DependencyTree; diff --git a/packages/project/lib/graph/providers/NodePackageDependencies.js b/packages/project/lib/graph/providers/NodePackageDependencies.js new file mode 100644 index 00000000000..8ee824236b3 --- /dev/null +++ b/packages/project/lib/graph/providers/NodePackageDependencies.js @@ -0,0 +1,184 @@ +import path from "node:path"; +import {readPackageUp} from "read-package-up"; +import {readPackage} from "read-pkg"; +import {promisify} from "node:util"; +import fs from "graceful-fs"; +const realpath = promisify(fs.realpath); +import resolve from "resolve"; +const resolveModulePath = promisify(resolve); +import {getLogger} from "@ui5/logger"; +const log = getLogger("graph:providers:NodePackageDependencies"); + +// Packages to consider: +// * https://github.com/npm/read-package-json-fast +// * https://github.com/npm/name-from-folder ? + +/** + * @public + * @class + * @alias @ui5/project/graph/providers/NodePackageDependencies + */ +class NodePackageDependencies { + /** + * Generates a project graph from npm modules + * + * @public + * @param {object} options + * @param {string} options.cwd Directory to start searching for the root module + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml + */ + constructor({cwd, rootConfiguration, rootConfigPath}) { + this._cwd = cwd; + this._rootConfiguration = rootConfiguration; + this._rootConfigPath = rootConfigPath; + } + + async getRootNode() { + const rootPkg = await readPackageUp({ + cwd: this._cwd, + normalize: false + }); + + if (!rootPkg || !rootPkg.packageJson) { + throw new Error( + `Failed to locate package.json for directory ${path.resolve(this._cwd)}`); + } + + const modulePath = path.dirname(rootPkg.path); + if (!rootPkg.packageJson.name) { + throw new Error(`Missing or empty 'name' attribute in package.json at ${modulePath}`); + } + if (!rootPkg.packageJson.version) { + throw new Error(`Missing or empty 'version' attribute in package.json at ${modulePath}`); + } + + return { + id: rootPkg.packageJson.name, + version: rootPkg.packageJson.version, + path: modulePath, + configuration: this._rootConfiguration, + configPath: this._rootConfigPath, + _dependencies: await this._getDependencies(modulePath, rootPkg.packageJson, true) + }; + } + + async getDependencies(node, workspace) { + log.verbose(`Resolving dependencies of ${node.id}...`); + if (!node._dependencies) { + return []; + } + return Promise.all(node._dependencies.map(async ({name, optional}) => { + const modulePath = await this._resolveModulePath(node.path, name, workspace); + return this._getNode(modulePath, optional, name); + })); + } + + async _resolveModulePath(baseDir, moduleName, workspace) { + log.verbose(`Resolving module path for '${moduleName}'...`); + + if (workspace) { + // Check whether node can be resolved via the provided Workspace instance + // If yes, replace the node from NodeProvider with the one from Workspace + const workspaceNode = await workspace.getModuleByNodeId(moduleName); + if (workspaceNode) { + log.info(`Resolved module ${moduleName} via ${workspace.getName()} workspace ` + + `to version ${workspaceNode.getVersion()}`); + log.verbose(` Resolved module ${moduleName} to path ${workspaceNode.getPath()}`); + return workspaceNode.getPath(); + } + } + + try { + let packageJsonPath = await resolveModulePath(moduleName + "/package.json", { + basedir: baseDir, + preserveSymlinks: false + }); + packageJsonPath = await realpath(packageJsonPath); + + const modulePath = path.dirname(packageJsonPath); + log.verbose(`Resolved module ${moduleName} to path ${modulePath}`); + return modulePath; + } catch (err) { + throw new Error( + `Unable to locate module ${moduleName} via resolve logic: ${err.message}`); + } + } + + /** + * Resolves a Node module by reading its package.json + * + * @param {string} modulePath Path to the module. + * @param {boolean} optional Whether this dependency is optional. + * @param {string} [nameAlias] The name of the module. It's usually the same as the name definfed + * in package.json and could easily be skipped. Useful when defining dependency as an alias: + * {@link https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md} + * @returns {Promise} + */ + async _getNode(modulePath, optional, nameAlias) { + log.verbose(`Reading package.json in directory ${modulePath}...`); + const packageJson = await readPackage({ + cwd: modulePath, + normalize: false + }); + + return { + id: nameAlias || packageJson.name, + version: packageJson.version, + path: modulePath, + optional, + _dependencies: await this._getDependencies(modulePath, packageJson) + }; + } + + async _getDependencies(modulePath, packageJson, rootModule = false) { + const dependencies = []; + if (packageJson.dependencies) { + Object.keys(packageJson.dependencies).forEach((depName) => { + dependencies.push({ + name: depName, + optional: false + }); + }); + } + if (rootModule && packageJson.devDependencies) { + Object.keys(packageJson.devDependencies).forEach((depName) => { + dependencies.push({ + name: depName, + optional: false + }); + }); + } + if (!rootModule && packageJson.devDependencies) { + await Promise.all(Object.keys(packageJson.devDependencies).map(async (depName) => { + try { + await this._resolveModulePath(modulePath, depName); + dependencies.push({ + name: depName, + optional: true + }); + } catch { + // Ignore error since it's a development dependency of a non-root module + } + })); + } + if (packageJson.optionalDependencies) { + await Promise.all(Object.keys(packageJson.optionalDependencies).map(async (depName) => { + try { + await this._resolveModulePath(modulePath, depName); + dependencies.push({ + name: depName, + optional: false + }); + } catch { + // Ignore error since it's an optional dependency + } + })); + } + return dependencies; + } +} + +export default NodePackageDependencies; diff --git a/packages/project/lib/specifications/ComponentProject.js b/packages/project/lib/specifications/ComponentProject.js new file mode 100644 index 00000000000..5c42ba2c4dc --- /dev/null +++ b/packages/project/lib/specifications/ComponentProject.js @@ -0,0 +1,380 @@ +import {promisify} from "node:util"; +import Project from "./Project.js"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; + +/** + * Subclass for projects potentially containing Components + * + * @public + * @abstract + * @class + * @alias @ui5/project/specifications/ComponentProject + * @extends @ui5/project/specifications/Project + * @hideconstructor + */ +class ComponentProject extends Project { + constructor(parameters) { + super(parameters); + if (new.target === ComponentProject) { + throw new TypeError("Class 'ComponentProject' is abstract. Please use one of the 'types' subclasses"); + } + + this._pPom = null; + this._namespace = null; + this._isRuntimeNamespaced = true; + } + + /* === Attributes === */ + /** + * Get the project namespace + * + * @public + * @returns {string} Project namespace in slash notation (e.g. my/project/name) + */ + getNamespace() { + return this._namespace; + } + + /** + * @private + */ + getCopyright() { + return this._config.metadata.copyright; + } + + /** + * @private + */ + getComponentPreloadPaths() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.paths || []; + } + + /** + * @private + */ + getComponentPreloadNamespaces() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.namespaces || []; + } + + /** + * @private + */ + getComponentPreloadExcludes() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.excludes || []; + } + + /** + * @private + */ + getMinificationExcludes() { + return this._config.builder && this._config.builder.minification && + this._config.builder.minification.excludes || []; + } + + /** + * @private + */ + getBundles() { + return this._config.builder && this._config.builder.bundles || []; + } + + /** + * @private + */ + getPropertiesFileSourceEncoding() { + return this._config.resources && this._config.resources.configuration && + this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; + } + + /* === Resource Access === */ + + /** + * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
    + *
  • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace. + * Any configured build-excludes are applied
  • + *
  • dist: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * Any configured build-excludes are applied
  • + *
  • runtime: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * This style is typically used for serving resources directly. Therefore, build-excludes are not applied + *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that + * project types like "theme-library", which can have multiple namespaces, can't omit them. + * Any configured build-excludes are applied
  • + *
+ * + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * + * Resource readers always use POSIX-style paths. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. + * Can be "buildtime", "dist", "runtime" or "flat" + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime"} = {}) { + // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? + + // Apply builder excludes to all styles but "runtime" + const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); + + if ((style === "runtime" || style === "dist") && this._isRuntimeNamespaced) { + // If the project's type requires a namespace at runtime, the + // dist- and runtime-style paths are identical to buildtime-style paths + style = "buildtime"; + } + let reader = this._getReader(excludes); + switch (style) { + case "buildtime": + break; + case "runtime": + case "dist": + // Use buildtime reader and link it to / + // No test-resources for runtime resource access, + // unless runtime is namespaced + reader = resourceFactory.createFlatReader({ + reader, + namespace: this._namespace + }); + break; + case "flat": + // Use buildtime reader and link it to / + // No test-resources for runtime resource access, + // unless runtime is namespaced + reader = resourceFactory.createFlatReader({ + reader, + namespace: this._namespace + }); + break; + default: + throw new Error(`Unknown path mapping style ${style}`); + } + + reader = this._addWriter(reader, style); + return reader; + } + + /** + * Get a resource reader for the resources of the project + * + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getSourceReader() { + throw new Error(`_getSourceReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get a resource reader for the test resources of the project + * + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getTestReader() { + throw new Error(`_getTestReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getWorkspace() { + // Workspace is always of style "buildtime" + // Therefore builder resource-excludes are always to be applied + const excludes = this.getBuilderResourcesExcludes(); + return resourceFactory.createWorkspace({ + name: `Workspace for project ${this.getName()}`, + reader: this._getReader(excludes), + writer: this._getWriter().collection + }); + } + + _getWriter() { + if (!this._writers) { + // writer is always of style "buildtime" + const namespaceWriter = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + + const generalWriter = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + + const collection = resourceFactory.createWriterCollection({ + name: `Writers for project ${this.getName()}`, + writerMapping: { + [`/resources/${this._namespace}/`]: namespaceWriter, + [`/test-resources/${this._namespace}/`]: namespaceWriter, + [`/`]: generalWriter + } + }); + + this._writers = { + namespaceWriter, + generalWriter, + collection + }; + } + return this._writers; + } + + _getReader(excludes) { + let reader = this._getSourceReader(excludes); + const testReader = this._getTestReader(excludes); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); + } + return reader; + } + + _addWriter(reader, style) { + const {namespaceWriter, generalWriter} = this._getWriter(); + + if ((style === "runtime" || style === "dist") && this._isRuntimeNamespaced) { + // If the project's type requires a namespace at runtime, the + // dist- and runtime-style paths are identical to buildtime-style paths + style = "buildtime"; + } + const readers = []; + switch (style) { + case "buildtime": + // Writer already uses buildtime style + readers.push(namespaceWriter); + readers.push(generalWriter); + break; + case "runtime": + case "dist": + // Runtime is not namespaced: link namespace to / + readers.push(resourceFactory.createFlatReader({ + reader: namespaceWriter, + namespace: this._namespace + })); + // Add general writer as is + readers.push(generalWriter); + break; + case "flat": + // Rewrite paths from "flat" to "buildtime" + readers.push(resourceFactory.createFlatReader({ + reader: namespaceWriter, + namespace: this._namespace + })); + // General writer resources can't be flattened, so they are not available + break; + default: + throw new Error(`Unknown path mapping style ${style}`); + } + readers.push(reader); + + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + + async _getNamespace() { + throw new Error(`_getNamespace must be implemented by subclass ${this.constructor.name}`); + } + + /* === Helper === */ + /** + * Checks whether a given string contains a maven placeholder. + * E.g. ${appId}. + * + * @param {string} value String to check + * @returns {boolean} True if given string contains a maven placeholder + */ + _hasMavenPlaceholder(value) { + return !!value.match(/^\$\{(.*)\}$/); + } + + /** + * Resolves a maven placeholder in a given string using the projects pom.xml + * + * @param {string} value String containing a maven placeholder + * @returns {Promise} Resolved string + */ + async _resolveMavenPlaceholder(value) { + const parts = value && value.match(/^\$\{(.*)\}$/); + if (parts) { + this._log.verbose( + `"${value}" contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); + const pom = await this._getPom(); + let mvnValue; + if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { + mvnValue = pom.project.properties[parts[1]]; + } else { + let obj = pom; + parts[1].split(".").forEach((part) => { + obj = obj && obj[part]; + }); + mvnValue = obj; + } + if (!mvnValue) { + throw new Error(`"${value}" couldn't be resolved from maven property ` + + `"${parts[1]}" of pom.xml of project ${this.getName()}`); + } + return mvnValue; + } else { + throw new Error(`"${value}" is not a maven placeholder`); + } + } + + /** + * Reads the projects pom.xml file + * + * @returns {Promise} Resolves with a JSON representation of the content + */ + async _getPom() { + if (this._pPom) { + return this._pPom; + } + + return this._pPom = this.getRootReader().byPath("/pom.xml") + .then(async (resource) => { + if (!resource) { + throw new Error( + `Could not find pom.xml in project ${this.getName()}`); + } + const content = await resource.getString(); + const { + default: xml2js + } = await import("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + ignoreAttrs: true + }); + const readXML = promisify(parser.parseString); + return readXML(content); + }).catch((err) => { + throw new Error( + `Failed to read pom.xml for project ${this.getName()}: ${err.message}`); + }); + } +} + +export default ComponentProject; diff --git a/packages/project/lib/specifications/Extension.js b/packages/project/lib/specifications/Extension.js new file mode 100644 index 00000000000..a30b11c35b6 --- /dev/null +++ b/packages/project/lib/specifications/Extension.js @@ -0,0 +1,45 @@ +import Specification from "./Specification.js"; + +/** + * Extension + * + * @public + * @abstract + * @class + * @alias @ui5/project/specifications/Extension + * @extends @ui5/project/specifications/Specification + * @hideconstructor + */ +class Extension extends Specification { + constructor(parameters) { + super(parameters); + if (new.target === Extension) { + throw new TypeError("Class 'Extension' is abstract. Please use one of the 'types' subclasses"); + } + } + + /** + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath File System path to access resources + * @param {object} parameters.configuration Configuration object + */ + async init(parameters) { + await super.init(parameters); + + try { + await this._validateConfig(); + } catch (err) { + throw new Error( + `Failed to validate configuration of ${this.getType()} extension ${this.getName()}: ` + + err.message); + } + + return this; + } + + async _validateConfig() {} +} + +export default Extension; diff --git a/packages/project/lib/specifications/Project.js b/packages/project/lib/specifications/Project.js new file mode 100644 index 00000000000..17177b08536 --- /dev/null +++ b/packages/project/lib/specifications/Project.js @@ -0,0 +1,292 @@ +import Specification from "./Specification.js"; +import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; + +/** + * Project + * + * @public + * @abstract + * @class + * @alias @ui5/project/specifications/Project + * @extends @ui5/project/specifications/Specification + * @hideconstructor + */ +class Project extends Specification { + constructor(parameters) { + super(parameters); + if (new.target === Project) { + throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); + } + + this._resourceTagCollection = null; + } + + /** + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath File System path to access resources + * @param {object} parameters.configuration Configuration object + * @param {object} [parameters.buildManifest] Build metadata object + */ + async init(parameters) { + await super.init(parameters); + + this._buildManifest = parameters.buildManifest; + + await this._configureAndValidatePaths(this._config); + await this._parseConfiguration(this._config, this._buildManifest); + + return this; + } + + /* === Attributes === */ + /** + * Get the project namespace. Returns `null` for projects that have none or multiple namespaces, + * for example Modules or Theme Libraries. + * + * @public + * @returns {string|null} Project namespace in slash notation (e.g. my/project/name) or null + */ + getNamespace() { + // Default namespace for general Projects: + // Their resources should be structured with globally unique paths, hence their namespace is undefined + return null; + } + + /** + * Check whether the project is a UI5-Framework project + * + * @public + * @returns {boolean} True if the project is a framework project + */ + isFrameworkProject() { + const id = this.getId(); + return id.startsWith("@openui5/") || id.startsWith("@sapui5/"); + } + + /** + * Get the project's customConfiguration + * + * @public + * @returns {object} Custom Configuration + */ + getCustomConfiguration() { + return this._config.customConfiguration; + } + + /** + * Get the path of the project's source directory. This might not be POSIX-style on some platforms. + * Projects with multiple source paths will throw an error. For example Modules. + * + * @public + * @returns {string} Absolute path to the source directory of the project + * @throws {Error} In case a project has multiple source directories + */ + getSourcePath() { + throw new Error(`getSourcePath must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get the project's framework name configuration + * + * @public + * @returns {string} Framework name configuration, either OpenUI5 or SAPUI5 + */ + getFrameworkName() { + return this._config.framework?.name; + } + + /** + * Get the project's framework version configuration + * + * @public + * @returns {string} Framework version configuration, e.g 1.110.0 + */ + getFrameworkVersion() { + return this._config.framework?.version; + } + + + /** + * Framework dependency entry of the project configuration. + * Also see [Framework Configuration: Dependencies]{@link https://ui5.github.io/cli/stable/pages/Configuration/#dependencies} + * + * @public + * @typedef {object} @ui5/project/specifications/Project~FrameworkDependency + * @property {string} name Name of the framework library. For example sap.ui.core + * @property {boolean} development Whether the dependency is meant for development purposes only + * @property {boolean} optional Whether the dependency should be treated as optional + */ + + /** + * Get the project's framework dependencies configuration + * + * @public + * @returns {@ui5/project/specifications/Project~FrameworkDependency[]} Framework dependencies configuration + */ + getFrameworkDependencies() { + return this._config.framework?.libraries || []; + } + + /** + * Get the project's deprecated configuration + * + * @private + * @returns {boolean} True if the project is flagged as deprecated + */ + isDeprecated() { + return !!this._config.metadata.deprecated; + } + + /** + * Get the project's sapInternal configuration + * + * @private + * @returns {boolean} True if the project is flagged as SAP-internal + */ + isSapInternal() { + return !!this._config.metadata.sapInternal; + } + + /** + * Get the project's allowSapInternal configuration + * + * @private + * @returns {boolean} True if the project allows for using SAP-internal projects + */ + getAllowSapInternal() { + return !!this._config.metadata.allowSapInternal; + } + + /** + * Get the project's builderResourcesExcludes configuration + * + * @private + * @returns {string[]} BuilderResourcesExcludes configuration + */ + getBuilderResourcesExcludes() { + return this._config.builder?.resources?.excludes || []; + } + + /** + * Get the project's customTasks configuration + * + * @private + * @returns {object[]} CustomTasks configuration + */ + getCustomTasks() { + return this._config.builder?.customTasks || []; + } + + /** + * Get the project's customMiddleware configuration + * + * @private + * @returns {object[]} CustomMiddleware configuration + */ + getCustomMiddleware() { + return this._config.server?.customMiddleware || []; + } + + /** + * Get the project's serverSettings configuration + * + * @private + * @returns {object} ServerSettings configuration + */ + getServerSettings() { + return this._config.server?.settings; + } + + /** + * Get the project's builderSettings configuration + * + * @private + * @returns {object} BuilderSettings configuration + */ + getBuilderSettings() { + return this._config.builder?.settings; + } + + /** + * Get the project's buildManifest configuration + * + * @private + * @returns {object|null} BuildManifest configuration or null if none is available + */ + getBuildManifest() { + return this._buildManifest || null; + } + + /* === Resource Access === */ + /** + * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
    + *
  • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace. + * Any configured build-excludes are applied
  • + *
  • dist: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * Any configured build-excludes are applied
  • + *
  • runtime: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * This style is typically used for serving resources directly. Therefore, build-excludes are not applied + *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that + * project types like "theme-library", which can have multiple namespaces, can't omit them. + * Any configured build-excludes are applied
  • + *
+ * + * Resource readers always use POSIX-style paths. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. + * Can be "buildtime", "dist", "runtime" or "flat" + * @returns {@ui5/fs/ReaderCollection} Reader collection allowing access to all resources of the project + */ + getReader(options) { + throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); + } + + getResourceTagCollection() { + if (!this._resourceTagCollection) { + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], + allowedNamespaces: ["project"], + tags: this.getBuildManifest()?.tags + }); + } + return this._resourceTagCollection; + } + + /** + * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a + * project's resources. This is always of style buildtime. + * + * @public + * @returns {@ui5/fs/DuplexCollection} DuplexCollection + */ + getWorkspace() { + throw new Error(`getWorkspace must be implemented by subclass ${this.constructor.name}`); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) {} + + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) {} +} + +export default Project; diff --git a/packages/project/lib/specifications/Specification.js b/packages/project/lib/specifications/Specification.js new file mode 100644 index 00000000000..71553e73af7 --- /dev/null +++ b/packages/project/lib/specifications/Specification.js @@ -0,0 +1,296 @@ +import path from "node:path"; +import {getLogger} from "@ui5/logger"; +import {createReader} from "@ui5/fs/resourceFactory"; +import SpecificationVersion from "./SpecificationVersion.js"; + +/** + * Abstract superclass for all projects and extensions + * + * @public + * @abstract + * @class + * @alias @ui5/project/specifications/Specification + * @hideconstructor + */ +class Specification { + /** + * Create a Specification instance for the given parameters + * + * @param {object} parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath Absolute File System path to access resources + * @param {object} parameters.configuration + * Type-dependent configuration object. Typically defined in a ui5.yaml + * @static + * @public + */ + static async create(parameters) { + if (!parameters.configuration) { + throw new Error( + `Unable to create Specification instance: Missing configuration parameter`); + } + const {kind, type} = parameters.configuration; + if (!["project", "extension"].includes(kind)) { + throw new Error(`Unable to create Specification instance: Unknown kind '${kind}'`); + } + + switch (type) { + case "application": { + return createAndInitializeSpec("types/Application.js", parameters); + } + case "library": { + return createAndInitializeSpec("types/Library.js", parameters); + } + case "theme-library": { + return createAndInitializeSpec("types/ThemeLibrary.js", parameters); + } + case "module": { + return createAndInitializeSpec("types/Module.js", parameters); + } + case "task": { + return createAndInitializeSpec("extensions/Task.js", parameters); + } + case "server-middleware": { + return createAndInitializeSpec("extensions/ServerMiddleware.js", parameters); + } + case "project-shim": { + return createAndInitializeSpec("extensions/ProjectShim.js", parameters); + } + default: + throw new Error( + `Unable to create Specification instance: Unknown specification type '${type}'`); + } + } + + constructor() { + if (new.target === Specification) { + throw new TypeError("Class 'Specification' is abstract. Please use one of the 'types' subclasses"); + } + this._log = getLogger(`specifications:types:${this.constructor.name}`); + } + + /** + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath Absolute File System path to access resources + * @param {object} parameters.configuration Configuration object + */ + async init({id, version, modulePath, configuration}) { + if (!id) { + throw new Error(`Could not create Specification: Missing or empty parameter 'id'`); + } + if (!version) { + throw new Error(`Could not create Specification: Missing or empty parameter 'version'`); + } + if (!modulePath) { + throw new Error(`Could not create Specification: Missing or empty parameter 'modulePath'`); + } + if (!path.isAbsolute(modulePath)) { + throw new Error(`Could not create Specification: Parameter 'modulePath' must contain an absolute path`); + } + if (!configuration) { + throw new Error(`Could not create Specification: Missing or empty parameter 'configuration'`); + } + + this._version = version; + this._modulePath = modulePath; + + // The ID property is filled from the provider (e.g. package.json "name") and might differ between providers. + // It is mainly used to detect framework libraries marked by @openui5 / @sapui5 scopes of npm package. + // (see Project#isFrameworkProject) + // In general, the configured name (metadata.name) should be used instead as the unique identifier of a project. + this.__id = id; + + // Deep clone config to prevent changes by reference + const config = JSON.parse(JSON.stringify(configuration)); + const {validate} = await import("../validation/validator.js"); + + if (SpecificationVersion.major(config.specVersion) <= 1) { + const originalSpecVersion = config.specVersion; + this._log.verbose(`Detected legacy Specification Version ${config.specVersion}, defined for ` + + `${config.kind} ${config.metadata.name}. ` + + `Attempting to migrate the project to a supported specification version...`); + this._migrateLegacyProject(config); + try { + await validate({ + config, + project: { + id + } + }); + } catch (err) { + this._log.verbose( + `Validation error after migration of ${config.kind} ${config.metadata.name}:`); + this._log.verbose(err.message); + throw new Error( + `${config.kind} ${config.metadata.name} defines unsupported Specification Version ` + + `${originalSpecVersion}. Please manually upgrade to 3.0 or higher. ` + + `For details see https://ui5.github.io/cli/pages/Configuration/#specification-versions - ` + + `An attempted migration to a supported specification version failed, ` + + `likely due to unrecognized configuration. Check verbose log for details.`); + } + } else { + await validate({ + config, + project: { + id + } + }); + } + + // Check whether the given configuration matches the class by guessing the type name from the class name + if (config.type.replace("-", "") !== this.constructor.name.toLowerCase()) { + throw new Error( + `Configuration mismatch: Supplied configuration of type '${config.type}' does not match with ` + + `specification class ${this.constructor.name}`); + } + + this._name = config.metadata.name; + this._kind = config.kind; + this._type = config.type; + this._specVersionString = config.specVersion; + this._specVersion = new SpecificationVersion(this._specVersionString); + this._config = config; + + return this; + } + + /* === Attributes === */ + /** + * Gets the ID of this specification. + * + *

Note: Only to be used for special occasions as it is specific to the provider that was used and does + * not necessarily represent something defined by the project.

+ * + * For general purposes of a unique identifier use + * {@link @ui5/project/specifications/Specification#getName getName} instead. + * + * @public + * @returns {string} Specification ID + */ + getId() { + return this.__id; + } + + /** + * Gets the name of this specification. Represents a unique identifier. + * + * @public + * @returns {string} Specification name + */ + getName() { + return this._name; + } + + /** + * Gets the kind of this specification, for example project or extension + * + * @public + * @returns {string} Specification kind + */ + getKind() { + return this._kind; + } + + /** + * Gets the type of this specification, + * for example application or library in case of projects, + * and task or server-middleware in case of extensions + * + * @public + * @returns {string} Specification type + */ + getType() { + return this._type; + } + + /** + * Returns an instance of a helper class representing a Specification Version + * + * @public + * @returns {@ui5/project/specifications/SpecificationVersion} + */ + getSpecVersion() { + return this._specVersion; + } + + /** + * Gets the specification's generic version, as typically defined in a package.json + * + * @public + * @returns {string} Project version + */ + getVersion() { + return this._version; + } + + /** + * Gets the specification's file system path. This might not be POSIX-style on some platforms + * + * @public + * @returns {string} Project root path + */ + getRootPath() { + return this._modulePath; + } + + /* === Resource Access === */ + /** + * Gets a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for the root directory of the specification. + * Resource readers always use POSIX-style + * + * @public + * @param {object} [parameters] Parameters + * @param {object} [parameters.useGitignore=true] + * Whether to apply any excludes defined in an optional .gitignore in the root directory + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + getRootReader({useGitignore=true} = {}) { + return createReader({ + fsBasePath: this.getRootPath(), + virBasePath: "/", + name: `Root reader for ${this.getType()} ${this.getKind()} ${this.getName()}`, + useGitignore + }); + } + + /* === Internals === */ + /* === Helper === */ + /** + * @private + * @param {string} dirPath Directory path, relative to the specification root + */ + async _dirExists(dirPath) { + const resource = await this.getRootReader().byPath(dirPath, {nodir: false}); + if (resource && resource.getStatInfo().isDirectory()) { + return true; + } + return false; + } + + _migrateLegacyProject(config) { + // Stick to 2.6 since 3.0 adds further restrictions (i.e. for the name) and enables + // functionality for extensions that shouldn't be enabled if the specVersion is not + // explicitly set to 3.x + config.specVersion = "2.6"; + + // propertiesFileSourceEncoding (relevant for applications and libraries) default + // has been changed to UTF-8 with specVersion 2.0 + // Adding back the old default if no configuration is provided. + if (config.kind === "project" && ["application", "library"].includes(config.type) && + !config.resources?.configuration?.propertiesFileSourceEncoding) { + config.resources = config.resources || {}; + config.resources.configuration = config.resources.configuration || {}; + config.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + } + } +} + +async function createAndInitializeSpec(moduleName, params) { + const {default: Spec} = await import(`./${moduleName}`); + return new Spec().init(params); +} + +export default Specification; diff --git a/packages/project/lib/specifications/SpecificationVersion.js b/packages/project/lib/specifications/SpecificationVersion.js new file mode 100644 index 00000000000..6448f1638ad --- /dev/null +++ b/packages/project/lib/specifications/SpecificationVersion.js @@ -0,0 +1,310 @@ +import semver from "semver"; + +const SPEC_VERSION_PATTERN = /^\d+\.\d+$/; +const SUPPORTED_VERSIONS = [ + "0.1", "1.0", "1.1", + "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", + "3.0", "3.1", "3.2", + "4.0" +]; + +/** + * Helper class representing a Specification Version. Featuring helper functions for easy comparison + * of versions. + * + * @public + * @class + * @alias @ui5/project/specifications/utils/SpecificationVersion + */ +class SpecificationVersion { + #specVersion; + #semverVersion; + + /** + * @public + * @param {string} specVersion Specification Version to use for all comparison operations + * @throws {Error} Throws if provided Specification Version is not supported by this version of @ui5/project + */ + constructor(specVersion) { + this.#specVersion = specVersion; + this.#semverVersion = getSemverCompatibleVersion(specVersion); // Throws for unsupported versions + } + + /** + * Returns the Specification Version + * + * @public + * @returns {string} Specification Version + */ + toString() { + return this.#specVersion; + } + + /** + * Returns the major-version of the instance's Specification Version + * + * @public + * @returns {integer} Major version + */ + major() { + return semver.major(this.#semverVersion); + } + + /** + * Returns the minor-version of the instance's Specification Version + * + * @public + * @returns {integer} Minor version + */ + minor() { + return semver.minor(this.#semverVersion); + } + + /** + * Test whether the instance's Specification Version falls into the provided range + * + * @public + * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range, + * for example 2.2 - 2.4 or =3.0 + * @returns {boolean} True if the instance's Specification Version falls into the provided range + */ + satisfies(range) { + return semver.satisfies(this.#semverVersion, range); + } + + /** + * Test whether the instance's Specification Version is greater than the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is greater than the provided version + */ + gt(testVersion) { + return handleSemverComparator(semver.gt, this.#semverVersion, testVersion); + } + + /** + * Test whether the instance's Specification Version is greater than or equal to the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is greater than or equal to the provided version + */ + gte(testVersion) { + return handleSemverComparator(semver.gte, this.#semverVersion, testVersion); + } + + /** + * Test whether the instance's Specification Version is smaller than the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is smaller than the provided version + */ + lt(testVersion) { + return handleSemverComparator(semver.lt, this.#semverVersion, testVersion); + } + + /** + * Test whether the instance's Specification Version is smaller than or equal to the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is smaller than or equal to the provided version + */ + lte(testVersion) { + return handleSemverComparator(semver.lte, this.#semverVersion, testVersion); + } + + /** + * Test whether the instance's Specification Version is equal to the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is equal to the provided version + */ + eq(testVersion) { + return handleSemverComparator(semver.eq, this.#semverVersion, testVersion); + } + + /** + * Test whether the instance's Specification Version is not equal to the provided test version + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the instance's Specification Version is not equal to the provided version + */ + neq(testVersion) { + return handleSemverComparator(semver.neq, this.#semverVersion, testVersion); + } + + /** + * Test whether the provided Specification Version is supported by this version of @ui5/project + * + * @public + * @param {string} testVersion A Specification Version to compare the instance's Specification Version to + * @returns {boolean} True if the provided Specification Version is supported + */ + static isSupportedSpecVersion(testVersion) { + return SUPPORTED_VERSIONS.includes(testVersion); + } + + /** + * Returns the major-version of the provided Specification Version + * + * @public + * @param {string} specVersion Specification Version + * @returns {integer} Major version + */ + static major(specVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.major(); + } + + /** + * Returns the minor-version of the provided Specification Version + * + * @public + * @param {string} specVersion Specification Version + * @returns {integer} Minor version + */ + static minor(specVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.minor(); + } + + /** + * Test whether the provided Specification Version falls into the provided range + * + * @public + * @param {string} specVersion Specification Version + * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range, + * for example 2.2 - 2.4 + * @returns {boolean} True if the provided Specification Version falls into the provided range + */ + static satisfies(specVersion, range) { + const comparator = new SpecificationVersion(specVersion); + return comparator.satisfies(range); + } + + /** + * Test whether the provided Specification Version is greater than the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is greater than the provided version + */ + static gt(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.gt(testVersion); + } + + /** + * Test whether the provided Specification Version is greater than or equal to the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is greater than or equal to the provided version + */ + static gte(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.gte(testVersion); + } + + /** + * Test whether the provided Specification Version is smaller than the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is smaller than the provided version + */ + static lt(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.lt(testVersion); + } + + /** + * Test whether the provided Specification Version is smaller than or equal to the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is smaller than or equal to the provided version + */ + static lte(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.lte(testVersion); + } + + /** + * Test whether the provided Specification Version is equal to the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is equal to the provided version + */ + static eq(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.eq(testVersion); + } + + /** + * Test whether the provided Specification Version is not equal to the provided test version + * + * @public + * @param {string} specVersion Specification Version + * @param {string} testVersion A Specification Version to compare the provided Specification Version to + * @returns {boolean} True if the provided Specification Version is not equal to the provided version + */ + static neq(specVersion, testVersion) { + const comparator = new SpecificationVersion(specVersion); + return comparator.neq(testVersion); + } + + /** + * Creates an array of Specification Versions that match with the provided range. This is mainly used + * for testing purposes. I.e. to execute identical tests for a range of specification versions. + * + * @public + * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range, + * for example 2.2 - 2.4 or =3.0 + * @returns {string[]} Array of versions that match the specified range + */ + static getVersionsForRange(range) { + return SUPPORTED_VERSIONS.filter((specVersion) => { + const comparator = new SpecificationVersion(specVersion); + return comparator.satisfies(range); + }); + } +} + +function getUnsupportedSpecVersionMessage(specVersion) { + return `Unsupported Specification Version ${specVersion} defined. Your UI5 CLI installation might be outdated. ` + + `For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions`; +} + +function getSemverCompatibleVersion(specVersion) { + if (SpecificationVersion.isSupportedSpecVersion(specVersion)) { + return specVersion + ".0"; + } + throw new Error(getUnsupportedSpecVersionMessage(specVersion)); +} + +function handleSemverComparator(comparator, baseVersion, testVersion) { + if (SPEC_VERSION_PATTERN.test(testVersion)) { + const a = baseVersion; + const b = testVersion + ".0"; + return comparator(a, b); + } + throw new Error("Invalid spec version expectation given in comparator: " + testVersion); +} + +export default SpecificationVersion; + +// Export local function for testing only +export const __localFunctions__ = (process.env.NODE_ENV === "test") ? + {getSemverCompatibleVersion, handleSemverComparator} : /* istanbul ignore next */ undefined; diff --git a/packages/project/lib/specifications/extensions/ProjectShim.js b/packages/project/lib/specifications/extensions/ProjectShim.js new file mode 100644 index 00000000000..6bf00140fca --- /dev/null +++ b/packages/project/lib/specifications/extensions/ProjectShim.js @@ -0,0 +1,60 @@ +import Extension from "../Extension.js"; + +/** + * ProjectShim + * + * @public + * @class + * @alias @ui5/project/specifications/extensions/ProjectShim + * @extends @ui5/project/specifications/Extension + * @hideconstructor + */ +class ProjectShim extends Extension { + constructor(parameters) { + super(parameters); + } + + + /* === Attributes === */ + /** + * @public + */ + getDependencyShims() { + return this._config.shims.dependencies || {}; + } + + /** + * @public + */ + getConfigurationShims() { + return this._config.shims.configurations || {}; + } + + /** + * @public + */ + getCollectionShims() { + return this._config.shims.collections || {}; + } + + /* === Internals === */ + /** + * @private + */ + async _validateConfig() { + if (this._config.shims.collections) { + const { + default: path + } = await import("path"); + for (const dependencyDefinition of Object.values(this._config.shims.collections)) { + Object.values(dependencyDefinition.modules).forEach((depPath) => { + if (path.isAbsolute(depPath)) { + throw new Error("All module paths of collections defined in a project-shim must be relative"); + } + }); + } + } + } +} + +export default ProjectShim; diff --git a/packages/project/lib/specifications/extensions/ServerMiddleware.js b/packages/project/lib/specifications/extensions/ServerMiddleware.js new file mode 100644 index 00000000000..d18e237ffa5 --- /dev/null +++ b/packages/project/lib/specifications/extensions/ServerMiddleware.js @@ -0,0 +1,41 @@ +import path from "node:path"; +import Extension from "../Extension.js"; +import {pathToFileURL} from "node:url"; + +/** + * ServerMiddleware + * + * @public + * @class + * @alias @ui5/project/specifications/extensions/ServerMiddleware + * @extends @ui5/project/specifications/Extension + * @hideconstructor + */ +class ServerMiddleware extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Attributes === */ + /** + * @public + */ + async getMiddleware() { + const middlewarePath = path.join(this.getRootPath(), this._config.middleware.path); + const {default: middleware} = await import(pathToFileURL(middlewarePath)); + return middleware; + } + /* === Internals === */ + /** + * @private + */ + async _validateConfig() { + // TODO: Move to validator + if (/--\d+$/.test(this.getName())) { + throw new Error(`Server middleware name must not end with '--'`); + } + // TODO: Check that paths exist + } +} + +export default ServerMiddleware; diff --git a/packages/project/lib/specifications/extensions/Task.js b/packages/project/lib/specifications/extensions/Task.js new file mode 100644 index 00000000000..c5aee60b7a0 --- /dev/null +++ b/packages/project/lib/specifications/extensions/Task.js @@ -0,0 +1,58 @@ +import path from "node:path"; +import Extension from "../Extension.js"; +import {pathToFileURL} from "node:url"; + +/** + * Task + * + * @public + * @class + * @alias @ui5/project/specifications/extensions/Task + * @extends @ui5/project/specifications/Extension + * @hideconstructor + */ +class Task extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Attributes === */ + /** + * @public + */ + async getTask() { + return (await this._getImplementation()).task; + } + + /** + * @public + */ + async getRequiredDependenciesCallback() { + return (await this._getImplementation()).determineRequiredDependencies; + } + + /* === Internals === */ + /** + * @private + */ + async _getImplementation() { + const taskPath = path.join(this.getRootPath(), this._config.task.path); + const {default: task, determineRequiredDependencies} = await import(pathToFileURL(taskPath)); + return { + task, determineRequiredDependencies + }; + } + + /** + * @private + */ + async _validateConfig() { + // TODO: Move to validator + if (/--\d+$/.test(this.getName())) { + throw new Error(`Task name must not end with '--'`); + } + // TODO: Check that paths exist + } +} + +export default Task; diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js new file mode 100644 index 00000000000..1dc17b4bc1c --- /dev/null +++ b/packages/project/lib/specifications/types/Application.js @@ -0,0 +1,244 @@ +import fsPath from "node:path"; +import ComponentProject from "../ComponentProject.js"; +import {createReader} from "@ui5/fs/resourceFactory"; + +/** + * Application + * + * @public + * @class + * @alias @ui5/project/specifications/types/Application + * @extends @ui5/project/specifications/ComponentProject + * @hideconstructor + */ +class Application extends ComponentProject { + constructor(parameters) { + super(parameters); + + this._pManifests = Object.create(null); + + this._webappPath = "webapp"; + + this._isRuntimeNamespaced = false; + } + + + /* === Attributes === */ + + /** + * Get the cachebuster signature type configuration of the project + * + * @returns {string} time or hash + */ + getCachebusterSignatureType() { + return this._config.builder && this._config.builder.cachebuster && + this._config.builder.cachebuster.signatureType || "time"; + } + + /** + * Get the path of the project's source directory. This might not be POSIX-style on some platforms. + * + * @public + * @returns {string} Absolute path to the source directory of the project + */ + getSourcePath() { + return fsPath.join(this.getRootPath(), this._webappPath); + } + + /* === Resource Access === */ + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * + * @param {string[]} excludes List of glob patterns to exclude + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getSourceReader(excludes) { + return createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: `/resources/${this._namespace}/`, + name: `Source reader for application project ${this.getName()}`, + project: this, + excludes + }); + } + + _getTestReader() { + return null; // Applications do not have a dedicated test directory + } + + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * without a virtual base path + * + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getRawSourceReader() { + return createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: "/", + name: `Raw source reader for application project ${this.getName()}`, + project: this + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && + config.resources.configuration.paths && config.resources.configuration.paths.webapp) { + this._webappPath = config.resources.configuration.paths.webapp; + } + + this._log.verbose(`Path mapping for application project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getRootPath()}`); + this._log.verbose(` Mapped to: ${this._webappPath}`); + + if (!(await this._dirExists("/" + this._webappPath))) { + throw new Error( + `Unable to find source directory '${this._webappPath}' in application project ${this.getName()}`); + } + } + + /** + * @private + * @param {object} config Configuration object + * @param {object} buildDescription Cache metadata object + */ + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); + + if (buildDescription) { + this._namespace = buildDescription.namespace; + return; + } + this._namespace = await this._getNamespace(); + } + + /** + * Determine application namespace either based on a project`s + * manifest.json or manifest.appdescr_variant (fallback if present) + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespace() { + try { + return await this._getNamespaceFromManifestJson(); + } catch (manifestJsonError) { + if (manifestJsonError.code !== "ENOENT") { + throw manifestJsonError; + } + // No manifest.json present + // => attempt fallback to manifest.appdescr_variant (typical for App Variants) + try { + return await this._getNamespaceFromManifestAppDescVariant(); + } catch (appDescVarError) { + if (appDescVarError.code === "ENOENT") { + // Fallback not possible: No manifest.appdescr_variant present + // => Throw error indicating missing manifest.json + // (do not mention manifest.appdescr_variant since it is only + // relevant for the rather "uncommon" App Variants) + throw new Error( + `Could not find required manifest.json for project ` + + `${this.getName()}: ${manifestJsonError.message}\n\n` + + `If you are about to start a new project, please refer to:\n` + + `https://ui5.github.io/cli/v4/pages/GettingStarted/#starting-a-new-project`, { + cause: manifestJsonError + }); + } + throw appDescVarError; + } + } + } + + /** + * Determine application namespace by checking manifest.json. + * Any maven placeholders are resolved from the projects pom.xml + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestJson() { + const manifest = await this._getManifest("/manifest.json"); + let appId; + // check for a proper sap.app/id in manifest.json to determine namespace + if (manifest["sap.app"] && manifest["sap.app"].id) { + appId = manifest["sap.app"].id; + } else { + throw new Error( + `No sap.app/id configuration found in manifest.json of project ${this.getName()}`); + } + + if (this._hasMavenPlaceholder(appId)) { + try { + appId = await this._resolveMavenPlaceholder(appId); + } catch (err) { + throw new Error( + `Failed to resolve namespace of project ${this.getName()}: ${err.message}`); + } + } + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.json)`); + return namespace; + } + + /** + * Determine application namespace by checking manifest.appdescr_variant. + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestAppDescVariant() { + const manifest = await this._getManifest("/manifest.appdescr_variant"); + let appId; + // check for the id property in manifest.appdescr_variant to determine namespace + if (manifest && manifest.id) { + appId = manifest.id; + } else { + throw new Error( + `No "id" property found in manifest.appdescr_variant of project ${this.getName()}`); + } + + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`); + return namespace; + } + + /** + * Reads and parses a JSON file with the provided name from the projects source directory + * + * @param {string} filePath Name of the JSON file to read. Typically "manifest.json" or "manifest.appdescr_variant" + * @returns {Promise} resolves with an object containing the content requested manifest file + */ + async _getManifest(filePath) { + if (this._pManifests[filePath]) { + return this._pManifests[filePath]; + } + return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath) + .then(async (resource) => { + if (!resource) { + const error = new Error( + `Could not find resource ${filePath} in project ${this.getName()}`); + error.code = "ENOENT"; // "File or directory does not exist" + throw error; + } + return JSON.parse(await resource.getString()); + }).catch((err) => { + if (err.code === "ENOENT") { + throw err; + } + throw new Error( + `Failed to read ${filePath} for project ` + + `${this.getName()}: ${err.message}`); + }); + } +} + +export default Application; diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js new file mode 100644 index 00000000000..d3d2059a055 --- /dev/null +++ b/packages/project/lib/specifications/types/Library.js @@ -0,0 +1,541 @@ +import fsPath from "node:path"; +import posixPath from "node:path/posix"; +import {promisify} from "node:util"; +import ComponentProject from "../ComponentProject.js"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; + +/** + * Library + * + * @public + * @class + * @alias @ui5/project/specifications/types/Library + * @extends @ui5/project/specifications/ComponentProject + * @hideconstructor + */ +class Library extends ComponentProject { + constructor(parameters) { + super(parameters); + + this._pManifest = null; + this._pDotLibrary = null; + this._pLibraryJs = null; + + this._srcPath = "src"; + this._testPath = "test"; + this._testPathExists = false; + this._isSourceNamespaced = true; + + this._propertiesFilesSourceEncoding = "UTF-8"; + } + + /* === Attributes === */ + /** + * + * @private + */ + getLibraryPreloadExcludes() { + return this._config.builder && this._config.builder.libraryPreload && + this._config.builder.libraryPreload.excludes || []; + } + + /** + * @private + */ + getJsdocExcludes() { + return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; + } + + /** + * Get the path of the project's source directory. This might not be POSIX-style on some platforms. + * + * @public + * @returns {string} Absolute path to the source directory of the project + */ + getSourcePath() { + return fsPath.join(this.getRootPath(), this._srcPath); + } + + /* === Resource Access === */ + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * + * @param {string[]} excludes List of glob patterns to exclude + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getSourceReader(excludes) { + // TODO: Throw for libraries with additional namespaces like sap.ui.core? + let virBasePath = "/resources/"; + if (!this._isSourceNamespaced) { + // In case the namespace is not represented in the source directory + // structure, add it to the virtual base path + virBasePath += `${this._namespace}/`; + } + return resourceFactory.createReader({ + fsBasePath: this.getSourcePath(), + virBasePath, + name: `Source reader for library project ${this.getName()}`, + project: this, + excludes + }); + } + + /** + * Get a resource reader for the test-resources of the project + * + * @param {string[]} excludes List of glob patterns to exclude + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getTestReader(excludes) { + if (!this._testPathExists) { + return null; + } + let virBasePath = "/test-resources/"; + if (!this._isSourceNamespaced) { + // In case the namespace is not represented in the source directory + // structure, add it to the virtual base path + virBasePath += `${this._namespace}/`; + } + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getRootPath(), this._testPath), + virBasePath, + name: `Runtime test-resources reader for library project ${this.getName()}`, + project: this, + excludes + }); + return testReader; + } + + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * without a virtual base path. + * In the future the path structure can be flat or namespaced depending on the project + * setup + * + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getRawSourceReader() { + return resourceFactory.createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: "/", + name: `Raw source reader for library project ${this.getName()}`, + project: this + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources.configuration.paths.src) { + this._srcPath = config.resources.configuration.paths.src; + } + if (config.resources.configuration.paths.test) { + this._testPath = config.resources.configuration.paths.test; + } + } + if (!(await this._dirExists("/" + this._srcPath))) { + throw new Error( + `Unable to find source directory '${this._srcPath}' in library project ${this.getName()}`); + } + this._testPathExists = await this._dirExists("/" + this._testPath); + + this._log.verbose(`Path mapping for library project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getRootPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + this._log.verbose( + ` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`); + } + + /** + * @private + * @param {object} config Configuration object + * @param {object} buildDescription Cache metadata object + */ + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); + + if (buildDescription) { + this._namespace = buildDescription.namespace; + return; + } + + this._namespace = await this._getNamespace(); + + if (!config.metadata.copyright) { + const copyright = await this._getCopyrightFromDotLibrary(); + if (copyright) { + config.metadata.copyright = copyright; + } + } + + if (this.isFrameworkProject()) { + // Only framework projects are allowed to provide preload-excludes in their .library file, + // and only if it is not already defined in the ui5.yaml + if (config.builder?.libraryPreload?.excludes) { + this._log.verbose( + `Using preload excludes for framework library ${this.getName()} from project configuration`); + } else { + this._log.verbose( + `No preload excludes defined in project configuration of framework library ` + + `${this.getName()}. Falling back to .library...`); + const excludes = await this._getPreloadExcludesFromDotLibrary(); + if (excludes) { + if (!config.builder) { + config.builder = {}; + } + if (!config.builder.libraryPreload) { + config.builder.libraryPreload = {}; + } + config.builder.libraryPreload.excludes = excludes; + } + } + } + } + + /** + * Determine library namespace by checking manifest.json with fallback to .library. + * Any maven placeholders are resolved from the projects pom.xml + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespace() { + // Trigger both reads asynchronously + const [{ + namespace: manifestNs, + filePath: manifestPath + }, { + namespace: dotLibraryNs, + filePath: dotLibraryPath + }] = await Promise.all([ + this._getNamespaceFromManifest(), + this._getNamespaceFromDotLibrary() + ]); + + let libraryNs; + let namespacePath; + if (manifestNs && dotLibraryNs) { + // Both files present + // => check whether they are on the same level + const manifestDepth = manifestPath.split("/").length; + const dotLibraryDepth = dotLibraryPath.split("/").length; + + if (manifestDepth < dotLibraryDepth) { + // We see the .library file as the "leading" file of a library + // Therefore, a manifest.json on a higher level is something we do not except + throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + + `Found a manifest.json on a higher directory level than the .library file. ` + + `It should be on the same or a lower level. ` + + `Note that a manifest.json on a lower level will be ignored.\n` + + ` manifest.json path: ${manifestPath}\n` + + ` is higher than\n` + + ` .library path: ${dotLibraryPath}`); + } + if (manifestDepth === dotLibraryDepth) { + if (posixPath.dirname(manifestPath) !== posixPath.dirname(dotLibraryPath)) { + // This just should not happen in your project + throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + + `Found a manifest.json on the same directory level but in a different directory ` + + `than the .library file. They should be in the same directory.\n` + + ` manifest.json path: ${manifestPath}\n` + + ` is different to\n` + + ` .library path: ${dotLibraryPath}`); + } + // Typical scenario if both files are present + this._log.verbose( + `Found a manifest.json and a .library file on the same level for ` + + `project ${this.getName()}.`); + this._log.verbose( + `Resolving namespace of project ${this.getName()} from manifest.json...`); + libraryNs = manifestNs; + namespacePath = posixPath.dirname(manifestPath); + } else { + // Typical scenario: Some nested component has a manifest.json but the library itself only + // features a .library. => Ignore the manifest.json + this._log.verbose( + `Ignoring manifest.json found on a lower level than the .library file of ` + + `project ${this.getName()}.`); + this._log.verbose( + `Resolving namespace of project ${this.getName()} from .library...`); + libraryNs = dotLibraryNs; + namespacePath = posixPath.dirname(dotLibraryPath); + } + } else if (manifestNs) { + // Only manifest available + this._log.verbose( + `Resolving namespace of project ${this.getName()} from manifest.json...`); + libraryNs = manifestNs; + namespacePath = posixPath.dirname(manifestPath); + } else if (dotLibraryNs) { + // Only .library available + this._log.verbose( + `Resolving namespace of project ${this.getName()} from .library...`); + libraryNs = dotLibraryNs; + namespacePath = posixPath.dirname(dotLibraryPath); + } else { + this._log.verbose( + `Failed to resolve namespace of project ${this.getName()} from manifest.json ` + + `or .library file. Falling back to library.js file path...`); + } + + let namespace; + if (libraryNs) { + // Maven placeholders can only exist in manifest.json or .library configuration + if (this._hasMavenPlaceholder(libraryNs)) { + try { + libraryNs = await this._resolveMavenPlaceholder(libraryNs); + } catch (err) { + throw new Error( + `Failed to resolve namespace maven placeholder of project ` + + `${this.getName()}: ${err.message}`); + } + } + + namespace = libraryNs.replace(/\./g, "/"); + if (namespacePath === "/") { + this._log.verbose(`Detected flat library source structure for project ${this.getName()}`); + this._isSourceNamespaced = false; + } else { + namespacePath = namespacePath.replace("/", ""); // remove leading slash + if (namespacePath !== namespace) { + throw new Error( + `Detected namespace "${namespace}" does not match detected directory ` + + `structure "${namespacePath}" for project ${this.getName()}`); + } + } + } else { + try { + const libraryJsPath = await this._getLibraryJsPath(); + namespacePath = posixPath.dirname(libraryJsPath); + namespace = namespacePath.replace("/", ""); // remove leading slash + if (namespace === "") { + throw new Error(`Found library.js file in root directory. ` + + `Expected it to be in namespace directory.`); + } + this._log.verbose( + `Deriving namespace for project ${this.getName()} from ` + + `path of library.js file`); + } catch (err) { + this._log.verbose( + `Namespace resolution from library.js file path failed for project ` + + `${this.getName()}: ${err.message}`); + } + } + + if (!namespace) { + throw new Error(`Failed to detect namespace or namespace is empty for ` + + `project ${this.getName()}. Check verbose log for details.`); + } + + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace}`); + return namespace; + } + + async _getNamespaceFromManifest() { + try { + const {content: manifest, filePath} = await this._getManifest(); + // check for a proper sap.app/id in manifest.json to determine namespace + if (manifest["sap.app"] && manifest["sap.app"].id) { + const namespace = manifest["sap.app"].id; + this._log.verbose( + `Found namespace ${namespace} in manifest.json of project ${this.getName()} ` + + `at ${filePath}`); + return { + namespace, + filePath + }; + } else { + throw new Error( + `No sap.app/id configuration found in manifest.json of project ${this.getName()} ` + + `at ${filePath}`); + } + } catch (err) { + this._log.verbose( + `Namespace resolution from manifest.json failed for project ` + + `${this.getName()}: ${err.message}`); + } + return {}; + } + + async _getNamespaceFromDotLibrary() { + try { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + const namespace = dotLibrary?.library?.name?._; + if (namespace) { + this._log.verbose( + `Found namespace ${namespace} in .library file of project ${this.getName()} ` + + `at ${filePath}`); + return { + namespace, + filePath + }; + } else { + throw new Error( + `No library name found in .library of project ${this.getName()} ` + + `at ${filePath}`); + } + } catch (err) { + this._log.verbose( + `Namespace resolution from .library failed for project ` + + `${this.getName()}: ${err.message}`); + } + return {}; + } + + /** + * Determines library copyright from given project configuration with fallback to .library. + * + * @returns {string|null} Copyright of the project + */ + async _getCopyrightFromDotLibrary() { + try { + // If no copyright replacement was provided by ui5.yaml, + // check if the .library file has a valid copyright replacement + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + if (dotLibrary?.library?.copyright?._) { + this._log.verbose( + `Using copyright from ${filePath} for project ${this.getName()}...`); + return dotLibrary.library.copyright._; + } else { + this._log.verbose( + `No copyright configuration found in ${filePath} ` + + `of project ${this.getName()}`); + return null; + } + } catch (err) { + this._log.verbose( + `Copyright determination from .library failed for project ` + + `${this.getName()}: ${err.message}`); + return null; + } + } + + async _getPreloadExcludesFromDotLibrary() { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + let excludes = dotLibrary?.library?.appData?.packaging?.["all-in-one"]?.exclude; + if (excludes) { + if (!Array.isArray(excludes)) { + excludes = [excludes]; + } + this._log.verbose( + `Found ${excludes.length} preload excludes in .library file of ` + + `project ${this.getName()} at ${filePath}`); + return excludes.map((exclude) => { + return exclude.$.name; + }); + } else { + this._log.verbose( + `No preload excludes found in .library of project ${this.getName()} ` + + `at ${filePath}`); + return null; + } + } + + /** + * Reads the projects manifest.json + * + * @returns {Promise} resolves with an object containing the content (as JSON) and + * filePath (as string) of the manifest.json file + */ + async _getManifest() { + if (this._pManifest) { + return this._pManifest; + } + return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json") + .then(async (manifestResources) => { + if (!manifestResources.length) { + throw new Error(`Could not find manifest.json file for project ${this.getName()}`); + } + if (manifestResources.length > 1) { + throw new Error(`Found multiple (${manifestResources.length}) manifest.json files ` + + `for project ${this.getName()}`); + } + const resource = manifestResources[0]; + try { + return { + content: JSON.parse(await resource.getString()), + filePath: resource.getPath() + }; + } catch (err) { + throw new Error( + `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); + } + }); + } + + /** + * Reads the .library file + * + * @returns {Promise} resolves with an object containing the content (as JSON) and + * filePath (as string) of the .library file + */ + async _getDotLibrary() { + if (this._pDotLibrary) { + return this._pDotLibrary; + } + return this._pDotLibrary = this._getRawSourceReader().byGlob("**/.library") + .then(async (dotLibraryResources) => { + if (!dotLibraryResources.length) { + throw new Error(`Could not find .library file for project ${this.getName()}`); + } + if (dotLibraryResources.length > 1) { + throw new Error(`Found multiple (${dotLibraryResources.length}) .library files ` + + `for project ${this.getName()}`); + } + const resource = dotLibraryResources[0]; + const content = await resource.getString(); + + try { + const { + default: xml2js + } = await import("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + explicitCharkey: true + }); + const readXML = promisify(parser.parseString); + return { + content: await readXML(content), + filePath: resource.getPath() + }; + } catch (err) { + throw new Error( + `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); + } + }); + } + + /** + * Determines the path of the library.js file + * + * @returns {Promise} resolves with an a string containing the file system path + * of the library.js file + */ + async _getLibraryJsPath() { + if (this._pLibraryJs) { + return this._pLibraryJs; + } + return this._pLibraryJs = this._getRawSourceReader().byGlob("**/library.js") + .then(async (libraryJsResources) => { + if (!libraryJsResources.length) { + throw new Error(`Could not find library.js file for project ${this.getName()}`); + } + if (libraryJsResources.length > 1) { + throw new Error(`Found multiple (${libraryJsResources.length}) library.js files ` + + `for project ${this.getName()}`); + } + // Content is not yet relevant, so don't read it + return libraryJsResources[0].getPath(); + }); + } +} + +export default Library; diff --git a/packages/project/lib/specifications/types/Module.js b/packages/project/lib/specifications/types/Module.js new file mode 100644 index 00000000000..14e6a116442 --- /dev/null +++ b/packages/project/lib/specifications/types/Module.js @@ -0,0 +1,166 @@ +import fsPath from "node:path"; +import Project from "../Project.js"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; + +/** + * Module + * + * @public + * @class + * @alias @ui5/project/specifications/types/Module + * @extends @ui5/project/specifications/Project + * @hideconstructor + */ +class Module extends Project { + constructor(parameters) { + super(parameters); + + this._paths = null; + this._writer = null; + } + + /* === Attributes === */ + + /** + * Since Modules have multiple source paths, this function always throws with an exception + * + * @public + * @throws {Error} Projects of type module have more than one source path + */ + getSourcePath() { + throw new Error(`Projects of type module have more than one source path`); + } + + /* === Resource Access === */ + + /** + * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
    + *
  • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace. + * Any configured build-excludes are applied
  • + *
  • dist: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * Any configured build-excludes are applied
  • + *
  • runtime: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * This style is typically used for serving resources directly. Therefore, build-excludes are not applied + *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that + * project types like "theme-library", which can have multiple namespaces, can't omit them. + * Any configured build-excludes are applied
  • + *
+ * + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * + * Resource readers always use POSIX-style paths. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. + * Can be "buildtime", "dist", "runtime" or "flat" + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime"} = {}) { + // Apply builder excludes to all styles but "runtime" + const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); + + const readers = this._paths.map(({name, virBasePath, fsBasePath}) => { + return resourceFactory.createReader({ + name, + virBasePath, + fsBasePath, + project: this, + excludes + }); + }); + if (readers.length === 1) { + return readers[0]; + } + const readerCollection = resourceFactory.createReaderCollection({ + name: `Reader collection for module project ${this.getName()}`, + readers + }); + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [this._getWriter(), readerCollection] + }); + } + + /** + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getWorkspace() { + const reader = this.getReader(); + + const writer = this._getWriter(); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter() { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + } + + return this._writer; + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + this._log.verbose(`Path mapping for module project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getRootPath()}`); + this._log.verbose(` Mapped to:`); + + if (config.resources?.configuration?.paths) { + const pathMappings = Object.entries(config.resources.configuration.paths); + if (this._log.isLevelEnabled("verbose")) { + // Log synchronously before async dir-exists checks + pathMappings.forEach(([virBasePath, relFsPath]) => { + this._log.verbose(` ${virBasePath} => ${relFsPath}`); + }); + } + this._paths = await Promise.all(pathMappings.map(async ([virBasePath, relFsPath]) => { + if (!(await this._dirExists("/" + relFsPath))) { + throw new Error( + `Unable to find source directory '${relFsPath}' in module project ${this.getName()}`); + } + return { + name: `'${relFsPath}'' reader for module project ${this.getName()}`, + virBasePath, + fsBasePath: fsPath.join(this.getRootPath(), relFsPath) + }; + })); + } else { + this._log.verbose(` / => `); + if (!(await this._dirExists("/"))) { + throw new Error( + `Unable to find root directory of module project ${this.getName()}`); + } + this._paths = [{ + name: `Root reader for module project ${this.getName()}`, + virBasePath: "/", + fsBasePath: this.getRootPath() + }]; + } + } +} + +export default Module; diff --git a/packages/project/lib/specifications/types/ThemeLibrary.js b/packages/project/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 00000000000..e0bcd90785d --- /dev/null +++ b/packages/project/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,170 @@ +import Project from "../Project.js"; +import fsPath from "node:path"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; + +/** + * ThemeLibrary + * + * @public + * @class + * @alias @ui5/project/specifications/types/ThemeLibrary + * @extends @ui5/project/specifications/Project + * @hideconstructor + */ +class ThemeLibrary extends Project { + constructor(parameters) { + super(parameters); + + this._srcPath = "src"; + this._testPath = "test"; + this._testPathExists = false; + this._writer = null; + } + + /* === Attributes === */ + /** + * @private + */ + getCopyright() { + return this._config.metadata.copyright; + } + + /** + * Get the path of the project's source directory + * + * @public + * @returns {string} Absolute path to the source directory of the project + */ + getSourcePath() { + return fsPath.join(this.getRootPath(), this._srcPath); + } + + /* === Resource Access === */ + /** + * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
    + *
  • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace. + * Any configured build-excludes are applied
  • + *
  • dist: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * Any configured build-excludes are applied
  • + *
  • runtime: Resource paths always match with what the UI5 runtime expects. + * This means that paths generally depend on the project type. Applications for example use a "flat"-like + * structure, while libraries use a "buildtime"-like structure. + * This style is typically used for serving resources directly. Therefore, build-excludes are not applied + *
  • flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that + * project types like "theme-library", which can have multiple namespaces, can't omit them. + * Any configured build-excludes are applied
  • + *
+ * + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * + * Resource readers always use POSIX-style paths. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. + * Can be "buildtime", "dist", "runtime" or "flat" + * @returns {@ui5/fs/ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime"} = {}) { + // Apply builder excludes to all styles but "runtime" + const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); + + let reader = resourceFactory.createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: "/resources/", + name: `Runtime resources reader for theme-library project ${this.getName()}`, + project: this, + excludes + }); + if (this._testPathExists) { + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getRootPath(), this._testPath), + virBasePath: "/test-resources/", + name: `Runtime test-resources reader for theme-library project ${this.getName()}`, + project: this, + excludes + }); + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for theme-library project ${this.getName()}`, + readers: [reader, testReader] + }); + } + const writer = this._getWriter(); + + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [writer, reader] + }); + } + + /** + * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a + * project's resources. + * + * This is always of style buildtime, wich for theme libraries is identical to style + * runtime. + * + * @public + * @returns {@ui5/fs/DuplexCollection} DuplexCollection + */ + getWorkspace() { + const reader = this.getReader(); + + const writer = this._getWriter(); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter() { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + } + + return this._writer; + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources.configuration.paths.src) { + this._srcPath = config.resources.configuration.paths.src; + } + if (config.resources.configuration.paths.test) { + this._testPath = config.resources.configuration.paths.test; + } + } + + if (!(await this._dirExists("/" + this._srcPath))) { + throw new Error( + `Unable to find source directory '${this._srcPath}' in theme-library project ${this.getName()}`); + } + this._testPathExists = await this._dirExists("/" + this._testPath); + + this._log.verbose(`Path mapping for theme-library project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getRootPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + this._log.verbose( + ` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`); + } +} + +export default ThemeLibrary; diff --git a/packages/project/lib/ui5Framework/AbstractInstaller.js b/packages/project/lib/ui5Framework/AbstractInstaller.js new file mode 100644 index 00000000000..e13dea7f6e0 --- /dev/null +++ b/packages/project/lib/ui5Framework/AbstractInstaller.js @@ -0,0 +1,63 @@ +import path from "node:path"; +import {mkdirp} from "../utils/fs.js"; +import {promisify} from "node:util"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:Installer"); + +// File name must not start with one or multiple dots and should not contain characters other than: +// * alphanumeric +// * Slash (typically present in package names, hence is accepted and then replaced with a dash) +// * Dot, dash, underscore, at-sign +const illegalFileNameRegExp = /[^0-9a-zA-Z\-._@/]/; + +class AbstractInstaller { + /** + * @param {string} ui5DataDir UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. + */ + constructor(ui5DataDir) { + if (new.target === AbstractInstaller) { + throw new TypeError("Class 'AbstractInstaller' is abstract"); + } + if (!ui5DataDir) { + throw new Error(`Installer: Missing parameter "ui5DataDir"`); + } + this._lockDir = path.join(ui5DataDir, "framework", "locks"); + } + + async _synchronize(lockName, callback) { + const { + default: lockfile + } = await import("lockfile"); + const lock = promisify(lockfile.lock); + const unlock = promisify(lockfile.unlock); + const lockPath = this._getLockPath(lockName); + await mkdirp(this._lockDir); + log.verbose("Locking " + lockPath); + await lock(lockPath, { + wait: 10000, + stale: 60000, + retries: 10 + }); + try { + const res = await callback(); + return res; + } finally { + log.verbose("Unlocking " + lockPath); + await unlock(lockPath); + } + } + + _sanitizeFileName(fileName) { + if (fileName.startsWith(".") || illegalFileNameRegExp.test(fileName)) { + throw new Error(`Illegal file name: ${fileName}`); + } + return fileName.replace(/\//g, "-"); + } + + _getLockPath(lockName) { + return path.join(this._lockDir, `${this._sanitizeFileName(lockName)}.lock`); + } +} + +export default AbstractInstaller; diff --git a/packages/project/lib/ui5Framework/AbstractResolver.js b/packages/project/lib/ui5Framework/AbstractResolver.js new file mode 100644 index 00000000000..6cb532d98a6 --- /dev/null +++ b/packages/project/lib/ui5Framework/AbstractResolver.js @@ -0,0 +1,315 @@ +import path from "node:path"; +import os from "node:os"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:AbstractResolver"); +import semver from "semver"; + +// Reduced Semantic Versioning pattern +// Matches MAJOR or MAJOR.MINOR as a simple version range to be resolved to the latest minor/patch +const VERSION_RANGE_REGEXP = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-SNAPSHOT)?$/; + +/** + * Abstract Resolver + * + * @abstract + * @public + * @class + * @alias @ui5/project/ui5Framework/AbstractResolver + * @hideconstructor + */ +class AbstractResolver { + /* eslint-disable max-len */ + /** + * @param {*} options options + * @param {string} [options.version] Framework version to use. When omitted, all libraries need to be available + * via providedLibraryMetadata parameter. Otherwise an error is thrown. + * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or + * pre-built (with build manifest) + * @param {string} [options.cwd=process.cwd()] Current working directory + * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {object.} [options.providedLibraryMetadata] + * Resolver skips installing listed libraries and uses the dependency information to resolve their dependencies. + * version can be omitted in case all libraries can be resolved via the providedLibraryMetadata. + * Otherwise an error is thrown. + */ + /* eslint-enable max-len */ + constructor({cwd, version, sources, ui5DataDir, providedLibraryMetadata}) { + if (new.target === AbstractResolver) { + throw new TypeError("Class 'AbstractResolver' is abstract"); + } + + // In some CI environments, the homedir might be set explicitly to a relative + // path (e.g. "./"), but tooling requires an absolute path + this._ui5DataDir = path.resolve( + ui5DataDir || path.join(os.homedir(), ".ui5") + ); + this._cwd = cwd ? path.resolve(cwd) : process.cwd(); + this._version = version; + + // Environment variable should always enforce usage of sources + if (process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES) { + sources = true; + } + this._sources = !!sources; + + this._providedLibraryMetadata = providedLibraryMetadata; + } + + async _processLibrary(libraryName, libraryMetadata, errors) { + // Check if library is already processed + if (libraryMetadata[libraryName]) { + return; + } + // Mark library as handled + libraryMetadata[libraryName] = Object.create(null); + + log.verbose("Processing " + libraryName); + + let promises; + const providedLibraryMetadata = this._providedLibraryMetadata?.[libraryName]; + if (providedLibraryMetadata) { + log.verbose(`Skipping install for ${libraryName} (provided)`); + promises = { + // Use existing metadata if library is provided from outside (e.g. workspace) + metadata: Promise.resolve(providedLibraryMetadata), + // Provided libraries are already "installed" + install: Promise.resolve({ + pkgPath: providedLibraryMetadata.path + }) + }; + } else if (!this._version) { + throw new Error(`Unable to install library ${libraryName}. No framework version provided.`); + } else { + promises = await this.handleLibrary(libraryName); + } + + const [metadata, {pkgPath}] = await Promise.all([ + promises.metadata.then((metadata) => + this._processDependencies(libraryName, metadata, libraryMetadata, errors)), + promises.install + ]); + + // Add path to installed package to metadata + metadata.path = pkgPath; + + // Add metadata entry + libraryMetadata[libraryName] = metadata; + } + + async _processDependencies(libraryName, metadata, libraryMetadata, errors) { + if (metadata.dependencies.length > 0) { + log.verbose("Processing dependencies of " + libraryName); + await this._processLibraries(metadata.dependencies, libraryMetadata, errors); + log.verbose("Done processing dependencies of " + libraryName); + } + return metadata; + } + + async _processLibraries(libraryNames, libraryMetadata, errors) { + const sourceErrors = new Set(); + const results = await Promise.all(libraryNames.map(async (libraryName) => { + try { + await this._processLibrary(libraryName, libraryMetadata, errors); + } catch (err) { + if (sourceErrors.has(err.message)) { + return `Failed to resolve library ${libraryName}: Error already logged`; + } + sourceErrors.add(err.message); + log.verbose(`Failed to process library ${libraryName}`); + log.verbose(`Error: ${err.message}`); + log.verbose(`Call stack: ${err.stack}`); + return `Failed to resolve library ${libraryName}: ${err.message}`; + } + })); + // Don't add empty results (success) + errors.push(...results.filter(($) => $)); + } + + /** + * Library metadata entry + * + * @example + * const libraryMetadataEntry = { + * "id": "@openui5/sap.ui.core", + * "version": "1.75.0", + * "path": "~/.ui5/framework/packages/@openui5/sap.ui.core/1.75.0", + * "dependencies": [], + * "optionalDependencies": [] + * }; + * + * @public + * @typedef {object} @ui5/project/ui5Framework/AbstractResolver~LibraryMetadataEntry + * @property {string} id Identifier + * @property {string} version Version + * @property {string} path Path + * @property {string[]} dependencies List of dependency ids + * @property {string[]} optionalDependencies List of optional dependency ids + */ + + /** + * Install result + * + * @example + * const resolverInstallResult = { + * "libraryMetadata": { + * "sap.ui.core": { + * // ... + * }, + * "sap.m": { + * // ... + * } + * } + * }; + * + * @public + * @typedef {object} @ui5/project/ui5Framework/AbstractResolver~ResolverInstallResult + * @property {object.} libraryMetadata + * Object containing all installed libraries with library name as key + */ + + /** + * Installs the provided libraries and their dependencies + * + * @example + * const resolver = new Sapui5Resolver({version: "1.76.0"}); + * // Or for OpenUI5: + * // const resolver = new Openui5Resolver({version: "1.76.0"}); + * + * resolver.install(["sap.ui.core", "sap.m"]).then(({libraryMetadata}) => { + * // Installation done + * }).catch((err) => { + * // Handle installation errors + * }); + * + * @public + * @param {string[]} libraryNames List of library names to be installed + * @returns {@ui5/project/ui5Framework/AbstractResolver~ResolverInstallResult} + * Resolves with an object containing the libraryMetadata + */ + async install(libraryNames) { + const libraryMetadata = Object.create(null); + const errors = []; + + await this._processLibraries(libraryNames, libraryMetadata, errors); + + if (errors.length === 1) { + throw new Error(errors[0]); + } if (errors.length > 1) { + const msg = errors.map((err, idx) => ` ${idx + 1}. ${err}`).join("\n"); + throw new Error(`Resolution of framework libraries failed with errors:\n${msg}`); + } + + return { + libraryMetadata + }; + } + + static async resolveVersion(version, {ui5DataDir, cwd} = {}) { + // Don't allow nullish values + // An empty string is a valid semver range that converts to "*", which should not be supported + if (!version) { + throw new Error(`Framework version specifier "${version}" is incorrect or not supported`); + } + + const spec = await this._getVersionSpec(version, {ui5DataDir, cwd}); + + // For all invalid cases which are not explicitly handled in _getVersionSpec + if (!spec) { + throw new Error(`Framework version specifier "${version}" is incorrect or not supported`); + } + + const versions = await this.fetchAllVersions({ui5DataDir, cwd}); + const resolvedVersion = semver.maxSatisfying(versions, spec, { + // Allow ranges that end with -SNAPSHOT to match any -SNAPSHOT version + // like a normal version in order to support ranges like 1.x.x-SNAPSHOT. + includePrerelease: this._isSnapshotVersionOrRange(version) + }); + + if (!resolvedVersion) { + if (semver.valid(spec)) { + if (this.name === "Sapui5Resolver" && semver.lt(spec, "1.76.0")) { + throw new Error(`Could not resolve framework version ${version}. ` + + `Note that SAPUI5 framework libraries can only be consumed by the UI5 CLI ` + + `starting with SAPUI5 v1.76.0`); + } else if (this.name === "Openui5Resolver" && semver.lt(spec, "1.52.5")) { + throw new Error(`Could not resolve framework version ${version}. ` + + `Note that OpenUI5 framework libraries can only be consumed by the UI5 CLI ` + + `starting with OpenUI5 v1.52.5`); + } + } + throw new Error( + `Could not resolve framework version ${version}. ` + + `Make sure the version is valid and available in the configured registry.`); + } + + return resolvedVersion; + } + + static async _getVersionSpec(version, {ui5DataDir, cwd}) { + if (this._isSnapshotVersionOrRange(version)) { + const versionMatch = version.match(VERSION_RANGE_REGEXP); + if (versionMatch) { + // For snapshot version ranges we need to insert a stand-in "x" for the patch level + // and - in case none is provided - another "x" for the major version in order to + // convert it to a valid semver range: + // "1-SNAPSHOT" becomes "1.x.x-SNAPSHOT" and "1.112-SNAPSHOT" becomes "1.112.x-SNAPSHOT" + return `${versionMatch[1]}.${versionMatch[2] || "x"}.x-SNAPSHOT`; + } + } + + // Covers versions and ranges, as versions are also valid ranges + if (semver.validRange(version)) { + return version; + } + + // Check for invalid tag name (same check as npm does) + if (encodeURIComponent(version) !== version) { + return null; + } + + const allTags = await this.fetchAllTags({ui5DataDir, cwd}); + + if (!allTags) { + // Resolver doesn't support tags (e.g. Sapui5MavenSnapshotResolver) + // Only latest and latest-snapshot are supported which both resolve + // to the latest available version. + // See "isSnapshotVersionOrRange" for -snapshot handling + if ((version === "latest" || version === "latest-snapshot")) { + return "*"; + } else { + return null; + } + } + + if (!allTags[version]) { + throw new Error( + `Could not resolve framework version via tag '${version}'. ` + + `Make sure the tag is available in the configured registry.` + ); + } + + // Use version from tag + return allTags[version]; + } + + static _isSnapshotVersionOrRange(version) { + return version.toLowerCase().endsWith("-snapshot"); + } + + // To be implemented by resolver + async getLibraryMetadata(libraryName) { + throw new Error("AbstractResolver: getLibraryMetadata must be implemented!"); + } + async handleLibrary(libraryName) { + throw new Error("AbstractResolver: handleLibrary must be implemented!"); + } + static fetchAllVersions(options) { + throw new Error("AbstractResolver: static fetchAllVersions must be implemented!"); + } + static fetchAllTags(options) { + return null; + } +} + +export default AbstractResolver; diff --git a/packages/project/lib/ui5Framework/Openui5Resolver.js b/packages/project/lib/ui5Framework/Openui5Resolver.js new file mode 100644 index 00000000000..a6a9c4fc02a --- /dev/null +++ b/packages/project/lib/ui5Framework/Openui5Resolver.js @@ -0,0 +1,107 @@ +import path from "node:path"; +import os from "node:os"; +import AbstractResolver from "./AbstractResolver.js"; +import Installer from "./npm/Installer.js"; + +const OPENUI5_CORE_PACKAGE = "@openui5/sap.ui.core"; + +/** + * Resolver for the OpenUI5 framework + * + * @public + * @class + * @alias @ui5/project/ui5Framework/Openui5Resolver + * @extends @ui5/project/ui5Framework/AbstractResolver + */ +class Openui5Resolver extends AbstractResolver { + /** + * @param {*} options options + * @param {string} options.version OpenUI5 version to use + * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc + * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} [options.cacheDir] Where to store temp/cached packages. + * @param {string} [options.packagesDir] Where to install packages + * @param {string} [options.stagingDir] The staging directory for the packages + */ + constructor(options) { + super(options); + + const {cacheDir, packagesDir, stagingDir} = options; + + this._installer = new Installer({ + cwd: this._cwd, + ui5DataDir: this._ui5DataDir, + cacheDir, packagesDir, stagingDir + }); + this._loadLibraryMetadata = Object.create(null); + } + static _getNpmPackageName(libraryName) { + return "@openui5/" + libraryName; + } + static _getLibaryName(pkgName) { + return pkgName.replace(/^@openui5\//, ""); + } + getLibraryMetadata(libraryName) { + if (!this._loadLibraryMetadata[libraryName]) { + this._loadLibraryMetadata[libraryName] = Promise.resolve().then(async () => { + // Trigger manifest request to gather transitive dependencies + const pkgName = Openui5Resolver._getNpmPackageName(libraryName); + const libraryManifest = await this._installer.fetchPackageManifest({pkgName, version: this._version}); + let dependencies = []; + if (libraryManifest.dependencies) { + const depNames = Object.keys(libraryManifest.dependencies); + dependencies = depNames.map(Openui5Resolver._getLibaryName); + } + + // npm devDependencies are handled as "optionalDependencies" + // in terms of the UI5 framework metadata structure + let optionalDependencies = []; + if (libraryManifest.devDependencies) { + const devDepNames = Object.keys(libraryManifest.devDependencies); + optionalDependencies = devDepNames.map(Openui5Resolver._getLibaryName); + } + + return { + id: pkgName, + version: this._version, + dependencies, + optionalDependencies + }; + }); + } + return this._loadLibraryMetadata[libraryName]; + } + async handleLibrary(libraryName) { + const pkgName = Openui5Resolver._getNpmPackageName(libraryName); + return { + // Trigger metadata request + metadata: this.getLibraryMetadata(libraryName), + // Also trigger installation of package + install: this._installer.installPackage({ + pkgName, + version: this._version + }) + }; + } + static async fetchAllVersions(options) { + const installer = this._getInstaller(options); + return await installer.fetchPackageVersions({pkgName: OPENUI5_CORE_PACKAGE}); + } + + static async fetchAllTags(options) { + const installer = this._getInstaller(options); + return installer.fetchPackageDistTags({pkgName: OPENUI5_CORE_PACKAGE}); + } + + static _getInstaller({ui5DataDir, cwd} = {}) { + return new Installer({ + cwd: cwd ? path.resolve(cwd) : process.cwd(), + ui5DataDir: + ui5DataDir ? path.resolve(ui5DataDir) : + path.join(os.homedir(), ".ui5") + }); + } +} + +export default Openui5Resolver; diff --git a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js new file mode 100644 index 00000000000..7002bddbd27 --- /dev/null +++ b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js @@ -0,0 +1,275 @@ +import path from "node:path"; +import os from "node:os"; +import semver from "semver"; +import AbstractResolver from "./AbstractResolver.js"; +import Installer from "./maven/Installer.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:Sapui5MavenSnapshotResolver"); + +const DIST_PKG_NAME = "@sapui5/distribution-metadata"; +const DIST_GROUP_ID = "com.sap.ui5.dist"; +const DIST_ARTIFACT_ID = "sapui5-sdk-dist"; + +/** + * Resolver for the SAPUI5 framework + * + * This Resolver downloads and installs SNAPSHOTS of UI5 libraries from + * a Maven repository. It's meant for internal usage only as no use cases + * outside of SAP are known. + * + * @public + * @class + * @alias @ui5/project/ui5Framework/Sapui5MavenSnapshotResolver + * @extends @ui5/project/ui5Framework/AbstractResolver + */ +class Sapui5MavenSnapshotResolver extends AbstractResolver { + /** + * @param {*} options options + * @param {string} [options.snapshotEndpointUrl] Maven Repository Snapshot URL. Can by overruled + * by setting the UI5_MAVEN_SNAPSHOT_ENDPOINT_URL environment variable. If neither is provided, + * falling back to the standard Maven settings.xml file (if existing). + * @param {string} options.version SAPUI5 version to use + * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or + * pre-built (with build manifest) + * @param {string} [options.cwd=process.cwd()] Current working directory + * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default] + * Cache mode to use + */ + constructor(options) { + super(options); + + const { + cacheMode, + } = options; + + this._installer = new Installer({ + ui5DataDir: this._ui5DataDir, + snapshotEndpointUrlCb: + Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl), + cacheMode, + }); + this._loadDistMetadata = null; + + // TODO 5.0: Remove support for legacy snapshot versions + this._isLegacySnapshotVersion = semver.lt(this._version, "1.116.0-SNAPSHOT", { + includePrerelease: true + }); + } + loadDistMetadata() { + if (!this._loadDistMetadata) { + this._loadDistMetadata = Promise.resolve().then(async () => { + const version = this._version; + log.verbose( + `Installing ${DIST_ARTIFACT_ID} in version ${version}...` + ); + + const {pkgPath: distPkgPath} = await this._installer.installPackage({ + pkgName: DIST_PKG_NAME, + groupId: DIST_GROUP_ID, + artifactId: DIST_ARTIFACT_ID, + version, + classifier: "npm-sources", + extension: "zip", + }); + + return await this._installer.readJson( + path.join(distPkgPath, "metadata.json") + ); + }); + } + return this._loadDistMetadata; + } + async getLibraryMetadata(libraryName) { + const distMetadata = await this.loadDistMetadata(); + const metadata = distMetadata.libraries[libraryName]; + + if (!metadata) { + throw new Error(`Could not find library "${libraryName}"`); + } + + return metadata; + } + async handleLibrary(libraryName) { + const metadata = await this.getLibraryMetadata(libraryName); + if (!metadata.gav) { + throw new Error( + "Metadata is missing GAV (group, artifact and version) " + + "information. This might indicate an unsupported SNAPSHOT version." + ); + } + const gav = metadata.gav.split(":"); + let pkgName = metadata.npmPackageName; + + // Use "npm-dist" artifact by default + let classifier; + let extension; + if (this._sources) { + // Use npm-sources artifact if sources are requested + classifier = "npm-sources"; + extension = "zip"; + } else { + // Add "prebuilt" suffix to package name + pkgName += "-prebuilt"; + + if (this._isLegacySnapshotVersion) { + // For legacy versions < 1.116.0-SNAPSHOT where npm-dist artifact is not + // yet available, use "default" JAR + classifier = null; + extension = "jar"; + } else { + // Use "npm-dist" artifact by default + classifier = "npm-dist"; + extension = "zip"; + } + } + + return { + metadata: Promise.resolve({ + id: pkgName, + version: metadata.version, + dependencies: metadata.dependencies, + optionalDependencies: metadata.optionalDependencies, + }), + // Trigger installation of package + install: this._installer.installPackage({ + pkgName, + groupId: gav[0], + artifactId: gav[1], + version: metadata.version, + classifier, + extension, + }), + }; + } + + static async fetchAllVersions({ui5DataDir, cwd, snapshotEndpointUrl} = {}) { + const installer = new Installer({ + cwd: cwd ? path.resolve(cwd) : process.cwd(), + ui5DataDir: path.resolve( + ui5DataDir || path.join(os.homedir(), ".ui5") + ), + snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(snapshotEndpointUrl), + }); + return await installer.fetchPackageVersions({ + groupId: DIST_GROUP_ID, + artifactId: DIST_ARTIFACT_ID, + }); + } + + static _createSnapshotEndpointUrlCallback(snapshotEndpointUrl) { + snapshotEndpointUrl = process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL || snapshotEndpointUrl; + + if (!snapshotEndpointUrl) { + // Here we return a function which returns a promise that resolves with the URL. + // If we would already start resolving the settings.xml at this point, we'd need to always ask the + // end user for confirmation whether the resolved URL should be used. In some cases where the resources + // are already cached, this is actually not necessary and could be skipped + return Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl; + } else { + return () => Promise.resolve(snapshotEndpointUrl); + } + } + + /** + * Read the Maven repository snapshot endpoint URL from the central + * UI5 CLI configuration, with a fallback to central Maven configuration (is existing) + * + * @returns {Promise} The resolved snapshotEndpointUrl + */ + static async _resolveSnapshotEndpointUrl() { + const {default: Configuration} = await import("../config/Configuration.js"); + const config = await Configuration.fromFile(); + let url = config.getMavenSnapshotEndpointUrl(); + if (url) { + log.verbose(`Using UI5 CLI configuration for mavenSnapshotEndpointUrl: ${url}`); + } else { + log.verbose(`No mavenSnapshotEndpointUrl configuration found`); + url = await Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven(); + if (url) { + log.verbose(`Updating UI5 CLI configuration with new mavenSnapshotEndpointUrl: ${url}`); + const configJson = config.toJson(); + configJson.mavenSnapshotEndpointUrl = url; + await Configuration.toFile(new Configuration(configJson)); + } + } + return url; + } + + /** + * Tries to detect whether ~/.m2/settings.xml exist, and if so, whether + * the snapshot.build URL is extracted from there + * + * @param {string} [settingsXML=~/.m2/settings.xml] Path to the settings.xml. + * If not provided, the default location is used + * @returns {Promise} The resolved snapshot.build URL from ~/.m2/settings.xml + */ + static async _resolveSnapshotEndpointUrlFromMaven(settingsXML) { + if (!process.stdout.isTTY) { + // We can't prompt the user if stdout is non-interactive (i.e. in CI environments) + // Therefore skip resolution from Maven settings.xml altogether + return null; + } + + settingsXML = + settingsXML || path.resolve(path.join(os.homedir(), ".m2", "settings.xml")); + + const {default: fs} = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const readFile = promisify(fs.readFile); + const xml2js = await import("xml2js"); + const parser = new xml2js.Parser({ + preserveChildrenOrder: true, + xmlns: true, + }); + let url; + + log.verbose(`Attempting to resolve snapshot endpoint URL from Maven configuration file at ${settingsXML}...`); + try { + const fileContent = await readFile(settingsXML); + const xmlContents = await parser.parseStringPromise(fileContent); + + const snapshotBuildChunk = xmlContents?.settings?.profiles[0]?.profile.filter( + (prof) => prof.id[0]._ === "snapshot.build" + )[0]; + + url = + snapshotBuildChunk?.repositories?.[0]?.repository?.[0]?.url?.[0]?._ || + snapshotBuildChunk?.pluginRepositories?.[0]?.pluginRepository?.[0]?.url?.[0]?._; + + if (!url) { + log.verbose(`"snapshot.build" attribute could not be found in ${settingsXML}`); + return null; + } + } catch (err) { + if (err.code === "ENOENT") { + // "File or directory does not exist" + log.verbose(`File does not exist: ${settingsXML}`); + } else { + log.warning(`Failed to read Maven configuration file from ${settingsXML}: ${err.message}`); + } + return null; + } + + const {default: yesno} = await import("yesno"); + const ok = await yesno({ + question: + "\nA Maven repository endpoint URL is required for consuming snapshot versions of UI5 libraries.\n" + + "You can configure one using the command: 'ui5 config set mavenSnapshotEndpointUrl '\n\n" + + `The following URL has been found in a Maven configuration file at ${settingsXML}:\n${url}\n\n` + + `Continue with this endpoint URL and remember it for the future? (yes)`, + defaultValue: true, + }); + + if (ok) { + log.verbose(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`); + return url; + } else { + log.verbose(`User rejected usage of the resolved URL`); + return null; + } + } +} + +export default Sapui5MavenSnapshotResolver; diff --git a/packages/project/lib/ui5Framework/Sapui5Resolver.js b/packages/project/lib/ui5Framework/Sapui5Resolver.js new file mode 100644 index 00000000000..300020dce25 --- /dev/null +++ b/packages/project/lib/ui5Framework/Sapui5Resolver.js @@ -0,0 +1,128 @@ +import path from "node:path"; +import os from "node:os"; +import semver from "semver"; +import AbstractResolver from "./AbstractResolver.js"; +import Installer from "./npm/Installer.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:Sapui5Resolver"); + +const DIST_PKG_NAME = "@sapui5/distribution-metadata"; + +/** + * Resolver for the SAPUI5 framework + * + * @public + * @class + * @alias @ui5/project/ui5Framework/Sapui5Resolver + * @extends @ui5/project/ui5Framework/AbstractResolver + */ +class Sapui5Resolver extends AbstractResolver { + /** + * @param {*} options options + * @param {string} options.version SAPUI5 version to use + * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc + * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. Relative to `process.cwd()` + * @param {string} [options.cacheDir] Where to store temp/cached packages. + * @param {string} [options.packagesDir] Where to install packages + * @param {string} [options.stagingDir] The staging directory for packages + */ + constructor(options) { + super(options); + + const {cacheDir, packagesDir, stagingDir} = options; + + this._installer = new Installer({ + cwd: this._cwd, + ui5DataDir: this._ui5DataDir, + cacheDir, packagesDir, stagingDir + }); + this._loadDistMetadata = null; + } + loadDistMetadata() { + if (!this._loadDistMetadata) { + this._loadDistMetadata = Promise.resolve().then(async () => { + const version = this._version; + log.verbose(`Installing ${DIST_PKG_NAME} in version ${version}...`); + const pkgName = DIST_PKG_NAME; + const {pkgPath} = await this._installer.installPackage({ + pkgName, + version + }); + + const metadata = await this._installer.readJson(path.join(pkgPath, "metadata.json")); + return metadata; + }); + } + return this._loadDistMetadata; + } + async getLibraryMetadata(libraryName) { + const distMetadata = await this.loadDistMetadata(); + const metadata = distMetadata.libraries[libraryName]; + + if (!metadata) { + throw new Error(`Could not find library "${libraryName}"`); + } + + if (metadata.npmPackageName.startsWith("@openui5/") && + semver.satisfies(this._version, "1.77.x")) { + // TODO: Remove this workaround once SAPUI5 1.77.x isn't used anymore. + // As of Dec 2022 there are still ~80 downloads per week (npmjs.com stats). + // 1.77.x (at least 1.77.0-1.77.2) distribution metadata.json is missing + // dependency information for all OpenUI5 libraries. + // Therefore we need to request those from the registry like it is done + // for OpenUI5 projects. + const {default: Openui5Resolver} = await import("./Openui5Resolver.js"); + const openui5Resolver = new Openui5Resolver({ + cwd: this._cwd, + version: metadata.version + }); + const openui5Metadata = await openui5Resolver.getLibraryMetadata(libraryName); + return { + npmPackageName: openui5Metadata.id, + version: openui5Metadata.version, + dependencies: openui5Metadata.dependencies, + optionalDependencies: openui5Metadata.optionalDependencies + }; + } + + return metadata; + } + async handleLibrary(libraryName) { + const metadata = await this.getLibraryMetadata(libraryName); + + return { + metadata: Promise.resolve({ + id: metadata.npmPackageName, + version: metadata.version, + dependencies: metadata.dependencies, + optionalDependencies: metadata.optionalDependencies + }), + // Trigger installation of package + install: this._installer.installPackage({ + pkgName: metadata.npmPackageName, + version: metadata.version + }) + }; + } + static async fetchAllVersions(options) { + const installer = this._getInstaller(options); + return await installer.fetchPackageVersions({pkgName: DIST_PKG_NAME}); + } + + static async fetchAllTags(options) { + const installer = this._getInstaller(options); + return installer.fetchPackageDistTags({pkgName: DIST_PKG_NAME}); + } + + static _getInstaller({ui5DataDir, cwd} = {}) { + return new Installer({ + cwd: cwd ? path.resolve(cwd) : process.cwd(), + ui5DataDir: + ui5DataDir ? path.resolve(ui5DataDir) : + path.join(os.homedir(), ".ui5") + }); + } +} + +export default Sapui5Resolver; diff --git a/packages/project/lib/ui5Framework/maven/CacheMode.js b/packages/project/lib/ui5Framework/maven/CacheMode.js new file mode 100644 index 00000000000..d1b5af0d422 --- /dev/null +++ b/packages/project/lib/ui5Framework/maven/CacheMode.js @@ -0,0 +1,18 @@ + + +/** + * Cache modes for maven consumption + * + * @public + * @readonly + * @enum {string} + * @property {string} Default Cache everything, invalidate after 9 hours + * @property {string} Force Use cache only. Do not send any requests to the repository + * @property {string} Off Invalidate the cache and update from the repository + * @module @ui5/project/ui5Framework/maven/CacheMode + */ +export default { + Default: "Default", + Force: "Force", + Off: "Off" +}; diff --git a/packages/project/lib/ui5Framework/maven/Installer.js b/packages/project/lib/ui5Framework/maven/Installer.js new file mode 100644 index 00000000000..4dd6d1bc8cd --- /dev/null +++ b/packages/project/lib/ui5Framework/maven/Installer.js @@ -0,0 +1,525 @@ +import path from "node:path"; +import {mkdirp} from "../../utils/fs.js"; +import fs from "graceful-fs"; +import _StreamZip from "node-stream-zip"; +const StreamZip = _StreamZip.async; +import {promisify} from "node:util"; +import Registry from "./Registry.js"; +import AbstractInstaller from "../AbstractInstaller.js"; +import CacheMode from "./CacheMode.js"; +import {rmrf} from "../../utils/fs.js"; +const stat = promisify(fs.stat); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); +const rename = promisify(fs.rename); +const rm = promisify(fs.rm); +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:maven:Installer"); +const mvnTimestampRegex = /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/; + +const CACHE_TIME = 32400000; // 9 hours + +class Installer extends AbstractInstaller { + /** + * @param {object} parameters Parameters + * @param {string} parameters.ui5DataDir UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. + * @param {Function} parameters.snapshotEndpointUrlCb Callback that returns a Promise , + * resolving to the Maven repository URL. + * Example: https://registry.corp/vendor/build-snapshots/ + * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [parameters.cacheMode=Default] Cache mode to use + */ + constructor({ui5DataDir, snapshotEndpointUrlCb, cacheMode = CacheMode.Default}) { + super(ui5DataDir); + + this._artifactsDir = path.join(ui5DataDir, "framework", "artifacts"); + this._packagesDir = path.join(ui5DataDir, "framework", "packages"); + this._metadataDir = path.join(ui5DataDir, "framework", "metadata"); + this._stagingDir = path.join(ui5DataDir, "framework", "staging"); + + this._cacheMode = cacheMode; + this._snapshotEndpointUrlCb = snapshotEndpointUrlCb; + + if (!this._snapshotEndpointUrlCb) { + throw new Error(`Installer: Missing Snapshot-Endpoint URL callback parameter`); + } + if (!Object.values(CacheMode).includes(cacheMode)) { + throw new Error(`Installer: Invalid value '${cacheMode}' for cacheMode parameter. ` + + `Must be one of ${Object.values(CacheMode).join(", ")}`); + } + + log.verbose(`Installing Maven artifacts to: ${this._artifactsDir}`); + log.verbose(`Installing Packages to: ${this._packagesDir}`); + log.verbose(`Caching mode: ${this._cacheMode}`); + } + + async getRegistry() { + if (this._cachedRegistry) { + return this._cachedRegistry; + } + return (this._cachedRegistry = Promise.resolve().then(async () => { + const snapshotEndpointUrl = await this._snapshotEndpointUrlCb(); + if (!snapshotEndpointUrl) { + throw new Error( + `Installer: Missing or empty Maven repository URL for snapshot consumption. ` + + `This URL is required for consuming snapshot versions of UI5 libraries. ` + + `Please configure the correct URL using the following command: ` + + `'ui5 config set mavenSnapshotEndpointUrl '`); + } else { + return new Registry({endpointUrl: snapshotEndpointUrl}); + } + })); + } + + async readJson(jsonPath) { + return JSON.parse(await readFile(jsonPath, {encoding: "utf8"})); + } + + async _writeJson(jsonPath, jsonObject) { + return writeFile(jsonPath, JSON.stringify(jsonObject)); + } + + async fetchPackageVersions({groupId, artifactId}) { + const reg = await this.getRegistry(); + const metadata = await reg.requestMavenMetadata({groupId, artifactId}); + + if (!metadata?.versioning?.versions?.version) { + throw new Error(`Missing Maven metadata for artifact ${groupId}:${artifactId}`); + } + return metadata.versioning.versions.version.filter((version) => { + // This resolver can only handle SNAPSHOT versions + return version.endsWith("-SNAPSHOT"); + }); + } + + + /** + * Metadata for an artifact as identified by it's Maven coordinates + * + * @typedef {object} @ui5/project/ui5Framework/maven/Installer~LocalMetadata + * @property {integer} lastCheck Timestamp of the last time these metadata have been compared with the repository + * @property {integer} lastUpdate Timestamp of the last time the artifact has been updated in the repository + * (typically older than last check) + * @property {string} revision Current revision of the artifact + * @property {string[]} staleRevisions Previously installed revisions of the artifact + */ + + /** + * Fills and maintains locally cached metadata for the given artifact coordinates + * + * @param {object} coordinates + * @param {string} coordinates.groupId GroupId of the requested artifact + * @param {string} coordinates.artifactId ArtifactId of the requested artifact + * @param {string} coordinates.version Version of the requested artifact + * @param {string|null} coordinates.classifier Classifier of the requested artifact + * @param {string} coordinates.extension Extension of the requested artifact + * @param {string} [coordinates.pkgName] npm package name the artifact corresponds to (if any) + * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata} + */ + async _fetchArtifactMetadata(coordinates) { + const fsId = this._generateFsIdFromCoordinates(coordinates); + const logId = this._generateLogIdFromCoordinates(coordinates); + return this._synchronize("metadata-" + fsId, async () => { + const localMetadata = await this._getLocalArtifactMetadata(fsId); + + if (this._cacheMode === CacheMode.Force && !localMetadata.revision) { + throw new Error(`Could not find artifact ` + + `${logId} in local cache`); + } + + const now = new Date().getTime(); + const timeSinceLastCheck = now - localMetadata.lastCheck; + + if (this._cacheMode !== CacheMode.Force && + (timeSinceLastCheck > CACHE_TIME || this._cacheMode === CacheMode.Off)) { + // No cached metadata (-> timeSinceLastCheck equals time since 1970) or + // too old metadata or disabled cache + // => Retrieve metadata from repository + if (localMetadata.lastCheck === 0) { + log.verbose( + `Could not find metadata for artifact ${logId} in local cache. Fetching from repository...`); + } else { + log.verbose( + `Refreshing metadata cache for artifact ${logId} ` + + // TODO better formatting of elapsed time + `(last checked ${timeSinceLastCheck/1000} seconds ago)`); + } + + log.info( + `Fetching latest metadata for artifact ${coordinates.artifactId} version ${coordinates.version} ` + + `from Maven registry...`); + const {lastUpdate, revision} = await this._getRemoteArtifactMetadata(coordinates); + + // TODO better formatting of elapsed time + log.verbose(`Retrieved metadata for artifact ${logId} is ` + + `${(lastUpdate - localMetadata.lastUpdate) / 1000} seconds younger than local metadata`); + log.verbose(`Retrieved deployment version is ${revision}`); + + this._rotateRevision(localMetadata, revision); + + await this._removeStaleRevisions(logId, localMetadata, coordinates); + + localMetadata.lastCheck = now; + localMetadata.lastUpdate = lastUpdate; + await this._writeLocalArtifactMetadata(fsId, localMetadata); + } else { + log.verbose(`Using metadata for artifact ${logId} from local cache`); + } + return localMetadata; + }); + } + + /** + * Fills and maintains locally cached metadata for the given artifact coordinates + * + * @param {object} coordinates + * @param {string} coordinates.groupId GroupId of the requested artifact + * @param {string} coordinates.artifactId ArtifactId of the requested artifact + * @param {string} coordinates.version Version of the requested artifact + * @param {string|null} coordinates.classifier Classifier of the requested artifact + * @param {string} coordinates.extension Extension of the requested artifact + * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata} + */ + async _getRemoteArtifactMetadata({groupId, artifactId, version, classifier, extension}) { + const reg = await this.getRegistry(); + const metadata = await reg.requestMavenMetadata({groupId, artifactId, version}); + + if (!metadata?.versioning?.snapshotVersions?.snapshotVersion) { + throw new Error(`Missing Maven snapshot metadata for artifact ${groupId}:${artifactId}:${version}`); + } + + const snapshotVersion = metadata.versioning.snapshotVersions.snapshotVersion; + const deploymentMetadata = snapshotVersion.find(({ + classifier: candidateClassifier, // Classifier can be null, e.g. for the default "jar" artifact + extension: candidateExtension + }) => (!classifier || candidateClassifier === classifier) && candidateExtension === extension); + + if (!deploymentMetadata) { + const optionalClassifier = classifier ? `${classifier}.` : ""; + throw new Error( + `Could not find ${optionalClassifier}${extension} deployment for artifact ` + + `${groupId}:${artifactId}:${version} in snapshot metadata:\n` + + `${JSON.stringify(snapshotVersion)}`); + } + // Convert Maven timestamp (yyyyMMddHHmmss UTC) to ISO string (YYYY-MM-DDTHH:mm:ss.sssZ) + // E.g. 20220828080910 becomes 2022-08-28T08:09:10.000Z + const isoTimestamp = deploymentMetadata.updated.replace(mvnTimestampRegex, "$1-$2-$3T$4:$5:$6.000Z"); + const ts = new Date(isoTimestamp); + + const logId = this._generateLogIdFromCoordinates({groupId, artifactId, version, classifier, extension}); + log.verbose(`Retrieved metadata for ${logId}:` + + `\n Last update was at: ${ts.toISOString()}` + + `\n Current deployment version is: ${deploymentMetadata.value}`); + return { + lastUpdate: ts.getTime(), + revision: deploymentMetadata.value + }; + } + + /** + * Reads locally cached metadata for the given artifact coordinates + * + * @param {string} id File System identifier for the artifact. Typically derived from the coordinates + * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata} + */ + async _getLocalArtifactMetadata(id) { + try { + return await this.readJson(path.join(this._metadataDir, `${id}.json`)); + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + // If not found, initialize metadata + return { + lastCheck: 0, + lastUpdate: 0, + revision: null, + staleRevisions: [] + }; + } else { + throw err; + } + } + } + + async _writeLocalArtifactMetadata(id, content) { + await mkdirp(this._metadataDir); + return await this._writeJson(path.join(this._metadataDir, `${id}.json`), content); + } + + _rotateRevision(metadata, newRevision) { + if (metadata.revision) { + metadata.staleRevisions.push(metadata.revision); + } + metadata.revision = newRevision; + } + + async _removeStaleRevisions(logId, metadata, {pkgName, groupId, artifactId, classifier, extension}) { + if (metadata.staleRevisions.length <= 1) { + // Keep at least one revision. Nothing to do + return; + } + log.verbose(`Removing ${metadata.staleRevisions.length - 1} stale revision for ${logId}`); + while (metadata.staleRevisions.length > 3) { + const revision = metadata.staleRevisions.shift(); + const artifactPath = this._getTargetPathForArtifact({ + groupId, + artifactId, + revision, + classifier, + extension + }); + log.verbose(`Removing ${artifactPath}...`); + await rm(artifactPath, { + force: true + }); + + if (pkgName) { + const packageDir = this._getTargetDirForPackage(pkgName, revision); + log.verbose(`Removing directory ${packageDir}...`); + await rmrf(packageDir); + } + } + } + + /** + * @typedef {object} @ui5/project/ui5Framework/maven/Installer~InstalledPackage + * @property {string} pkgPath + */ + + /** + * Downloads the respective artifact and extracts the zip archive into a structure similar to + * the npm installer + * + * @param {object} parameters + * @param {string} parameters.pkgName Name of the npm package + * @param {string} parameters.groupId GroupId of the requested artifact + * @param {string} parameters.artifactId ArtifactId of the requested artifact + * @param {string} parameters.version Version of the requested artifact + * @param {string|null} parameters.classifier Classifier of the requested artifact + * @param {string} parameters.extension Extension of the requested artifact + * @returns {@ui5/project/ui5Framework/maven/Installer~InstalledPackage} + */ + async installPackage({pkgName, groupId, artifactId, version, classifier, extension}) { + const {revision} = await this._fetchArtifactMetadata({ + pkgName, groupId, artifactId, version, classifier, extension + }); + + const coordinates = { + groupId, artifactId, + version, revision, + classifier, extension + }; + + const targetDir = this._getTargetDirForPackage(pkgName, revision); + const installed = await this._projectExists(targetDir); + + if (!installed) { + await this._synchronize(`package-${pkgName}@${revision}`, async () => { + const installed = await this._projectExists(targetDir); + + if (installed) { + log.verbose(`Already installed: ${pkgName} in SNAPSHOT version ${revision}`); + return; + } + + const stagingDir = this._getStagingDirForPackage(pkgName, revision); + + // Check whether staging dir already exists and remove it + if (await this._pathExists(stagingDir)) { + log.verbose(`Removing stale staging directory at ${stagingDir}...`); + await rmrf(stagingDir); + } + + await mkdirp(stagingDir); + + const {artifactPath, removeArtifact} = await this.installArtifact(coordinates); + + log.verbose(`Extracting archive at ${artifactPath} to ${stagingDir}...`); + const zip = new StreamZip({file: artifactPath}); + let rootDir = null; + if (extension === "jar") { + rootDir = "META-INF"; + } + await zip.extract(rootDir, stagingDir); + await zip.close(); + + // Check whether target dir already exists and remove it + if (await this._pathExists(targetDir)) { + log.verbose(`Removing existing target directory at ${targetDir}...`); + await rmrf(targetDir); + } + + // Do not create target dir itself to prevent EPERM error in following rename operation + // (https://github.com/UI5/cli/issues/487) + await mkdirp(path.dirname(targetDir)); + log.verbose(`Promoting staging directory from ${stagingDir} to ${targetDir}...`); + await rename(stagingDir, targetDir); + + await removeArtifact(); + }); + } else { + log.verbose(`Already installed: ${pkgName} in SNAPSHOT version ${revision}`); + } + return { + pkgPath: targetDir + }; + } + + /** + * @typedef {object} @ui5/project/ui5Framework/maven/Installer~InstalledArtifact + * @property {string} artifactPath + * @property {Function} removeArtifact Callback to trigger removal of the artifact file in case it + * is no longer required. + */ + + /** + * @param {object} parameters + * @param {string} parameters.groupId GroupId of the requested artifact + * @param {string} parameters.artifactId ArtifactId of the requested artifact + * @param {string} parameters.version Version of the requested artifact + * @param {string|null} parameters.classifier Classifier of the requested artifact + * @param {string} parameters.extension Extension of the requested artifact + * @param {string} [parameters.revision] Optional revision of the artifact to request. + * If not provided, the latest revision will be determined from the registry metadata. + * @returns {@ui5/project/ui5Framework/maven/Installer~InstalledArtifact} + */ + async installArtifact({groupId, artifactId, version, classifier, extension, revision}) { + if (!revision) { + const metadata = await this._fetchArtifactMetadata({ + groupId, artifactId, version, classifier, extension + }); + revision = metadata.revision; + } + const coordinates = { + groupId, artifactId, + version, revision, + classifier, extension + }; + + const targetPath = this._getTargetPathForArtifact(coordinates); + const installed = await this._pathExists(targetPath); + const logId = this._generateLogIdFromCoordinates(coordinates); + const fsId = this._generateFsIdFromCoordinates(coordinates); + if (!installed) { + await this._synchronize(`artifact-${fsId}`, async () => { + // check again whether the artifact is now installed + const installed = await this._pathExists(targetPath); + if (installed) { + log.verbose(`Already installed: ${artifactId} in version ${revision}`); + return; + } + + const stagingPath = this._getStagingPathForArtifact(coordinates); + log.info(`Installing missing artifact ${logId}...`); + + // Check whether staging dir already exists and remove it + if (await this._pathExists(stagingPath)) { + log.verbose(`Removing existing file in staging dir at ${stagingPath}...`); + await rm(stagingPath); + } + await mkdirp(path.dirname(stagingPath)); + + log.verbose(`Installing ${artifactId} in version ${version} to ${stagingPath}...`); + + // TODO: Stream response body to installPackage and unzip directly via + // https://github.com/isaacs/minizlib (already in dependencies through pacote) + // This way we do not store the archive unnecessarily + const reg = await this.getRegistry(); + await reg.requestArtifact(coordinates, stagingPath); + + await mkdirp(path.dirname(targetPath)); + log.verbose( + `Promoting artifact from staging path ${stagingPath} to target path at ${targetPath}...`); + await rename(stagingPath, targetPath); + }); + } else { + log.verbose(`Already installed: ${artifactId} in version ${revision}`); + } + return { + artifactPath: targetPath, + removeArtifact: () => { + return rm(targetPath); + } + }; + } + + async _projectExists(targetDir) { + return this._pathExists(path.join(targetDir, "package.json")); + } + + async _pathExists(targetPath) { + try { + await stat(targetPath); + return true; + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + return false; + } else { + throw err; + } + } + } + + _getStagingPathForArtifact(coordinates) { + // Staging dir should only contain single files, no directory hierarchy. + // This makes cleanups after promoting artifacts easier and does not leave empty directories. + return path.join(this._stagingDir, this._generateFsIdFromCoordinates(coordinates)); + } + + _getTargetPathForArtifact({groupId, artifactId, revision, classifier, extension}) { + if (!classifier) { + classifier = revision; + revision = ""; + } + return path.join(this._artifactsDir, + `${groupId}-${artifactId}`.replaceAll(".", "_"), revision, `${classifier}.${extension}`); + } + + _getStagingDirForPackage(pkgName, version) { + // Staging dir should only contain single files, no directory hierarchy. + // This makes cleanups after promoting artifacts easier and does not leave empty directories. + return path.join(this._stagingDir, `${pkgName.replaceAll("/", "-")}-${version}`); + } + + _getTargetDirForPackage(pkgName, version) { + return path.join(this._packagesDir, ...pkgName.split("/"), version); + } + + /** + * Generate an identifier for an artifact that is safe to use in file names. + * Used for naming metadata- and lock-files + * + * @param {object} parameters + * @param {string} parameters.groupId GroupId of the artifact + * @param {string} parameters.artifactId ArtifactId of the artifact + * @param {string} parameters.extension Extension of the artifact + * @param {string} [parameters.classifier] Optional classifier of the artifact + * @param {string} [parameters.version] Version of the artifact. Optional if revision is provided + * @param {string} [parameters.revision] Optional revision of the artifact + * @returns {string} A unique identifier for the provided combination of parameters + */ + _generateFsIdFromCoordinates({groupId, artifactId, version, classifier, extension, revision}) { + // Using underscores instead of colons, since the colon is a reserved character for + // filenames on Windows and macOS + const optionalClassifier = classifier ? `${classifier}.` : ""; + return `${groupId}_${artifactId}_${revision || version}_${optionalClassifier}${extension}`; + } + + /** + * Generate an identifier for an artifact that is suitable for logging purposes + * + * @param {object} parameters + * @param {string} parameters.groupId GroupId of the artifact + * @param {string} parameters.artifactId ArtifactId of the artifact + * @param {string} parameters.version Version of the artifact + * @param {string} parameters.extension Extension of the artifact + * @param {string} [parameters.classifier] Optional classifier of the artifact + * @param {string} [parameters.revision] Optional revision of the artifact + * @returns {string} A string with the Maven-typical formatting of the provided coordinates + */ + _generateLogIdFromCoordinates({groupId, artifactId, version, classifier, extension, revision}) { + const optionalClassifier = classifier ? `${classifier}.` : ""; + return `${groupId}:${artifactId}:${revision || version}:${optionalClassifier}${extension}`; + } +} + +export default Installer; diff --git a/packages/project/lib/ui5Framework/maven/Registry.js b/packages/project/lib/ui5Framework/maven/Registry.js new file mode 100644 index 00000000000..ec5b2293e75 --- /dev/null +++ b/packages/project/lib/ui5Framework/maven/Registry.js @@ -0,0 +1,121 @@ +import {getLogger} from "@ui5/logger"; +import fetch from "make-fetch-happen"; +import xml2js from "xml2js"; +import {promisify} from "node:util"; +import {pipeline} from "node:stream/promises"; +import fs from "graceful-fs"; +const log = getLogger("ui5Framework:maven:Registry"); + +class Registry { + /** + * @param {object} parameters Parameters + * @param {string} parameters.endpointUrl Maven's endpoint URL + */ + constructor({endpointUrl}) { + if (!endpointUrl) { + throw new Error(`Registry: Missing parameter "endpointUrl"`); + } + this._endpointUrl = endpointUrl; + if (!this._endpointUrl.endsWith("/")) { + this._endpointUrl += "/"; + log.verbose(`Registry: Effective "endpointUrl" resolved to "${this._endpointUrl}"`); + } + } + + /** + * Requests a maven-metadata.xml file from the repository + * + * @param {object} options options + * @param {string} options.groupId + * @param {string} options.artifactId + * @param {string} [options.version] If given, the version must be a SNAPSHOT version. + * In this case, the resulting metadata will list all artifact versions + * (and timestamps) deployed for that SNAPSHOT. + * If not provided, the resulting metadata will list all versions available for the artifact. + */ + async requestMavenMetadata({groupId, artifactId, version}) { + try { + const optionalVersion = version ? version + "/" : ""; + const url = this._endpointUrl + + `${groupId.replaceAll(".", "/")}/${artifactId}/${optionalVersion}maven-metadata.xml`; + + log.verbose(`Fetching: ${url}`); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`[HTTP Error] ${res.status} ${res.statusText}`); + } + + const parser = new xml2js.Parser({ + explicitArray: false, + ignoreAttrs: true + }); + const readXML = promisify(parser.parseString); + const content = await res.buffer(); + const parsedXml = await readXML(content); + if (!parsedXml?.metadata) { + throw new Error( + `Empty or unexpected response body:\n${content}\nParsed as:\n${JSON.stringify(parsedXml)}`); + } + return parsedXml.metadata; + } catch (err) { + if (err.code === "ENOTFOUND") { + throw new Error( + `Failed to connect to Maven registry at ${this._endpointUrl}. ` + + `Please check the correct endpoint URL is maintained and can be reached. ` + + `You can change the configured URL using the following command: ` + + `'ui5 config set mavenSnapshotEndpointUrl '`); + + // TODO: Allow cacheMode to be set from outside + // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // ` or use the --offline flag`); // TODO: Implement --offline flag + } + throw new Error( + `Failed to retrieve maven-metadata.xml for ${groupId}:${artifactId}:${version}: ${err.message}`); + } + } + + async requestArtifact({groupId, artifactId, version, revision, classifier, extension}, targetPath) { + try { + // Classifier can be null, e.g. for the default "jar" artifact + const optionalClassifier = classifier ? `-${classifier}` : ""; + const url = this._endpointUrl + + `${groupId.replaceAll(".", "/")}/${artifactId}/${version}/` + + `${artifactId}-${revision}${optionalClassifier}.${extension}`; + + log.verbose(`Fetching: ${url}`); + const res = await fetch(url, { + cache: "no-store", // Do not cache these large artifacts. We store them right away anyways + + // Disable usage of shared keep-alive agents. + // make-fetch-happen uses a hard-coded 15 seconds freeSocketTimeout + // that can be easily reached (Error: Socket timeout) and there doesn't + // seem to be another way to disable or increase it. + // Also see: https://github.com/node-modules/agentkeepalive/issues/106 + // The same applies in npm/Registry.js + agent: false, + }); + if (!res.ok) { + throw new Error(`[HTTP Error] ${res.status} ${res.statusText}`); + } + + // Write to target + await pipeline(res.body, fs.createWriteStream(targetPath)); + } catch (err) { + if (err.code === "ENOTFOUND") { + throw new Error( + `Failed to connect to Maven registry at ${this._endpointUrl}. ` + + `Please check the correct endpoint URL is maintained and can be reached. ` + + `You can change the configured URL using the following command: ` + + `'ui5 config set mavenSnapshotEndpointUrl '`); + + // TODO: Allow cacheMode to be set from outside + // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // ` or use the --offline flag`); // TODO: Implement --offline flag + } + throw new Error(`Failed to retrieve artifact ` + + `${groupId}:${artifactId}:${version}:${classifier}:${extension} ${err.message}`); + } + } +} + +export default Registry; diff --git a/packages/project/lib/ui5Framework/npm/Installer.js b/packages/project/lib/ui5Framework/npm/Installer.js new file mode 100644 index 00000000000..40d1dae9814 --- /dev/null +++ b/packages/project/lib/ui5Framework/npm/Installer.js @@ -0,0 +1,160 @@ +import path from "node:path"; +import {mkdirp} from "../../utils/fs.js"; +import fs from "graceful-fs"; +import {promisify} from "node:util"; +import Registry from "./Registry.js"; +import AbstractInstaller from "../AbstractInstaller.js"; +import {rmrf} from "../../utils/fs.js"; +const stat = promisify(fs.stat); +const readFile = promisify(fs.readFile); +const rename = promisify(fs.rename); +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:npm:Installer"); + +class Installer extends AbstractInstaller { + /** + * @param {object} parameters Parameters + * @param {string} parameters.cwd Current working directory + * @param {string} parameters.ui5DataDir UI5 home directory location. This will be used to store packages, + * metadata and configuration used by the resolvers. + * @param {string} [parameters.packagesDir="${ui5DataDir}/framework/packages"] Where to install packages + * @param {string} [parameters.stagingDir="${ui5DataDir}/framework/staging"] The staging directory for the packages + * @param {string} [parameters.cacheDir="${ui5DataDir}/framework/cacache"] Where to store temp/cached packages. + */ + constructor({cwd, ui5DataDir, packagesDir, stagingDir, cacheDir}) { + super(ui5DataDir); + if (!cwd) { + throw new Error(`Installer: Missing parameter "cwd"`); + } + this._packagesDir = packagesDir ? + path.resolve(packagesDir) : path.join(ui5DataDir, "framework", "packages"); + + log.verbose(`Installing to: ${this._packagesDir}`); + + this._cwd = cwd; + this._caCacheDir = cacheDir ? + path.resolve(cacheDir) : path.join(ui5DataDir, "framework", "cacache"); + this._stagingDir = stagingDir ? + path.resolve(stagingDir) : path.join(ui5DataDir, "framework", "staging"); + } + + getRegistry() { + if (this._cachedRegistry) { + return this._cachedRegistry; + } + return this._cachedRegistry = new Registry({ + cwd: this._cwd, + cacheDir: this._caCacheDir + }); + } + + async readJson(jsonPath) { + return JSON.parse(await readFile(jsonPath, {encoding: "utf8"})); + } + + async fetchPackageVersions({pkgName}) { + const packument = await this.getRegistry().requestPackagePackument(pkgName); + return Object.keys(packument.versions); + } + + async fetchPackageDistTags({pkgName}) { + const packument = await this.getRegistry().requestPackagePackument(pkgName); + return packument["dist-tags"]; + } + + async fetchPackageManifest({pkgName, version}) { + const targetDir = this._getTargetDirForPackage({pkgName, version}); + try { + const pkg = await this.readJson(path.join(targetDir, "package.json")); + return { + name: pkg.name, + dependencies: pkg.dependencies, + devDependencies: pkg.devDependencies + }; + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + const manifest = await this.getRegistry().requestPackageManifest(pkgName, version); + return { + name: manifest.name, + dependencies: manifest.dependencies, + devDependencies: manifest.devDependencies + }; + } else { + throw err; + } + } + } + + async installPackage({pkgName, version}) { + const targetDir = this._getTargetDirForPackage({pkgName, version}); + const installed = await this._packageJsonExists(targetDir); + if (!installed) { + await this._synchronize(`package-${pkgName}@${version}`, async () => { + // check again whether package is now installed + const installed = await this._packageJsonExists(targetDir); + if (!installed) { + const stagingDir = this._getStagingDirForPackage({pkgName, version}); + log.info(`Installing missing package ${pkgName}...`); + + // Check whether staging dir already exists and remove it + if (await this._pathExists(stagingDir)) { + log.verbose(`Removing existing staging directory at ${stagingDir}...`); + await rmrf(stagingDir); + } + + // Check whether target dir already exists and remove it. + // A target directory already existing but missing a package.json should + // never happen. However, we want to be *really* sure that there is no target + // directory so that the rename operation won't have any no trouble. + if (await this._pathExists(targetDir)) { + log.verbose(`Removing existing target directory at ${targetDir}...`); + await rmrf(targetDir); + } + + log.verbose(`Installing ${pkgName} in version ${version} to ${stagingDir}...`); + await this.getRegistry().extractPackage(pkgName, version, stagingDir); + + // Do not create target dir itself to prevent EPERM error in following rename operation + // (https://github.com/UI5/cli/issues/487) + await mkdirp(path.dirname(targetDir)); + log.verbose(`Promoting staging directory from ${stagingDir} to ${targetDir}...`); + await rename(stagingDir, targetDir); + } else { + log.verbose(`Already installed: ${pkgName} in version ${version}`); + } + }); + } else { + log.verbose(`Already installed: ${pkgName} in version ${version}`); + } + return { + pkgPath: targetDir + }; + } + + async _packageJsonExists(targetDir) { + return this._pathExists(path.join(targetDir, "package.json")); + } + + async _pathExists(targetPath) { + try { + await stat(targetPath); + return true; + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + return false; + } else { + throw err; + } + } + } + + _getTargetDirForPackage({pkgName, version}) { + return path.join(this._packagesDir, ...pkgName.split("/"), version); + } + + _getStagingDirForPackage({pkgName, version}) { + return path.join(this._stagingDir, `${pkgName.replaceAll("/", "-")}-${version}`); + } +} + +export default Installer; diff --git a/packages/project/lib/ui5Framework/npm/Registry.js b/packages/project/lib/ui5Framework/npm/Registry.js new file mode 100644 index 00000000000..4d60b4d2011 --- /dev/null +++ b/packages/project/lib/ui5Framework/npm/Registry.js @@ -0,0 +1,94 @@ +import {getLogger} from "@ui5/logger"; +const log = getLogger("ui5Framework:npm:Registry"); + +function logConfig(config, configName) { + const configValue = config[configName]; + if (configValue) { + log.verbose(` ${configName}: ${configValue}`); + } +} + +class Registry { + /** + * @param {object} parameters Parameters + * @param {string} parameters.cwd Current working directory + * @param {string} parameters.cacheDir Cache directory + */ + constructor({cwd, cacheDir}) { + if (!cwd) { + throw new Error(`Registry: Missing parameter "cwd"`); + } + if (!cacheDir) { + throw new Error(`Registry: Missing parameter "cacheDir"`); + } + this._cwd = cwd; + this._cacheDir = cacheDir; + } + async requestPackagePackument(pkgName) { + const {pacote, pacoteOptions} = await this._getPacote(); + return pacote.packument(pkgName, pacoteOptions); + } + async requestPackageManifest(pkgName, version) { + const {pacote, pacoteOptions} = await this._getPacote(); + + return pacote.manifest(`${pkgName}@${version}`, pacoteOptions); + } + async extractPackage(pkgName, version, targetDir) { + const {pacote, pacoteOptions} = await this._getPacote(); + try { + await pacote.extract(`${pkgName}@${version}`, targetDir, pacoteOptions); + } catch (err) { + throw new Error(`Failed to extract package ${pkgName}@${version}: ${err.message}`); + } + } + + async _getPacote() { + if (this._pGetPacote) { + return this._pGetPacote; + } + return this._pGetPacote = (async () => { + return { + pacote: (await import("pacote")).default, + pacoteOptions: await this._getPacoteOptions() + }; + })(); + } + + async _getPacoteOptions() { + const {default: Config} = await import("@npmcli/config"); + const { + default: {flatten, definitions, shorthands, defaults}, + } = await import("@npmcli/config/lib/definitions/index.js"); + + const configuration = new Config({ + cwd: this._cwd, + npmPath: this._cwd, + definitions, + flatten, + shorthands, + defaults + }); + + await configuration.load(); // Reads through the configurations + const config = configuration.flat; // JSON. Formatted via "flatten" + + // Always use our cache dir instead of the configured one + config.cache = this._cacheDir; + + log.verbose(`Using npm configuration (extract):`); + // Do not log full configuration as it may contain authentication tokens + logConfig(config, "registry"); + logConfig(config, "@sapui5:registry"); + logConfig(config, "@openui5:registry"); + logConfig(config, "proxy"); + logConfig(config, "httpsProxy"); + logConfig(config, "globalconfig"); + logConfig(config, "userconfig"); + logConfig(config, "cache"); + logConfig(config, "cwd"); + + return config; + } +} + +export default Registry; diff --git a/packages/project/lib/utils/fs.js b/packages/project/lib/utils/fs.js new file mode 100644 index 00000000000..a8852509b82 --- /dev/null +++ b/packages/project/lib/utils/fs.js @@ -0,0 +1,12 @@ +import fs from "graceful-fs"; +import {promisify} from "node:util"; +const mkdir = promisify(fs.mkdir); +const rm = promisify(fs.rm); + +export async function mkdirp(dirPath) { + return mkdir(dirPath, {recursive: true}); +} + +export async function rmrf(dirPath) { + return rm(dirPath, {recursive: true, force: true}); +} diff --git a/packages/project/lib/validation/ValidationError.js b/packages/project/lib/validation/ValidationError.js new file mode 100644 index 00000000000..fabdbd6e8a6 --- /dev/null +++ b/packages/project/lib/validation/ValidationError.js @@ -0,0 +1,283 @@ +import chalk from "chalk"; +import escapeStringRegExp from "escape-string-regexp"; + +/** + * Error class for validation of project configuration. + * + * @public + * @class + * @alias @ui5/project/validation/ValidationError + * @extends Error + * @hideconstructor + */ +class ValidationError extends Error { + constructor({errors, project, yaml}) { + super(); + + /** + * ValidationError + * + * @constant + * @default + * @type {string} + * @readonly + * @public + */ + this.name = "ValidationError"; + + this.project = project; + this.yaml = yaml; + + this.errors = ValidationError.filterErrors(errors); + + /** + * Formatted error message + * + * @type {string} + * @readonly + * @public + */ + this.message = this.formatErrors(); + + Error.captureStackTrace(this, this.constructor); + } + + formatErrors() { + let separator = "\n\n"; + if (process.stdout.isTTY) { + // Add a horizontal separator line between errors in case a terminal is used + separator += chalk.grey.dim("\u2500".repeat(process.stdout.columns || 80)); + } + separator += "\n\n"; + let message; + + if (this.project) { // ui5-workspace.yaml is project independent, so in that case, no project is available + message = chalk.red(`Invalid ui5.yaml configuration for project ${this.project.id}`) + "\n\n"; + } else { + message = chalk.red(`Invalid workspace configuration.`) + "\n\n"; + } + + message += this.errors.map((error) => { + return this.formatError(error); + }).join(separator); + return message; + } + + formatError(error) { + let errorMessage = ValidationError.formatMessage(error); + if (this.yaml && this.yaml.path && this.yaml.source) { + const yamlExtract = ValidationError.getYamlExtract({error, yaml: this.yaml}); + const errorLines = errorMessage.split("\n"); + errorLines.splice(1, 0, "\n" + yamlExtract); + errorMessage = errorLines.join("\n"); + } + return errorMessage; + } + + static formatMessage(error) { + if (error.keyword === "errorMessage") { + return error.message; + } + + let message = "Configuration "; + if (error.dataPath) { + message += chalk.underline(chalk.red(error.dataPath.substr(1))) + " "; + } + + switch (error.keyword) { + case "additionalProperties": + message += `property ${error.params.additionalProperty} must not be provided here`; + break; + case "type": + message += `must be of type '${error.params.type}'`; + break; + case "required": + message += `must have required property '${error.params.missingProperty}'`; + break; + case "enum": + message += "must be equal to one of the allowed values\n"; + message += "Allowed values: " + error.params.allowedValues.join(", "); + break; + default: + message += error.message; + } + + return message; + } + + static _findDuplicateError(error, errorIndex, errors) { + const foundIndex = errors.findIndex(($) => { + if ($.dataPath !== error.dataPath) { + return false; + } else if ($.keyword !== error.keyword) { + return false; + } else if (JSON.stringify($.params) !== JSON.stringify(error.params)) { + return false; + } else { + return true; + } + }); + return foundIndex !== errorIndex; + } + + static filterErrors(allErrors) { + return allErrors.filter((error, i, errors) => { + if (error.keyword === "if" || error.keyword === "oneOf") { + return false; + } + + return !ValidationError._findDuplicateError(error, i, errors); + }); + } + + static analyzeYamlError({error, yaml}) { + if (error.dataPath === "" && error.keyword === "required") { + // There is no line/column for a missing required property on root level + return {line: -1, column: -1}; + } + + // Skip leading / + const objectPath = error.dataPath.substr(1).split("/"); + + if (error.keyword === "additionalProperties") { + objectPath.push(error.params.additionalProperty); + } + + let currentSubstring; + let currentIndex; + if (yaml.documentIndex) { + const matchDocumentSeparator = /^---/gm; + let currentDocumentIndex = 0; + let document; + while ((document = matchDocumentSeparator.exec(yaml.source)) !== null) { + // If the first separator is not at the beginning of the file + // we are already at document index 1 + // Using String#trim() to remove any whitespace characters + if (currentDocumentIndex === 0 && yaml.source.substring(0, document.index).trim().length > 0) { + currentDocumentIndex = 1; + } + + if (currentDocumentIndex === yaml.documentIndex) { + currentIndex = document.index; + currentSubstring = yaml.source.substring(currentIndex); + break; + } + + currentDocumentIndex++; + } + // Document could not be found + if (!currentSubstring) { + return {line: -1, column: -1}; + } + } else { + // In case of index 0 or no index, use whole source + currentIndex = 0; + currentSubstring = yaml.source; + } + + const matchArrayElementIndentation = /([ ]*)-/; + + for (let i = 0; i < objectPath.length; i++) { + const property = objectPath[i]; + let newIndex; + + if (isNaN(property)) { + // Try to find a property + + // Creating a regular expression that matches the property name a line + // except for comments, indicated by a hash sign "#". + const propertyRegExp = new RegExp(`^[^#]*?${escapeStringRegExp(property)}`, "m"); + + const propertyMatch = propertyRegExp.exec(currentSubstring); + if (!propertyMatch) { + return {line: -1, column: -1}; + } + newIndex = propertyMatch.index + propertyMatch[0].length; + } else { + // Try to find the right index within an array definition. + // This currently only works for arrays defined with "-" in multiple lines. + // Arrays using square brackets are not supported. + + const matchArrayElement = /(^|\r?\n)([ ]*-[^\r\n]*)/g; + const arrayIndex = parseInt(property); + let a = 0; + let firstIndentation = -1; + let match; + while ((match = matchArrayElement.exec(currentSubstring)) !== null) { + const indentationMatch = match[2].match(matchArrayElementIndentation); + if (!indentationMatch) { + return {line: -1, column: -1}; + } + const currentIndentation = indentationMatch[1].length; + if (firstIndentation === -1) { + firstIndentation = currentIndentation; + } else if (currentIndentation !== firstIndentation) { + continue; + } + if (a === arrayIndex) { + // match[1] might be a line-break + newIndex = match.index + match[1].length + currentIndentation; + break; + } + a++; + } + if (!newIndex) { + // Could not find array element + return {line: -1, column: -1}; + } + } + currentIndex += newIndex; + currentSubstring = yaml.source.substring(currentIndex); + } + + const linesUntilMatch = yaml.source.substring(0, currentIndex).split(/\r?\n/); + const line = linesUntilMatch.length; + let column = linesUntilMatch[line - 1].length + 1; + const lastPathSegment = objectPath[objectPath.length - 1]; + if (isNaN(lastPathSegment)) { + column -= lastPathSegment.length; + } + + return { + line, + column + }; + } + + static getSourceExtract(yamlSource, line, column) { + let source = ""; + const lines = yamlSource.split(/\r?\n/); + + // Using line numbers instead of array indices + const startLine = Math.max(line - 2, 1); + const endLine = Math.min(line, lines.length); + const padLength = String(endLine).length; + + for (let currentLine = startLine; currentLine <= endLine; currentLine++) { + const currentLineContent = lines[currentLine - 1]; + let string = chalk.gray( + String(currentLine).padStart(padLength, " ") + ":" + ) + " " + currentLineContent + "\n"; + if (currentLine === line) { + string = chalk.bgRed(string); + } + source += string; + } + + source += " ".repeat(column + padLength + 1) + chalk.red("^"); + + return source; + } + + static getYamlExtract({error, yaml}) { + const {line, column} = ValidationError.analyzeYamlError({error, yaml}); + if (line !== -1 && column !== -1) { + return chalk.grey(yaml.path + ":" + line) + + "\n\n" + ValidationError.getSourceExtract(yaml.source, line, column); + } else { + return chalk.grey(yaml.path) + "\n"; + } + } +} + +export default ValidationError; diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension.json b/packages/project/lib/validation/schema/specVersion/kind/extension.json new file mode 100644 index 00000000000..274d67748a2 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/extension.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/extension.json", + + "type": "object", + "required": ["specVersion", "kind", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": [ + "task", + "server-middleware", + "project-shim" + ] + }, + "metadata": { + "$ref": "#/definitions/metadata" + } + }, + "if": { + "properties": { + "type": {"const": null} + }, + "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors." + }, + "then": {}, + "else": { + "if": { + "properties": { + "type": {"const": "task"} + } + }, + "then": { + "$ref": "extension/task.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "server-middleware"} + } + }, + "then": { + "$ref": "extension/server-middleware.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "project-shim"} + } + }, + "then": { + "$ref": "extension/project-shim.json" + } + } + } + }, + "definitions": { + "metadata": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "copyright": { + "type": "string" + } + } + }, + "metadata-3.0": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 80, + "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + "title": "Extension Name", + "description": "Unique identifier for the extension, for example: ui5-task-fearless-rock", + "errorMessage": "Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name" + }, + "copyright": { + "type": "string" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json b/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json new file mode 100644 index 00000000000..d7877e3af93 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json @@ -0,0 +1,137 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/extension/project-shim.json", + + "type": "object", + "required": ["specVersion", "kind", "type", "metadata", "shims"], + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { + "enum": ["3.0", "3.1", "3.2", "4.0"] + }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["project-shim"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata-3.0" + }, + "shims": { + "$ref": "#/definitions/shims" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { + "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] + }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["project-shim"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "shims": { + "$ref": "#/definitions/shims" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { + "enum": ["2.0"] + }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["project-shim"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "shims": { + "$ref": "#/definitions/shims" + } + } + } + }, + "definitions": { + "shims": { + "type": "object", + "additionalProperties": false, + "properties": { + "configurations": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "object" + } + } + }, + "dependencies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "collections": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "object", + "additionalProperties": false, + "properties": { + "modules": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json b/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json new file mode 100644 index 00000000000..de4628ecb92 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/extension/server-middleware.json", + + "type": "object", + + "required": ["specVersion", "kind", "type", "metadata", "middleware"], + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["server-middleware"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata-3.0" + }, + "middleware": { + "$ref": "#/definitions/middleware" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["server-middleware"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "middleware": { + "$ref": "#/definitions/middleware" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["server-middleware"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "middleware": { + "$ref": "#/definitions/middleware" + } + } + } + }, + "definitions": { + "middleware": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/task.json b/packages/project/lib/validation/schema/specVersion/kind/extension/task.json new file mode 100644 index 00000000000..4fb3496a2d4 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/extension/task.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/extension/task.json", + + "type": "object", + "required": ["specVersion", "kind", "type", "metadata", "task"], + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["task"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata-3.0" + }, + "task": { + "$ref": "#/definitions/task" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["task"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "task": { + "$ref": "#/definitions/task" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["task"] + }, + "metadata": { + "$ref": "../extension.json#/definitions/metadata" + }, + "task": { + "$ref": "#/definitions/task" + } + } + } + }, + "definitions": { + "task": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/project.json b/packages/project/lib/validation/schema/specVersion/kind/project.json new file mode 100644 index 00000000000..d1167eba986 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project.json @@ -0,0 +1,856 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project.json", + + "type": "object", + "required": ["specVersion", "type"], + "properties": { + "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] }, + "kind": { + "enum": ["project", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + }, + "type": { + "enum": [ + "application", + "library", + "theme-library", + "module" + ] + } + }, + "if": { + "properties": { + "type": {"const": null} + }, + "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors." + }, + "then": {}, + "else": { + "if": { + "properties": { + "type": {"const": "application"} + } + }, + "then": { + "$ref": "project/application.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "library"} + } + }, + "then": { + "$ref": "project/library.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "theme-library"} + } + }, + "then": { + "$ref": "project/theme-library.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "module"} + } + }, + "then": { + "$ref": "project/module.json" + } + } + } + } + }, + + "definitions": { + "metadata": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "copyright": { + "type": "string" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "sapInternal": { + "type": "boolean", + "default": false + }, + "allowSapInternal": { + "type": "boolean", + "default": false + } + } + }, + "metadata-3.0": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 80, + "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + "title": "Project Name", + "description": "Unique identifier for the project, for example: organization.product.project", + "errorMessage": "Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name" + }, + "copyright": { + "type": "string" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "sapInternal": { + "type": "boolean", + "default": false + }, + "allowSapInternal": { + "type": "boolean", + "default": false + } + } + }, + "resources-configuration-propertiesFileSourceEncoding": { + "enum": ["UTF-8", "ISO-8859-1"], + "default": "UTF-8", + "title": "Encoding of *.properties files", + "description": "By default, the UI5 CLI expects *.properties files to be UTF-8 encoded." + }, + "builder-resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder-bundles": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "$ref": "#/definitions/builder-bundles-bundleDefinition" + }, + "bundleOptions": { + "$ref": "#/definitions/builder-bundles-bundleOptions" + } + } + } + }, + "builder-bundles-2.4": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "$ref": "#/definitions/builder-bundles-bundleDefinition-2.4" + }, + "bundleOptions": { + "$ref": "#/definitions/builder-bundles-bundleOptions" + } + } + } + }, + "builder-bundles-3.0": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "$ref": "#/definitions/builder-bundles-bundleDefinition-2.4" + }, + "bundleOptions": { + "$ref": "#/definitions/builder-bundles-bundleOptions-3.0" + } + } + } + }, + "builder-bundles-3.2": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "$ref": "#/definitions/builder-bundles-bundleDefinition-3.2" + }, + "bundleOptions": { + "$ref": "#/definitions/builder-bundles-bundleOptions-3.0" + } + } + } + }, + "builder-bundles-4.0": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "$ref": "#/definitions/builder-bundles-bundleDefinition-4.0" + }, + "bundleOptions": { + "$ref": "#/definitions/builder-bundles-bundleOptions-4.0" + } + } + } + }, + "builder-bundles-bundleDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "defaultFileTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "name": { + "type": "string" + }, + "mode": { + "enum": ["raw", "preload", "require", "provided"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + }, + "declareRawModules": { + "type": "boolean", + "default": false + } + } + } + } + } + }, + "builder-bundles-bundleDefinition-2.4": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "defaultFileTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "name": { + "type": "string" + }, + "mode": { + "enum": ["raw", "preload", "require", "provided", "bundleInfo"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + }, + "declareRawModules": { + "type": "boolean", + "default": false + } + } + } + } + } + }, + "builder-bundles-bundleDefinition-3.2": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "defaultFileTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "name": { + "type": "string" + }, + "mode": { + "enum": ["raw", "preload", "require", "provided", "bundleInfo", "depCache"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + }, + "declareRawModules": { + "type": "boolean", + "default": false + } + } + } + } + } + }, + "builder-bundles-bundleDefinition-4.0": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "defaultFileTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sections": { + "type": "array", + "items": { + "if": { + "properties": { + "mode": { + "const": "require" + } + }, + "$comment": "Add async prop only if mode = 'require'" + }, + "then": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "name": { + "type": "string" + }, + "mode": { + "enum": ["require"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + }, + "declareRawModules": { + "type": "boolean", + "default": false + }, + "async": { + "type": "boolean", + "default": true + } + } + }, + "else": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "name": { + "type": "string" + }, + "mode": { + "enum": ["raw", "preload", "require", "provided", "bundleInfo", "depCache"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + }, + "declareRawModules": { + "type": "boolean", + "default": false + } + } + } + } + } + } + }, + "builder-bundles-bundleOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "optimize": { + "type": "boolean", + "default": false + }, + "decorateBootstrapModule": { + "type": "boolean", + "default": false + }, + "addTryCatchRestartWrapper": { + "type": "boolean", + "default": false + }, + "usePredefineCalls": { + "type": "boolean", + "default": false + }, + "numberOfParts": { + "type": "number", + "default": 1 + } + } + }, + "builder-bundles-bundleOptions-3.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "optimize": { + "type": "boolean", + "default": true + }, + "decorateBootstrapModule": { + "type": "boolean", + "default": false + }, + "addTryCatchRestartWrapper": { + "type": "boolean", + "default": false + }, + "usePredefineCalls": { + "type": "boolean", + "default": false + }, + "numberOfParts": { + "type": "number", + "default": 1 + }, + "sourceMap": { + "type": "boolean", + "default": true + } + } + }, + "builder-bundles-bundleOptions-4.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "optimize": { + "type": "boolean", + "default": true + }, + "decorateBootstrapModule": { + "type": "boolean", + "default": false + }, + "addTryCatchRestartWrapper": { + "type": "boolean", + "default": false + }, + "numberOfParts": { + "type": "number", + "default": 1 + }, + "sourceMap": { + "type": "boolean", + "default": true + } + } + }, + "builder-componentPreload": { + "type": "object", + "additionalProperties": false, + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder-componentPreload-specVersion-2.3": { + "type": "object", + "additionalProperties": false, + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder-libraryPreload": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "server": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "httpPort": { + "type": "number" + }, + "httpsPort": { + "type": "number" + } + } + }, + "customMiddleware": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["name", "beforeMiddleware"], + "properties": { + "name": { + "type": "string" + }, + "mountPath": { + "type": "string" + }, + "beforeMiddleware": { + "type": "string" + }, + "configuration": {} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "afterMiddleware"], + "properties": { + "name": { + "type": "string" + }, + "mountPath": { + "type": "string" + }, + "afterMiddleware": { + "type": "string" + }, + "configuration": {} + } + } + ] + } + } + } + }, + "framework": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "enum": ["OpenUI5", "SAPUI5"] + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Version", + "description": "Framework version to use in this project", + "errorMessage": "Not a valid version according to the Semantic Versioning specification (https://semver.org/)" + }, + "libraries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "optional": { + "type": "boolean", + "default": false + }, + "development": { + "type": "boolean", + "default": false + } + }, + "if": { + "not": { + "anyOf": [ + { + "properties": { + "optional": {"enum": [false, null]} + } + }, + { + "properties": { + "development": {"enum": [false, null]} + } + }, + { + "not": { + "properties": { + "optional": {"type": "boolean"} + } + } + }, + { + "not": { + "properties": { + "development": {"type": "boolean"} + } + } + } + ], + "$comment": "Unfortunately it doesn't seem to work to check for both properties to be true, so instead checking for not having any of the properties to other values like false, not defined or any other type." + } + }, + "then": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + } + }, + "errorMessage": "Either \"development\" or \"optional\" can be true, but not both", + "$comment": "Defining a custom error message and only allowing the \"name\" property causes editors to show the custom error on both properties." + } + } + } + } + }, + "customTasks": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["name", "beforeTask"], + "properties": { + "name": { + "type": "string" + }, + "beforeTask": { + "type": "string" + }, + "configuration": {} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "afterTask"], + "properties": { + "name": { + "type": "string" + }, + "afterTask": { + "type": "string" + }, + "configuration": {} + } + } + ] + } + }, + "builder-settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "includeDependency": { + "type": "array", + "items": { + "type": "string" + } + }, + "includeDependencyRegExp": { + "type": "array", + "items": { + "type": "string" + } + }, + "includeDependencyTree": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder-minification": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/application.json b/packages/project/lib/validation/schema/specVersion/kind/project/application.json new file mode 100644 index 00000000000..46f05b9753b --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project/application.json @@ -0,0 +1,608 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project/application.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "if": { + "properties": { + "specVersion": { "enum": ["4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["4.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-4.0" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["3.2"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.2"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-3.2" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0", "3.1"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-3.0" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.6"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.6" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.5"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.5"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.4"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.4"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.4" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.3"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.3"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.3" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + } + } + } + } + } + } + } + } + }, + "definitions": { + + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "propertiesFileSourceEncoding": { + "$ref": "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "properties": { + "webapp": { + "type": "string" + } + } + } + } + } + } + }, + + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.3": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.4": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.5": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-2.6": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-3.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-3.0" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-3.2": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-3.2" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-4.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-4.0" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/library.json b/packages/project/lib/validation/schema/specVersion/kind/project/library.json new file mode 100644 index 00000000000..8a039254010 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project/library.json @@ -0,0 +1,593 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project/library.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "if": { + "properties": { + "specVersion": { "enum": ["4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["4.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-4.0" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["3.2"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.2"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-3.2" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0", "3.1"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-3.0" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.6"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.6" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.5"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.5"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.4"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.4"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.4" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.3"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.3"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.3" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + } + } + } + } + } + } + } + } + }, + "definitions": { + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "propertiesFileSourceEncoding": { + "$ref": "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "properties": { + "src": { + "type": "string" + }, + "test": { + "type": "string" + } + } + } + } + } + } + }, + "builder-jsdoc": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.3": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.4": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.5": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-2.6": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-2.4" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-3.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-3.0" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-3.2": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-3.2" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-4.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "$ref": "#/definitions/builder-jsdoc" + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-4.0" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "libraryPreload": { + "$ref": "../project.json#/definitions/builder-libraryPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/module.json b/packages/project/lib/validation/schema/specVersion/kind/project/module.json new file mode 100644 index 00000000000..1e38cfcdf0f --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project/module.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project/module.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "if": { + "properties": { + "specVersion": { "enum": ["3.1", "3.2", "4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.1", "3.2", "4.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-3.1" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["3.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.5", "2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.5", "2.6"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "resources": { + "$ref": "#/definitions/resources" + } + } + } + } + } + }, + "definitions": { + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "paths": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "string" + } + } + } + } + } + } + }, + "builder-specVersion-2.5": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + }, + "builder-specVersion-3.1": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json b/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json new file mode 100644 index 00000000000..db0853b33a2 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json @@ -0,0 +1,175 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project/theme-library.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "if": { + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["theme-library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "library.json#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.5", "2.6"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.5", "2.6"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["theme-library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "library.json#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-2.5" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["theme-library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "library.json#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["theme-library"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "library.json#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + } + } + } + }, + "definitions": { + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + }, + "builder-specVersion-2.5": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/specVersion/specVersion.json b/packages/project/lib/validation/schema/specVersion/specVersion.json new file mode 100644 index 00000000000..1126cb0e8a7 --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/specVersion.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0.json", + + "type": "object", + "required": ["specVersion"], + "properties": { + "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] }, + "kind": { + "enum": ["project", "extension", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + } + }, + "if": { + "properties": { + "kind": { + "enum": ["project", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + } + } + }, + "then": { + "$ref": "kind/project.json" + }, + "else": { + "if": { + "properties": { + "kind": { + "enum": ["extension"] + } + } + }, + "then": { + "$ref": "kind/extension.json" + } + } +} diff --git a/packages/project/lib/validation/schema/ui5-workspace.json b/packages/project/lib/validation/schema/ui5-workspace.json new file mode 100644 index 00000000000..41bd129edd0 --- /dev/null +++ b/packages/project/lib/validation/schema/ui5-workspace.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/ui5-workspace.json", + "title": "ui5-workspace.yaml", + "description": "Schema for UI5 CLI Workspace Configuration File (ui5-workspace.yaml)", + "$comment": "See https://ui5.github.io/cli/", + "type": "object", + "required": ["specVersion", "metadata", "dependencyManagement"], + "properties": { + "additionalProperties": false, + "specVersion": { + "enum": ["workspace/1.0"], + "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"workspace/1.0\"\nFor details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions" + }, + "metadata": { + "$ref": "#/definitions/metadata" + }, + "dependencyManagement": { + "$ref": "#/definitions/dependencyManagement" + } + }, + "definitions": { + "metadata": { + "type": "object", + "required": ["name"], + "properties": { + "additionalProperties": false, + "name": { + "type": "string", + "minLength": 3, + "maxLength": 80, + "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + "title": "Workspace Name", + "description": "Identifier for the workspace configuration. Workspaces named 'default' will be used automatically by UI5 CLI", + "errorMessage": "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name" + } + } + }, + "dependencyManagement": { + "type": "object", + "properties": { + "additionalProperties": false, + "resolutions": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + } + } + } + } + } + } +} diff --git a/packages/project/lib/validation/schema/ui5.json b/packages/project/lib/validation/schema/ui5.json new file mode 100644 index 00000000000..0511c7fdd2a --- /dev/null +++ b/packages/project/lib/validation/schema/ui5.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/ui5.json", + "title": "ui5.yaml", + "description": "Schema for UI5 CLI Configuration File (ui5.yaml)", + "$comment": "See https://ui5.github.io/cli/", + + "type": "object", + "required": ["specVersion"], + "properties": { + "specVersion": { + "enum": [ + "4.0", + "3.2", "3.1", "3.0", + "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0", + "1.1", "1.0", "0.1" + ], + "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"4.0\", \"3.2\", \"3.1\", \"3.0\", \"2.6\", \"2.5\", \"2.4\", \"2.3\", \"2.2\", \"2.1\", \"2.0\", \"1.1\", \"1.0\", \"0.1\"\nFor details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions" + } + }, + + "if": { + "properties": { + "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] } + } + }, + "then": { + "$ref": "specVersion/specVersion.json" + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["1.1", "1.0", "0.1"] } + } + }, + "then": { + "additionalProperties": true + } + } +} diff --git a/packages/project/lib/validation/validator.js b/packages/project/lib/validation/validator.js new file mode 100644 index 00000000000..233e792cb0b --- /dev/null +++ b/packages/project/lib/validation/validator.js @@ -0,0 +1,214 @@ +import {fileURLToPath} from "node:url"; +import {readFile} from "node:fs/promises"; + +/** + * @module @ui5/project/validation/validator + * @description A collection of validation related APIs + * @public + */ + +/** + * @enum {string} + * @private + * @readonly + */ +export const SCHEMA_VARIANTS = { + "ui5": "ui5.json", + "ui5-workspace": "ui5-workspace.json" +}; + +class Validator { + constructor({Ajv, ajvErrors, schemaName, ajvConfig}) { + if (!schemaName || !SCHEMA_VARIANTS[schemaName]) { + throw new Error( + `"schemaName" is missing or incorrect. The available schemaName variants are ${Object.keys( + SCHEMA_VARIANTS + ).join(", ")}` + ); + } + + this._schemaName = SCHEMA_VARIANTS[schemaName]; + + ajvConfig = Object.assign({ + allErrors: true, + jsonPointers: true, + loadSchema: Validator.loadSchema + }, ajvConfig); + this.ajv = new Ajv(ajvConfig); + ajvErrors(this.ajv); + } + + _compileSchema() { + const schemaName = this._schemaName; + + if (!this._compiling) { + this._compiling = Promise.resolve().then(async () => { + const schema = await Validator.loadSchema(schemaName); + const validate = await this.ajv.compileAsync(schema); + return validate; + }); + } + return this._compiling; + } + + async validate({config, project, yaml}) { + const fnValidate = await this._compileSchema(); + const valid = fnValidate(config); + if (!valid) { + // Read errors/schema from fnValidate before lazy loading ValidationError module. + // Otherwise they might be cleared already. + const {errors, schema} = fnValidate; + const {default: ValidationError} = await import("./ValidationError.js"); + throw new ValidationError({ + errors, + schema, + project, + yaml + }); + } + } + + static async loadSchema(schemaPath) { + const filePath = schemaPath.replace("http://ui5.sap/schema/", ""); + const schemaFile = await readFile( + fileURLToPath(new URL(`./schema/${filePath}`, import.meta.url)), {encoding: "utf8"} + ); + return JSON.parse(schemaFile); + } +} + +const validator = Object.create(null); +const defaultsValidator = Object.create(null); + +async function _validate(schemaName, options) { + if (!validator[schemaName]) { + validator[schemaName] = (async () => { + const {default: Ajv} = await import("ajv"); + const {default: ajvErrors} = await import("ajv-errors"); + return new Validator({Ajv, ajvErrors, schemaName}); + })(); + } + + const schemaValidator = await validator[schemaName]; + await schemaValidator.validate(options); +} + +async function _validateAndSetDefaults(schemaName, options) { + if (!defaultsValidator[schemaName]) { + defaultsValidator[schemaName] = (async () => { + const {default: Ajv} = await import("ajv"); + const {default: ajvErrors} = await import("ajv-errors"); + return new Validator({Ajv, ajvErrors, ajvConfig: {useDefaults: true}, schemaName}); + })(); + } + + // When AJV is configured with useDefaults: true, it may add properties to the + // provided configuration that were not initially present. This behavior can + // lead to unexpected side effects and potential issues. To avoid these + // problems, we create a copy of the configuration. If we need the altered + // configuration later, we return this copied version. + const optionsCopy = structuredClone(options); + const schemaValidator = await defaultsValidator[schemaName]; + await schemaValidator.validate(optionsCopy); + + return optionsCopy; +} + +/** + * Validates the given ui5 configuration. + * + * @public + * @function + * @static + * @param {object} options + * @param {object} options.config UI5 Configuration to validate + * @param {object} options.project Project information + * @param {string} options.project.id ID of the project + * @param {object} [options.yaml] YAML information + * @param {string} options.yaml.path Path of the YAML file + * @param {string} options.yaml.source Content of the YAML file + * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents + * @throws {@ui5/project/validation/ValidationError} + * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError} + * when the validation fails. + * @returns {Promise} Returns a Promise that resolves when the validation succeeds + */ +export async function validate(options) { + await _validate("ui5", options); +} + +/** + * Validates the given ui5 configuration and returns default values if none are provided. + * + * @public + * @function + * @static + * @param {object} options + * @param {object} options.config The UI5 Configuration to validate + * @param {object} options.project Project information + * @param {string} options.project.id ID of the project + * @param {object} [options.yaml] YAML information + * @param {string} options.yaml.path Path of the YAML file + * @param {string} options.yaml.source Content of the YAML file + * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents + * @throws {module:@ui5/project/validation/ValidationError} + * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError} + * when the validation fails. + * @returns {Promise} Returns a Promise that resolves when the validation succeeds + */ +export async function getDefaults(options) { + return await _validateAndSetDefaults("ui5", options); +} + +/** + * Enhances bundleDefinition by adding missing properties with their respective default values. + * + * @param {object[]} bundles Bundles to be enhanced + * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} bundles[].bundleDefinition + * Module bundle definition + * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [bundles[].bundleOptions] + * Module bundle options + * @param {module:@ui5/project/specifications/Project} project The project to get metadata from + * @returns {Promise} The enhanced BundleDefinition & BundleOptions + */ +export async function enhanceBundlesWithDefaults(bundles, project) { + const config = { + specVersion: `${project.getSpecVersion()}`, + type: `${project.getType()}`, + metadata: {name: project.getName()}, + builder: {bundles} + }; + const result = await getDefaults({config, project: {id: project.getName()}}); + + return result.config.builder.bundles; +} + +/** + * Validates the given ui5-workspace configuration. + * + * @public + * @function + * @static + * @param {object} options + * @param {object} options.config ui5-workspace Configuration to validate + * @param {object} [options.yaml] YAML information + * @param {string} options.yaml.path Path of the YAML file + * @param {string} options.yaml.source Content of the YAML file + * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents + * @throws {@ui5/project/validation/ValidationError} + * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError} + * when the validation fails. + * @returns {Promise} Returns a Promise that resolves when the validation succeeds + */ +export async function validateWorkspace(options) { + await _validate("ui5-workspace", options); +} + +export { + /** + * For testing only! + * + * @private + */ + Validator as _Validator +}; diff --git a/packages/project/package-lock.json b/packages/project/package-lock.json new file mode 100644 index 00000000000..413fb26d7a6 --- /dev/null +++ b/packages/project/package-lock.json @@ -0,0 +1,8807 @@ +{ + "name": "@ui5/project", + "version": "4.0.6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ui5/project", + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@npmcli/config": "^10.4.0", + "@ui5/fs": "^4.0.2", + "@ui5/logger": "^4.0.2", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "chalk": "^5.6.2", + "escape-string-regexp": "^5.0.0", + "globby": "^14.1.0", + "graceful-fs": "^4.2.11", + "js-yaml": "^4.1.0", + "lockfile": "^1.0.4", + "make-fetch-happen": "^14.0.3", + "node-stream-zip": "^1.15.0", + "pacote": "^19.0.1", + "pretty-hrtime": "^1.0.3", + "read-package-up": "^11.0.0", + "read-pkg": "^9.0.1", + "resolve": "^1.22.10", + "semver": "^7.7.2", + "xml2js": "^0.6.2", + "yesno": "^0.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.8.0", + "@istanbuljs/esm-loader-hook": "^0.3.0", + "ava": "^6.4.1", + "chokidar-cli": "^3.0.0", + "cross-env": "^7.0.3", + "depcheck": "^1.4.7", + "docdash": "^2.0.2", + "eslint": "^9.36.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-ava": "^15.1.0", + "eslint-plugin-jsdoc": "^52.0.4", + "esmock": "^2.7.3", + "globals": "^16.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-instrument": "^6.0.3", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "js-beautify": "^1.15.4", + "jsdoc": "^4.0.4", + "nyc": "^17.1.0", + "open-cli": "^8.0.0", + "rimraf": "^6.0.1", + "sinon": "^21.0.0", + "tap-xunit": "^2.4.1" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + }, + "peerDependencies": { + "@ui5/builder": "^4.0.11" + }, + "peerDependenciesMeta": { + "@ui5/builder": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.52.0.tgz", + "integrity": "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.34.1", + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/esm-loader-hook": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/esm-loader-hook/-/esm-loader-hook-0.3.0.tgz", + "integrity": "sha512-lEnYroBUYfNQuJDYrPvre8TSwPZnyIQv9qUT3gACvhr3igZr+BbrdyIcz4+2RnEXZzi12GqkUW600+QQPpIbVg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.8.7", + "@babel/plugin-syntax-decorators": "^7.25.9", + "@babel/preset-typescript": "^7.26.0", + "@istanbuljs/load-nyc-config": "^1.1.0", + "@istanbuljs/schema": "^0.1.3", + "babel-plugin-istanbul": "^6.0.0", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=16.12.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz", + "integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==", + "dev": true, + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/@npmcli/config": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-10.4.0.tgz", + "integrity": "sha512-0l6f/q/qfB726SWOGIEooh7u6aB1SOgRxGLu7DeJ6Z9Vvq1gG1s3x+Mq+qv9wt0Q0t53mVHIEBokfJZpeaWDyA==", + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.1.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-4.0.2.tgz", + "integrity": "sha512-mnuMuibEbkaBTYj9HQ3dMe6L0ylYW+s/gfz7tBDMFY/la0w9Kf44P9aLn4/+/t3aTR3YUHKoT6XQL9rlicIe3Q==", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-3.0.0.tgz", + "integrity": "sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", + "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sigstore/bundle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", + "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", + "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", + "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", + "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ui5/fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@ui5/fs/-/fs-4.0.2.tgz", + "integrity": "sha512-0R7eb9xEMswvkN2wIiyYJtQY83evQJ7LQhTnRf5Ms0o2R29twGLP4XewqH+IoGWyT3T4SuDNTWmUU2UaTRY4zg==", + "dependencies": { + "@ui5/logger": "^4.0.1", + "clone": "^2.1.2", + "escape-string-regexp": "^5.0.0", + "globby": "^14.1.0", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "minimatch": "^10.0.3", + "pretty-hrtime": "^1.0.3", + "random-int": "^3.0.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/fs/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/logger": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@ui5/logger/-/logger-4.0.2.tgz", + "integrity": "sha512-uscDCQyHFeenh4r2RbYuffTMn6IQdcNC1tXrQ4BF+apAFjmDGP11IHdAwVCKwxgyPrIC17HT2gub3ZugGM8kpQ==", + "dependencies": { + "chalk": "^5.6.0", + "cli-progress": "^3.12.0", + "figures": "^6.1.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@vercel/nft": { + "version": "0.29.4", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.4.tgz", + "integrity": "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==", + "dev": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^10.4.5", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "dev": true + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrgv": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz", + "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/arrify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz", + "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true + }, + "node_modules/ava": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/ava/-/ava-6.4.1.tgz", + "integrity": "sha512-vxmPbi1gZx9zhAjHBgw81w/iEDKcrokeRk/fqDTyA2DQygZ0o+dUGRHFOtX8RA5N0heGJTTsIk7+xYxitDb61Q==", + "dev": true, + "dependencies": { + "@vercel/nft": "^0.29.4", + "acorn": "^8.15.0", + "acorn-walk": "^8.3.4", + "ansi-styles": "^6.2.1", + "arrgv": "^1.0.2", + "arrify": "^3.0.0", + "callsites": "^4.2.0", + "cbor": "^10.0.9", + "chalk": "^5.4.1", + "chunkd": "^2.0.1", + "ci-info": "^4.3.0", + "ci-parallel-vars": "^1.0.1", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "common-path-prefix": "^3.0.0", + "concordance": "^5.0.4", + "currently-unhandled": "^0.4.1", + "debug": "^4.4.1", + "emittery": "^1.2.0", + "figures": "^6.1.0", + "globby": "^14.1.0", + "ignore-by-default": "^2.1.0", + "indent-string": "^5.0.0", + "is-plain-object": "^5.0.0", + "is-promise": "^4.0.0", + "matcher": "^5.0.0", + "memoize": "^10.1.0", + "ms": "^2.1.3", + "p-map": "^7.0.3", + "package-config": "^5.0.0", + "picomatch": "^4.0.2", + "plur": "^5.1.0", + "pretty-ms": "^9.2.0", + "resolve-cwd": "^3.0.0", + "stack-utils": "^2.0.6", + "strip-ansi": "^7.1.0", + "supertap": "^3.0.1", + "temp-dir": "^3.0.0", + "write-file-atomic": "^6.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "ava": "entrypoints/cli.mjs" + }, + "engines": { + "node": "^18.18 || ^20.8 || ^22 || ^23 || >=24" + }, + "peerDependencies": { + "@ava/typescript": "*" + }, + "peerDependenciesMeta": { + "@ava/typescript": { + "optional": true + } + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cbor": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.11.tgz", + "integrity": "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==", + "dev": true, + "dependencies": { + "nofilter": "^3.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz", + "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "yargs": "^13.3.0" + }, + "bin": { + "chokidar": "index.js" + }, + "engines": { + "node": ">= 8.10.0" + } + }, + "node_modules/chokidar-cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/chokidar-cli/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/chokidar-cli/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/chokidar-cli/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/chokidar-cli/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/chunkd": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz", + "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==", + "dev": true + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/ci-parallel-vars": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz", + "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "dev": true, + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/depcheck/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/docdash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/docdash/-/docdash-2.0.2.tgz", + "integrity": "sha512-3SDDheh9ddrwjzf6dPFe1a16M6ftstqTNjik2+1fx46l24H9dD2osT2q9y+nBEC1wWz4GIqA48JmicOLQ0R8xA==", + "dev": true, + "dependencies": { + "@jsdoc/salty": "^0.2.1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "dev": true + }, + "node_modules/emittery": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.0.tgz", + "integrity": "sha512-KxdRyyFcS85pH3dnU8Y5yFUm2YJdaHwcBZWrfG8o89ZY9a13/f9itbN+YG3ELbBo9Pg5zvIozstmuV8bX13q6g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/enhance-visitors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/enhance-visitors/-/enhance-visitors-1.0.0.tgz", + "integrity": "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==", + "dev": true, + "dependencies": { + "lodash": "^4.13.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-ava": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-ava/-/eslint-plugin-ava-15.1.0.tgz", + "integrity": "sha512-+6Zxk1uYW3mf7lxCLWIQsFYgn3hfuCMbsKc0MtqfloOz1F6fiV5/PaWEaLgkL1egrSQmnyR7vOFP1wSPJbVUbw==", + "dev": true, + "dependencies": { + "enhance-visitors": "^1.0.0", + "eslint-utils": "^3.0.0", + "espree": "^9.0.0", + "espurify": "^2.1.1", + "import-modules": "^2.1.0", + "micro-spelling-correcter": "^1.1.1", + "pkg-dir": "^5.0.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "peerDependencies": { + "eslint": ">=9" + } + }, + "node_modules/eslint-plugin-ava/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-ava/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "52.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.4.tgz", + "integrity": "sha512-be5OzGlLExvcK13Il3noU7/v7WmAQGenTmCaBKf1pwVtPOb6X+PGFVnJad0QhMj4KKf45XjE4hbsBxv25q1fTg==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.52.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.4.1", + "escape-string-regexp": "^4.0.0", + "espree": "^10.4.0", + "esquery": "^1.6.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esmock": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz", + "integrity": "sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==", + "dev": true, + "engines": { + "node": ">=14.16.0" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/espurify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-2.1.1.tgz", + "integrity": "sha512-zttWvnkhcDyGOhSH4vO2qCBILpdCMv/MX8lp4cqgRkQoDRGK2oZxi2GfWhlP2dIXmk7BaKeOTuzbHhyC68o8XQ==", + "dev": true + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-to-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", + "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", + "dev": true + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", + "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", + "dev": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz", + "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==", + "dev": true, + "engines": { + "node": ">=10 <11 || >=12 <13 || >=14" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-modules": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.1.0.tgz", + "integrity": "sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/load-json-file": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", + "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/matcher": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", + "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/memoize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", + "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micro-spelling-correcter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/micro-spelling-correcter/-/micro-spelling-correcter-1.1.1.tgz", + "integrity": "sha512-lkJ3Rj/mtjlRcHk6YyCbvZhyWTOzdBvTHsxMmZSk5jxN1YyVSQ+JETAom55mdzfcyDrY/49Z7UCW760BK30crg==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz", + "integrity": "sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", + "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open-cli": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-8.0.0.tgz", + "integrity": "sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==", + "dev": true, + "dependencies": { + "file-type": "^18.7.0", + "get-stdin": "^9.0.0", + "meow": "^12.1.1", + "open": "^10.0.0", + "tempy": "^3.1.0" + }, + "bin": { + "open-cli": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-config": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz", + "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==", + "dev": true, + "dependencies": { + "find-up-simple": "^1.0.0", + "load-json-file": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/pacote": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-19.0.1.tgz", + "integrity": "sha512-zIpxWAsr/BvhrkSruspG8aqCQUUrWtpwx0GjiRZQhEM/pZXrigA32ElN3vTcCPUDOFmHr6SFxwYrvVUs5NTEUg==", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/pacote/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/pacote/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pacote/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pacote/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pacote/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pacote/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pacote/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pacote/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-readable": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", + "integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/plur": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", + "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==", + "dev": true, + "dependencies": { + "irregular-plurals": "^3.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/random-int": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/random-int/-/random-int-3.1.0.tgz", + "integrity": "sha512-h8CRz8cpvzj0hC/iH/1Gapgcl2TQ6xtnCpyOI5WvWfXf/yrDx2DOU+tD9rX23j36IF11xg1KqB9W11Z18JPMdw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dev": true, + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-wrap/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz", + "integrity": "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.1.3" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supertap": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", + "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==", + "dev": true, + "dependencies": { + "indent-string": "^5.0.0", + "js-yaml": "^3.14.1", + "serialize-error": "^7.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/supertap/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/supertap/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tap-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-1.2.2.tgz", + "integrity": "sha512-uXKcosa0qoSjeh73dhmX+OpJvpigDxUciOhBcbGUKtmwzEFJjUT1Ql5dpg4M9I1UjXT9b+6n1W05FB8QmKossA==", + "dev": true, + "dependencies": { + "events-to-array": "^1.0.1", + "inherits": "~2.0.1", + "js-yaml": "^3.2.7" + }, + "bin": { + "tap-parser": "bin/cmd.js" + }, + "optionalDependencies": { + "readable-stream": "^2" + } + }, + "node_modules/tap-parser/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/tap-parser/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/tap-parser/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/tap-parser/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "optional": true + }, + "node_modules/tap-parser/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tap-xunit": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tap-xunit/-/tap-xunit-2.4.1.tgz", + "integrity": "sha512-qcZStDtjjYjMKAo7QNiCtOW256g3tuSyCSe5kNJniG1Q2oeOExJq4vm8CwboHZURpkXAHvtqMl4TVL7mcbMVVA==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "minimist": "~1.2.0", + "tap-parser": "~1.2.2", + "through2": "~2.0.0", + "xmlbuilder": "~4.2.0", + "xtend": "~4.0.0" + }, + "bin": { + "tap-xunit": "bin/tap-xunit", + "txunit": "bin/tap-xunit" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tuf-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", + "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha512-oEePiEefhQhAeUnwRnIBLBWmk/fsWWbQ53EEWsRuzECbQ3m5o/Esmq6H47CYYwSLW+Ynt0rS9hd0pd2ogMAWjg==", + "dev": true, + "dependencies": { + "lodash": "^4.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yesno": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz", + "integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/project/package.json b/packages/project/package.json new file mode 100644 index 00000000000..b72d0d254ce --- /dev/null +++ b/packages/project/package.json @@ -0,0 +1,179 @@ +{ + "name": "@ui5/project", + "version": "4.0.6", + "description": "UI5 CLI - Project ", + "author": { + "name": "SAP SE", + "email": "openui5@sap.com", + "url": "https://www.sap.com" + }, + "license": "Apache-2.0", + "keywords": [ + "openui5", + "sapui5", + "ui5", + "build", + "development", + "tool" + ], + "type": "module", + "exports": { + "./config/Configuration": "./lib/config/Configuration.js", + "./specifications/Specification": "./lib/specifications/Specification.js", + "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js", + "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js", + "./ui5Framework/Openui5Resolver": "./lib/ui5Framework/Openui5Resolver.js", + "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js", + "./ui5Framework/maven/CacheMode": "./lib/ui5Framework/maven/CacheMode.js", + "./validation/validator": "./lib/validation/validator.js", + "./validation/ValidationError": "./lib/validation/ValidationError.js", + "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", + "./graph/projectGraphBuilder": "./lib/graph/projectGraphBuilder.js", + "./graph": "./lib/graph/graph.js", + "./package.json": "./package.json" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + }, + "scripts": { + "test": "npm run lint && npm run jsdoc-generate && npm run coverage && npm run depcheck", + "test-azure": "npm run coverage-xunit", + "lint": "eslint ./", + "unit": "rimraf test/tmp && ava", + "unit-verbose": "rimraf test/tmp && cross-env UI5_LOG_LVL=verbose ava --verbose --serial", + "unit-watch": "npm run unit -- --watch", + "unit-xunit": "rimraf test/tmp && ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml", + "unit-inspect": "cross-env UI5_LOG_LVL=verbose ava debug --break", + "coverage": "rimraf test/tmp && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", + "coverage-xunit": "nyc --reporter=text --reporter=text-summary --reporter=cobertura npm run unit-xunit", + "jsdoc": "npm run jsdoc-generate && open-cli jsdocs/index.html", + "jsdoc-generate": "jsdoc -c ./jsdoc.json -t $(node -p 'path.dirname(require.resolve(\"docdash\"))') ./lib/ || (echo 'Error during JSDoc generation! Check log.' && exit 1)", + "jsdoc-watch": "npm run jsdoc && chokidar \"./lib/**/*.js\" -c \"npm run jsdoc-generate\"", + "preversion": "npm test", + "version": "git-chglog --sort semver --next-tag v$npm_package_version -o CHANGELOG.md v4.0.0.. && git add CHANGELOG.md", + "prepublishOnly": "git push --follow-tags", + "release-note": "git-chglog --sort semver -c .chglog/release-config.yml v$npm_package_version", + "depcheck": "depcheck --ignores @ui5/project,docdash,@istanbuljs/esm-loader-hook,rimraf" + }, + "files": [ + "CHANGELOG.md", + "CONTRIBUTING.md", + "jsdoc.json", + "lib/**", + "LICENSES/**", + ".reuse/**" + ], + "ava": { + "files": [ + "test/lib/**/*.js", + "!test/**/__helper__/**" + ], + "nodeArguments": [ + "--loader=esmock", + "--no-warnings" + ], + "workerThreads": false + }, + "nyc": { + "reporter": [ + "lcov", + "text", + "text-summary" + ], + "exclude": [ + "docs/**", + "jsdocs/**", + "coverage/**", + "test/**", + ".eslintrc.cjs", + "jsdoc-plugin.cjs" + ], + "check-coverage": true, + "statements": 90, + "branches": 85, + "functions": 90, + "lines": 90, + "watermarks": { + "statements": [ + 70, + 90 + ], + "branches": [ + 70, + 90 + ], + "functions": [ + 70, + 90 + ], + "lines": [ + 70, + 90 + ] + }, + "cache": true, + "all": true + }, + "repository": { + "type": "git", + "url": "git@github.com:SAP/ui5-project.git" + }, + "dependencies": { + "@npmcli/config": "^10.4.0", + "@ui5/fs": "^4.0.2", + "@ui5/logger": "^4.0.2", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "chalk": "^5.6.2", + "escape-string-regexp": "^5.0.0", + "globby": "^14.1.0", + "graceful-fs": "^4.2.11", + "js-yaml": "^4.1.0", + "lockfile": "^1.0.4", + "make-fetch-happen": "^14.0.3", + "node-stream-zip": "^1.15.0", + "pacote": "^19.0.1", + "pretty-hrtime": "^1.0.3", + "read-package-up": "^11.0.0", + "read-pkg": "^9.0.1", + "resolve": "^1.22.10", + "semver": "^7.7.2", + "xml2js": "^0.6.2", + "yesno": "^0.4.0" + }, + "peerDependencies": { + "@ui5/builder": "^4.0.11" + }, + "peerDependenciesMeta": { + "@ui5/builder": { + "optional": true + } + }, + "devDependencies": { + "@eslint/js": "^9.8.0", + "@istanbuljs/esm-loader-hook": "^0.3.0", + "ava": "^6.4.1", + "chokidar-cli": "^3.0.0", + "cross-env": "^7.0.3", + "depcheck": "^1.4.7", + "docdash": "^2.0.2", + "eslint": "^9.36.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-ava": "^15.1.0", + "eslint-plugin-jsdoc": "^52.0.4", + "esmock": "^2.7.3", + "globals": "^16.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-instrument": "^6.0.3", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "js-beautify": "^1.15.4", + "jsdoc": "^4.0.4", + "nyc": "^17.1.0", + "open-cli": "^8.0.0", + "rimraf": "^6.0.1", + "sinon": "^21.0.0", + "tap-xunit": "^2.4.1" + } +} diff --git a/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js new file mode 100644 index 00000000000..c7a5c0758dd --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js @@ -0,0 +1,2 @@ +export default () => "extension module"; +export function determineRequiredDependencies () { return "required dependencies function" }; diff --git a/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json new file mode 100644 index 00000000000..97c63b1660f --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json @@ -0,0 +1,5 @@ +{ + "name": "extension.a", + "type": "module", + "version": "1.0.0" +} diff --git a/packages/project/test/fixtures/application.a.aliases/package.json b/packages/project/test/fixtures/application.a.aliases/package.json new file mode 100644 index 00000000000..b15c70dfe0a --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/package.json @@ -0,0 +1,12 @@ +{ + "name": "application.a.aliases", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "extension.a.esm.alias": "file:../extension.a.esm" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a.aliases/ui5.yaml b/packages/project/test/fixtures/application.a.aliases/ui5.yaml new file mode 100644 index 00000000000..82579b7d86a --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/ui5.yaml @@ -0,0 +1,23 @@ +--- +specVersion: "3.1" +type: application +metadata: + name: application.a.aliases + +--- # Everything below this line could also be put into the ui5.yaml of a standalone extension module +specVersion: "3.1" +kind: extension +type: project-shim +metadata: + name: my.application.thirdparty +shims: + configurations: + extension.a.esm.alias: # name as defined in package.json + specVersion: "3.1" + type: module # Use module type + metadata: + name: extension.a.esm.alias + resources: + configuration: + paths: + /resources/my/application/thirdparty/: "" # map root directory of lodash module diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/index.html b/packages/project/test/fixtures/application.a.aliases/webapp/index.html new file mode 100644 index 00000000000..1b8755901bf --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/manifest.json b/packages/project/test/fixtures/application.a.aliases/webapp/manifest.json new file mode 100644 index 00000000000..d33902df74e --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/test.js b/packages/project/test/fixtures/application.a.aliases/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/application.a.aliases/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/application.a/middleware.a.js b/packages/project/test/fixtures/application.a/middleware.a.js new file mode 100644 index 00000000000..ea41b01de46 --- /dev/null +++ b/packages/project/test/fixtures/application.a/middleware.a.js @@ -0,0 +1 @@ +module.exports = function () {}; diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/package.json new file mode 100644 index 00000000000..2179673d41d --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/package.json @@ -0,0 +1,17 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "name": "library.a", + "type": "library", + "settings": { + "src": "src", + "test": "test" + } + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..25c8603f31a --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + ${copyright} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..36052acebdc --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + ${copyright} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/package.json new file mode 100644 index 00000000000..81b948438bd --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/package.json @@ -0,0 +1,18 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "collection": { + "modules": { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c" + } + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/ui5.yaml new file mode 100644 index 00000000000..e47048de6a7 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/collection/ui5.yaml @@ -0,0 +1,12 @@ +specVersion: "2.1" +metadata: + name: application.a.collection.dependency.shim +kind: extension +type: project-shim +shims: + collections: + collection: + modules: + "library.a": "./library.a" + "library.b": "./library.b" + "library.c": "./library.c" \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.a/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/package.json b/packages/project/test/fixtures/application.a/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.a/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.a/package.json b/packages/project/test/fixtures/application.a/package.json new file mode 100644 index 00000000000..7da37b86a56 --- /dev/null +++ b/packages/project/test/fixtures/application.a/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.a/task.a.js b/packages/project/test/fixtures/application.a/task.a.js new file mode 100644 index 00000000000..ea41b01de46 --- /dev/null +++ b/packages/project/test/fixtures/application.a/task.a.js @@ -0,0 +1 @@ +module.exports = function () {}; diff --git a/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml b/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml new file mode 100644 index 00000000000..a50b3c48b99 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml @@ -0,0 +1,7 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a +customConfiguration: + configPathTest: true \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml b/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml new file mode 100644 index 00000000000..ecce9d7e78b --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml @@ -0,0 +1 @@ +|-\nfoo\nbar diff --git a/packages/project/test/fixtures/application.a/ui5-test-empty.yaml b/packages/project/test/fixtures/application.a/ui5-test-empty.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.a/ui5-test-error.yaml b/packages/project/test/fixtures/application.a/ui5-test-error.yaml new file mode 100644 index 00000000000..f5ad909d259 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-test-error.yaml @@ -0,0 +1,7 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a +xyz: + foo: true \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/ui5.yaml b/packages/project/test/fixtures/application.a/ui5.yaml new file mode 100644 index 00000000000..b9dde7b16b2 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a diff --git a/packages/project/test/fixtures/application.a/webapp/index.html b/packages/project/test/fixtures/application.a/webapp/index.html new file mode 100644 index 00000000000..77b0207cc80 --- /dev/null +++ b/packages/project/test/fixtures/application.a/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/webapp/manifest.json b/packages/project/test/fixtures/application.a/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.a/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.a/webapp/test.js b/packages/project/test/fixtures/application.a/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/application.a/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/package.json new file mode 100644 index 00000000000..2179673d41d --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/package.json @@ -0,0 +1,17 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "name": "library.a", + "type": "library", + "settings": { + "src": "src", + "test": "test" + } + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..25c8603f31a --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + ${copyright} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..36052acebdc --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + ${copyright} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/package.json new file mode 100644 index 00000000000..81b948438bd --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/collection/package.json @@ -0,0 +1,18 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "collection": { + "modules": { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c" + } + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.b/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/package.json b/packages/project/test/fixtures/application.b/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.b/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.b/package.json b/packages/project/test/fixtures/application.b/package.json new file mode 100644 index 00000000000..0243e3a9001 --- /dev/null +++ b/packages/project/test/fixtures/application.b/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.b/ui5.yaml b/packages/project/test/fixtures/application.b/ui5.yaml new file mode 100644 index 00000000000..7b5e5dd2359 --- /dev/null +++ b/packages/project/test/fixtures/application.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.b diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties new file mode 100644 index 00000000000..d93c8a39c0e --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties @@ -0,0 +1 @@ +title=embedded-i18n \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties new file mode 100644 index 00000000000..e513333c842 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties @@ -0,0 +1 @@ +title=embedded-i18n_de \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties new file mode 100644 index 00000000000..85e162740f9 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties @@ -0,0 +1 @@ +title=embedded-i18n_fr-wrong \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json b/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json new file mode 100644 index 00000000000..5ef0c362425 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1.embedded", + "type": "component", + "applicationVersion": { + "version": "1.2.2" + }, + "embeddedBy": "../", + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/i18n.properties b/packages/project/test/fixtures/application.b/webapp/i18n.properties new file mode 100644 index 00000000000..88b84f602f5 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/i18n.properties @@ -0,0 +1 @@ +title=app-i18n-wrong \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties b/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties new file mode 100644 index 00000000000..575fb20d0c3 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties @@ -0,0 +1 @@ +title=app-i18n \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties b/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties new file mode 100644 index 00000000000..f5885803892 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties @@ -0,0 +1 @@ +title=app-i18n_de \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties b/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties new file mode 100644 index 00000000000..88b84f602f5 --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties @@ -0,0 +1 @@ +title=app-i18n-wrong \ No newline at end of file diff --git a/packages/project/test/fixtures/application.b/webapp/manifest.json b/packages/project/test/fixtures/application.b/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.b/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..7b731df83f6 --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -0,0 +1,3 @@ +--- +name: library.d +type: library diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c/node_modules/library.e/package.json new file mode 100644 index 00000000000..07ed6e5f4bf --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..88ba07e82dd --- /dev/null +++ b/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.c/package.json b/packages/project/test/fixtures/application.c/package.json new file mode 100644 index 00000000000..1cd37e82d67 --- /dev/null +++ b/packages/project/test/fixtures/application.c/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through root project", + "main": "index.html", + "dependencies": { + "library.e": "file:../library.e", + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c/src/manifest.json b/packages/project/test/fixtures/application.c/src/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.c/src/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c/ui5.yaml b/packages/project/test/fixtures/application.c/ui5.yaml new file mode 100644 index 00000000000..fd28471bad3 --- /dev/null +++ b/packages/project/test/fixtures/application.c/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.c +resources: + configuration: + paths: + webapp: src diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/some.js b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/package.json new file mode 100644 index 00000000000..9f88fc95f0c --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.d-depender", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml new file mode 100644 index 00000000000..51744218833 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d-depender +resources: + configuration: + paths: + src: main/src + test: main/test + diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..e05b61880d8 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json new file mode 100644 index 00000000000..07ed6e5f4bf --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..99dbbf3bcc3 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.c2/package.json b/packages/project/test/fixtures/application.c2/package.json new file mode 100644 index 00000000000..7af4231b145 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.c2", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through other project", + "main": "index.html", + "dependencies": { + "library.e": "file:../library.e", + "library.d-depender": "file:../library.d-depender" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c2/src/manifest.json b/packages/project/test/fixtures/application.c2/src/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.c2/src/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c2/ui5.yaml b/packages/project/test/fixtures/application.c2/ui5.yaml new file mode 100644 index 00000000000..c982fa9dc26 --- /dev/null +++ b/packages/project/test/fixtures/application.c2/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.c2 +resources: + configuration: + paths: + webapp: src diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library new file mode 100644 index 00000000000..42efe5f9d84 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library @@ -0,0 +1,11 @@ + + + + library.d-depender + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json new file mode 100644 index 00000000000..9f88fc95f0c --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.d-depender", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml new file mode 100644 index 00000000000..51744218833 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d-depender +resources: + configuration: + paths: + src: main/src + test: main/test + diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.e/package.json new file mode 100644 index 00000000000..07ed6e5f4bf --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..99dbbf3bcc3 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.c3/package.json b/packages/project/test/fixtures/application.c3/package.json new file mode 100644 index 00000000000..b51a0f27e23 --- /dev/null +++ b/packages/project/test/fixtures/application.c3/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.c3", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through other project (but got hoisted)", + "main": "index.html", + "dependencies": { + "library.e": "file:../library.e", + "library.d-depender": "file:../library.d-depender" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.c3/src/manifest.json b/packages/project/test/fixtures/application.c3/src/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.c3/src/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.c3/ui5.yaml b/packages/project/test/fixtures/application.c3/ui5.yaml new file mode 100644 index 00000000000..3cecacec12e --- /dev/null +++ b/packages/project/test/fixtures/application.c3/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.c3 +resources: + configuration: + paths: + webapp: src diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..e05b61880d8 --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/package.json b/packages/project/test/fixtures/application.d/node_modules/library.e/package.json new file mode 100644 index 00000000000..9ce874ff55a --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.d/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..99dbbf3bcc3 --- /dev/null +++ b/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.d/package.json b/packages/project/test/fixtures/application.d/package.json new file mode 100644 index 00000000000..e99f770892b --- /dev/null +++ b/packages/project/test/fixtures/application.d/package.json @@ -0,0 +1,12 @@ +{ + "name": "application.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for dev dependencies", + "main": "index.html", + "dependencies": { + "library.e": "file:../library.e" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.d/ui5.yaml b/packages/project/test/fixtures/application.d/ui5.yaml new file mode 100644 index 00000000000..1b43352b18a --- /dev/null +++ b/packages/project/test/fixtures/application.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.d diff --git a/packages/project/test/fixtures/application.d/webapp/manifest.json b/packages/project/test/fixtures/application.d/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.d/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/package.json b/packages/project/test/fixtures/application.e/node_modules/library.e/package.json new file mode 100644 index 00000000000..07ed6e5f4bf --- /dev/null +++ b/packages/project/test/fixtures/application.e/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..20c99070048 --- /dev/null +++ b/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.e/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..3852a732d57 --- /dev/null +++ b/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e +builder: + configuration: + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.e/package.json b/packages/project/test/fixtures/application.e/package.json new file mode 100644 index 00000000000..50f927fcc52 --- /dev/null +++ b/packages/project/test/fixtures/application.e/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for circular dependencies", + "main": "index.html", + "dependencies": { + "library.f": "file:../library.f", + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.e/ui5.yaml b/packages/project/test/fixtures/application.e/ui5.yaml new file mode 100644 index 00000000000..97537e5ca03 --- /dev/null +++ b/packages/project/test/fixtures/application.e/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.e diff --git a/packages/project/test/fixtures/application.e/webapp/manifest.json b/packages/project/test/fixtures/application.e/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.e/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.f/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/package.json b/packages/project/test/fixtures/application.f/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.f/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/package.json b/packages/project/test/fixtures/application.f/node_modules/library.e/package.json new file mode 100644 index 00000000000..9ce874ff55a --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.f/node_modules/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml new file mode 100644 index 00000000000..99dbbf3bcc3 --- /dev/null +++ b/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/application.f/package.json b/packages/project/test/fixtures/application.f/package.json new file mode 100644 index 00000000000..68a71be8c58 --- /dev/null +++ b/packages/project/test/fixtures/application.f/package.json @@ -0,0 +1,18 @@ +{ + "name": "application.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for ui5-dependencies configuration", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "library.e": "file:../library.e" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "dependencies": [ + "library.d" + ] + } +} diff --git a/packages/project/test/fixtures/application.f/ui5.yaml b/packages/project/test/fixtures/application.f/ui5.yaml new file mode 100644 index 00000000000..3df51b3a974 --- /dev/null +++ b/packages/project/test/fixtures/application.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.f diff --git a/packages/project/test/fixtures/application.f/webapp/manifest.json b/packages/project/test/fixtures/application.f/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.f/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.g/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/package.json b/packages/project/test/fixtures/application.g/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/application.g/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.g/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/application.g/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/application.g/package.json b/packages/project/test/fixtures/application.g/package.json new file mode 100644 index 00000000000..41d1ea32cf2 --- /dev/null +++ b/packages/project/test/fixtures/application.g/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.g", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for npm optionalDependencies", + "main": "index.html", + "optionalDependencies": { + "library.d": "file:../library.d", + "library.nonexistent": "file:../library.nonexistent" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/application.g/ui5.yaml b/packages/project/test/fixtures/application.g/ui5.yaml new file mode 100644 index 00000000000..d4e5b20f996 --- /dev/null +++ b/packages/project/test/fixtures/application.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.g diff --git a/packages/project/test/fixtures/application.g/webapp/manifest.json b/packages/project/test/fixtures/application.g/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/application.g/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/application.h/pom.xml b/packages/project/test/fixtures/application.h/pom.xml new file mode 100644 index 00000000000..478ebc85c8c --- /dev/null +++ b/packages/project/test/fixtures/application.h/pom.xml @@ -0,0 +1,41 @@ + + + + + + + 4.0.0 + + + + + com.sap.test + application.h + 1.0.0 + war + + + + + application.h + Simple SAPUI5 based application + + + + + + + application.h + + + + + diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml new file mode 100644 index 00000000000..1eee899301b --- /dev/null +++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml @@ -0,0 +1,7 @@ +--- +version: "0.0.1" +path: "../application.a" +dependencies: +- id: static-library.e + version: "0.0.1" + path: "../library.e" diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml new file mode 100644 index 00000000000..8604cc66aba --- /dev/null +++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml @@ -0,0 +1,7 @@ +--- +id: static-application.a +version: "0.0.1" +path: "../application.a" +dependencies: +- id: static-library.e + version: "0.0.1" diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml new file mode 100644 index 00000000000..3cecad748ba --- /dev/null +++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml @@ -0,0 +1,7 @@ +--- +id: static-application.a +path: "../application.a" +dependencies: +- id: static-library.e + version: "0.0.1" + path: "../library.e" diff --git a/packages/project/test/fixtures/application.h/projectDependencies.yaml b/packages/project/test/fixtures/application.h/projectDependencies.yaml new file mode 100644 index 00000000000..b06c3121380 --- /dev/null +++ b/packages/project/test/fixtures/application.h/projectDependencies.yaml @@ -0,0 +1,8 @@ +--- +id: static-application.a +version: "0.0.1" +path: "../application.a" +dependencies: +- id: static-library.e + version: "0.0.1" + path: "../library.e" diff --git a/packages/project/test/fixtures/application.h/webapp-project.artifactId/manifest.json b/packages/project/test/fixtures/application.h/webapp-project.artifactId/manifest.json new file mode 100644 index 00000000000..7de6072ce82 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp-project.artifactId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${project.artifactId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/application.h/webapp-properties.appId/manifest.json b/packages/project/test/fixtures/application.h/webapp-properties.appId/manifest.json new file mode 100644 index 00000000000..e1515df7025 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp-properties.appId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${appId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/application.h/webapp-properties.componentName/manifest.json b/packages/project/test/fixtures/application.h/webapp-properties.componentName/manifest.json new file mode 100644 index 00000000000..7d63e359cdf --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp-properties.componentName/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${componentName}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/application.h/webapp/Component.js b/packages/project/test/fixtures/application.h/webapp/Component.js new file mode 100644 index 00000000000..cb9bd406864 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/manifest.json b/packages/project/test/fixtures/application.h/webapp/manifest.json new file mode 100644 index 00000000000..32b7e4a8458 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "application.h", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsA/section1.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section1.js new file mode 100644 index 00000000000..ac4a8129651 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsA/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsA/section2.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section2.js new file mode 100644 index 00000000000..e009c828602 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsA/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsA/section3.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section3.js new file mode 100644 index 00000000000..5fd9349d49b --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsA/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsB/section1.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section1.js new file mode 100644 index 00000000000..ac4a8129651 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsB/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsB/section2.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section2.js new file mode 100644 index 00000000000..e009c828602 --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsB/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/packages/project/test/fixtures/application.h/webapp/sectionsB/section3.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section3.js new file mode 100644 index 00000000000..5fd9349d49b --- /dev/null +++ b/packages/project/test/fixtures/application.h/webapp/sectionsB/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/packages/project/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json b/packages/project/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json new file mode 100644 index 00000000000..03ff08f24bd --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json @@ -0,0 +1,42 @@ +{ + "project": { + "specVersion": "2.3", + "type": "application", + "metadata": { + "name": "application.a" + }, + "resources": { + "configuration": { + "paths": { + "webapp": "resources/id1" + } + } + } + }, + "buildManifest": { + "manifestVersion": "0.1", + "timestamp": "2022-05-04T12:45:30.024Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "application.a", + "version": "0.2.0", + "namespace": "id1", + "tags": { + "/resources/id1/test.js": { + "ui5:HasDebugVariant": true + }, + "/resources/id1/test-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } +} diff --git a/packages/project/test/fixtures/build-manifest/application.a/package.json b/packages/project/test/fixtures/build-manifest/application.a/package.json new file mode 100644 index 00000000000..b5401c1e6e9 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.a-archive", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/index.html b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/index.html new file mode 100644 index 00000000000..77b0207cc80 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + \ No newline at end of file diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/manifest.json b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json b/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json new file mode 100644 index 00000000000..5205a51a854 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json @@ -0,0 +1,43 @@ +{ + "project": { + "specVersion": "2.3", + "type": "library", + "metadata": { + "name": "library.e" + }, + "resources": { + "configuration": { + "paths": { + "src": "resources", + "test": "test-resources" + } + } + } + }, + "buildManifest": { + "manifestVersion": "0.1", + "timestamp": "2022-05-06T09:54:29.051Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "library.e", + "version": "1.0.0", + "namespace": "library/e", + "tags": { + "/resources/library/e/some.js": { + "ui5:HasDebugVariant": true + }, + "/resources/library/e/some-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } +} diff --git a/packages/project/test/fixtures/build-manifest/library.e/package.json b/packages/project/test/fixtures/build-manifest/library.e/package.json new file mode 100644 index 00000000000..9ce874ff55a --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library new file mode 100644 index 00000000000..c1f37d77222 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + Some fancy copyright + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html b/packages/project/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection.b/library.a/package.json b/packages/project/test/fixtures/collection.b/library.a/package.json new file mode 100644 index 00000000000..aec498f7283 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.a/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection.b/library.a/src/library/a/.library b/packages/project/test/fixtures/collection.b/library.a/src/library/a/.library new file mode 100644 index 00000000000..ef0ea1065bc --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/collection.b/library.a/test/library/a/Test.html b/packages/project/test/fixtures/collection.b/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection.b/library.a/ui5.yaml b/packages/project/test/fixtures/collection.b/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/collection.b/library.b/package.json b/packages/project/test/fixtures/collection.b/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection.b/library.b/src/library/b/.library b/packages/project/test/fixtures/collection.b/library.b/src/library/b/.library new file mode 100644 index 00000000000..7128151f3f4 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection.b/library.b/test/library/b/Test.html b/packages/project/test/fixtures/collection.b/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection.b/library.b/ui5.yaml b/packages/project/test/fixtures/collection.b/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/collection.b/library.c/package.json b/packages/project/test/fixtures/collection.b/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection.b/library.c/src/library/c/.library b/packages/project/test/fixtures/collection.b/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection.b/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/collection.b/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection.b/library.c/ui5.yaml b/packages/project/test/fixtures/collection.b/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/collection.b/package.json b/packages/project/test/fixtures/collection.b/package.json new file mode 100644 index 00000000000..25e37da0426 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/package.json @@ -0,0 +1,17 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection used for Workspace testing", + "dependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "workspaces": [ + "library.{a,b}", + "*c", + "sub-*" + ] + } +} diff --git a/packages/project/test/fixtures/collection.b/sub-collection/package.json b/packages/project/test/fixtures/collection.b/sub-collection/package.json new file mode 100644 index 00000000000..046177300dc --- /dev/null +++ b/packages/project/test/fixtures/collection.b/sub-collection/package.json @@ -0,0 +1,10 @@ +{ + "name": "sub-collection", + "version": "1.0.0", + "description": "Sub-Collection package", + "ui5": { + "workspaces": [ + "../../library.d" + ] + } +} diff --git a/packages/project/test/fixtures/collection.b/sub-empty/.keep b/packages/project/test/fixtures/collection.b/sub-empty/.keep new file mode 100644 index 00000000000..a550add6add --- /dev/null +++ b/packages/project/test/fixtures/collection.b/sub-empty/.keep @@ -0,0 +1,2 @@ +This file is a stand-in for an empty project directory. +This directory, even though matching the npm workspace configuration pattern, should be ignored by UI5 CLI. diff --git a/packages/project/test/fixtures/collection.b/test.js b/packages/project/test/fixtures/collection.b/test.js new file mode 100644 index 00000000000..d063db1e726 --- /dev/null +++ b/packages/project/test/fixtures/collection.b/test.js @@ -0,0 +1,4 @@ +import {globby} from 'globby'; + +const paths = await globby(["library.a"]); +console.log("paths") diff --git a/packages/project/test/fixtures/collection/library.a/package.json b/packages/project/test/fixtures/collection/library.a/package.json new file mode 100644 index 00000000000..aec498f7283 --- /dev/null +++ b/packages/project/test/fixtures/collection/library.a/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..ef0ea1065bc --- /dev/null +++ b/packages/project/test/fixtures/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection/library.a/ui5.yaml b/packages/project/test/fixtures/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/collection/library.b/package.json b/packages/project/test/fixtures/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..7128151f3f4 --- /dev/null +++ b/packages/project/test/fixtures/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + Some fancy copyright ${currentYear} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection/library.b/ui5.yaml b/packages/project/test/fixtures/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/collection/library.c/package.json b/packages/project/test/fixtures/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection/library.c/ui5.yaml b/packages/project/test/fixtures/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/collection/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/collection/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/collection/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/collection/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/collection/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/collection/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/collection/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/collection/package.json b/packages/project/test/fixtures/collection/package.json new file mode 100644 index 00000000000..24849dbe4a8 --- /dev/null +++ b/packages/project/test/fixtures/collection/package.json @@ -0,0 +1,16 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "workspaces": [ + "library.a", + "library.b", + "library.c" + ] +} diff --git a/packages/project/test/fixtures/collection/test.js b/packages/project/test/fixtures/collection/test.js new file mode 100644 index 00000000000..d063db1e726 --- /dev/null +++ b/packages/project/test/fixtures/collection/test.js @@ -0,0 +1,4 @@ +import {globby} from 'globby'; + +const paths = await globby(["library.a"]); +console.log("paths") diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json new file mode 100644 index 00000000000..5077e3ac9f1 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json @@ -0,0 +1,12 @@ +{ + "name": "@ui5-internal/application.cycle.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic UI5 dependencies", + "main": "index.html", + "dependencies": { + "@ui5-internal/component.cycle.a": "file:../component.cycle.a" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml new file mode 100644 index 00000000000..7501387233f --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.cycle.a diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html new file mode 100644 index 00000000000..daa631f7ba8 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle A + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json new file mode 100644 index 00000000000..68458d55e78 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ui5-internal/application.cycle.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Cycle on second level via dev dependency", + "main": "index.html", + "dependencies": { + "@ui5-internal/module.d": "file:../module.d", + "@ui5-internal/module.e": "file:../module.e" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml new file mode 100644 index 00000000000..33e181aa9bb --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml @@ -0,0 +1,23 @@ +--- +specVersion: "2.2" +type: application +metadata: + name: application.cycle.b +--- +specVersion: "2.2" +kind: extension +type: project-shim +metadata: + name: application.cycle.b-shim +shims: + configurations: + module.d: + specVersion: "2.2" + type: module + metadata: + name: module.d + module.e: + specVersion: "2.2" + type: module + metadata: + name: module.e \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html new file mode 100644 index 00000000000..fb845822a8e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle B + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json new file mode 100644 index 00000000000..f7ca7ac8d93 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json @@ -0,0 +1,15 @@ +{ + "name": "@ui5-internal/application.cycle.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Cycle on third level", + "_ui5_test_comment": "This scenario can't be tested using file: URLs as npm can't create cyclic symlinks.", + "_ui5_test_comment2": "However publishing to and installing from a registry works.", + "main": "index.html", + "dependencies": { + "@ui5-internal/module.f": "^1.0.0", + "@ui5-internal/module.g": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml new file mode 100644 index 00000000000..18e66978522 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.cycle.c diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html new file mode 100644 index 00000000000..a845ac4258e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle C + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json new file mode 100644 index 00000000000..cf2ff767d66 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json @@ -0,0 +1,17 @@ +{ + "name": "@ui5-internal/application.cycle.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Heavily influenced by npm package 'es6-map'", + "_ui5_test_comment": "This scenario can't be tested using file: URLs as npm can't create cyclic symlinks.", + "_ui5_test_comment2": "However publishing to and installing from a registry works.", + "main": "index.html", + "dependencies": { + "@ui5-internal/module.h": "^1.0.0", + "@ui5-internal/module.i": "^1.0.0", + "@ui5-internal/module.j": "^1.0.0", + "@ui5-internal/module.k": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml new file mode 100644 index 00000000000..72d79ae25db --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.cycle.d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html new file mode 100644 index 00000000000..aefe63db523 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle D + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json new file mode 100644 index 00000000000..b2b7bc31eef --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ui5-internal/application.cycle.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Indirect cycle via devDependency (pending dev)", + "main": "index.html", + "dependencies": { + "@ui5-internal/module.l": "^1.0.0", + "@ui5-internal/module.m": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml new file mode 100644 index 00000000000..c3d85496b83 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.cycle.e diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html new file mode 100644 index 00000000000..43cb37dc30b --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle E + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json new file mode 100644 index 00000000000..f48e64bb3ed --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ui5-internal/application.cycle.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication", + "main": "index.html", + "dependencies": { + "@ui5-internal/library.cycle.c": "^1.0.0", + "@ui5-internal/library.cycle.d": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml new file mode 100644 index 00000000000..6c39c5b3f56 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.cycle.f diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html new file mode 100644 index 00000000000..38e2bf69eee --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html @@ -0,0 +1,9 @@ + + + + Application Cycle F + + + + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json new file mode 100644 index 00000000000..1a930a22dac --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json @@ -0,0 +1,15 @@ +{ + "name": "@ui5-internal/component.cycle.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic dependencies", + "dependencies": { + "@ui5-internal/library.cycle.a": "file:../library.cycle.a", + "@ui5-internal/library.cycle.b": "file:../library.cycle.b" + }, + "devDependencies": { + "@ui5-internal/application.cycle.a": "file:../application.cycle.a" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library new file mode 100644 index 00000000000..e2eb7bd4c7c --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library @@ -0,0 +1,11 @@ + + + + component.cycle.a + SAP SE + ${copyright} + ${version} + + Component Cycle A + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/test/component/cycle/a/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/test/component/cycle/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml new file mode 100644 index 00000000000..f63b38ea3ba --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: component.cycle.a + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json new file mode 100644 index 00000000000..2b6d08c4f2b --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/library.cycle.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic dependencies", + "devDependencies": { + "@ui5-internal/component.cycle.a": "file:../component.cycle.a" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library new file mode 100644 index 00000000000..f9cfdf7313d --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library @@ -0,0 +1,11 @@ + + + + cycle.a + SAP SE + ${copyright} + ${version} + + Library Cycle A + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/test/cycle/a/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/test/cycle/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml new file mode 100644 index 00000000000..5c42ee2e188 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.a + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json new file mode 100644 index 00000000000..303a3ad4cbc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/library.cycle.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic dependencies", + "devDependencies": { + "@ui5-internal/component.cycle.a": "file:../component.cycle.a" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library new file mode 100644 index 00000000000..d7b129ae80e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library @@ -0,0 +1,11 @@ + + + + cycle.b + SAP SE + ${copyright} + ${version} + + Library Cycle B + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/test/cycle/b/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/test/cycle/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml new file mode 100644 index 00000000000..fb6a25de09a --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.b + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json new file mode 100644 index 00000000000..32a39279058 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json @@ -0,0 +1,12 @@ +{ + "name": "@ui5-internal/library.cycle.d", + "version": "0.9.0", + "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication", + "dependencies": { + "@ui5-internal/library.cycle.c": "^1.0.0", + "@ui5-internal/library.cycle.e": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library new file mode 100644 index 00000000000..af0aa5c6492 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library @@ -0,0 +1,11 @@ + + + + library.cycle.b + SAP SE + ${copyright} + ${version} + + Library Cycle D + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml new file mode 100644 index 00000000000..52bb969a878 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.d + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json new file mode 100644 index 00000000000..491f90eaf79 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/library.cycle.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication", + "dependencies": { + "@ui5-internal/library.cycle.d": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library new file mode 100644 index 00000000000..773fb893c19 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library @@ -0,0 +1,11 @@ + + + + library.cycle.b + SAP SE + ${copyright} + ${version} + + Library Cycle C + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/test/cycle/c/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/test/cycle/c/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml new file mode 100644 index 00000000000..f6b4f2ac1e9 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.c + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json new file mode 100644 index 00000000000..b0b7a230c40 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json @@ -0,0 +1,12 @@ +{ + "name": "@ui5-internal/library.cycle.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication", + "dependencies": { + "@ui5-internal/library.cycle.c": "^1.0.0", + "@ui5-internal/library.cycle.e": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library new file mode 100644 index 00000000000..af0aa5c6492 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library @@ -0,0 +1,11 @@ + + + + library.cycle.b + SAP SE + ${copyright} + ${version} + + Library Cycle D + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml new file mode 100644 index 00000000000..52bb969a878 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.d + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json new file mode 100644 index 00000000000..9d5562b40ed --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/library.cycle.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication", + "dependencies": { + "@ui5-internal/library.cycle.c": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library new file mode 100644 index 00000000000..dc182abd9b5 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library @@ -0,0 +1,11 @@ + + + + library.cycle.b + SAP SE + ${copyright} + ${version} + + Library Cycle E + + diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/test/cycle/e/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/test/cycle/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml new file mode 100644 index 00000000000..d20d4477a28 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.cycle.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json new file mode 100644 index 00000000000..5b7d5e167ac --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.a", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c", + "dependencies": { + "@ui5-internal/module.b": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json new file mode 100644 index 00000000000..ab0bc9b7d03 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.b", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies", + "dependencies": { + "@ui5-internal/module.c": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json new file mode 100644 index 00000000000..630afc451bc --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.c", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies- cycle via module.a", + "dependencies": { + "@ui5-internal/module.a": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml new file mode 100644 index 00000000000..f79d97826f9 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.c diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json new file mode 100644 index 00000000000..35546680df3 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.d", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle with module.e", + "dependencies": { + "@ui5-internal/module.e": "file:../module.e" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml new file mode 100644 index 00000000000..c65780ec58d --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.d diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json new file mode 100644 index 00000000000..72763a7dcad --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.e", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle with module.d", + "devDependencies": { + "@ui5-internal/module.d": "file:../module.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml new file mode 100644 index 00000000000..e6487041ace --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.e diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json new file mode 100644 index 00000000000..dc4677fa6b1 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.f", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c", + "dependencies": { + "@ui5-internal/module.a": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml new file mode 100644 index 00000000000..5834bf479ab --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.f diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc new file mode 100644 index 00000000000..32165d31db9 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc @@ -0,0 +1 @@ +registry=http://127.0.0.1:99999/ \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json new file mode 100644 index 00000000000..71d0fd3468e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.g", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c", + "dependencies": { + "@ui5-internal/module.a": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml new file mode 100644 index 00000000000..dfadd749b12 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.g diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json new file mode 100644 index 00000000000..82ab89eae2e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json @@ -0,0 +1,12 @@ +{ + "name": "@ui5-internal/module.h", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es6-symbol'", + "dependencies": { + "@ui5-internal/module.i": "^1.0.0", + "@ui5-internal/module.j": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml new file mode 100644 index 00000000000..d80eec70ea4 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.h diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json new file mode 100644 index 00000000000..62588d1f5f9 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.i", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es5-ext'", + "dependencies": { + "@ui5-internal/module.k": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml new file mode 100644 index 00000000000..d2872d2544a --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.i diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json new file mode 100644 index 00000000000..cbf6388cb19 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.j", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'd'", + "dependencies": { + "@ui5-internal/module.i": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml new file mode 100644 index 00000000000..e9cb9133d6f --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.j diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json new file mode 100644 index 00000000000..e930465f54c --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ui5-internal/module.k", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es6-iterator'", + "dependencies": { + "@ui5-internal/module.h": "^1.0.0", + "@ui5-internal/module.i": "^1.0.0", + "@ui5-internal/module.j": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml new file mode 100644 index 00000000000..6c763884743 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.k diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json new file mode 100644 index 00000000000..29c6723878c --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.l", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies", + "devDependencies": { + "@ui5-internal/module.m": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml new file mode 100644 index 00000000000..9c50648e65e --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.l diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc new file mode 100644 index 00000000000..79a2e805cae --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:99999/ +ignore-scripts=true +offline=true \ No newline at end of file diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json new file mode 100644 index 00000000000..017b5e13cf0 --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ui5-internal/module.m", + "version": "1.0.0", + "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies", + "dependencies": { + "@ui5-internal/module.l": "^1.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml new file mode 100644 index 00000000000..5f02be6185b --- /dev/null +++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.m diff --git a/packages/project/test/fixtures/err.application.a/package.json b/packages/project/test/fixtures/err.application.a/package.json new file mode 100644 index 00000000000..17af8f18fe5 --- /dev/null +++ b/packages/project/test/fixtures/err.application.a/package.json @@ -0,0 +1,12 @@ +{ + "name": "err.application.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based application - test for missing dependencies", + "main": "index.html", + "dependencies": { + "library.xx": "file:../not.existing" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/err.application.a/ui5.yaml b/packages/project/test/fixtures/err.application.a/ui5.yaml new file mode 100644 index 00000000000..00090cd96ea --- /dev/null +++ b/packages/project/test/fixtures/err.application.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: err.app.a diff --git a/packages/project/test/fixtures/err.application.a/webapp/index.html b/packages/project/test/fixtures/err.application.a/webapp/index.html new file mode 100644 index 00000000000..d86c19d3d0c --- /dev/null +++ b/packages/project/test/fixtures/err.application.a/webapp/index.html @@ -0,0 +1,9 @@ + + + + Error Application A + + + + + diff --git a/packages/project/test/fixtures/err.application.a/webapp/manifest.json b/packages/project/test/fixtures/err.application.a/webapp/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/err.application.a/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/err.application.a/webapp/test.js b/packages/project/test/fixtures/err.application.a/webapp/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/err.application.a/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js b/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js new file mode 100644 index 00000000000..c7a5c0758dd --- /dev/null +++ b/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js @@ -0,0 +1,2 @@ +export default () => "extension module"; +export function determineRequiredDependencies () { return "required dependencies function" }; diff --git a/packages/project/test/fixtures/extension.a.esm/package.json b/packages/project/test/fixtures/extension.a.esm/package.json new file mode 100644 index 00000000000..872a636590d --- /dev/null +++ b/packages/project/test/fixtures/extension.a.esm/package.json @@ -0,0 +1,4 @@ +{ + "name": "extension.a", + "type": "module" +} diff --git a/packages/project/test/fixtures/extension.a/lib/extensionModule.js b/packages/project/test/fixtures/extension.a/lib/extensionModule.js new file mode 100644 index 00000000000..44b74979801 --- /dev/null +++ b/packages/project/test/fixtures/extension.a/lib/extensionModule.js @@ -0,0 +1,2 @@ +module.exports = () => "extension module"; +module.exports.determineRequiredDependencies = () => "required dependencies function"; diff --git a/packages/project/test/fixtures/extension.a/package.json b/packages/project/test/fixtures/extension.a/package.json new file mode 100644 index 00000000000..c3f37004f9f --- /dev/null +++ b/packages/project/test/fixtures/extension.a/package.json @@ -0,0 +1,4 @@ +{ + "name": "extension.a", + "version": "1.0.0" +} diff --git a/packages/project/test/fixtures/extension.a/ui5.yaml b/packages/project/test/fixtures/extension.a/ui5.yaml new file mode 100644 index 00000000000..1ae473d86b2 --- /dev/null +++ b/packages/project/test/fixtures/extension.a/ui5.yaml @@ -0,0 +1,8 @@ +--- +specVersion: "2.0" +kind: extension +type: task +metadata: + name: extension.a-ui5-yaml +task: + path: lib/extensionModule.js diff --git a/packages/project/test/fixtures/fsInterface/foo.txt b/packages/project/test/fixtures/fsInterface/foo.txt new file mode 100644 index 00000000000..538451fc5e6 --- /dev/null +++ b/packages/project/test/fixtures/fsInterface/foo.txt @@ -0,0 +1 @@ +content of /foo.txt \ No newline at end of file diff --git a/packages/project/test/fixtures/glob/application.a/package.json b/packages/project/test/fixtures/glob/application.a/package.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.a/ui5.yaml b/packages/project/test/fixtures/glob/application.a/ui5.yaml new file mode 100644 index 00000000000..b9dde7b16b2 --- /dev/null +++ b/packages/project/test/fixtures/glob/application.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a diff --git a/packages/project/test/fixtures/glob/application.a/webapp/index.html b/packages/project/test/fixtures/glob/application.a/webapp/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.a/webapp/test.js b/packages/project/test/fixtures/glob/application.a/webapp/test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/package.json b/packages/project/test/fixtures/glob/application.b/package.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/ui5.yaml b/packages/project/test/fixtures/glob/application.b/ui5.yaml new file mode 100644 index 00000000000..7b5e5dd2359 --- /dev/null +++ b/packages/project/test/fixtures/glob/application.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.b diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/manifest.json b/packages/project/test/fixtures/glob/application.b/webapp/embedded/manifest.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/l10n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/l10n.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/application.b/webapp/manifest.json b/packages/project/test/fixtures/glob/application.b/webapp/manifest.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/glob/package.json b/packages/project/test/fixtures/glob/package.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-application/package.json b/packages/project/test/fixtures/init-application/package.json new file mode 100644 index 00000000000..63696d06cbd --- /dev/null +++ b/packages/project/test/fixtures/init-application/package.json @@ -0,0 +1,12 @@ +{ + "name": "init-application", + "version": "1.0.0", + "private": true, + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "UNLICENSED" +} diff --git a/packages/project/test/fixtures/init-application/webapp/.gitkeep b/packages/project/test/fixtures/init-application/webapp/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-invalid-no-package-json/webapp/.gitkeep b/packages/project/test/fixtures/init-invalid-no-package-json/webapp/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/package.json b/packages/project/test/fixtures/init-invalid-webapp-src/package.json new file mode 100644 index 00000000000..a9eff2a7949 --- /dev/null +++ b/packages/project/test/fixtures/init-invalid-webapp-src/package.json @@ -0,0 +1,12 @@ +{ + "name": "init-invalid", + "version": "1.0.0", + "private": true, + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "UNLICENSED" +} diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/src/.gitkeep b/packages/project/test/fixtures/init-invalid-webapp-src/src/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/webapp/.gitkeep b/packages/project/test/fixtures/init-invalid-webapp-src/webapp/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-library/package.json b/packages/project/test/fixtures/init-library/package.json new file mode 100644 index 00000000000..5037d8cbea4 --- /dev/null +++ b/packages/project/test/fixtures/init-library/package.json @@ -0,0 +1,12 @@ +{ + "name": "init-library", + "version": "1.0.0", + "private": true, + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "UNLICENSED" +} diff --git a/packages/project/test/fixtures/init-library/src/.gitkeep b/packages/project/test/fixtures/init-library/src/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/init-library/test/.gitkeep b/packages/project/test/fixtures/init-library/test/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/lbt/modules/bundle.js b/packages/project/test/fixtures/lbt/modules/bundle.js new file mode 100644 index 00000000000..2ee4601c9a3 --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/bundle.js @@ -0,0 +1,12 @@ +sap.ui.predefine("sap/m/CheckBox",["sap/ui/core/Control"],function(o){"use strict";var n=o.extend("sap.m.CheckBox");return n}); +sap.ui.predefine("sap/ui/core/Core",[],function(){"use strict";return {}}); +sap.ui.predefine("todo/Component",["sap/ui/core/UIComponent"],function(e){"use strict";return e.extend("todo.Component",{metadata:{manifest:"json"}})}); +sap.ui.predefine("todo/controller/App.controller",["sap/ui/core/mvc/Controller"],function(e){"use strict";return e.extend("todo.controller.App")}); +jQuery.sap.registerPreloadedModules({ +"version":"2.0", +"modules":{ + "sap/m/messagebundle.properties":'#This is the resource bundle for the SAPUI5 sar\r\n', + "todo/manifest.json":'{"sap.app":{"id":"todo","type":"application"},"sap.ui5":{"rootView":{"viewName":"todo.view.App","type":"XML","id":"app"},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","settings":{"bundleName":"todo.i18n.messageBundle"}},"":{"type":"sap.ui.model.json.JSONModel","settings":"/model/todoitems.json"}},"resources":{"css":[{"uri":"css/styles.css"}]}}}', + "todo/model/todoitems.json":'{"newTodo":"","todos":[{"title":"Start this app","completed":true},{"title":"Learn OpenUI5","completed":false}],"itemsRemovable":true,"someCompleted":true,"completedCount":1}', + "todo/view/App.view.xml":'
\n' +}}); diff --git a/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js b/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js new file mode 100644 index 00000000000..661eb022477 --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js @@ -0,0 +1,43 @@ +// uses a function expr + additional arguments to create a scope +// Note that this scope style is old-fashioned and results in a JSLint warning! +(function($1, window) { + + //declares module sap.ui.testmodule + jQuery.sap.declare("sap.ui.testmodule"); + + // top level statements in the scope + jQuery.sap.require("top.require.void"); + var x = jQuery.sap.require("top.require.var"); + x = jQuery.sap.require("top.require.assign"); + var xs = sap.ui.requireSync("top/requireSync/var"); + xs = sap.ui.requireSync("top/requireSync/assign"); + + // a block with require statements + { + jQuery.sap.require("block.require.void"); + var z = jQuery.sap.require("block.require.var"); + z = jQuery.sap.require("block.require.assign"); + var zs = sap.ui.requireSync("block/requireSync/var"); + zs = sap.ui.requireSync("block/requireSync/assign"); + } + + // a nested function invocation with require statements + (function() { + jQuery.sap.require("nested.scope.require.void"); + var z = jQuery.sap.require("nested.scope.require.var"); + z = jQuery.sap.require("nested.scope.require.assign"); + var zs = sap.ui.requireSync("nested/scope/requireSync/var"); + zs = sap.ui.requireSync("nested/scope/requireSync/assign"); + }()); + + //a nested function expression with require statements + (function() { + jQuery.sap.require("nested.scope2.require.void"); + var z = jQuery.sap.require("nested.scope2.require.var"); + z = jQuery.sap.require("nested.scope2.require.assign"); + var zs = sap.ui.requireSync("nested/scope2/requireSync/var"); + zs = sap.ui.requireSync("nested/scope2/requireSync/assign"); + })(); + +})(jQuery, this); + diff --git a/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js b/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js new file mode 100644 index 00000000000..a1ba6cbba41 --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js @@ -0,0 +1,41 @@ +//uses a function invocation expr to create a scope +(function($2, window){ + + //declares module sap.ui.testmodule + jQuery.sap.declare("sap.ui.testmodule"); + + // top level statements in the scope + jQuery.sap.require("top.require.void"); + var x = jQuery.sap.require("top.require.var"); + x = jQuery.sap.require("top.require.assign"); + var xs = sap.ui.requireSync("top/requireSync/var"); + xs = sap.ui.requireSync("top/requireSync/assign"); + + // a block with require statements + { + jQuery.sap.require("block.require.void"); + var z = jQuery.sap.require("block.require.var"); + z = jQuery.sap.require("block.require.assign"); + var zs = sap.ui.requireSync("block/requireSync/var"); + zs = sap.ui.requireSync("block/requireSync/assign"); + } + + // a nested function invocation with require statements + (function() { + jQuery.sap.require("nested.scope.require.void"); + var z = jQuery.sap.require("nested.scope.require.var"); + z = jQuery.sap.require("nested.scope.require.assign"); + var zs = sap.ui.requireSync("nested/scope/requireSync/var"); + zs = sap.ui.requireSync("nested/scope/requireSync/assign"); + }()); + + //a nested function expression with require statements + (function() { + jQuery.sap.require("nested.scope2.require.void"); + var z = jQuery.sap.require("nested.scope2.require.var"); + z = jQuery.sap.require("nested.scope2.require.assign"); + var zs = sap.ui.requireSync("nested/scope2/requireSync/var"); + zs = sap.ui.requireSync("nested/scope2/requireSync/assign"); + })(); + +}(jQuery, this)); diff --git a/packages/project/test/fixtures/lbt/modules/declare_toplevel.js b/packages/project/test/fixtures/lbt/modules/declare_toplevel.js new file mode 100644 index 00000000000..f849f88de0d --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/declare_toplevel.js @@ -0,0 +1,36 @@ +//declares module sap.ui.testmodule +jQuery.sap.declare("sap.ui.testmodule"); + +// top level statements in the scope +jQuery.sap.require("top.require.void"); +var x = jQuery.sap.require("top.require.var"); +x = jQuery.sap.require("top.require.assign"); +var xs = sap.ui.requireSync("top/requireSync/var"); +xs = sap.ui.requireSync("top/requireSync/assign"); + +// a block with require statements +{ + jQuery.sap.require("block.require.void"); + var z = jQuery.sap.require("block.require.var"); + z = jQuery.sap.require("block.require.assign"); + var zs = sap.ui.requireSync("block/requireSync/var"); + zs = sap.ui.requireSync("block/requireSync/assign"); +} + +// a nested function invocation with require statements +(function() { + jQuery.sap.require("nested.scope.require.void"); + var z = jQuery.sap.require("nested.scope.require.var"); + z = jQuery.sap.require("nested.scope.require.assign"); + var zs = sap.ui.requireSync("nested/scope/requireSync/var"); + zs = sap.ui.requireSync("nested/scope/requireSync/assign"); +}()); + +//a nested function expression with require statements +(function() { + jQuery.sap.require("nested.scope2.require.void"); + var z = jQuery.sap.require("nested.scope2.require.var"); + z = jQuery.sap.require("nested.scope2.require.assign"); + var zs = sap.ui.requireSync("nested/scope2/requireSync/var"); + zs = sap.ui.requireSync("nested/scope2/requireSync/assign"); +})(); diff --git a/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js b/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js new file mode 100644 index 00000000000..4f3f19cbcce --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js @@ -0,0 +1,4 @@ +//declares module sap.ui.testmodule +sap.ui.define("sap/ui/testmodule", ["define/arg1","define/arg2"], function(Strings,Dom,Something) { + +}); diff --git a/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js b/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js new file mode 100644 index 00000000000..245c0697a0c --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js @@ -0,0 +1,4 @@ +//declares module sap.ui.testmodule +sap.ui.define(["define/arg1","define/arg2"], function(Strings,Dom,Something) { + +}); diff --git a/packages/project/test/fixtures/lbt/modules/not_a_module.js b/packages/project/test/fixtures/lbt/modules/not_a_module.js new file mode 100644 index 00000000000..786cc378cb2 --- /dev/null +++ b/packages/project/test/fixtures/lbt/modules/not_a_module.js @@ -0,0 +1 @@ +// this file does not contain a UI5 module definition \ No newline at end of file diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json new file mode 100644 index 00000000000..d90b72ffa99 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json @@ -0,0 +1,10 @@ +{ + "name": "legacy.library.x", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml", + "devDependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library new file mode 100644 index 00000000000..af7a248dd3c --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library @@ -0,0 +1,11 @@ + + + + legacy.library.x + SAP SE + ${copyright} + ${version} + + Legacy Library X + + diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/test/legacy/library/x/Test.html b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/test/legacy/library/x/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json new file mode 100644 index 00000000000..a46d2e8f452 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json @@ -0,0 +1,10 @@ +{ + "name": "legacy.library.y", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml", + "devDependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library new file mode 100644 index 00000000000..6c70bac8492 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library @@ -0,0 +1,11 @@ + + + + legacy.library.y + SAP SE + ${copyright} + ${version} + + Legacy Library Y + + diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/test/legacy/library/y/Test.html b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/test/legacy/library/y/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/legacy.library.a/package.json b/packages/project/test/fixtures/legacy.library.a/package.json new file mode 100644 index 00000000000..d88840fbbb0 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.a/package.json @@ -0,0 +1,10 @@ +{ + "name": "legacy.library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml", + "devDependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library new file mode 100644 index 00000000000..432c95e95db --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library @@ -0,0 +1,11 @@ + + + + legacy.library.a + SAP SE + ${copyright} + ${version} + + Legacy Library A + + diff --git a/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/legacy.library.a/test/legacy/library/a/Test.html b/packages/project/test/fixtures/legacy.library.a/test/legacy/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/legacy.library.b/package.json b/packages/project/test/fixtures/legacy.library.b/package.json new file mode 100644 index 00000000000..643d8019f7d --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.b/package.json @@ -0,0 +1,10 @@ +{ + "name": "legacy.library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml", + "devDependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library new file mode 100644 index 00000000000..0a8645fd7d1 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library @@ -0,0 +1,11 @@ + + + + legacy.library.b + SAP SE + ${copyright} + ${version} + + Legacy Library B + + diff --git a/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/legacy.library.b/test/legacy/library/b/Test.html b/packages/project/test/fixtures/legacy.library.b/test/legacy/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/.library b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js new file mode 100644 index 00000000000..719155d1e6d --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); diff --git a/packages/project/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library new file mode 100644 index 00000000000..c45172d48b8 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library @@ -0,0 +1,11 @@ + + + + library.f + SAP SE + ${copyright} + ${version} + + Library F + + diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json new file mode 100644 index 00000000000..fff39011db1 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.g", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "devDependencies": { + "library.f": "file:../library.f" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library new file mode 100644 index 00000000000..4d884278e90 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library @@ -0,0 +1,11 @@ + + + + library.g + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml new file mode 100644 index 00000000000..a20d2d4991c --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.g diff --git a/packages/project/test/fixtures/library.d-adtl-deps/package.json b/packages/project/test/fixtures/library.d-adtl-deps/package.json new file mode 100644 index 00000000000..10cd27bded1 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.d", + "version": "2.0.0", + "description": "Version of library.d that has additional dependencies defined. Used for testing UI5 Workspace resolutions", + "dependencies": { + "library.f": "file:../library.f" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml new file mode 100644 index 00000000000..9c8d48e1127 --- /dev/null +++ b/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml @@ -0,0 +1,9 @@ +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/library.d-depender/main/src/library/d/.library b/packages/project/test/fixtures/library.d-depender/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/library.d-depender/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-depender/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-depender/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/package.json b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/library.d-depender/package.json b/packages/project/test/fixtures/library.d-depender/package.json new file mode 100644 index 00000000000..9f88fc95f0c --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.d-depender", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d-depender/ui5.yaml b/packages/project/test/fixtures/library.d-depender/ui5.yaml new file mode 100644 index 00000000000..51744218833 --- /dev/null +++ b/packages/project/test/fixtures/library.d-depender/ui5.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d-depender +resources: + configuration: + paths: + src: main/src + test: main/test + diff --git a/packages/project/test/fixtures/library.d/main/src/library/d/.library b/packages/project/test/fixtures/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.d/package.json b/packages/project/test/fixtures/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.d/ui5.yaml b/packages/project/test/fixtures/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/library.e/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/library.e/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/library.e/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/library.e/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/library.e/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..7b731df83f6 --- /dev/null +++ b/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml @@ -0,0 +1,3 @@ +--- +name: library.d +type: library diff --git a/packages/project/test/fixtures/library.e/package.json b/packages/project/test/fixtures/library.e/package.json new file mode 100644 index 00000000000..9ce874ff55a --- /dev/null +++ b/packages/project/test/fixtures/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.e/src/library/e/.library b/packages/project/test/fixtures/library.e/src/library/e/.library new file mode 100644 index 00000000000..26ff954f7b1 --- /dev/null +++ b/packages/project/test/fixtures/library.e/src/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + ${copyright} + ${version} + + Library E + + diff --git a/packages/project/test/fixtures/library.e/src/library/e/some.js b/packages/project/test/fixtures/library.e/src/library/e/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.e/src/library/e/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.e/test/library/e/Test.html b/packages/project/test/fixtures/library.e/test/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.e/ui5-workspace.yaml b/packages/project/test/fixtures/library.e/ui5-workspace.yaml new file mode 100644 index 00000000000..7e8fd36b8f1 --- /dev/null +++ b/packages/project/test/fixtures/library.e/ui5-workspace.yaml @@ -0,0 +1,13 @@ +specVersion: workspace/1.0 +metadata: + name: config-a +dependencyManagement: + resolutions: + - path: ../library.d +--- +specVersion: workspace/1.0 +metadata: + name: config-b +dependencyManagement: + resolutions: + - path: ../library.x diff --git a/packages/project/test/fixtures/library.e/ui5.yaml b/packages/project/test/fixtures/library.e/ui5.yaml new file mode 100644 index 00000000000..88ba07e82dd --- /dev/null +++ b/packages/project/test/fixtures/library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/fixtures/library.f/node_modules/library.g/package.json b/packages/project/test/fixtures/library.f/node_modules/library.g/package.json new file mode 100644 index 00000000000..fff39011db1 --- /dev/null +++ b/packages/project/test/fixtures/library.f/node_modules/library.g/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.g", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "devDependencies": { + "library.f": "file:../library.f" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml b/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml new file mode 100644 index 00000000000..a20d2d4991c --- /dev/null +++ b/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.g diff --git a/packages/project/test/fixtures/library.f/package.json b/packages/project/test/fixtures/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.f/src/library/f/.library b/packages/project/test/fixtures/library.f/src/library/f/.library new file mode 100644 index 00000000000..c45172d48b8 --- /dev/null +++ b/packages/project/test/fixtures/library.f/src/library/f/.library @@ -0,0 +1,11 @@ + + + + library.f + SAP SE + ${copyright} + ${version} + + Library F + + diff --git a/packages/project/test/fixtures/library.f/src/library/f/some.js b/packages/project/test/fixtures/library.f/src/library/f/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.f/src/library/f/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.f/ui5.yaml b/packages/project/test/fixtures/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/library.g/node_modules/library.f/package.json b/packages/project/test/fixtures/library.g/node_modules/library.f/package.json new file mode 100644 index 00000000000..d8f009d42d7 --- /dev/null +++ b/packages/project/test/fixtures/library.g/node_modules/library.f/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.f", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "dependencies": { + "library.g": "file:../library.g" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml new file mode 100644 index 00000000000..52c17922bfd --- /dev/null +++ b/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.f diff --git a/packages/project/test/fixtures/library.g/package.json b/packages/project/test/fixtures/library.g/package.json new file mode 100644 index 00000000000..fff39011db1 --- /dev/null +++ b/packages/project/test/fixtures/library.g/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.g", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for circular dependencies", + "devDependencies": { + "library.f": "file:../library.f" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/library.g/src/library/g/.library b/packages/project/test/fixtures/library.g/src/library/g/.library new file mode 100644 index 00000000000..4d884278e90 --- /dev/null +++ b/packages/project/test/fixtures/library.g/src/library/g/.library @@ -0,0 +1,11 @@ + + + + library.g + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/packages/project/test/fixtures/library.g/src/library/g/some.js b/packages/project/test/fixtures/library.g/src/library/g/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.g/src/library/g/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.g/ui5.yaml b/packages/project/test/fixtures/library.g/ui5.yaml new file mode 100644 index 00000000000..a20d2d4991c --- /dev/null +++ b/packages/project/test/fixtures/library.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.g diff --git a/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml new file mode 100644 index 00000000000..ecce9d7e78b --- /dev/null +++ b/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml @@ -0,0 +1 @@ +|-\nfoo\nbar diff --git a/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml new file mode 100644 index 00000000000..177dea5f67c --- /dev/null +++ b/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml @@ -0,0 +1,15 @@ +specVersion: workspace/1.0 +metadata: + name: library-d +dependencyManagement: + resolutions: + - path: ../library.d +--- +specVersion: workspace/1.0 +metadata: + name: all-libraries +dependencyManagement: + resolutions: + - path: ../library.d + - path: ../library.e + - path: ../library.f diff --git a/packages/project/test/fixtures/library.h/empty-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/empty-ui5-workspace.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml new file mode 100644 index 00000000000..ca4ad00f4d3 --- /dev/null +++ b/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml @@ -0,0 +1,6 @@ +specVersion: wörkspace/1.0 +metadata: + name: default +dependencyManagement: + resolutions: + - path: ../library.d diff --git a/packages/project/test/fixtures/library.h/src/.library b/packages/project/test/fixtures/library.h/src/.library new file mode 100644 index 00000000000..8de6bd2ebba --- /dev/null +++ b/packages/project/test/fixtures/library.h/src/.library @@ -0,0 +1,11 @@ + + + + library.h + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/packages/project/test/fixtures/library.h/src/manifest.json b/packages/project/test/fixtures/library.h/src/manifest.json new file mode 100644 index 00000000000..2279cb6ce3d --- /dev/null +++ b/packages/project/test/fixtures/library.h/src/manifest.json @@ -0,0 +1,26 @@ +{ + "_version": "1.21.0", + "sap.app": { + "id": "library.h", + "type": "library", + "embeds": [], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "Library H", + "description": "Library H" + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.0", + "libs": {} + }, + "library": { + "i18n": false + } + } +} diff --git a/packages/project/test/fixtures/library.h/src/some.js b/packages/project/test/fixtures/library.h/src/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/library.h/src/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/library.h/ui5-workspace.yaml b/packages/project/test/fixtures/library.h/ui5-workspace.yaml new file mode 100644 index 00000000000..ac0ea1c35ba --- /dev/null +++ b/packages/project/test/fixtures/library.h/ui5-workspace.yaml @@ -0,0 +1,15 @@ +specVersion: workspace/1.0 +metadata: + name: default +dependencyManagement: + resolutions: + - path: ../library.d +--- +specVersion: workspace/1.0 +metadata: + name: all-libraries +dependencyManagement: + resolutions: + - path: ../library.d + - path: ../library.e + - path: ../library.f diff --git a/packages/project/test/fixtures/library.h/ui5.yaml b/packages/project/test/fixtures/library.h/ui5.yaml new file mode 100644 index 00000000000..cbea83db544 --- /dev/null +++ b/packages/project/test/fixtures/library.h/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.6" +type: library +metadata: + name: library.h diff --git a/packages/project/test/fixtures/module.a/dev/devTools.js b/packages/project/test/fixtures/module.a/dev/devTools.js new file mode 100644 index 00000000000..e035bfaeab6 --- /dev/null +++ b/packages/project/test/fixtures/module.a/dev/devTools.js @@ -0,0 +1 @@ +console.log("dev dev dev"); diff --git a/packages/project/test/fixtures/module.a/dist/index.js b/packages/project/test/fixtures/module.a/dist/index.js new file mode 100644 index 00000000000..019c0f4bc8e --- /dev/null +++ b/packages/project/test/fixtures/module.a/dist/index.js @@ -0,0 +1 @@ +console.log("Hello World!"); diff --git a/packages/project/test/fixtures/module.a/ui5.yaml b/packages/project/test/fixtures/module.a/ui5.yaml new file mode 100644 index 00000000000..af957cf1ee8 --- /dev/null +++ b/packages/project/test/fixtures/module.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.6" +type: module +metadata: + name: module.a diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme new file mode 100644 index 00000000000..4c62f26114c --- /dev/null +++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme @@ -0,0 +1,9 @@ + + + + my_theme + me + ${copyright} + ${version} + + \ No newline at end of file diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming new file mode 100644 index 00000000000..83b6c785a87 --- /dev/null +++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming @@ -0,0 +1,27 @@ +{ + "sEntity": "Theme", + "sId": "sap_belize", + "oExtends": "base", + "sVendor": "SAP", + "aBundled": ["sap_belize_plus"], + "mCssScopes": { + "library": { + "sBaseFile": "library", + "sEmbeddingMethod": "APPEND", + "aScopes": [ + { + "sLabel": "Contrast", + "sSelector": "sapContrast", + "sEmbeddedFile": "sap_belize_plus.library", + "sEmbeddedCompareFile": "library", + "sThemeIdSuffix": "Contrast", + "sThemability": "PUBLIC", + "aThemabilityFilter": [ + "Color" + ], + "rExcludeSelector": "\\.sapContrastPlus\\W" + } + ] + } + } +} diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less new file mode 100644 index 00000000000..d3286002bfe --- /dev/null +++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less @@ -0,0 +1,9 @@ +/*! + * ${copyright} + */ + +@mycolor: blue; + +.sapUiBody { + background-color: @mycolor; +} diff --git a/packages/project/test/fixtures/theme.library.e/test/theme/library/e/Test.html b/packages/project/test/fixtures/theme.library.e/test/theme/library/e/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/theme.library.e/ui5.yaml b/packages/project/test/fixtures/theme.library.e/ui5.yaml new file mode 100644 index 00000000000..cf89c2432e8 --- /dev/null +++ b/packages/project/test/fixtures/theme.library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "1.1" +type: theme-library +metadata: + name: theme.library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/packages/project/test/lib/build/ProjectBuilder.js b/packages/project/test/lib/build/ProjectBuilder.js new file mode 100644 index 00000000000..64b36ab85e9 --- /dev/null +++ b/packages/project/test/lib/build/ProjectBuilder.js @@ -0,0 +1,812 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import path from "node:path"; +import esmock from "esmock"; +import {setLogLevel} from "@ui5/logger"; +import OutputStyleEnum from "../../../lib/build/helpers/ProjectBuilderOutputStyle.js"; + +function noop() {} + +function getMockProject(type, id = "b") { + return { + getName: () => "project." + id, + getNamespace: () => "project/" + id, + getType: () => type, + getCopyright: noop, + getVersion: noop, + getReader: () => "reader", + getWorkspace: () => "workspace", + }; +} + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + t.context.getRootNameStub = sinon.stub().returns("project.a"); + t.context.getRootTypeStub = sinon.stub().returns("application"); + t.context.taskRepository = "taskRepository"; + t.context.graph = { + getRoot: () => { + return { + getName: t.context.getRootNameStub, + getType: t.context.getRootTypeStub, + }; + }, + isSealed: sinon.stub().returns(true), + getProjectNames: sinon.stub().returns([ + "project.a", + "project.b", + "project.c", + ]), + getSize: sinon.stub().returns(3), + getDependencies: sinon.stub().returns([]).withArgs("project.a").returns(["project.b"]), + traverseBreadthFirst: async (start, callback) => { + if (callback) { + await callback({ + project: getMockProject("library", "c") + }); + return; + } + await start({ + project: getMockProject("library", "a") + }); + await start({ + project: getMockProject("library", "c") + }); + await start({ + project: getMockProject("library", "b") + }); + }, + traverseDepthFirst: async (callback) => { + await callback({ + project: getMockProject("library", "a") + }); + await callback({ + project: getMockProject("library", "b") + }); + await callback({ + project: getMockProject("library", "c") + }); + }, + getProject: sinon.stub().callsFake((projectName) => { + return getMockProject(...projectName.split(".")); + }) + }; + + t.context.ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js"); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Missing graph parameters", (t) => { + const {ProjectBuilder} = t.context; + const err1 = t.throws(() => { + new ProjectBuilder({}); + }); + t.is(err1.message, "Missing parameter 'graph'", + "Threw with expected error message"); + + const err2 = t.throws(() => { + new ProjectBuilder({graph: "graph"}); + }); + t.is(err2.message, "Missing parameter 'taskRepository'", + "Threw with expected error message"); +}); + +test("build", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const filterProjectStub = sinon.stub().returns(true); + const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + + const requiresBuildStub = sinon.stub().returns(true); + const runTasksStub = sinon.stub().resolves(); + const projectBuildContextMock = { + getTaskRunner: () => { + return { + runTasks: runTasksStub, + }; + }, + requiresBuild: requiresBuildStub, + getProject: sinon.stub().returns(getMockProject("library")) + }; + const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts") + .resolves(new Map().set("project.a", projectBuildContextMock)); + + const registerCleanupSigHooksStub = sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); + + const writeResultsStub = sinon.stub(builder, "_writeResults").resolves(); + const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks"); + const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); + + await builder.build({ + destPath: "dest/path", + includedDependencies: ["dep a"], + excludedDependencies: ["dep b"] + }); + + t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once"); + t.deepEqual(getProjectFilterStub.getCall(0).args[0], { + explicitIncludes: ["dep a"], + explicitExcludes: ["dep b"], + dependencyIncludes: undefined + }, "_getProjectFilter got called with correct arguments"); + + t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once"); + t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [ + "project.a", "project.b", "project.c" + ], "_createRequiredBuildContexts got called with correct arguments"); + + t.is(requiresBuildStub.callCount, 1, "ProjectBuildContext#requiresBuild got called once"); + t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once"); + + t.is(runTasksStub.callCount, 1, "TaskRunner#runTasks got called once"); + + t.is(writeResultsStub.callCount, 1, "_writeResults got called once"); + t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMock, + "_writeResults got called with correct first argument"); + t.is(writeResultsStub.getCall(0).args[1]._fsBasePath, path.resolve("dest/path") + path.sep, + "_writeResults got called with correct second argument"); + + t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once"); + t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks", + "_deregisterCleanupSigHooks got called with correct arguments"); + t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once"); +}); + +test("build: Missing dest parameter", async (t) => { + const {graph, taskRepository, ProjectBuilder} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const err = await t.throwsAsync(builder.build({ + destPath: "dest/path", + dependencyIncludes: "dependencyIncludes", + includedDependencies: ["dep a"], + excludedDependencies: ["dep b"] + })); + + t.is(err.message, + "Parameter 'dependencyIncludes' can't be used in conjunction " + + "with parameters 'includedDependencies' or 'excludedDependencies", + "Threw with expected error message"); +}); + +test("build: Too many dependency parameters", async (t) => { + const {graph, taskRepository, ProjectBuilder} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const err = await t.throwsAsync(builder.build({ + includedDependencies: ["dep a"], + excludedDependencies: ["dep b"] + })); + + t.is(err.message, "Missing parameter 'destPath'", "Threw with expected error message"); +}); + +test("build: createBuildManifest in conjunction with dependencies", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + t.context.getRootTypeStub = sinon.stub().returns("library"); + const builder = new ProjectBuilder({graph, taskRepository, + buildConfig: { + createBuildManifest: true + } + }); + + const filterProjectStub = sinon.stub().returns(true); + sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + const err = await t.throwsAsync(builder.build({ + destPath: "dest/path", + includedDependencies: ["dep a"] + })); + + t.is(err.message, + "It is currently not supported to request the creation of a build manifest while " + + "including any dependencies into the build result", + "Threw with expected error message"); +}); + +test("build: Failure", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const filterProjectStub = sinon.stub().returns(true); + sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + + const requiresBuildStub = sinon.stub().returns(true); + const runTasksStub = sinon.stub().rejects(new Error("Some Error")); + const projectBuildContextMock = { + requiresBuild: requiresBuildStub, + getTaskRunner: () => { + return { + runTasks: runTasksStub + }; + }, + getProject: sinon.stub().returns(getMockProject("library")) + }; + sinon.stub(builder, "_createRequiredBuildContexts") + .resolves(new Map().set("project.a", projectBuildContextMock)); + + sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); + const writeResultsStub = sinon.stub(builder, "_writeResults").resolves(); + const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks"); + const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); + + const err = await t.throwsAsync(builder.build({ + destPath: "dest/path", + includedDependencies: ["dep a"], + excludedDependencies: ["dep b"] + })); + + t.is(err.message, "Some Error", "Threw with expected error message"); + + t.is(writeResultsStub.callCount, 0, "_writeResults did not get called"); + + t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once"); + t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks", + "_deregisterCleanupSigHooks got called with correct arguments"); + t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once"); +}); + +test.serial("build: Multiple projects", async (t) => { + const {graph, taskRepository, sinon} = t.context; + + const buildLoggerMock = { + isLevelEnabled: sinon.stub(), + setProjects: sinon.stub(), + startProjectBuild: sinon.stub(), + endProjectBuild: sinon.stub(), + skipProjectBuild: sinon.stub(), + + info: sinon.stub(), + verbose: sinon.stub(), + error: sinon.stub(), + }; + // Function acts as constructor for our class mock + function CreateBuildLoggerMock(moduleName) { + t.is(moduleName, "ProjectBuilder", "BuildLogger created with expected moduleName"); + return buildLoggerMock; + } + const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", { + "@ui5/logger/internal/loggers/Build": CreateBuildLoggerMock + }); + + const builder = new ProjectBuilder({graph, taskRepository}); + + const filterProjectStub = sinon.stub().returns(true).onFirstCall().returns(false); + const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub); + + const requiresBuildAStub = sinon.stub().returns(true); + const requiresBuildBStub = sinon.stub().returns(false); + const requiresBuildCStub = sinon.stub().returns(true); + const getBuildMetadataStub = sinon.stub().returns({ + timestamp: "2022-07-28T12:00:00.000Z", + age: "xx days" + }); + const runTasksStub = sinon.stub().resolves(); + const projectBuildContextMockA = { + getTaskRunner: () => { + return { + runTasks: runTasksStub + }; + }, + requiresBuild: requiresBuildAStub, + getProject: sinon.stub().returns(getMockProject("library", "a")) + }; + const projectBuildContextMockB = { + getTaskRunner: () => { + return { + runTasks: runTasksStub + }; + }, + getBuildMetadata: getBuildMetadataStub, + requiresBuild: requiresBuildBStub, + getProject: sinon.stub().returns(getMockProject("library", "b")) + }; + const projectBuildContextMockC = { + getTaskRunner: () => { + return { + runTasks: runTasksStub + }; + }, + requiresBuild: requiresBuildCStub, + getProject: sinon.stub().returns(getMockProject("library", "c")) + }; + const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts") + .resolves(new Map() + .set("project.a", projectBuildContextMockA) + .set("project.b", projectBuildContextMockB) + .set("project.c", projectBuildContextMockC) + ); + + const registerCleanupSigHooksStub = sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); + const writeResultsStub = sinon.stub(builder, "_writeResults").resolves(); + const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks"); + const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves(); + + setLogLevel("verbose"); + await builder.build({ + destPath: path.join("dest", "path"), + dependencyIncludes: "dependencyIncludes" + }); + setLogLevel("info"); + + t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once"); + t.deepEqual(getProjectFilterStub.getCall(0).args[0], { + explicitIncludes: [], + explicitExcludes: [], + dependencyIncludes: "dependencyIncludes" + }, "_getProjectFilter got called with correct arguments"); + + t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once"); + t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [ + "project.b", "project.c" + ], "_createRequiredBuildContexts got called with correct arguments"); + + t.is(requiresBuildAStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.a"); + t.is(requiresBuildBStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.b"); + t.is(requiresBuildCStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.c"); + t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once"); + + t.is(runTasksStub.callCount, 2, "TaskRunner#runTasks got called twice"); // library.b does not require a build + + t.is(writeResultsStub.callCount, 2, "_writeResults got called twice"); // library.a has not been requested + t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMockB, + "_writeResults got called with correct first argument"); + t.is(writeResultsStub.getCall(0).args[1]._fsBasePath, path.resolve("dest/path") + path.sep, + "_writeResults got called with correct second argument"); + t.is(writeResultsStub.getCall(1).args[0], projectBuildContextMockC, + "_writeResults got called with correct first argument"); + t.is(writeResultsStub.getCall(1).args[1]._fsBasePath, path.resolve("dest/path") + path.sep, + "_writeResults got called with correct second argument"); + + t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once"); + t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks", + "_deregisterCleanupSigHooks got called with correct arguments"); + t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once"); + + t.is(buildLoggerMock.setProjects.callCount, 1, "BuildLogger#setProjects got called once"); + t.deepEqual(buildLoggerMock.setProjects.firstCall.firstArg, [ + "project.a", + "project.b", + "project.c", + ], "BuildLogger#setProjects got called with expected argument"); + t.is(buildLoggerMock.startProjectBuild.callCount, 2, + "BuildLogger#startProjectBuild got called twice"); + t.is(buildLoggerMock.startProjectBuild.getCall(0).firstArg, "project.a", + "BuildLogger#startProjectBuild got called with expected argument on first call"); + t.is(buildLoggerMock.startProjectBuild.getCall(1).firstArg, "project.c", + "BuildLogger#startProjectBuild got called with expected argument on second call"); + t.is(buildLoggerMock.endProjectBuild.callCount, 2, + "BuildLogger#endProjectBuild got called twice"); + t.is(buildLoggerMock.endProjectBuild.getCall(0).firstArg, "project.a", + "BuildLogger#endProjectBuild got called with expected argument on first call"); + t.is(buildLoggerMock.endProjectBuild.getCall(1).firstArg, "project.c", + "BuildLogger#endProjectBuild got called with expected argument on second call"); + t.is(buildLoggerMock.skipProjectBuild.callCount, 1, + "BuildLogger#skipProjectBuild got called once"); + t.is(buildLoggerMock.skipProjectBuild.getCall(0).firstArg, "project.b", + "BuildLogger#skipProjectBuild got called with expected argument"); +}); + +test("_createRequiredBuildContexts", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const requiresBuildStub = sinon.stub().returns(true); + const getRequiredDependenciesStub = sinon.stub() + .returns(new Set()) + .onFirstCall().returns(new Set(["project.b"])); // required dependency of project.a + + const projectBuildContextMock = { + requiresBuild: requiresBuildStub, + getTaskRunner: () => { + return { + getRequiredDependencies: getRequiredDependenciesStub + }; + } + }; + const createProjectContextStub = sinon.stub(builder._buildContext, "createProjectContext") + .returns(projectBuildContextMock); + const projectBuildContexts = await builder._createRequiredBuildContexts(["project.a", "project.c"]); + + t.is(requiresBuildStub.callCount, 3, "TaskRunner#requiresBuild got called three times"); + t.is(getRequiredDependenciesStub.callCount, 3, "TaskRunner#getRequiredDependencies got called three times"); + + t.deepEqual(Object.fromEntries(projectBuildContexts), { + "project.a": projectBuildContextMock, + "project.b": projectBuildContextMock, // is a required dependency of project.a + "project.c": projectBuildContextMock, + }, "Returned expected project build contexts"); + + t.is(createProjectContextStub.callCount, 3, "BuildContext#createProjectContextStub got called three times"); + t.is(createProjectContextStub.getCall(0).args[0].project.getName(), "project.a", + "First call to BuildContext#createProjectContextStub with expected project"); + t.is(createProjectContextStub.getCall(1).args[0].project.getName(), "project.c", + "Second call to BuildContext#createProjectContextStub with expected project"); + t.is(createProjectContextStub.getCall(2).args[0].project.getName(), "project.b", + "Third call to BuildContext#createProjectContextStub with expected project"); +}); + +test.serial("_getProjectFilter with dependencyIncludes", async (t) => { + const {graph, taskRepository, sinon} = t.context; + const composeProjectListStub = sinon.stub().returns({ + includedDependencies: ["project.b", "project.c"], + excludedDependencies: ["project.d", "project.e", "project.a"], + }); + const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", { + "../../../lib/build/helpers/composeProjectList.js": composeProjectListStub + }); + + const builder = new ProjectBuilder({graph, taskRepository}); + + const filterProject = await builder._getProjectFilter({ + dependencyIncludes: "dependencyIncludes", + explicitIncludes: "explicitIncludes", + explicitExcludes: "explicitExcludes", + }); + + t.is(composeProjectListStub.callCount, 1, "composeProjectList got called once"); + t.is(composeProjectListStub.getCall(0).args[0], graph, + "composeProjectList got called with correct graph argument"); + t.is(composeProjectListStub.getCall(0).args[1], "dependencyIncludes", + "composeProjectList got called with correct include/exclude argument"); + + t.true(filterProject("project.a"), "project.a (root project) is always allowed"); + t.true(filterProject("project.b"), "project.b is allowed"); + t.true(filterProject("project.c"), "project.c is allowed"); + t.false(filterProject("project.d"), "project.d is not allowed"); + t.false(filterProject("project.e"), "project.e is not allowed"); +}); + +test.serial("_getProjectFilter with explicit include/exclude", async (t) => { + const {graph, taskRepository, sinon} = t.context; + const composeProjectListStub = sinon.stub().returns({ + includedDependencies: ["project.b", "project.c"], + excludedDependencies: ["project.d", "project.e", "project.a"], + }); + const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", { + "../../../lib/build/helpers/composeProjectList.js": composeProjectListStub + }); + + const builder = new ProjectBuilder({graph, taskRepository}); + + const filterProject = await builder._getProjectFilter({ + explicitIncludes: "explicitIncludes", + explicitExcludes: "explicitExcludes", + }); + + t.is(composeProjectListStub.callCount, 1, "composeProjectList got called once"); + t.is(composeProjectListStub.getCall(0).args[0], graph, + "composeProjectList got called with correct graph argument"); + t.deepEqual(composeProjectListStub.getCall(0).args[1], { + includeDependencyTree: "explicitIncludes", + excludeDependencyTree: "explicitExcludes", + }, "composeProjectList got called with correct include/exclude argument"); + + t.true(filterProject("project.a"), "project.a (root project) is always allowed"); + t.true(filterProject("project.b"), "project.b is allowed"); + t.true(filterProject("project.c"), "project.c is allowed"); + t.false(filterProject("project.d"), "project.d is not allowed"); + t.false(filterProject("project.e"), "project.e is not allowed"); +}); + +test("_writeResults", async (t) => { + const {ProjectBuilder, sinon} = t.context; + t.context.getRootTypeStub = sinon.stub().returns("library"); + const {graph, taskRepository} = t.context; + const builder = new ProjectBuilder({ + graph, taskRepository, + buildConfig: { + createBuildManifest: false, + otherBuildConfig: "yes" + } + }); + + const mockResources = [{ + _resourceName: "resource.a", + getPath: () => "resource.a" + }, { + _resourceName: "resource.b", + getPath: () => "resource.b" + }, { + _resourceName: "resource.c", + getPath: () => "resource.c" + }]; + const byGlobStub = sinon.stub().resolves(mockResources); + const getReaderStub = sinon.stub().returns({ + byGlob: byGlobStub + }); + const mockProject = getMockProject("library", "c"); + mockProject.getReader = getReaderStub; + + const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true); + const projectBuildContextMock = { + getProject: () => mockProject, + getTaskUtil: () => { + return { + isRootProject: () => false, + getTag: getTagStub, + STANDARD_TAGS: { + OmitFromBuildResult: "OmitFromBuildResultTag" + } + }; + } + }; + const writerMock = { + write: sinon.stub().resolves() + }; + + await builder._writeResults(projectBuildContextMock, writerMock); + + t.is(getReaderStub.callCount, 1, "One reader requested"); + t.deepEqual(getReaderStub.getCall(0).args[0], { + style: "dist" + }, "Reader requested expected style"); + + t.is(byGlobStub.callCount, 1, "One byGlob call"); + t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern"); + + t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times"); + t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource"); + t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource"); + t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource"); + t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + + t.is(writerMock.write.callCount, 2, "Write got called twice"); + t.is(writerMock.write.getCall(0).args[0], mockResources[1], "Write got called with second resource"); + t.is(writerMock.write.getCall(1).args[0], mockResources[2], "Write got called with third resource"); +}); + +test.serial("_writeResults: Create build manifest", async (t) => { + const {sinon} = t.context; + t.context.getRootTypeStub = sinon.stub().returns("library"); + const {graph, taskRepository} = t.context; + + const createBuildManifestStub = sinon.stub().returns({"build": "manifest"}); + const createResourceStub = sinon.stub().returns("build manifest resource"); + const ProjectBuilder = await esmock.p("../../../lib/build/ProjectBuilder.js", { + "../../../lib/build/helpers/createBuildManifest.js": createBuildManifestStub, + "@ui5/fs/resourceFactory": { + createResource: createResourceStub + } + }); + + const builder = new ProjectBuilder({ + graph, taskRepository, + buildConfig: { + createBuildManifest: true, + otherBuildConfig: "yes" + } + }); + + const mockResources = [{ + _resourceName: "resource.a", + getPath: () => "resource.a" + }, { + _resourceName: "resource.b", + getPath: () => "resource.b" + }, { + _resourceName: "resource.c", + getPath: () => "resource.c" + }]; + const byGlobStub = sinon.stub().resolves(mockResources); + const getReaderStub = sinon.stub().returns({ + byGlob: byGlobStub + }); + const mockProject = getMockProject("library", "c"); + mockProject.getReader = getReaderStub; + + const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true); + const projectBuildContextMock = { + getProject: () => mockProject, + getTaskUtil: () => { + return { + isRootProject: () => true, + getTag: getTagStub, + STANDARD_TAGS: { + OmitFromBuildResult: "OmitFromBuildResultTag" + } + }; + } + }; + const writerMock = { + write: sinon.stub().resolves() + }; + + await builder._writeResults(projectBuildContextMock, writerMock); + + t.is(getReaderStub.callCount, 1, "One reader requested"); + t.deepEqual(getReaderStub.getCall(0).args[0], { + style: "buildtime" + }, "Reader requested expected style"); + + t.is(byGlobStub.callCount, 1, "One byGlob call"); + t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern"); + + t.is(createBuildManifestStub.callCount, 1, "createBuildManifest got called once"); + t.is(createBuildManifestStub.getCall(0).args[0], mockProject, + "createBuildManifest got called with correct project"); + t.deepEqual(createBuildManifestStub.getCall(0).args[1], { + createBuildManifest: true, + outputStyle: OutputStyleEnum.Default, + cssVariables: false, + excludedTasks: [], + includedTasks: [], + jsdoc: false, + selfContained: false, + }, "createBuildManifest got called with correct build configuration"); + + t.is(createResourceStub.callCount, 1, "One resource has been created"); + t.deepEqual(createResourceStub.getCall(0).args[0], { + path: "/.ui5/build-manifest.json", + string: `{ + "build": "manifest" +}` + }, "Build manifest resource has been created with correct arguments"); + + t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times"); + t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource"); + t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource"); + t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource"); + t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + + t.is(writerMock.write.callCount, 3, "Write got called three times"); + t.is(writerMock.write.getCall(0).args[0], "build manifest resource", "Write got called with build manifest"); + t.is(writerMock.write.getCall(1).args[0], mockResources[1], "Write got called with second resource"); + t.is(writerMock.write.getCall(2).args[0], mockResources[2], "Write got called with third resource"); + + esmock.purge(ProjectBuilder); +}); + +test.serial("_writeResults: Flat build output", async (t) => { + const {sinon, ProjectBuilder} = t.context; + t.context.getRootTypeStub = sinon.stub().returns("library"); + const {graph, taskRepository} = t.context; + + const builder = new ProjectBuilder({ + graph, taskRepository, + buildConfig: { + outputStyle: OutputStyleEnum.Flat, + otherBuildConfig: "yes" + } + }); + + const mockResources = [{ + _resourceName: "resource.a", + getPath: () => "resource.a" + }, { + _resourceName: "resource.b", + getPath: () => "resource.b" + }, { + _resourceName: "resource.c", + getPath: () => "resource.c" + }]; + const byGlobStub = sinon.stub().resolves(mockResources); + const getReaderStub = sinon.stub().returns({ + byGlob: byGlobStub + }); + const mockProject = getMockProject("library", "a"); + mockProject.getReader = getReaderStub; + + const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true); + const projectBuildContextMock = { + getProject: () => mockProject, + getTaskUtil: () => { + return { + isRootProject: () => true, + getTag: getTagStub, + STANDARD_TAGS: { + OmitFromBuildResult: "OmitFromBuildResultTag" + } + }; + } + }; + const writerMock = { + write: sinon.stub().resolves() + }; + + await builder._writeResults(projectBuildContextMock, writerMock); + + t.is(getReaderStub.callCount, 2, "One reader requested"); + t.deepEqual(getReaderStub.getCall(0).args[0], { + style: "flat" + }, "Reader requested expected style"); + + t.is(byGlobStub.callCount, 2, "One byGlob call"); + t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern"); + + t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times"); + t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource"); + t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource"); + t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource"); + t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value"); + + t.is(writerMock.write.callCount, 2, "Write got called twice"); + t.is(writerMock.write.getCall(0).args[0], mockResources[1], "Write got called with second resource"); + t.is(writerMock.write.getCall(1).args[0], mockResources[2], "Write got called with third resource"); +}); + + +test("_executeCleanupTasks", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + const builder = new ProjectBuilder({graph, taskRepository}); + + const executeCleanupTasksStub = sinon.stub(builder._buildContext, "executeCleanupTasks"); + await builder._executeCleanupTasks(); + t.is(executeCleanupTasksStub.callCount, 1, "BuildContext#executeCleanupTasks got called once"); + t.deepEqual(executeCleanupTasksStub.getCall(0).args, [undefined], + "BuildContext#executeCleanupTasks got called with correct arguments"); + + // reset stub + executeCleanupTasksStub.reset(); + // Call with enforcement flag + await builder._executeCleanupTasks(true); + t.is(executeCleanupTasksStub.callCount, 1, "BuildContext#executeCleanupTasks got called once"); + t.deepEqual(executeCleanupTasksStub.getCall(0).args, [true], + "BuildContext#executeCleanupTasks got called with correct arguments"); +}); + +test("instantiate new logger for every ProjectBuilder", async (t) => { + function CreateBuildLoggerMock(moduleName) { + t.is(moduleName, "ProjectBuilder", "BuildLogger created with expected moduleName"); + return {}; + } + + const {graph, taskRepository, sinon} = t.context; + const createBuildLoggerMockSpy = sinon.spy(CreateBuildLoggerMock); + const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", { + "@ui5/logger/internal/loggers/Build": createBuildLoggerMockSpy + }); + + new ProjectBuilder({graph, taskRepository}); + new ProjectBuilder({graph, taskRepository}); + + t.is(createBuildLoggerMockSpy.callCount, 2, "BuildLogger is instantiated for every ProjectBuilder instance"); +}); + + +function getProcessListenerCount() { + return ["SIGHUP", "SIGINT", "SIGTERM", "SIGBREAK"].map((eventName) => { + return process.listenerCount(eventName); + }); +} +test("_registerCleanupSigHooks/_deregisterCleanupSigHooks", (t) => { + const listenersBefore = getProcessListenerCount(); + + const {graph, taskRepository, ProjectBuilder} = t.context; + const builder = new ProjectBuilder({graph, taskRepository}); + + const signals = builder._registerCleanupSigHooks(); + + t.deepEqual(Object.keys(signals), ["SIGHUP", "SIGINT", "SIGTERM", "SIGBREAK"], + "Returned four signal listeners"); + + t.deepEqual(getProcessListenerCount(), listenersBefore.map((x) => x+1), + "For every signal one new listener got registered"); + + builder._deregisterCleanupSigHooks(signals); + + t.deepEqual(getProcessListenerCount(), listenersBefore, + "All signal listeners got de-registered"); +}); + +test("_getElapsedTime", (t) => { + const {graph, taskRepository, ProjectBuilder} = t.context; + const builder = new ProjectBuilder({graph, taskRepository}); + + const res = builder._getElapsedTime(process.hrtime()); + t.truthy(res, "Returned a value"); +}); diff --git a/packages/project/test/lib/build/TaskRunner.js b/packages/project/test/lib/build/TaskRunner.js new file mode 100644 index 00000000000..73c9e187648 --- /dev/null +++ b/packages/project/test/lib/build/TaskRunner.js @@ -0,0 +1,1580 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import {setLogLevel} from "@ui5/logger"; +setLogLevel("perf"); + +function noop() {} +function emptyarray() { + return []; +} + +const buildConfig = { + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] +}; + +function getMockProject(type) { + return { + getName: () => "project.b", + getNamespace: () => "project/b", + getType: () => type, + getPropertiesFileSourceEncoding: noop, + getCopyright: noop, + getVersion: noop, + getMinificationExcludes: emptyarray, + getSpecVersion: () => { + return { + gte: () => false + }; + }, + getComponentPreloadPaths: () => [ + "project/b/**/Component.js" + ], + getComponentPreloadNamespaces: emptyarray, + getComponentPreloadExcludes: emptyarray, + getLibraryPreloadExcludes: emptyarray, + getBundles: () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }], + sort: true + }, + bundleOptions: { + optimize: true, + usePredefinedCalls: true + } + }], + getCachebusterSignatureType: noop, + getCustomTasks: () => [], + hasBuildManifest: () => false, + getWorkspace: () => "workspace", + isFrameworkProject: () => false + }; +} + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.taskUtil = { + isRootProject: sinon.stub().returns(true), + getBuildOption: sinon.stub(), + getProject: sinon.stub(), + getDependencies: sinon.stub().returns(["dep.a", "dep.b"]), + getInterface: sinon.stub(), + }; + t.context.taskUtil.getInterface.returns(t.context.taskUtil); + + t.context.taskRepository = { + getTask: sinon.stub().callsFake(async (taskName) => { + throw new Error(`taskRepository: Unknown Task ${taskName}`); + }), + getAllTaskNames: sinon.stub().returns(["replaceVersion"]), + getRemovedTaskNames: sinon.stub().returns(["removedTask"]), + }; + + t.context.customTaskSpecVersionGteStub = sinon.stub().returns(true); + t.context.getRequiredDependenciesCallbackStub = sinon.stub().resolves(null); + t.context.customTask = { + getName: () => "custom task name", + getSpecVersion: () => { + return { + gte: t.context.customTaskSpecVersionGteStub + }; + }, + getRequiredDependenciesCallback: t.context.getRequiredDependenciesCallbackStub, + }; + + t.context.graph = { + getRoot: () => { + return { + getName: () => "graph-root" + }; + }, + getExtension: sinon.stub().returns(t.context.customTask), + traverseBreadthFirst: sinon.stub(), + getTransitiveDependencies: sinon.stub().returns(["dep.a", "dep.b", "dep.c"]) + }; + + t.context.logger = { + getLogger: sinon.stub().returns("group logger") + }; + + t.context.projectBuildLogger = { + setTasks: sinon.stub(), + startTask: sinon.stub(), + endTask: sinon.stub(), + verbose: sinon.stub(), + perf: sinon.stub(), + isLevelEnabled: sinon.stub().returns(true), + }; + + t.context.resourceFactory = { + createReaderCollection: sinon.stub() + .returns("reader collection") + }; + + t.context.TaskRunner = await esmock("../../../lib/build/TaskRunner.js", { + "@ui5/logger": t.context.logger, + "@ui5/fs/resourceFactory": t.context.resourceFactory + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Missing parameters", (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + t.throws(() => { + new TaskRunner({ + graph, + taskUtil, + taskRepository, + log: projectBuildLogger, + buildConfig + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing project parameter"); + t.throws(() => { + new TaskRunner({ + project: getMockProject("application"), + taskUtil, + taskRepository, + log: projectBuildLogger, + buildConfig + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing graph parameter"); + t.throws(() => { + new TaskRunner({ + project: getMockProject("application"), + graph, + taskRepository, + log: projectBuildLogger, + buildConfig + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing taskUtil parameter"); + t.throws(() => { + new TaskRunner({ + project: getMockProject("application"), + graph, + taskUtil, + log: projectBuildLogger, + buildConfig + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing taskRepository parameter"); + t.throws(() => { + new TaskRunner({ + project: getMockProject("application"), + graph, + taskUtil, + taskRepository, + buildConfig + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing log parameter"); + t.throws(() => { + new TaskRunner({ + project: getMockProject("application"), + graph, + taskUtil, + taskRepository, + log: projectBuildLogger, + }); + }, { + message: "TaskRunner: One or more mandatory parameters not provided" + }, "Threw with expected error message for missing buildConfig parameter"); +}); + +test("_initTasks: Project of type 'application'", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskRunner = new TaskRunner({ + project: getMockProject("application"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + t.deepEqual(taskRunner._taskExecutionOrder, [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "replaceVersion", + "minify", + "enhanceManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateStandaloneAppBundle", + "transformBootstrapHtml", + "generateBundle", + "generateVersionInfo", + "generateCachebusterInfo", + "generateApiIndex", + "generateResourcesJson" + ], "Correct standard tasks"); +}); + +test("_initTasks: Project of type 'library'", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskRunner = new TaskRunner({ + project: getMockProject("library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.deepEqual(taskRunner._taskExecutionOrder, [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "generateJsdoc", + "executeJsdocSdkTransformation", + "minify", + "generateLibraryManifest", + "enhanceManifest", + "generateComponentPreload", + "generateLibraryPreload", + "generateBundle", + "buildThemes", + "generateThemeDesignerResources", + "generateResourcesJson" + ], "Correct standard tasks"); +}); + +test("_initTasks: Project of type 'library' (framework project)", async (t) => { + const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + + const project = getMockProject("library"); + project.isFrameworkProject = () => true; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.deepEqual(taskRunner._taskExecutionOrder, [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "generateJsdoc", + "executeJsdocSdkTransformation", + "minify", + "generateLibraryManifest", + "enhanceManifest", + "generateComponentPreload", + "generateLibraryPreload", + "generateBundle", + "buildThemes", + "generateThemeDesignerResources", + "generateResourcesJson" + ], "Correct standard tasks"); +}); + +test("_initTasks: Project of type 'theme-library'", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskRunner = new TaskRunner({ + project: getMockProject("theme-library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.deepEqual(taskRunner._taskExecutionOrder, [ + "replaceCopyright", + "replaceVersion", + "buildThemes", + "generateThemeDesignerResources", + "generateResourcesJson" + ], "Correct standard tasks"); +}); + +test("_initTasks: Project of type 'theme-library' (framework project)", async (t) => { + const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + + const project = getMockProject("theme-library"); + project.isFrameworkProject = () => true; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.deepEqual(taskRunner._taskExecutionOrder, [ + "replaceCopyright", + "replaceVersion", + "buildThemes", + "generateThemeDesignerResources", + "generateResourcesJson" + ], "Correct standard tasks"); +}); + +test("_initTasks: Project of type 'module'", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskRunner = new TaskRunner({ + project: getMockProject("module"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.deepEqual(taskRunner._taskExecutionOrder, [], "Correct standard tasks"); +}); + +test("_initTasks: Unknown project type", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskRunner = new TaskRunner({ + project: getMockProject("pony"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(taskRunner._initTasks()); + + t.is(err.message, "Unknown project type pony", "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", afterTask: "minify"}, + {name: "myOtherTask", beforeTask: "replaceVersion"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + t.deepEqual(taskRunner._taskExecutionOrder, [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "myOtherTask", + "replaceVersion", + "minify", + "myTask", + "enhanceManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateStandaloneAppBundle", + "transformBootstrapHtml", + "generateBundle", + "generateVersionInfo", + "generateCachebusterInfo", + "generateApiIndex", + "generateResourcesJson" + ], "Custom tasks are inserted correctly"); +}); + +test("_initTasks: Custom tasks with no standard tasks", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask"}, + {name: "myOtherTask", beforeTask: "myTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + t.deepEqual(taskRunner._taskExecutionOrder, [ + "myOtherTask", + "myTask", + ], "ApplicationBuilder is still instantiated with standard tasks"); +}); + +test("_initTasks: Custom tasks with no standard tasks and second task defining no before-/afterTask", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask"}, + {name: "myOtherTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + `Custom task definition myOtherTask of project project.b defines neither a ` + + `"beforeTask" nor an "afterTask" parameter. One must be defined.`, + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks with both, before- and afterTask reference", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", beforeTask: "minify", afterTask: "replaceVersion"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + `Custom task definition myTask of project project.b defines both ` + + `"beforeTask" and "afterTask" parameters. Only one must be defined.`, + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks with no before-/afterTask reference", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + `Custom task definition myTask of project project.b defines neither a ` + + `"beforeTask" nor an "afterTask" parameter. One must be defined.`, + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks without name", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: ""} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + `Missing name for custom task in configuration of project project.b`, + "Threw with expected error message"); +}); + +test("_initTasks: Custom task with name of standard tasks", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "replaceVersion", afterTask: "minify"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + "Custom task configuration of project project.b references standard task replaceVersion. " + + "Only custom tasks must be provided here.", + "Threw with expected error message"); +}); + +test("_initTasks: Multiple custom tasks with same name", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", afterTask: "minify"}, + {name: "myTask", afterTask: "myTask"}, + {name: "myTask", afterTask: "minify"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + t.deepEqual(taskRunner._taskExecutionOrder, [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "replaceVersion", + "minify", + "myTask--3", + "myTask", + "myTask--2", + "enhanceManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateStandaloneAppBundle", + "transformBootstrapHtml", + "generateBundle", + "generateVersionInfo", + "generateCachebusterInfo", + "generateApiIndex", + "generateResourcesJson" + ], "Custom tasks are inserted correctly"); +}); + +test("_initTasks: Custom tasks with unknown beforeTask", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", beforeTask: "unknownTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + "Could not find task unknownTask, referenced by custom task myTask, " + + "to be scheduled for project project.b", + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks with unknown afterTask", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", afterTask: "unknownTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + "Could not find task unknownTask, referenced by custom task myTask, " + + "to be scheduled for project project.b", + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks is unknown", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + graph.getExtension.returns(undefined); + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", afterTask: "minify"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + "Could not find custom task myTask, referenced by project project.b in project " + + "graph with root node graph-root", + "Threw with expected error message"); +}); + +test("_initTasks: Custom tasks with removed beforeTask", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getCustomTasks = () => [ + {name: "myTask", beforeTask: "removedTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + const err = await t.throwsAsync(async () => { + await taskRunner._initTasks(); + }); + t.is(err.message, + `Standard task removedTask, referenced by custom task myTask in project project.b, ` + + `has been removed in this version of UI5 CLI and can't be referenced anymore. ` + + `Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`, + "Threw with expected error message"); +}); + +test("_initTasks: Create dependencies reader for all dependencies", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; + const project = getMockProject("application"); + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst called once"); + t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b", + "ProjectGraph#traverseBreadthFirst called with correct project name for start"); + const traversalCallback = graph.traverseBreadthFirst.getCall(0).args[1]; + + // Call with root project should be ignored + await traversalCallback({ + project: { + getName: () => "project.b", + getReader: () => "project.b reader", + } + }); + await traversalCallback({ + project: { + getName: () => "dep.a", + getReader: () => "dep.a reader", + } + }); + await traversalCallback({ + project: { + getName: () => "dep.b", + getReader: () => "dep.b reader", + } + }); + await traversalCallback({ + project: { + getName: () => "transitive.dep.a", + getReader: () => "transitive.dep.a reader", + } + }); + t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once"); + t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0], { + name: "Dependency reader collection of project project.b", + readers: [ + "dep.a reader", "dep.b reader", "transitive.dep.a reader" + ] + }, "createReaderCollection got called with correct arguments"); +}); + +test("Custom task is called correctly", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(false); + const mockSpecVersion = { + toString: () => "2.6", + gte: specVersionGteStub + }; + + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined); + graph.getExtension.returns({ + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + t.context.taskUtil.getInterface.returns("taskUtil interface"); + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + 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.a", "dep.b"]), + "Custom tasks requires all dependencies by default"); + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["myTask"].task(); + + t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + 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], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call"); + + t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], + new Set(["dep.a", "dep.b"]), + "_createDependenciesReader got called with correct arguments"); + + t.is(taskStub.callCount, 1, "Task got called once"); + t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + options: { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "configuration", + }, + taskUtil: "taskUtil interface" + }, "Task got called with one argument"); + + 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"); +}); + +test("Custom task with legacy spec version", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(false); + const mockSpecVersion = { + toString: () => "1.0", + gte: specVersionGteStub + }; + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined); + graph.getExtension.returns({ + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + 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.a", "dep.b"]), + "Custom tasks requires all dependencies by default"); + + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["myTask"].task(); + + t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + 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], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call"); + + t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], + new Set(["dep.a", "dep.b"]), + "_createDependenciesReader got called with correct arguments"); + + t.is(taskStub.callCount, 1, "Task got called once"); + t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + options: { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "configuration", + } + }, "Task got called with one argument"); + + 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"); +}); + +test("Custom task with legacy spec version and requiredDependenciesCallback", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(false); + const mockSpecVersion = { + toString: () => "1.0", + gte: specVersionGteStub + }; + const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); + const getRequiredDependenciesCallbackStub = sinon.stub().resolves(requiredDependenciesCallbackStub); + graph.getExtension.returns({ + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + 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"); + + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["myTask"].task(); + + t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + 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], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call"); + + t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], + new Set(["dep.b"]), + "_createDependenciesReader got called with correct arguments"); + + t.is(taskStub.callCount, 1, "Task got called once"); + t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + options: { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "configuration", + } + }, "Task got called with one argument"); + + 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"); +}); + +test("Custom task with specVersion 3.0", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(true); + const mockSpecVersion = { + toString: () => "3.0", + gte: specVersionGteStub + }; + + const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"])); + const getRequiredDependenciesCallbackStub = sinon.stub() + .resolves(requiredDependenciesCallbackStub); + + graph.getExtension.returns({ + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + 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"); + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["myTask"].task(); + + t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + 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], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call"); + + 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, 1, "_createDependenciesReader got called once"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], + new Set(["dep.b"]), + "_createDependenciesReader got called with correct arguments"); + + t.is(taskStub.callCount, 1, "Task got called once"); + t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + log: "group logger", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + taskName: "myTask", // specVersion 3.0 feature + configuration: "configuration", + }, + }, "Task got called with one argument"); +}); + +test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", async (t) => { + const {sinon, graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context; + const taskStub = sinon.stub(); + 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 + }); + + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map"); + t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(), + "Custom tasks requires no dependencies by default"); + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["myTask"].task(); + + t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice"); + 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], "3.0", + "SpecificationVersion#gte got called with correct arguments on second call"); + + 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(createDependencyReaderStub.callCount, 0, "_createDependenciesReader 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"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + log: "group logger", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + taskName: "myTask", // specVersion 3.0 feature + configuration: "configuration", + }, + }, "Task got called with one argument"); +}); + +test("Multiple custom tasks with same name are called correctly", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStubA = sinon.stub(); + const taskStubB = sinon.stub(); + const taskStubC = sinon.stub(); + const taskStubD = sinon.stub(); + const mockSpecVersionA = { + toString: () => "2.5", + gte: () => false + }; + const mockSpecVersionB = { + toString: () => "2.6", + gte: () => false + }; + const mockSpecVersionC = { + toString: () => "3.0", + gte: () => true + }; + const mockSpecVersionD = { + 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 + }); + graph.getExtension.onSecondCall().returns({ + getName: () => "Task Name B", + getTask: () => taskStubB, + getSpecVersion: () => mockSpecVersionB, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + graph.getExtension.onThirdCall().returns({ + getName: () => "Task Name C", + getTask: () => taskStubC, + getSpecVersion: () => mockSpecVersionC, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + graph.getExtension.onCall(3).returns({ + getName: () => "Task Name D", + getTask: () => taskStubD, + getSpecVersion: () => mockSpecVersionD, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "cat"}, + {name: "myTask", afterTask: "myTask", configuration: "dog"}, + {name: "myTask", afterTask: "myTask", configuration: "bird"}, + {name: "myTask", afterTask: "myTask", configuration: "bird"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + 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"); + t.deepEqual(taskRunner._tasks["myTask--2"].requiredDependencies, new Set(["dep.a", "dep.b"]), + "Custom tasks with legacy specVersion require all dependencies by default"); + 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"); + + // "Last in is the first out" + t.deepEqual(taskRunner._taskExecutionOrder, [ + "myTask", + "myTask--4", + "myTask--3", + "myTask--2", + ], "Correct order of custom tasks"); + + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner.runTasks(); + + t.is(projectBuildLogger.setTasks.callCount, 1, "ProjectBuildLogger#setTask got called once"); + t.deepEqual(projectBuildLogger.setTasks.firstCall.firstArg, [ + "myTask", + "myTask--4", + "myTask--3", + "myTask--2", + ], "ProjectBuildLogger#setTask got called with expected argument"); + + t.is(projectBuildLogger.startTask.callCount, 4, "ProjectBuildLogger#startTask got called four times"); + t.deepEqual(projectBuildLogger.startTask.getCalls().map((call) => call.firstArg), [ + "myTask", + "myTask--4", + "myTask--3", + "myTask--2", + ], "ProjectBuildLogger#startTask got called with expected arguments"); + t.is(projectBuildLogger.endTask.callCount, 4, "ProjectBuildLogger#endTask got called four times"); + t.deepEqual(projectBuildLogger.endTask.getCalls().map((call) => call.firstArg), [ + "myTask", + "myTask--4", + "myTask--3", + "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, 3, "_createDependenciesReader got called three times"); + t.deepEqual(createDependencyReaderStub.getCall(0).args[0], + new Set(["dep.b"]), + "_createDependenciesReader got called with correct arguments on first call"); + t.deepEqual(createDependencyReaderStub.getCall(1).args[0], + new Set(["dep.a"]), + "_createDependenciesReader got called with correct arguments on second call"); + t.deepEqual(createDependencyReaderStub.getCall(2).args[0], + new Set(["dep.a", "dep.b"]), + "_createDependenciesReader got called with correct arguments on third call"); + + 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"); + t.deepEqual(taskStubA.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "cat", + } + }, "Task A got called with one argument"); + + t.is(taskStubB.callCount, 1, "Task B got called once"); + t.is(taskStubB.getCall(0).args.length, 1, "Task B got called with one argument"); + t.deepEqual(taskStubB.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + configuration: "dog", + } + }, "Task B got called with one argument"); + + t.is(taskStubC.callCount, 1, "Task C got called once"); + t.is(taskStubC.getCall(0).args.length, 1, "Task C got called with one argument"); + t.deepEqual(taskStubC.getCall(0).args[0], { + workspace: "workspace", + log: "group logger", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + taskName: "myTask--3", + configuration: "bird", + } + }, "Task C got called with one argument"); + + t.is(taskStubD.callCount, 1, "Task D got called once"); + t.is(taskStubD.getCall(0).args.length, 1, "Task D got called with one argument"); + t.deepEqual(taskStubD.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + log: "group logger", + taskUtil, + options: { + projectName: "project.b", + projectNamespace: "project/b", + taskName: "myTask--4", + configuration: "bird", + } + }, "Task D got called with one argument"); +}); + +test("Custom task: requiredDependenciesCallback returns unknown dependency", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(true); + const mockSpecVersion = { + toString: () => "3.0", + gte: specVersionGteStub + }; + + const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b", "other.dep"])); + const getRequiredDependenciesCallbackStub = sinon.stub() + .resolves(requiredDependenciesCallbackStub); + + graph.getExtension.returns({ + getName: () => "custom.task.a", + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await t.throwsAsync(taskRunner._initTasks(), { + message: + `'determineRequiredDependencies' callback function of custom task custom.task.a ` + + `of project project.b must resolve with a subset of the the direct dependencies of the project. ` + + `other.dep is not a direct dependency of the project.` + }, "Threw with expected error message"); +}); + + +test("Custom task: requiredDependenciesCallback returns Array instead of Set", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const specVersionGteStub = sinon.stub().returns(true); + const mockSpecVersion = { + toString: () => "3.0", + gte: specVersionGteStub + }; + + const requiredDependenciesCallbackStub = sinon.stub().resolves(["dep.b"]); + const getRequiredDependenciesCallbackStub = sinon.stub() + .resolves(requiredDependenciesCallbackStub); + + graph.getExtension.returns({ + getName: () => "custom.task.a", + getTask: () => taskStub, + getSpecVersion: () => mockSpecVersion, + getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub + }); + + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask", configuration: "configuration"} + ]; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await t.throwsAsync(taskRunner._initTasks(), { + message: + `'determineRequiredDependencies' callback function of custom task custom.task.a ` + + `of project project.b must resolve with Set.` + }, "Threw with expected error message"); +}); + +test("Custom task attached to a disabled task", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, sinon, customTask} = t.context; + + const project = getMockProject("application"); + const customTaskFnStub = sinon.stub(); + project.getBundles = emptyarray; + project.getCustomTasks = () => [ + {name: "myTask", afterTask: "generateBundle", configuration: "dog"} + ]; + + taskRepository.getTask = sinon.stub().returns({task: sinon.stub()}); + customTask.getTask = () => customTaskFnStub; + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + + await taskRunner.runTasks(); + + const setTasksArgs = projectBuildLogger.setTasks.firstCall.args[0]; + t.true(setTasksArgs.includes("myTask"), "Custom task 'myTask' is queried"); + t.is(customTaskFnStub.calledOnce, true, "Custom task 'myTask' is executed"); + t.false(setTasksArgs.includes("generateBundle"), + "generateBundle standard task is excluded from the execution list"); + + t.deepEqual( + setTasksArgs, + [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "replaceVersion", + "minify", + "enhanceManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "myTask", + ], + "Correct tasks execution"); +}); + +test.serial("_addTask", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + + const taskStub = sinon.stub(); + taskRepository.getTask.withArgs("standardTask").resolves({ + task: taskStub + }); + + const project = getMockProject("module"); + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + taskRunner._addTask("standardTask"); + + t.truthy(taskRunner._tasks["standardTask"], "Task has been added to task map"); + t.deepEqual(taskRunner._tasks["standardTask"].requiredDependencies, new Set(), + "By default, no dependencies required"); + t.truthy(taskRunner._tasks["standardTask"].task, "Task function got set correctly"); + t.deepEqual(taskRunner._taskExecutionOrder, ["standardTask"], "Task got added to execution order"); + + await taskRunner._tasks["standardTask"].task({ + workspace: "workspace", + dependencies: "dependencies", + }); + + t.is(taskRepository.getTask.callCount, 1, "taskRepository#getTask got called once"); + t.is(taskRepository.getTask.getCall(0).args[0], "standardTask", + "taskRepository#getTask got called with correct argument"); + t.is(taskStub.callCount, 1, "Task got called once"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + // No dependencies + options: { + projectName: "project.b", + projectNamespace: "project/b" + }, + taskUtil + }, "Task got called with correct arguments"); +}); + +test.serial("_addTask with options", async (t) => { + const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const taskStub = sinon.stub(); + const project = getMockProject("module"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + taskRunner._addTask("standardTask", { + requiresDependencies: true, + options: { + myTaskOption: "cat", + }, + taskFunction: taskStub + }); + + t.truthy(taskRunner._tasks["standardTask"], "Task has been added to task map"); + t.deepEqual(taskRunner._tasks["standardTask"].requiredDependencies, new Set(["dep.a", "dep.b"]), + "All dependencies required"); + t.truthy(taskRunner._tasks["standardTask"].task, "Task function got set correctly"); + t.deepEqual(taskRunner._taskExecutionOrder, ["standardTask"], "Task got added to execution order"); + + const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies"); + await taskRunner._tasks["standardTask"].task({ + workspace: "workspace", + dependencies: "dependencies", + }); + + t.is(taskRepository.getTask.callCount, 0, "taskRepository#getTask did not get called"); + t.is(createDependencyReaderStub.callCount, 0, "_createDependenciesReader did not get called"); + + t.is(taskStub.callCount, 1, "Task got called once"); + t.deepEqual(taskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: taskRunner._allDependenciesReader, + options: { + projectName: "project.b", + projectNamespace: "project/b", + myTaskOption: "cat" + }, + taskUtil + }, "Task got called with correct arguments"); +}); + +test("_addTask: Duplicate task", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + taskRunner._addTask("standardTask", { + taskFunction: () => {} + }); + + const err = t.throws(() => { + taskRunner._addTask("standardTask", { + taskFunction: () => {} + }); + }); + t.is(err.message, "Failed to add duplicate task standardTask for project project.b", + "Threw with expected error message"); +}); + +test("_addTask: Task already added to execution order", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + + taskRunner._taskExecutionOrder.push("standardTask"); + const err = t.throws(() => { + 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"); +}); + +test("getRequiredDependencies: Custom Task", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + project.getCustomTasks = () => [ + {name: "myTask"} + ]; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), + "Project with custom task >= specVersion 3.0 and no requiredDependenciesCallback " + + "requires no dependencies"); +}); + +test("getRequiredDependencies: Default application", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("application"); + project.getBundles = () => []; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), + "Default application project does not require dependencies"); +}); + +test("getRequiredDependencies: Default library", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("library"); + project.getBundles = () => []; + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]), + "Default library project requires dependencies"); +}); + +test("getRequiredDependencies: Default theme-library", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("theme-library"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]), + "Default theme-library project requires dependencies"); +}); + +test("getRequiredDependencies: Default module", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context; + const project = getMockProject("module"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]), + "Default module project does not require dependencies"); +}); + +test("_createDependenciesReader", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; + const project = getMockProject("module"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + graph.traverseBreadthFirst.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.returns("custom reader collection"); + const res = await taskRunner._createDependenciesReader(new Set(["dep.a"])); + + t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once"); + t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b", + "ProjectGraph#traverseBreadthFirst called with correct project name for start"); + + const traversalCallback = graph.traverseBreadthFirst.getCall(0).args[1]; + + // Call with root project should be ignored + await traversalCallback({ + project: { + getName: () => "project.b", + getReader: () => "project.b reader", + } + }); + await traversalCallback({ + project: { + getName: () => "dep.a", + getReader: () => "dep.a reader", + } + }); + await traversalCallback({ + project: { + getName: () => "dep.b", + getReader: () => "dep.b reader", + } + }); + await traversalCallback({ + project: { + getName: () => "dep.c", + getReader: () => "dep.c reader", + } + }); + await traversalCallback({ + project: { + // Will be ignored as it is no (transitive) dependency of the project + getName: () => "other project", + getReader: () => "other project reader", + } + }); + t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once"); + t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0], { + name: "Reduced dependency reader collection of project project.b", + readers: [ + "dep.a reader", "dep.b reader", "dep.c reader" + ] + }, "createReaderCollection got called with correct arguments"); + t.is(res, "custom reader collection", "Returned expected value"); +}); + +test("_createDependenciesReader: All dependencies required", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; + const project = getMockProject("module"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + graph.traverseBreadthFirst.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.returns("custom reader collection"); + const res = await taskRunner._createDependenciesReader(new Set(["dep.a", "dep.b"])); + t.is(graph.traverseBreadthFirst.callCount, 0, "ProjectGraph#traverseBreadthFirst did not get called again"); + t.is(resourceFactory.createReaderCollection.callCount, 0, "createReaderCollection did not get called again"); + t.is(res, "reader collection", "Shared (all-)dependency reader returned"); +}); + +test("_createDependenciesReader: No dependencies required", async (t) => { + const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context; + const project = getMockProject("module"); + + const taskRunner = new TaskRunner({ + project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig + }); + await taskRunner._initTasks(); + graph.traverseBreadthFirst.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask + resourceFactory.createReaderCollection.returns("custom reader collection"); + const res = await taskRunner._createDependenciesReader(new Set()); + t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once"); + t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once"); + t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0].readers, [], + "createReaderCollection got called with no readers"); + t.is(res, "custom reader collection", "Shared (all-)dependency reader returned"); +}); + diff --git a/packages/project/test/lib/build/definitions/application.js b/packages/project/test/lib/build/definitions/application.js new file mode 100644 index 00000000000..742d398e988 --- /dev/null +++ b/packages/project/test/lib/build/definitions/application.js @@ -0,0 +1,521 @@ +import test from "ava"; +import sinon from "sinon"; +import application from "../../../../lib/build/definitions/application.js"; + +function emptyarray() { + return []; +} + +function getMockProject() { + return { + getName: () => "project.b", + getNamespace: () => "project/b", + getType: () => "application", + getPropertiesFileSourceEncoding: () => "UTF-412", + getCopyright: () => "copyright", + getVersion: () => "version", + getSpecVersion: () => { + return { + toString: () => "2.6", + gte: () => true + }; + }, + getMinificationExcludes: emptyarray, + getComponentPreloadPaths: emptyarray, + getComponentPreloadNamespaces: emptyarray, + getComponentPreloadExcludes: emptyarray, + getBundles: emptyarray, + getCachebusterSignatureType: () => "PONY", + getCustomTasks: emptyarray, + }; +} + +test.beforeEach((t) => { + t.context.project = getMockProject(); + t.context.taskUtil = { + getProject: sinon.stub().returns(t.context.project), + isRootProject: sinon.stub().returns(true), + getBuildOption: sinon.stub(), + getInterface: sinon.stub() + }; + + t.context.getTask = sinon.stub(); +}); + +test("Standard build", (t) => { + const {project, taskUtil, getTask} = t.context; + const tasks = application({ + project, taskUtil, getTask + }); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", pattern: "/**/*.{js,json}" + } + }, + replaceVersion: { + options: { + version: "version", pattern: "/**/*.{js,json}" + } + }, + minify: { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, + enhanceManifest: {}, + generateFlexChangesBundle: {}, + generateComponentPreload: { + options: { + namespaces: ["project/b"], + excludes: [], + skipBundles: [] + } + }, + generateStandaloneAppBundle: { + requiresDependencies: true + }, + transformBootstrapHtml: {}, + generateBundle: { + taskFunction: null + }, + generateVersionInfo: { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }, + generateCachebusterInfo: { + options: { + signatureType: "PONY" + } + }, + generateApiIndex: { + requiresDependencies: true + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); + + t.is(taskUtil.getBuildOption.callCount, 0, "taskUtil#getBuildOption has not been called"); +}); + +test("Standard build with legacy spec version", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getSpecVersion = () => { + return { + toString: () => "0.1", + gte: () => false + }; + }; + const generateBundleTaskStub = sinon.stub(); + getTask.returns({ + task: generateBundleTaskStub + }); + + const tasks = application({ + project, taskUtil, getTask + }); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", pattern: "/**/*.{js,json}" + } + }, + replaceVersion: { + options: { + version: "version", pattern: "/**/*.{js,json}" + } + }, + minify: { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, + enhanceManifest: {}, + generateFlexChangesBundle: {}, + generateComponentPreload: { + options: { + namespaces: ["project/b"], + excludes: [], + skipBundles: [] + } + }, + generateStandaloneAppBundle: { + requiresDependencies: true + }, + transformBootstrapHtml: {}, + generateBundle: { + taskFunction: null + }, + generateVersionInfo: { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }, + generateCachebusterInfo: { + options: { + signatureType: "PONY" + } + }, + generateApiIndex: { + requiresDependencies: true + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); +}); + +test("Custom bundles", async (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }, { + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ] + }] + }, + bundleOptions: { + optimize: false, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + const generateBundleTaskStub = sinon.stub(); + getTask.returns({ + task: generateBundleTaskStub + }); + + const tasks = application({ + project, taskUtil, getTask + }); + const generateBundleTaskDefinition = tasks.get("generateBundle"); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", pattern: "/**/*.{js,json}" + } + }, + replaceVersion: { + options: { + version: "version", pattern: "/**/*.{js,json}" + } + }, + minify: { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, + enhanceManifest: {}, + generateFlexChangesBundle: {}, + generateComponentPreload: { + options: { + namespaces: ["project/b"], + excludes: [], + skipBundles: [ + "project/b/sectionsA/customBundle.js", + "project/b/sectionsB/customBundle.js" + ] + } + }, + generateStandaloneAppBundle: { + requiresDependencies: true + }, + transformBootstrapHtml: {}, + generateBundle: { + requiresDependencies: true, + taskFunction: generateBundleTaskDefinition.taskFunction + }, + generateVersionInfo: { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }, + generateCachebusterInfo: { + options: { + signatureType: "PONY" + } + }, + generateApiIndex: { + requiresDependencies: true + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); + + + await generateBundleTaskDefinition.taskFunction({ + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName" + } + }); + + t.is(generateBundleTaskStub.callCount, 2, "generateBundle task got called twice"); + t.deepEqual(generateBundleTaskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + } + }, "generateBundle task got called with correct arguments"); + t.deepEqual(generateBundleTaskStub.getCall(1).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: false, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + } + }, "generateBundle task got called with correct arguments"); +}); + +test("Minification excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getMinificationExcludes = () => ["**.html"]; + + const tasks = application({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("minify"); + t.deepEqual(taskDefinition, { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + "!/resources/**.html", + ] + } + }, "Correct minify task definition"); +}); + +test("Minification excludes not applied for legacy specVersion", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getSpecVersion = () => { + return { + toString: () => "2.5", + gte: () => false + }; + }; + project.getMinificationExcludes = () => ["**.html"]; + + const tasks = application({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("minify"); + t.deepEqual(taskDefinition, { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, "Correct minify task definition"); +}); + +test("generateComponentPreload with custom paths, excludes and custom bundle", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + project.getComponentPreloadPaths = () => [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ]; + project.getComponentPreloadExcludes = () => ["project/b/dir/**"]; + + const tasks = application({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ], + namespaces: [], + excludes: ["project/b/dir/**"], + skipBundles: [ + "project/b/sectionsA/customBundle.js" + ] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateComponentPreload with custom namespaces and excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getComponentPreloadNamespaces = () => [ + "project/b/componentA", + "project/b/componentB" + ]; + project.getComponentPreloadExcludes = () => ["project/b/componentA/dir/**"]; + + const tasks = application({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [], + namespaces: [ + "project/b/componentA", + "project/b/componentB" + ], + excludes: ["project/b/componentA/dir/**"], + skipBundles: [] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateComponentPreload with excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getComponentPreloadExcludes = () => ["project/b/componentA/dir/**"]; + + const tasks = application({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + namespaces: [ + "project/b", + ], + excludes: ["project/b/componentA/dir/**"], + skipBundles: [] + } + }, "Correct generateComponentPreload task definition"); +}); diff --git a/packages/project/test/lib/build/definitions/library.js b/packages/project/test/lib/build/definitions/library.js new file mode 100644 index 00000000000..121e8951442 --- /dev/null +++ b/packages/project/test/lib/build/definitions/library.js @@ -0,0 +1,743 @@ +import test from "ava"; +import sinon from "sinon"; +import library from "../../../../lib/build/definitions/library.js"; + +function emptyarray() { + return []; +} + +function getMockProject() { + return { + getName: () => "project.b", + getNamespace: () => "project/b", + getType: () => "library", + getPropertiesFileSourceEncoding: () => "UTF-412", + getCopyright: () => "copyright", + getVersion: () => "version", + getSpecVersion: () => { + return { + toString: () => "2.6", + gte: () => true + }; + }, + getMinificationExcludes: emptyarray, + getComponentPreloadPaths: emptyarray, + getComponentPreloadNamespaces: emptyarray, + getComponentPreloadExcludes: emptyarray, + getLibraryPreloadExcludes: emptyarray, + getBundles: emptyarray, + getCachebusterSignatureType: () => "PONY", + getJsdocExcludes: () => [], + getCustomTasks: emptyarray, + isFrameworkProject: () => false + }; +} + +test.beforeEach((t) => { + t.context.project = getMockProject(); + t.context.taskUtil = { + getProject: sinon.stub().returns(t.context.project), + isRootProject: sinon.stub().returns(true), + getBuildOption: sinon.stub(), + getInterface: sinon.stub() + }; + + t.context.getTask = sinon.stub(); +}); + +test("Standard build", async (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: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }, + replaceBuildtime: { + options: { + pattern: "/resources/sap/ui/{Global,core/Core}.js" + } + }, + generateJsdoc: { + requiresDependencies: true, + taskFunction: generateJsdocTaskDefinition.taskFunction + }, + executeJsdocSdkTransformation: { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library" + } + }, + minify: { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + ] + } + }, + generateLibraryManifest: {}, + enhanceManifest: {}, + generateLibraryPreload: { + options: { + excludes: [], skipBundles: [] + } + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: undefined + } + }, + generateBundle: { + taskFunction: null + }, + generateComponentPreload: { + taskFunction: null + }, + generateThemeDesignerResources: { + taskFunction: null + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "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"); + + t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once"); + t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables", + "taskUtil#getBuildOption got called with correct argument"); +}); + +test("Standard build (framework project)", (t) => { + const {project, taskUtil, getTask} = t.context; + + project.isFrameworkProject = () => true; + + const generateJsdocTaskStub = sinon.stub(); + getTask.returns({ + task: generateJsdocTaskStub + }); + + const tasks = library({ + project, taskUtil, getTask + }); + + t.deepEqual(tasks.get("generateThemeDesignerResources"), { + requiresDependencies: true, options: { + version: "version" + } + }); +}); + +test("Standard build with legacy spec version", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getSpecVersion = () => { + return { + toString: () => "0.1", + gte: () => false + }; + }; + + const tasks = library({ + project, taskUtil, getTask + }); + const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }, + replaceBuildtime: { + options: { + pattern: "/resources/sap/ui/{Global,core/Core}.js" + } + }, + generateJsdoc: { + requiresDependencies: true, + taskFunction: generateJsdocTaskDefinition.taskFunction + }, + executeJsdocSdkTransformation: { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library" + } + }, + minify: { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + ] + } + }, + generateLibraryManifest: {}, + enhanceManifest: {}, + generateLibraryPreload: { + options: { + excludes: [], skipBundles: [] + } + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: undefined + } + }, + generateBundle: { + taskFunction: null + }, + generateComponentPreload: { + taskFunction: null + }, + generateThemeDesignerResources: { + taskFunction: null + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); +}); + +test("Custom bundles", async (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }, { + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ] + }] + }, + bundleOptions: { + optimize: false, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + const generateBundleTaskStub = sinon.stub(); + getTask.returns({ + task: generateBundleTaskStub + }); + + const tasks = library({ + project, taskUtil, getTask + }); + const generateJsdocTaskDefinition = tasks.get("generateJsdoc"); + const generateBundleTaskDefinition = tasks.get("generateBundle"); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }, + replaceBuildtime: { + options: { + pattern: "/resources/sap/ui/{Global,core/Core}.js" + } + }, + generateJsdoc: { + requiresDependencies: true, + taskFunction: generateJsdocTaskDefinition.taskFunction + }, + executeJsdocSdkTransformation: { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library" + } + }, + minify: { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + ] + } + }, + generateLibraryManifest: {}, + enhanceManifest: {}, + generateLibraryPreload: { + options: { + excludes: [], + skipBundles: [ + "project/b/sectionsA/customBundle.js", + "project/b/sectionsB/customBundle.js", + ] + } + }, + generateBundle: { + requiresDependencies: true, + taskFunction: generateBundleTaskDefinition.taskFunction + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: undefined + } + }, + generateComponentPreload: { + taskFunction: null + }, + generateThemeDesignerResources: { + taskFunction: null + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); + + + await generateBundleTaskDefinition.taskFunction({ + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName" + } + }); + + t.is(generateBundleTaskStub.callCount, 2, "generateBundle task got called twice"); + t.deepEqual(generateBundleTaskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + } + }, "generateBundle task got called with correct arguments"); + t.deepEqual(generateBundleTaskStub.getCall(1).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: false, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + } + }, "generateBundle task got called with correct arguments"); +}); + +test("Minification excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getMinificationExcludes = () => ["**.html"]; + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("minify"); + t.deepEqual(taskDefinition, { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + "!/resources/**.html", + ] + } + }, "Correct minify task definition"); +}); + +test("Minification excludes not applied for legacy specVersion", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getSpecVersion = () => { + return { + toString: () => "2.5", + gte: () => false + }; + }; + project.getMinificationExcludes = () => ["**.html"]; + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("minify"); + t.deepEqual(taskDefinition, { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + ] + } + }, "Correct minify task definition"); +}); + +test("generateComponentPreload with custom paths, excludes and custom bundle", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + project.getComponentPreloadPaths = () => [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ]; + project.getComponentPreloadExcludes = () => ["project/b/dir/**"]; + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ], + namespaces: [], + excludes: ["project/b/dir/**"], + skipBundles: [ + "project/b/sectionsA/customBundle.js" + ] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateComponentPreload with custom namespaces and excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getComponentPreloadNamespaces = () => [ + "project/b/componentA", + "project/b/componentB" + ]; + project.getComponentPreloadExcludes = () => ["project/b/componentA/dir/**"]; + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [], + namespaces: [ + "project/b/componentA", + "project/b/componentB" + ], + excludes: ["project/b/componentA/dir/**"], + skipBundles: [] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateLibraryPreload with excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getLibraryPreloadExcludes = () => ["project/b/dir/**"]; + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateLibraryPreload"); + t.deepEqual(taskDefinition, { + options: { + excludes: ["project/b/dir/**"], + skipBundles: [] + } + }, "Correct generateLibraryPreload task definition"); +}); + +test("buildThemes: Project is not root", (t) => { + const {project, taskUtil, getTask} = t.context; + taskUtil.isRootProject.returns(false); + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("buildThemes"); + t.deepEqual(taskDefinition, { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: "/resources/**/(*.library|library.js)", + themesPattern: "/resources/sap/ui/core/themes/*", + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: undefined + } + }, "Correct buildThemes task definition"); +}); +test("buildThemes: CSS Variables enabled", (t) => { + const {project, taskUtil, getTask} = t.context; + taskUtil.getBuildOption.returns(true); + + const tasks = library({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("buildThemes"); + t.deepEqual(taskDefinition, { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: true + } + }, "Correct buildThemes task definition"); + + t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once"); + t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables", + "taskUtil#getBuildOption got called with correct argument"); +}); + +test("Standard build: nulled taskFunction to skip tasks", (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"); + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }, + replaceBuildtime: { + options: { + pattern: "/resources/sap/ui/{Global,core/Core}.js" + } + }, + generateJsdoc: { + requiresDependencies: true, + taskFunction: async () => {}, + }, + executeJsdocSdkTransformation: { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library" + } + }, + minify: { + options: { + pattern: [ + "/resources/**/*.js", + "!**/*.support.js", + ] + } + }, + generateLibraryManifest: {}, + enhanceManifest: {}, + generateLibraryPreload: { + options: { + excludes: [], skipBundles: [] + } + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/project/b/themes/*/library.source.less", + cssVariables: undefined + } + }, + 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"); +}); diff --git a/packages/project/test/lib/build/definitions/module.js b/packages/project/test/lib/build/definitions/module.js new file mode 100644 index 00000000000..0a74ff702fa --- /dev/null +++ b/packages/project/test/lib/build/definitions/module.js @@ -0,0 +1,7 @@ +import test from "ava"; +import moduleDefinition from "../../../../lib/build/definitions/module.js"; + +test("Standard build", (t) => { + const tasks = moduleDefinition({}); + t.is(tasks.size, 0, "No tasks returned"); +}); diff --git a/packages/project/test/lib/build/definitions/themeLibrary.js b/packages/project/test/lib/build/definitions/themeLibrary.js new file mode 100644 index 00000000000..2da2457b538 --- /dev/null +++ b/packages/project/test/lib/build/definitions/themeLibrary.js @@ -0,0 +1,171 @@ +import test from "ava"; +import sinon from "sinon"; +import themeLibrary from "../../../../lib/build/definitions/themeLibrary.js"; + +function emptyarray() { + return []; +} + +function getMockProject() { + return { + getName: () => "project.b", + getNamespace: () => "project/b", + getType: () => "theme-library", + getCopyright: () => "copyright", + getVersion: () => "version", + getSpecVersion: () => { + return { + toString: () => "2.6" + }; + }, + getMinificationExcludes: emptyarray, + getComponentPreloadPaths: emptyarray, + getComponentPreloadNamespaces: emptyarray, + getComponentPreloadExcludes: emptyarray, + getLibraryPreloadExcludes: emptyarray, + getBundles: emptyarray, + getCachebusterSignatureType: () => "PONY", + getCustomTasks: emptyarray, + isFrameworkProject: () => false + }; +} + +test.beforeEach((t) => { + t.context.taskUtil = { + isRootProject: sinon.stub().returns(true), + getBuildOption: sinon.stub(), + getInterface: sinon.stub() + }; + + t.context.project = getMockProject(); + t.context.getTask = sinon.stub(); +}); + +test("Standard build", (t) => { + const {project, taskUtil, getTask} = t.context; + + const tasks = themeLibrary({ + project, taskUtil, getTask + }); + const generateThemeDesignerResourcesTaskFunction = tasks.get("generateThemeDesignerResources"); + t.deepEqual(Object.fromEntries(tasks), { + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/resources/**/*.{less,theme}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/resources/**/*.{less,theme}" + } + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: undefined + } + }, + generateResourcesJson: { + requiresDependencies: true + }, + generateThemeDesignerResources: { + taskFunction: null + } + }, "Correct task definitions"); + + t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once"); + t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables", + "taskUtil#getBuildOption got called with correct argument"); + + t.is(generateThemeDesignerResourcesTaskFunction.taskFunction, null, "taskFunction is explicitly set to null"); +}); + +test("Standard build (framework project)", (t) => { + const {project, taskUtil, getTask} = t.context; + + project.isFrameworkProject = () => true; + + const tasks = themeLibrary({ + project, taskUtil, getTask + }); + + t.deepEqual(tasks.get("generateThemeDesignerResources"), { + requiresDependencies: true, options: { + version: "version" + } + }); +}); + +test("Standard build for non root project", (t) => { + const {project, taskUtil, getTask} = t.context; + taskUtil.isRootProject.returns(false); + + const tasks = themeLibrary({ + project, taskUtil, getTask + }); + t.deepEqual(Object.fromEntries(tasks), { + replaceCopyright: { + options: { + copyright: "copyright", + pattern: "/resources/**/*.{less,theme}" + } + }, + replaceVersion: { + options: { + version: "version", + pattern: "/resources/**/*.{less,theme}" + } + }, + buildThemes: { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: "/resources/**/(*.library|library.js)", + themesPattern: "/resources/sap/ui/core/themes/*", + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: undefined + } + }, + generateResourcesJson: { + requiresDependencies: true + }, + generateThemeDesignerResources: { + taskFunction: null + } + }, "Correct task definitions"); + + t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once"); + t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables", + "taskUtil#getBuildOption got called with correct argument"); +}); + +test("CSS variables enabled", (t) => { + const {project, taskUtil, getTask} = t.context; + taskUtil.getBuildOption.returns(true); + + const tasks = themeLibrary({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("buildThemes"); + t.deepEqual(taskDefinition, { + requiresDependencies: true, + options: { + projectName: "project.b", + librariesPattern: undefined, + themesPattern: undefined, + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: true + } + }, "Correct buildThemes task definition"); + + t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once"); + t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables", + "taskUtil#getBuildOption got called with correct argument"); +}); diff --git a/packages/project/test/lib/build/definitions/utils.js b/packages/project/test/lib/build/definitions/utils.js new file mode 100644 index 00000000000..7cea787ceef --- /dev/null +++ b/packages/project/test/lib/build/definitions/utils.js @@ -0,0 +1,18 @@ +import test from "ava"; +import {enhancePatternWithExcludes} from "../../../../lib/build/definitions/_utils.js"; + +test("enhancePatternWithExcludes", (t) => { + const patterns = ["/default/pattern", "!/other/pattern"]; + const excludes = ["a", "!b", "c", "!d"]; + + enhancePatternWithExcludes(patterns, excludes, "/prefix/"); + + t.deepEqual(patterns, [ + "/default/pattern", + "!/other/pattern", + "!/prefix/a", + "/prefix/b", + "!/prefix/c", + "/prefix/d" + ]); +}); diff --git a/packages/project/test/lib/build/helpers/BuildContext.js b/packages/project/test/lib/build/helpers/BuildContext.js new file mode 100644 index 00000000000..cc09a7cd870 --- /dev/null +++ b/packages/project/test/lib/build/helpers/BuildContext.js @@ -0,0 +1,309 @@ +import test from "ava"; +import sinon from "sinon"; +import OutputStyleEnum from "../../../../lib/build/helpers/ProjectBuilderOutputStyle.js"; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +import BuildContext from "../../../../lib/build/helpers/BuildContext.js"; + +test("Missing parameters", (t) => { + const error1 = t.throws(() => { + new BuildContext(); + }); + + t.is(error1.message, `Missing parameter 'graph'`, "Threw with expected error message"); + + const error2 = t.throws(() => { + new BuildContext("graph"); + }); + + t.is(error2.message, `Missing parameter 'taskRepository'`, "Threw with expected error message"); +}); + +test("getRootProject", (t) => { + const rootProjectStub = sinon.stub() + .onFirstCall().returns({getType: () => "library"}) + .returns("pony"); + const graph = {getRoot: rootProjectStub}; + const buildContext = new BuildContext(graph, "taskRepository"); + + t.is(buildContext.getRootProject(), "pony", "Returned correct value"); +}); + +test("getGraph", (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository"); + + t.deepEqual(buildContext.getGraph(), graph, "Returned correct value"); +}); + +test("getTaskRepository", (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository"); + + t.is(buildContext.getTaskRepository(), "taskRepository", "Returned correct value"); +}); + +test("getBuildConfig: Default values", (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository"); + + t.deepEqual(buildContext.getBuildConfig(), { + selfContained: false, + outputStyle: OutputStyleEnum.Default, + cssVariables: false, + jsdoc: false, + createBuildManifest: false, + includedTasks: [], + excludedTasks: [], + }, "Returned correct value"); +}); + +test("getBuildConfig: Custom values", (t) => { + const buildContext = new BuildContext({ + getRoot: () => { + return { + getType: () => "library" + }; + } + }, "taskRepository", { + selfContained: true, + outputStyle: OutputStyleEnum.Namespace, + cssVariables: true, + jsdoc: true, + createBuildManifest: false, + includedTasks: ["included tasks"], + excludedTasks: ["excluded tasks"], + }); + + t.deepEqual(buildContext.getBuildConfig(), { + selfContained: true, + outputStyle: OutputStyleEnum.Namespace, + cssVariables: true, + jsdoc: true, + createBuildManifest: false, + includedTasks: ["included tasks"], + excludedTasks: ["excluded tasks"], + }, "Returned correct value"); +}); + +test("createBuildManifest not supported for type application", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "application" + }; + } + }, "taskRepository", { + createBuildManifest: true + }); + }); + t.is(err.message, + "Build manifest creation is currently not supported for projects of type application", + "Threw with expected error message"); +}); + +test("createBuildManifest not supported for type module", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "module" + }; + } + }, "taskRepository", { + createBuildManifest: true + }); + }); + t.is(err.message, + "Build manifest creation is currently not supported for projects of type module", + "Threw with expected error message"); +}); + +test("createBuildManifest not supported for self-contained build", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "library" + }; + } + }, "taskRepository", { + createBuildManifest: true, + selfContained: true + }); + }); + t.is(err.message, + "Build manifest creation is currently not supported for self-contained builds", + "Threw with expected error message"); +}); + +test("createBuildManifest supported for css-variables build", (t) => { + t.notThrows(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "library" + }; + } + }, "taskRepository", { + createBuildManifest: true, + cssVariables: true + }); + }); +}); + +test("createBuildManifest supported for jsdoc build", (t) => { + t.notThrows(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "library" + }; + } + }, "taskRepository", { + createBuildManifest: true, + jsdoc: true + }); + }); +}); + +test("outputStyle='Namespace' supported for type application", (t) => { + t.notThrows(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "application" + }; + } + }, "taskRepository", { + outputStyle: OutputStyleEnum.Namespace + }); + }); +}); + +test("outputStyle='Flat' not supported for type theme-library", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "theme-library" + }; + } + }, "taskRepository", { + outputStyle: OutputStyleEnum.Flat + }); + }); + t.is(err.message, + "Flat build output style is currently not supported for projects of typetheme-library since they" + + " commonly have more than one namespace. Currently only the Default output style is supported" + + " for this project type."); +}); + +test("outputStyle='Flat' not supported for type module", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "module" + }; + } + }, "taskRepository", { + outputStyle: OutputStyleEnum.Flat + }); + }); + t.is(err.message, + "Flat build output style is currently not supported for projects of typemodule. " + + "Their path mappings configuration can't be mapped to any namespace.Currently only the " + + "Default output style is supported for this project type."); +}); + +test("outputStyle='Flat' not supported for createBuildManifest build", (t) => { + const err = t.throws(() => { + new BuildContext({ + getRoot: () => { + return { + getType: () => "library" + }; + } + }, "taskRepository", { + createBuildManifest: true, + outputStyle: OutputStyleEnum.Flat + }); + }); + t.is(err.message, + "Build manifest creation is not supported in conjunction with flat build output", + "Threw with expected error message"); +}); + +test("getOption", (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository", { + cssVariables: "value", + }); + + t.is(buildContext.getOption("cssVariables"), "value", + "Returned correct value for build configuration 'cssVariables'"); + t.is(buildContext.getOption("selfContained"), undefined, + "Returned undefined for build configuration 'selfContained' " + + "(not exposed as build option)"); +}); + +test("createProjectContext", async (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository"); + const projectBuildContext = await buildContext.createProjectContext({ + project: { + getName: () => "project", + getType: () => "type", + }, + }); + + t.deepEqual(buildContext._projectBuildContexts, [projectBuildContext], + "Project build context has been added to internal array"); +}); + +test("executeCleanupTasks", async (t) => { + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const buildContext = new BuildContext(graph, "taskRepository"); + + const executeCleanupTasks = sinon.stub().resolves(); + + buildContext._projectBuildContexts.push({ + executeCleanupTasks + }); + buildContext._projectBuildContexts.push({ + executeCleanupTasks + }); + + await buildContext.executeCleanupTasks(); + + t.is(executeCleanupTasks.callCount, 2, + "Project context executeCleanupTasks got called twice"); + t.is(executeCleanupTasks.getCall(0).firstArg, false, + "Project context executeCleanupTasks got called with expected arguments"); + + + executeCleanupTasks.reset(); + await buildContext.executeCleanupTasks(true); + + t.is(executeCleanupTasks.callCount, 2, + "Project context executeCleanupTasks got called twice"); + t.is(executeCleanupTasks.getCall(0).firstArg, true, + "Project context executeCleanupTasks got called with expected arguments"); +}); diff --git a/packages/project/test/lib/build/helpers/ProjectBuildContext.js b/packages/project/test/lib/build/helpers/ProjectBuildContext.js new file mode 100644 index 00000000000..03f9a568325 --- /dev/null +++ b/packages/project/test/lib/build/helpers/ProjectBuildContext.js @@ -0,0 +1,463 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; + +test.beforeEach((t) => { + t.context.resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["me:MyTag"] + }); +}); +test.afterEach.always((t) => { + sinon.restore(); +}); + +import ProjectBuildContext from "../../../../lib/build/helpers/ProjectBuildContext.js"; + +test("Missing parameters", (t) => { + t.throws(() => { + new ProjectBuildContext({ + project: { + getName: () => "project", + getType: () => "type", + }, + }); + }, { + message: `Missing parameter 'buildContext'` + }, "Correct error message"); + + t.throws(() => { + new ProjectBuildContext({ + buildContext: "buildContext", + }); + }, { + message: `Missing parameter 'project'` + }, "Correct error message"); +}); + +test("isRootProject: true", (t) => { + const rootProject = { + getName: () => "root project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getRootProject: () => rootProject + }, + project: rootProject + }); + + t.true(projectBuildContext.isRootProject(), "Correctly identified root project"); +}); + +test("isRootProject: false", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getRootProject: () => "root project" + }, + project: { + getName: () => "not the root project", + getType: () => "type", + } + }); + + t.false(projectBuildContext.isRootProject(), "Correctly identified non-root project"); +}); + +test("getBuildOption", (t) => { + const getOptionStub = sinon.stub().returns("pony"); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getOption: getOptionStub + }, + project: { + getName: () => "project", + getType: () => "type", + } + }); + + t.is(projectBuildContext.getOption("option"), "pony", "Returned value is correct"); + t.is(getOptionStub.getCall(0).args[0], "option", "getOption called with correct argument"); +}); + +test("registerCleanupTask", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: { + getName: () => "project", + getType: () => "type", + } + }); + projectBuildContext.registerCleanupTask("my task 1"); + projectBuildContext.registerCleanupTask("my task 2"); + + t.is(projectBuildContext._queues.cleanup[0], "my task 1", "Cleanup task registered"); + t.is(projectBuildContext._queues.cleanup[1], "my task 2", "Cleanup task registered"); +}); + +test("executeCleanupTasks", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: { + getName: () => "project", + getType: () => "type", + } + }); + const task1 = sinon.stub().resolves(); + const task2 = sinon.stub().resolves(); + projectBuildContext.registerCleanupTask(task1); + projectBuildContext.registerCleanupTask(task2); + + projectBuildContext.executeCleanupTasks(); + + t.is(task1.callCount, 1, "Cleanup task 1 got called"); + t.is(task2.callCount, 1, "my task 2", "Cleanup task 2 got called"); +}); + +test.serial("getResourceTagCollection", async (t) => { + const projectAcceptsTagStub = sinon.stub().returns(false); + projectAcceptsTagStub.withArgs("project-tag").returns(true); + const projectContextAcceptsTagStub = sinon.stub().returns(false); + projectContextAcceptsTagStub.withArgs("project-context-tag").returns(true); + + class DummyResourceTagCollection { + constructor({allowedTags, allowedNamespaces}) { + t.deepEqual(allowedTags, [ + "ui5:OmitFromBuildResult", + "ui5:IsBundle" + ], + "Correct allowedTags parameter supplied"); + + t.deepEqual(allowedNamespaces, [ + "build" + ], + "Correct allowedNamespaces parameter supplied"); + } + acceptsTag(tag) { + // Redirect to stub + return projectContextAcceptsTagStub(tag); + } + } + + const ProjectBuildContext = await esmock("../../../../lib/build/helpers/ProjectBuildContext.js", { + "@ui5/fs/internal/ResourceTagCollection": DummyResourceTagCollection + }); + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: { + getName: () => "project", + getType: () => "type", + } + }); + + const fakeProjectCollection = { + acceptsTag: projectAcceptsTagStub + }; + const fakeResource = { + getProject: () => { + return { + getResourceTagCollection: () => fakeProjectCollection + }; + }, + getPath: () => "/resource/path", + hasProject: () => true + }; + const collection1 = projectBuildContext.getResourceTagCollection(fakeResource, "project-tag"); + t.is(collection1, fakeProjectCollection, "Returned tag collection of resource project"); + + const collection2 = projectBuildContext.getResourceTagCollection(fakeResource, "project-context-tag"); + t.true(collection2 instanceof DummyResourceTagCollection, + "Returned tag collection of project build context"); + + t.throws(() => { + projectBuildContext.getResourceTagCollection(fakeResource, "not-accepted-tag"); + }, { + message: `Could not find collection for resource /resource/path and tag not-accepted-tag` + }); +}); + +test("getResourceTagCollection: Assigns project to resource if necessary", (t) => { + const fakeProject = { + getName: () => "project", + getType: () => "type", + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: fakeProject, + log: { + silly: () => {} + } + }); + + const setProjectStub = sinon.stub(); + const fakeResource = { + getProject: () => { + return { + getResourceTagCollection: () => { + return { + acceptsTag: () => false + }; + } + }; + }, + getPath: () => "/resource/path", + hasProject: () => false, + setProject: setProjectStub + }; + projectBuildContext.getResourceTagCollection(fakeResource, "build:MyTag"); + t.is(setProjectStub.callCount, 1, "setProject got called once"); + t.is(setProjectStub.getCall(0).args[0], fakeProject, "setProject got called with correct argument"); +}); + +test("getProject", (t) => { + const project = { + getName: () => "project", + getType: () => "type", + }; + const getProjectStub = sinon.stub().returns("pony"); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getGraph: () => { + return { + getProject: getProjectStub + }; + } + }, + project + }); + + t.is(projectBuildContext.getProject("pony project"), "pony", "Returned correct value"); + t.is(getProjectStub.callCount, 1, "ProjectGraph#getProject got called once"); + t.is(getProjectStub.getCall(0).args[0], "pony project", "ProjectGraph#getProject got called with correct argument"); + + t.is(projectBuildContext.getProject(), project); + t.is(getProjectStub.callCount, 1, "ProjectGraph#getProject is not called when requesting current project"); +}); + +test("getProject: No name provided", (t) => { + const project = { + getName: () => "project", + getType: () => "type", + }; + const getProjectStub = sinon.stub().returns("pony"); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getGraph: () => { + return { + getProject: getProjectStub + }; + } + }, + project + }); + + t.is(projectBuildContext.getProject(), project, "Returned correct value"); + t.is(getProjectStub.callCount, 0, "ProjectGraph#getProject has not been called"); +}); + +test("getDependencies", (t) => { + const project = { + getName: () => "project", + getType: () => "type", + }; + const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getGraph: () => { + return { + getDependencies: getDependenciesStub + }; + } + }, + project + }); + + t.deepEqual(projectBuildContext.getDependencies("pony project"), ["dep a", "dep b"], "Returned correct value"); + t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once"); + t.is(getDependenciesStub.getCall(0).args[0], "pony project", + "ProjectGraph#getDependencies got called with correct arguments"); +}); + +test("getDependencies: No name provided", (t) => { + const project = { + getName: () => "project", + getType: () => "type", + }; + const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getGraph: () => { + return { + getDependencies: getDependenciesStub + }; + } + }, + project + }); + + t.deepEqual(projectBuildContext.getDependencies(), ["dep a", "dep b"], "Returned correct value"); + t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once"); + t.is(getDependenciesStub.getCall(0).args[0], "project", + "ProjectGraph#getDependencies got called with correct arguments"); +}); + +test("getTaskUtil", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: { + getName: () => "project", + getType: () => "type", + } + }); + + t.truthy(projectBuildContext.getTaskUtil(), "Returned a TaskUtil instance"); + t.is(projectBuildContext.getTaskUtil(), projectBuildContext.getTaskUtil(), "Caches TaskUtil instance"); +}); + +test.serial("getTaskRunner", async (t) => { + t.plan(3); + const project = { + getName: () => "project", + getType: () => "type", + }; + const {default: ProjectBuildLogger} = await import("@ui5/logger/internal/loggers/ProjectBuild"); + class TaskRunnerMock { + constructor(params) { + t.true(params.log instanceof ProjectBuildLogger, "TaskRunner receives an instance of ProjectBuildLogger"); + params.log = "log"; // replace log instance with string for deep comparison + t.deepEqual(params, { + graph: "graph", + project: project, + log: "log", + taskUtil: "taskUtil", + taskRepository: "taskRepository", + buildConfig: "buildConfig" + }, "TaskRunner created with expected constructor arguments"); + } + } + const ProjectBuildContext = await esmock("../../../../lib/build/helpers/ProjectBuildContext.js", { + "../../../../lib/build/TaskRunner.js": TaskRunnerMock + }); + + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getGraph: () => "graph", + getTaskRepository: () => "taskRepository", + getBuildConfig: () => "buildConfig", + }, + project + }); + + projectBuildContext.getTaskUtil = () => "taskUtil"; + + const taskRunner = projectBuildContext.getTaskRunner(); + t.is(projectBuildContext.getTaskRunner(), taskRunner, "Returns cached TaskRunner instance"); +}); + + +test.serial("createProjectContext", async (t) => { + t.plan(4); + + const project = { + getName: sinon.stub().returns("foo"), + getType: sinon.stub().returns("bar"), + }; + const taskRunner = {"task": "runner"}; + class ProjectContextMock { + constructor({buildContext, project}) { + t.is(buildContext, testBuildContext, "Correct buildContext parameter"); + t.is(project, project, "Correct project parameter"); + } + getTaskUtil() { + return "taskUtil"; + } + setTaskRunner(_taskRunner) { + t.is(_taskRunner, taskRunner); + } + } + const BuildContext = await esmock("../../../../lib/build/helpers/BuildContext.js", { + "../../../../lib/build/helpers/ProjectBuildContext.js": ProjectContextMock, + "../../../../lib/build/TaskRunner.js": { + create: sinon.stub().resolves(taskRunner) + } + }); + const graph = { + getRoot: () => ({getType: () => "library"}), + }; + const testBuildContext = new BuildContext(graph, "taskRepository"); + + const projectContext = await testBuildContext.createProjectContext({ + project + }); + + t.true(projectContext instanceof ProjectContextMock, + "Project context is an instance of ProjectContextMock"); + t.is(testBuildContext._projectBuildContexts[0], projectContext, + "BuildContext stored correct ProjectBuildContext"); +}); + +test("requiresBuild: has no build-manifest", (t) => { + const project = { + getName: sinon.stub().returns("foo"), + getType: sinon.stub().returns("bar"), + getBuildManifest: () => null + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project + }); + t.true(projectBuildContext.requiresBuild(), "Project without build-manifest requires to be build"); +}); + +test("requiresBuild: has build-manifest", (t) => { + const project = { + getName: sinon.stub().returns("foo"), + getType: sinon.stub().returns("bar"), + getBuildManifest: () => { + return { + timestamp: "2022-07-28T12:00:00.000Z" + }; + } + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project + }); + t.false(projectBuildContext.requiresBuild(), "Project with build-manifest does not require to be build"); +}); + +test.serial("getBuildMetadata", (t) => { + const project = { + getName: sinon.stub().returns("foo"), + getType: sinon.stub().returns("bar"), + getBuildManifest: () => { + return { + timestamp: "2022-07-28T12:00:00.000Z" + }; + } + }; + const getTimeStub = sinon.stub(Date.prototype, "getTime").callThrough().onFirstCall().returns(1659016800000); + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project + }); + + t.deepEqual(projectBuildContext.getBuildMetadata(), { + timestamp: "2022-07-28T12:00:00.000Z", + age: "7200 seconds" + }, "Project with build-manifest does not require to be build"); + getTimeStub.restore(); +}); + +test("getBuildMetadata: has no build-manifest", (t) => { + const project = { + getName: sinon.stub().returns("foo"), + getType: sinon.stub().returns("bar"), + getBuildManifest: () => null + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project + }); + t.is(projectBuildContext.getBuildMetadata(), null, "Project has no build manifest"); +}); diff --git a/packages/project/test/lib/build/helpers/TaskUtil.js b/packages/project/test/lib/build/helpers/TaskUtil.js new file mode 100644 index 00000000000..694cb84ed43 --- /dev/null +++ b/packages/project/test/lib/build/helpers/TaskUtil.js @@ -0,0 +1,509 @@ +import test from "ava"; +import sinon from "sinon"; +import TaskUtil from "../../../../lib/build/helpers/TaskUtil.js"; +import SpecificationVersion from "../../../../lib/specifications/SpecificationVersion.js"; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +function getSpecificationVersion(specVersion) { + return new SpecificationVersion(specVersion); +} + +const STANDARD_TAGS = Object.freeze({ + IsDebugVariant: "ui5:IsDebugVariant", + HasDebugVariant: "ui5:HasDebugVariant", + OmitFromBuildResult: "ui5:OmitFromBuildResult", + IsBundle: "ui5:IsBundle" +}); + +test("Instantiation", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + t.deepEqual(taskUtil.STANDARD_TAGS, STANDARD_TAGS, "Correct standard tags exposed"); +}); + +test("setTag", (t) => { + const setTagStub = sinon.stub(); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getResourceTagCollection: () => { + return { + setTag: setTagStub + }; + } + } + }); + + const dummyResource = {}; + taskUtil.setTag(dummyResource, "my tag", "my value"); + + t.is(setTagStub.callCount, 1, "ResourceTagCollection#setTag got called once"); + t.deepEqual(setTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied"); + t.is(setTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied"); + t.is(setTagStub.getCall(0).args[2], "my value", "Correct value parameter supplied"); +}); + +test("getTag", (t) => { + const getTagStub = sinon.stub().returns(42); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getResourceTagCollection: () => { + return { + getTag: getTagStub + }; + } + } + }); + + const dummyResource = {}; + const res = taskUtil.getTag(dummyResource, "my tag", "my value"); + + t.is(getTagStub.callCount, 1, "ResourceTagCollection#getTag got called once"); + t.deepEqual(getTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied"); + t.is(getTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied"); + t.is(res, 42, "Correct result"); +}); + +test("clearTag", (t) => { + const clearTagStub = sinon.stub(); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getResourceTagCollection: () => { + return { + clearTag: clearTagStub + }; + } + } + }); + + const dummyResource = {}; + taskUtil.clearTag(dummyResource, "my tag", "my value"); + + t.is(clearTagStub.callCount, 1, "ResourceTagCollection#clearTag got called once"); + t.deepEqual(clearTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied"); + t.is(clearTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied"); +}); + +test("setTag with resource path is not supported anymore", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const err = t.throws(() => { + taskUtil.setTag("my resource", "my tag", "my value"); + }); + t.is(err.message, + "Deprecated parameter: Since UI5 CLI 3.0, #setTag " + + "requires a resource instance. Strings are no longer accepted", + "Threw with expected error message"); +}); + +test("getTag with resource path is not supported anymore", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const err = t.throws(() => { + taskUtil.getTag("my resource", "my tag", "my value"); + }); + t.is(err.message, + "Deprecated parameter: Since UI5 CLI 3.0, #getTag " + + "requires a resource instance. Strings are no longer accepted", + "Threw with expected error message"); +}); + +test("clearTag with resource path is not supported anymore", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const err = t.throws(() => { + taskUtil.clearTag("my resource", "my tag", "my value"); + }); + t.is(err.message, + "Deprecated parameter: Since UI5 CLI 3.0, #clearTag " + + "requires a resource instance. Strings are no longer accepted", + "Threw with expected error message"); +}); + +test("isRootProject", (t) => { + const isRootProjectStub = sinon.stub().returns(true); + const taskUtil = new TaskUtil({ + projectBuildContext: { + isRootProject: isRootProjectStub + } + }); + + const res = taskUtil.isRootProject(); + + t.is(isRootProjectStub.callCount, 1, "ProjectBuildContext#isRootProject got called once"); + t.is(res, true, "Correct result"); +}); + +test("getBuildOption", (t) => { + const getOptionStub = sinon.stub().returns("Pony"); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getOption: getOptionStub + } + }); + + const res = taskUtil.getBuildOption("friend"); + + t.is(getOptionStub.callCount, 1, "ProjectBuildContext#getBuildOption got called once"); + t.is(res, "Pony", "Correct result"); +}); + +test("getProject", (t) => { + const getProjectStub = sinon.stub().returns("Pony farm!"); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getProject: getProjectStub + } + }); + + const res = taskUtil.getProject("pony farm"); + + t.is(getProjectStub.callCount, 1, "ProjectBuildContext#getProject got called once"); + t.is(getProjectStub.getCall(0).args[0], "pony farm", + "ProjectBuildContext#getProject got called with expected arguments"); + t.is(res, "Pony farm!", "Correct result"); +}); + +test("getProject: Default name", (t) => { + const getProjectStub = sinon.stub().returns("Pony farm!"); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getProject: getProjectStub + } + }); + + const res = taskUtil.getProject(); + + t.is(getProjectStub.callCount, 1, "ProjectBuildContext#getProject got called once"); + t.is(getProjectStub.getCall(0).args[0], undefined, + "ProjectBuildContext#getProject got called with no arguments"); + t.is(res, "Pony farm!", "Correct result"); +}); + +test("getProject: Resource", (t) => { + const getProjectStub = sinon.stub(); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getProject: getProjectStub + } + }); + + const mockResource = { + getProject: sinon.stub().returns("Pig farm!") + }; + const res = taskUtil.getProject(mockResource); + + t.is(getProjectStub.callCount, 0, "ProjectBuildContext#getProject has not been called"); + t.is(mockResource.getProject.callCount, 1, "Resource#getProject has been called once"); + t.is(res, "Pig farm!", "Correct result"); +}); + +test("getDependencies", (t) => { + const getDependenciesStub = sinon.stub().returns("Pony farm!"); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getDependencies: getDependenciesStub + } + }); + + const res = taskUtil.getDependencies("pony farm"); + + t.is(getDependenciesStub.callCount, 1, "ProjectBuildContext#getDependencies got called once"); + t.is(getDependenciesStub.getCall(0).args[0], "pony farm", + "ProjectBuildContext#getDependencies got called with expected arguments"); + t.is(res, "Pony farm!", "Correct result"); +}); + +test("getDependencies: Default name", (t) => { + const getDependenciesStub = sinon.stub().returns("Pony farm!"); + const taskUtil = new TaskUtil({ + projectBuildContext: { + getDependencies: getDependenciesStub + } + }); + + const res = taskUtil.getDependencies(); + + t.is(getDependenciesStub.callCount, 1, "ProjectBuildContext#getDependencies got called once"); + t.is(getDependenciesStub.getCall(0).args[0], undefined, + "ProjectBuildContext#getDependencies got called with no arguments"); + t.is(res, "Pony farm!", "Correct result"); +}); + +test("resourceFactory", (t) => { + const {resourceFactory} = new TaskUtil({ + projectBuildContext: {} + }); + t.is(typeof resourceFactory.createResource, "function", + "resourceFactory function createResource is available"); + t.is(typeof resourceFactory.createReaderCollection, "function", + "resourceFactory function createReaderCollection is available"); + t.is(typeof resourceFactory.createReaderCollectionPrioritized, "function", + "resourceFactory function createReaderCollectionPrioritized is available"); + t.is(typeof resourceFactory.createFilterReader, "function", + "resourceFactory function createFilterReader is available"); + t.is(typeof resourceFactory.createLinkReader, "function", + "resourceFactory function createLinkReader is available"); + t.is(typeof resourceFactory.createFlatReader, "function", + "resourceFactory function createFlatReader is available"); +}); + +test("registerCleanupTask", (t) => { + const registerCleanupTaskStub = sinon.stub(); + const taskUtil = new TaskUtil({ + projectBuildContext: { + registerCleanupTask: registerCleanupTaskStub + } + }); + + taskUtil.registerCleanupTask("my callback"); + + t.is(registerCleanupTaskStub.callCount, 1, "ProjectBuildContext#registerCleanupTask got called once"); + t.is(registerCleanupTaskStub.getCall(0).args[0], "my callback", "Correct callback parameter supplied"); +}); + +test("getInterface: specVersion 1.0", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("1.0")); + + t.is(interfacedTaskUtil, undefined, "no interface provided"); +}); + +test("getInterface: specVersion 2.2", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.2")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask" + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); +}); + +test("getInterface: specVersion 2.3", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.3")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask" + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); +}); + +test("getInterface: specVersion 2.4", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.4")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask" + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); +}); + +test("getInterface: specVersion 2.5", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.5")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask" + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); +}); + +test("getInterface: specVersion 2.6", (t) => { + const taskUtil = new TaskUtil({ + projectBuildContext: {} + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.6")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask" + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); +}); + +test("getInterface: specVersion 3.0", (t) => { + const getProjectStub = sinon.stub().returns({ + getSpecVersion: () => "specVersion", + getType: () => "type", + getName: () => "name", + getVersion: () => "version", + getNamespace: () => "namespace", + getRootReader: () => "rootReader", + getReader: () => "reader", + getRootPath: () => "rootPath", + getSourcePath: () => "sourcePath", + getCustomConfiguration: () => "customConfiguration", + isFrameworkProject: () => "isFrameworkProject", + getFrameworkVersion: () => "frameworkVersion", + getFrameworkName: () => "frameworkName", + getFrameworkDependencies: () => ["frameworkDependencies"], + hasBuildManifest: () => "hasBuildManifest", // Should not be exposed + }); + const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]); + + const taskUtil = new TaskUtil({ + projectBuildContext: { + getProject: getProjectStub, + getDependencies: getDependenciesStub + } + }); + + const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("3.0")); + + t.deepEqual(Object.keys(interfacedTaskUtil), [ + "STANDARD_TAGS", + "setTag", + "clearTag", + "getTag", + "isRootProject", + "registerCleanupTask", + "getProject", + "getDependencies", + "resourceFactory", + ], "Correct methods are provided"); + + t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided"); + t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided"); + t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided"); + t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided"); + t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided"); + t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided"); + t.is(typeof interfacedTaskUtil.getProject, "function", "function registerCleanupTask is provided"); + + // getProject + const interfacedProject = interfacedTaskUtil.getProject("pony"); + t.deepEqual(Object.keys(interfacedProject), [ + "getType", + "getName", + "getVersion", + "getNamespace", + "getRootReader", + "getReader", + "getRootPath", + "getSourcePath", + "getCustomConfiguration", + "isFrameworkProject", + "getFrameworkName", + "getFrameworkVersion", + "getFrameworkDependencies", + ], "Correct methods are provided"); + + t.is(interfacedProject.getType(), "type", "getType function is bound correctly"); + t.is(interfacedProject.getName(), "name", "getName function is bound correctly"); + t.is(interfacedProject.getVersion(), "version", "getVersion function is bound correctly"); + t.is(interfacedProject.getNamespace(), "namespace", "getNamespace function is bound correctly"); + t.is(interfacedProject.getRootPath(), "rootPath", "getRootPath function is bound correctly"); + t.is(interfacedProject.getRootReader(), "rootReader", "getRootReader function is bound correctly"); + t.is(interfacedProject.getSourcePath(), "sourcePath", "getSourcePath function is bound correctly"); + t.is(interfacedProject.getReader(), "reader", "getReader function is bound correctly"); + t.is(interfacedProject.getCustomConfiguration(), "customConfiguration", + "getCustomConfiguration function is bound correctly"); + t.is(interfacedProject.isFrameworkProject(), "isFrameworkProject", + "isFrameworkProject function is bound correctly"); + t.is(interfacedProject.getFrameworkVersion(), "frameworkVersion", + "getFrameworkVersion function is bound correctly"); + t.is(interfacedProject.getFrameworkName(), "frameworkName", + "getFrameworkName function is bound correctly"); + t.deepEqual(interfacedProject.getFrameworkDependencies(), ["frameworkDependencies"], + "getFrameworkDependencies function is bound correctly"); + + // getDependencies + t.deepEqual(interfacedTaskUtil.getDependencies("pony"), ["dep a", "dep b"], + "getDependencies function is available and bound correctly"); + + // resourceFactory + const resourceFactory = interfacedTaskUtil.resourceFactory; + t.is(typeof resourceFactory.createResource, "function", + "resourceFactory function createResource is available"); + t.is(typeof resourceFactory.createReaderCollection, "function", + "resourceFactory function createReaderCollection is available"); + t.is(typeof resourceFactory.createReaderCollectionPrioritized, "function", + "resourceFactory function createReaderCollectionPrioritized is available"); + t.is(typeof resourceFactory.createFilterReader, "function", + "resourceFactory function createFilterReader is available"); + t.is(typeof resourceFactory.createLinkReader, "function", + "resourceFactory function createLinkReader is available"); + t.is(typeof resourceFactory.createFlatReader, "function", + "resourceFactory function createFlatReader is available"); +}); diff --git a/packages/project/test/lib/build/helpers/composeProjectList.js b/packages/project/test/lib/build/helpers/composeProjectList.js new file mode 100644 index 00000000000..f8f58185f38 --- /dev/null +++ b/packages/project/test/lib/build/helpers/composeProjectList.js @@ -0,0 +1,326 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import {graphFromObject} from "../../../../lib/graph/graph.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); +const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f"); +const libraryGPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.g"); +const libraryDDependerPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d-depender"); + +test.beforeEach(async (t) => { + t.context.log = { + warn: sinon.stub() + }; + t.context.composeProjectList = await esmock("../../../../lib/build/helpers/composeProjectList", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("build:helpers:composeProjectList").returns(t.context.log) + } + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("_getFlattenedDependencyTree", async (t) => { + const {_getFlattenedDependencyTree} = t.context.composeProjectList; + const tree = { // Does not reflect actual dependencies in fixtures + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.e.id", + version: "1.0.0", + path: libraryEPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [{ + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }] + }] + }, { + id: "library.f.id", + version: "1.0.0", + path: libraryFPath, + dependencies: [{ + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }] + }] + }; + const graph = await graphFromObject({dependencyTree: tree}); + + t.deepEqual(await _getFlattenedDependencyTree(graph), { + "library.e": ["library.d", "library.a", "library.b", "library.c"], + "library.f": ["library.a", "library.b", "library.c"], + "library.d": ["library.a", "library.b", "library.c"], + "library.a": ["library.b", "library.c"], + "library.b": [], + "library.c": [] + }); +}); + +async function assertCreateDependencyLists(t, { + includeAllDependencies, + includeDependency, includeDependencyRegExp, includeDependencyTree, + excludeDependency, excludeDependencyRegExp, excludeDependencyTree, + defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree, + expectedIncludedDependencies, expectedExcludedDependencies, + expectedLogWarnCallCount = 0 +}) { + const tree = { // Does not reflect actual dependencies in fixtures + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.e.id", + version: "1.0.0", + path: libraryEPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [] + }, { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }] + }] + }, { + id: "library.f.id", + version: "1.0.0", + path: libraryFPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [] + }, { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }, { + id: "library.g.id", + version: "1.0.0", + path: libraryGPath, + dependencies: [{ + id: "library.d-depender.id", + version: "1.0.0", + path: libraryDDependerPath, + dependencies: [] + }] + }] + }; + + const graph = await graphFromObject({dependencyTree: tree}); + + const {includedDependencies, excludedDependencies} = await t.context.composeProjectList(graph, { + includeAllDependencies, + includeDependency, + includeDependencyRegExp, + includeDependencyTree, + excludeDependency, + excludeDependencyRegExp, + excludeDependencyTree, + defaultIncludeDependency, + defaultIncludeDependencyRegExp, + defaultIncludeDependencyTree + }); + t.deepEqual(includedDependencies, expectedIncludedDependencies, "Correct set of included dependencies"); + t.deepEqual(excludedDependencies, expectedExcludedDependencies, "Correct set of excluded dependencies"); + + t.is(t.context.log.warn.callCount, expectedLogWarnCallCount); +} + +test.serial("createDependencyLists: only includes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependency: ["library.f", "library.c"], + includeDependencyRegExp: ["^library\\.d$"], + includeDependencyTree: ["library.g"], + expectedIncludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"], + expectedExcludedDependencies: [] + }); +}); + +test.serial("createDependencyLists: only excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + excludeDependency: ["library.f", "library.c"], + excludeDependencyRegExp: ["^library\\.d$"], + excludeDependencyTree: ["library.g"], + expectedIncludedDependencies: [], + expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"] + }); +}); + +test.serial("createDependencyLists: include all + excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: [], + excludeDependency: ["library.f", "library.c"], + excludeDependencyRegExp: ["^library\\.d$"], + excludeDependencyTree: ["library.g"], + expectedIncludedDependencies: ["library.b", "library.a", "library.e"], + expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"] + }); +}); + +test.serial("createDependencyLists: include all", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: [], + excludeDependency: [], + excludeDependencyRegExp: [], + excludeDependencyTree: [], + expectedIncludedDependencies: [ + "library.d", "library.b", "library.c", + "library.d-depender", "library.a", "library.g", + "library.e", "library.f" + ], + expectedExcludedDependencies: [] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependencyTree: ["library.f"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd]$"], + expectedIncludedDependencies: ["library.b"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.a"] + }); +}); + +test.serial("createDependencyLists: excludeDependencyTree has lower priority than includes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependency: ["library.f"], + includeDependencyRegExp: ["^library\\.[acd]$"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: ["library.f", "library.d", "library.c", "library.a"], + expectedExcludedDependencies: ["library.b"] + }); +}); + +test.serial("createDependencyLists: include all, exclude tree and include single", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: ["library.f"], + includeDependencyRegExp: ["^library\\.[acd]$"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: [ + "library.f", "library.d", "library.c", "library.a", "library.d-depender", + "library.g", "library.e" + ], + expectedExcludedDependencies: ["library.b"] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has higher priority than excludeDependencyTree", + async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependencyTree: ["library.f"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: ["library.f", "library.d", "library.a", "library.b", "library.c"], + expectedExcludedDependencies: [] + }); + }); + +test.serial("createDependencyLists: defaultIncludeDependency/RegExp has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + defaultIncludeDependency: ["library.f", "library.c", "library.b"], + defaultIncludeDependencyRegExp: ["^library\\.d$"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], + expectedIncludedDependencies: ["library.b"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + }); +}); +test.serial("createDependencyLists: include all and defaultIncludeDependency/RegExp", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + defaultIncludeDependency: ["library.f", "library.c", "library.b"], + defaultIncludeDependencyRegExp: ["^library\\.d$"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], + expectedIncludedDependencies: ["library.b", "library.g", "library.e"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + }); +}); + +test.serial("createDependencyLists: defaultIncludeDependencyTree has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + defaultIncludeDependencyTree: ["library.f"], + excludeDependencyTree: ["library.a"], + expectedIncludedDependencies: ["library.f", "library.d", "library.c"], + expectedExcludedDependencies: ["library.a", "library.b"] + }); +}); + +test.serial("createDependencyLists: Could not find dependency", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependency: ["not.in.dependency.tree"], + expectedIncludedDependencies: [], + expectedExcludedDependencies: [], + expectedLogWarnCallCount: 1 + }); + t.deepEqual(t.context.log.warn.getCall(0).args, [ + `Could not find dependency "not.in.dependency.tree" for project application.a. Dependency filter is ignored` + ]); +}); diff --git a/packages/project/test/lib/build/helpers/composeTaskList.js b/packages/project/test/lib/build/helpers/composeTaskList.js new file mode 100644 index 00000000000..910994aabec --- /dev/null +++ b/packages/project/test/lib/build/helpers/composeTaskList.js @@ -0,0 +1,243 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + t.context.log = { + warn: sinon.stub() + }; + const getLoggerStub = sinon.stub().withArgs("build:helpers:composeTaskList").returns(t.context.log); + + t.context.composeTaskList = await esmock("../../../../lib/build/helpers/composeTaskList.js", { + "@ui5/logger": { + getLogger: getLoggerStub + } + }); +}); + +const allTasks = [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateVersionInfo", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateThemeDesignerResources", + "generateStandaloneAppBundle", + "generateBundle", + "generateLibraryPreload", + "generateCachebusterInfo", +]; + + +[ + [ + "composeTaskList: archive=false / selfContained=false / jsdoc=false", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: archive=true / selfContained=false / jsdoc=false", { + archive: true, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: archive=false / selfContained=true / jsdoc=false", { + archive: false, + selfContained: true, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateStandaloneAppBundle", + "generateBundle" + ] + ], + [ + "composeTaskList: archive=false / selfContained=false / jsdoc=true", { + archive: false, + selfContained: false, + jsdoc: true, + includedTasks: [], + excludedTasks: [] + }, [ + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "buildThemes", + "generateVersionInfo", + "generateBundle", + ] + ], + [ + "composeTaskList: includedTasks / excludedTasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["generateResourcesJson", "replaceVersion"], + excludedTasks: ["replaceCopyright", "generateApiIndex"] + }, [ + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: includedTasks=*", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["*"], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateVersionInfo", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateThemeDesignerResources", + "generateStandaloneAppBundle", + "generateBundle", + "generateLibraryPreload", + "generateCachebusterInfo", + ] + ], + [ + "composeTaskList: excludedTasks=*", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: ["*"] + }, [] + ], + [ + "composeTaskList: includedTasks with unknown tasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["foo", "bar"], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ], (t) => { + const {log} = t.context; + t.is(log.warn.callCount, 0); + } + ], + [ + "composeTaskList: excludedTasks with unknown tasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: ["foo", "bar"], + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ], (t) => { + const {log} = t.context; + t.is(log.warn.callCount, 0); + } + ], +].forEach(([testTitle, args, expectedTaskList, assertCb]) => { + test.serial(testTitle, (t) => { + const {composeTaskList, log} = t.context; + const taskList = composeTaskList(allTasks, args); + t.deepEqual(taskList, expectedTaskList); + if (assertCb) { + assertCb(t); + } else { + // When no cb is defined, no logs are expected + t.is(log.warn.callCount, 0); + } + }); +}); diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.integration.js b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js new file mode 100644 index 00000000000..015ca68bd1d --- /dev/null +++ b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js @@ -0,0 +1,108 @@ +import test from "ava"; +import path from "node:path"; +import createBuildManifest from "../../../../lib/build/helpers/createBuildManifest.js"; +import Module from "../../../../lib/graph/Module.js"; +import Specification from "../../../../lib/specifications/Specification.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const buildDescrApplicationAPath = + path.join(__dirname, "..", "..", "..", "fixtures", "build-manifest", "application.a"); +const applicationAConfig = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; +const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); +const buildDescrLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "build-manifest", "library.e"); +const libraryEConfig = { + id: "library.e.id", + version: "1.0.0", + modulePath: libraryEPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: {name: "library.e"} + } +}; + +const buildConfig = { + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] +}; + +// Note: The actual build-manifest.json files in the fixtures are never used in these tests + +test("Create project from application project providing a build manifest", async (t) => { + const inputProject = await Specification.create(applicationAConfig); + inputProject.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const taskRepository = { + getVersions: async () => ({a: "a", b: "b"}) + }; + + const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository); + const m = new Module({ + id: "build-descr-application.a.id", + version: "2.0.0", + modulePath: buildDescrApplicationAPath, + configuration: metadata + }); + + const {project} = await m.getSpecifications(); + t.truthy(project, "Module was able to create project from build manifest metadata"); + t.is(project.getName(), project.getName(), "Archive project has correct name"); + t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); + t.is(project.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); + t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const reader = project.getReader(); + const resources = await reader.byGlob("**/test.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/id1/test.js", + "Resource has expected path"); +}); + +test("Create project from library project providing a build manifest", async (t) => { + const inputProject = await Specification.create(libraryEConfig); + inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); + + const taskRepository = { + getVersions: async () => ({a: "a", b: "b"}) + }; + + const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository); + const m = new Module({ + id: "build-descr-library.e.id", + version: "2.0.0", + modulePath: buildDescrLibraryEPath, + configuration: metadata + }); + + const {project} = await m.getSpecifications(); + t.truthy(project, "Module was able to create project from build manifest metadata"); + t.is(project.getName(), project.getName(), "Archive project has correct name"); + t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); + t.is(project.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); + t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const reader = project.getReader(); + const resources = await reader.byGlob("**/some.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/library/e/some.js", + "Resource has expected path"); +}); diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.js b/packages/project/test/lib/build/helpers/createBuildManifest.js new file mode 100644 index 00000000000..7b2266b8897 --- /dev/null +++ b/packages/project/test/lib/build/helpers/createBuildManifest.js @@ -0,0 +1,176 @@ +import test from "ava"; +import path from "node:path"; +import semver from "semver"; +import createBuildManifest from "../../../../lib/build/helpers/createBuildManifest.js"; +import Specification from "../../../../lib/specifications/Specification.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const applicationProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const libraryProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryDPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "main/src", + test: "main/test" + } + } + }, + } +}; + +test("Missing parameter: project", async (t) => { + await t.throwsAsync(createBuildManifest(), { + message: "Missing parameter 'project'" + }); +}); + +test("Missing parameter: buildConfig", async (t) => { + const project = await Specification.create(applicationProjectInput); + + await t.throwsAsync(createBuildManifest(project), { + message: "Missing parameter 'buildConfig'" + }); +}); + +test("Missing parameter: taskRepository", async (t) => { + const project = await Specification.create(applicationProjectInput); + + await t.throwsAsync(createBuildManifest(project, "buildConfig"), { + message: "Missing parameter 'taskRepository'" + }); +}); + +test("Create application from project with build manifest", async (t) => { + const project = await Specification.create(applicationProjectInput); + project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const taskRepository = { + getVersions: async () => ({builderVersion: "", fsVersion: ""}) + }; + + const metadata = await createBuildManifest(project, "buildConfig", taskRepository); + + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; + + t.not(semver.valid(metadata.buildManifest.versions.fsVersion), null, "fs version should be filled"); + metadata.buildManifest.versions.fsVersion = ""; + + t.not(semver.valid(metadata.buildManifest.versions.projectVersion), null, "project version should be filled"); + metadata.buildManifest.versions.projectVersion = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + resources: { + configuration: { + paths: { + webapp: "resources/id1", + }, + }, + } + }, + buildManifest: { + manifestVersion: "0.2", + buildConfig: "buildConfig", + namespace: "id1", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: "", + fsVersion: "", + projectVersion: "", + builderFsVersion: "", + }, + tags: { + "/resources/id1/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + } + }, "Returned correct metadata"); +}); + +test("Create library from project with build manifest", async (t) => { + const project = await Specification.create(libraryProjectInput); + project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); + + const taskRepository = { + getVersions: async () => ({builderVersion: "", fsVersion: ""}) + }; + + const metadata = await createBuildManifest(project, "buildConfig", taskRepository); + + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; + + t.not(semver.valid(metadata.buildManifest.versions.fsVersion), null, "fs version should be filled"); + metadata.buildManifest.versions.fsVersion = ""; + + t.not(semver.valid(metadata.buildManifest.versions.projectVersion), null, "project version should be filled"); + metadata.buildManifest.versions.projectVersion = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "resources", + test: "test-resources", + }, + }, + } + }, + buildManifest: { + manifestVersion: "0.2", + buildConfig: "buildConfig", + namespace: "library/d", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: "", + fsVersion: "", + projectVersion: "", + builderFsVersion: "", + }, + tags: { + "/resources/library/d/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + } + }, "Returned correct metadata"); +}); diff --git a/packages/project/test/lib/config/Configuration.js b/packages/project/test/lib/config/Configuration.js new file mode 100644 index 00000000000..7acbadba201 --- /dev/null +++ b/packages/project/test/lib/config/Configuration.js @@ -0,0 +1,174 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.homedirStub = sinon.stub().returns("~"); + t.context.promisifyStub = sinon.stub(); + t.context.resolveStub = sinon.stub().callsFake((path) => path); + t.context.joinStub = sinon.stub().callsFake((...args) => args.join("/")); + t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", { + "node:path": { + resolve: t.context.resolveStub, + join: t.context.joinStub + }, + "node:util": { + "promisify": t.context.promisifyStub + }, + "node:os": { + "homedir": t.context.homedirStub + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Configuration); +}); + +test.serial("Configuration options", (t) => { + const {Configuration} = t.context; + t.deepEqual(Configuration.OPTIONS, [ + "mavenSnapshotEndpointUrl", + "ui5DataDir" + ]); +}); + +test.serial("Build configuration with defaults", (t) => { + const {Configuration} = t.context; + + const config = new Configuration({}); + + t.deepEqual(config.toJson(), { + mavenSnapshotEndpointUrl: undefined, + ui5DataDir: undefined, + }); +}); + +test.serial("Overwrite defaults defaults", (t) => { + const {Configuration} = t.context; + + const params = { + mavenSnapshotEndpointUrl: "https://snapshot.url", + ui5DataDir: "/custom/data/dir" + }; + + const config = new Configuration(params); + + t.deepEqual(config.toJson(), params); +}); + +test.serial("Unknown configuration option", (t) => { + const {Configuration} = t.context; + + const params = { + unknown: "foo" + }; + + t.throws(() => new Configuration(params), { + message: `Unknown configuration option 'unknown'` + }); +}); + +test.serial("Check getters", (t) => { + const {Configuration} = t.context; + + const params = { + mavenSnapshotEndpointUrl: "https://snapshot.url", + ui5DataDir: "/custom/data/dir" + }; + + const config = new Configuration(params); + + t.is(config.getMavenSnapshotEndpointUrl(), params.mavenSnapshotEndpointUrl); + t.is(config.getUi5DataDir(), params.ui5DataDir); +}); + + +test.serial("fromFile", async (t) => { + const fromFile = t.context.Configuration.fromFile; + const {promisifyStub, sinon} = t.context; + + const ui5rcContents = { + mavenSnapshotEndpointUrl: "https://snapshot.url", + ui5DataDir: "/custom/data/dir" + }; + const responseStub = sinon.stub().resolves(JSON.stringify(ui5rcContents)); + promisifyStub.callsFake(() => responseStub); + + const config = await fromFile("/custom/path/.ui5rc"); + + t.deepEqual(config.toJson(), ui5rcContents); +}); + +test.serial("fromFile: configuration file not found - fallback to default config", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const fromFile = Configuration.fromFile; + + const responseStub = sinon.stub().throws({code: "ENOENT"}); + promisifyStub.callsFake(() => responseStub); + + const config = await fromFile("/non-existing/path/.ui5rc"); + + t.is(config instanceof Configuration, true, "Created a default configuration"); + t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Default settings"); + t.is(config.getUi5DataDir(), undefined, "Default settings"); +}); + + +test.serial("fromFile: empty configuration file - fallback to default config", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const fromFile = Configuration.fromFile; + + const responseStub = sinon.stub().resolves(""); + promisifyStub.callsFake(() => responseStub); + + const config = await fromFile("/non-existing/path/.ui5rc"); + + t.is(config instanceof Configuration, true, "Created a default configuration"); + t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Default settings"); + t.is(config.getUi5DataDir(), undefined, "Default settings"); +}); + +test.serial("fromFile: throws", async (t) => { + const fromFile = t.context.Configuration.fromFile; + const {promisifyStub, sinon} = t.context; + + const responseStub = sinon.stub().throws(new Error("Error")); + promisifyStub.callsFake(() => responseStub); + + await t.throwsAsync(fromFile(), { + message: `Failed to read UI5 CLI configuration from ~/.ui5rc: Error` + }); +}); + +test.serial("toFile", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const toFile = Configuration.toFile; + + const writeStub = sinon.stub().resolves(); + promisifyStub.callsFake(() => writeStub); + + const config = new Configuration({mavenSnapshotEndpointUrl: "https://registry.corp/vendor/build-snapshots/"}); + await toFile(config, "/path/to/save/.ui5rc"); + + t.deepEqual( + writeStub.getCall(0).args, + ["/path/to/save/.ui5rc", JSON.stringify(config.toJson())], + "Write config to path" + ); +}); + +test.serial("toFile: throws", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const toFile = Configuration.toFile; + + const responseStub = sinon.stub().throws(new Error("Error")); + promisifyStub.callsFake(() => responseStub); + + await t.throwsAsync(toFile(new Configuration({})), { + message: "Failed to write UI5 CLI configuration to ~/.ui5rc: Error" + }); +}); diff --git a/packages/project/test/lib/graph/Module.js b/packages/project/test/lib/graph/Module.js new file mode 100644 index 00000000000..c87aac18542 --- /dev/null +++ b/packages/project/test/lib/graph/Module.js @@ -0,0 +1,509 @@ +import test from "ava"; +import sinon from "sinon"; +import path from "node:path"; +import Module from "../../../lib/graph/Module.js"; + +const __dirname = import.meta.dirname; + +const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); +const applicationAPath = path.join(fixturesPath, "application.a"); +const buildDescriptionApplicationAPath = + path.join(fixturesPath, "build-manifest", "application.a"); +const buildDescriptionLibraryAPath = + path.join(fixturesPath, "build-manifest", "library.e"); +const applicationHPath = path.join(fixturesPath, "application.h"); +const collectionPath = path.join(fixturesPath, "collection"); +const themeLibraryEPath = path.join(fixturesPath, "theme.library.e"); + +const basicModuleInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath +}; +const archiveAppProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: buildDescriptionApplicationAPath +}; + +const archiveLibProjectInput = { + id: "library.e.id", + version: "1.0.0", + modulePath: buildDescriptionLibraryAPath +}; + +test("Instantiate a basic module", (t) => { + const ui5Module = new Module(basicModuleInput); + t.is(ui5Module.getId(), "application.a.id", "Should return correct ID"); + t.is(ui5Module.getVersion(), "1.0.0", "Should return correct version"); + t.is(ui5Module.getPath(), applicationAPath, "Should return correct module path"); +}); + +test("Create module with missing id", (t) => { + t.throws(() => { + new Module({ + version: "1.0.0", + modulePath: "/module/path" + }); + }, { + message: "Could not create Module: Missing or empty parameter 'id'" + }); +}); + +test("Create module with missing version", (t) => { + t.throws(() => { + new Module({ + id: "application.a.id", + modulePath: "/module/path" + }); + }, { + message: "Could not create Module: Missing or empty parameter 'version'" + }); +}); + +test("Create module with missing modulePath", (t) => { + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + }); + }, { + message: "Could not create Module: Missing or empty parameter 'modulePath'" + }); +}); + +test("Create module with relative modulePath", (t) => { + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: "module/path" + }); + }, { + message: "Could not create Module: Parameter 'modulePath' must contain an absolute path" + }); +}); + +test("Access module root resources via reader", async (t) => { + const ui5Module = new Module(basicModuleInput); + const rootReader = ui5Module.getReader(); + const packageJsonResource = await rootReader.byPath("/package.json"); + t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); +}); + +test("Get specifications from module", async (t) => { + const ui5Module = new Module(basicModuleInput); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "application.a", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Get specifications from application project with build manifest", async (t) => { + const ui5Module = new Module(archiveAppProjectInput); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "application.a", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Get specifications from library project with build manifest", async (t) => { + const ui5Module = new Module(archiveLibProjectInput); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "library.e", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Use configuration from object", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a-object" + }, + customConfiguration: { + configurationTest: true + } + } + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "application.a-object", "Used name from config object"); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true + }, "Provided configuration is available"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Use configuration from array of objects", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: [{ + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + configurationTest: true + } + }, { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "my-project-shim" + }, + shims: {} + }] + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true + }, "Provided configuration is available"); + t.is(extensions.length, 1, "Should return one extension"); +}); + +test("Use configuration from configPath", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-configPath.yaml" + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configPathTest: true + }, "Provided configuration is available"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Use configuration from absolute configPath", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: path.join(applicationAPath, "ui5-test-configPath.yaml") + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configPathTest: true + }, "Provided configuration is available"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("configuration and configPath must not be provided together", (t) => { + // 'configuration' as object + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "test-ui5.yaml", + configuration: { + test: "configuration" + } + }); + }, { + message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'" + }); + // 'configuration' as array + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "test-ui5.yaml", + configuration: [{ + test: "configuration" + }] + }); + }, { + message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'" + }); +}); + +test("Use configuration from project shim", async (t) => { + const getProjectConfigurationShimsStub = sinon.stub().returns([{ + name: "shim-1", + shim: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.h" + }, + customConfiguration: { + configurationTest: true + } + } + }]); + + const ui5Module = new Module({ + id: "application.h.id", + version: "1.0.0", + modulePath: applicationHPath, + configuration: [], + shimCollection: { + getProjectConfigurationShims: getProjectConfigurationShimsStub + } + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(getProjectConfigurationShimsStub.callCount, 1, "Should request configuration shims from collection"); + t.is(getProjectConfigurationShimsStub.getCall(0).args[0], "application.h.id", + "Should request configuration shims for correct module ID"); + t.truthy(project, "Should create a project form shim configuration"); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true + }); + t.is(extensions.length, 0, "Should return no extension"); +}); + +test("Extend configuration via shim", async (t) => { + const getProjectConfigurationShimsStub = sinon.stub().returns([{ + name: "shim-1", + shim: { + customConfiguration: { // Overwrites whole object since merge is done with Object.assign + overwriteConfigurationTest: true + } + } + }]); + + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a-object" + }, + customConfiguration: { + configurationTest: true + } + }, + shimCollection: { + getProjectConfigurationShims: getProjectConfigurationShimsStub + } + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + overwriteConfigurationTest: true + }, "Provided configuration is available"); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("Module is a collection", async (t) => { + const ui5Module = new Module({ + id: "collection.a", + version: "1.0.0", + modulePath: collectionPath, + configuration: [{ + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a-object" + }, + customConfiguration: { + configurationTest: true + } + }, { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "collection-shim" + }, + shims: { + collections: { + "collection.a": { + modules: { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c", + } + } + } + } + }] + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.falsy(project, "Should ignore the project since the shim defines the module itself as a collection"); + t.is(extensions.length, 1, "Should return one extensions"); + t.deepEqual(extensions[0].getCollectionShims(), { + "collection.a": { + modules: { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c", + } + } + }, "Collection shim configured correctly"); +}); + +test("Module can't define config shim for itself", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: [{ + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "my-project-shim" + }, + shims: { + configurations: { + "application.a.id": { + customConfiguration: { + overwriteConfigurationTest: true + } + } + } + } + }, { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a-object" + }, + customConfiguration: { + configurationTest: true + } + }] + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true // Shim has not been applied + }, "Provided configuration is available"); + t.is(extensions.length, 1, "Should return one extension"); +}); + +test("Legacy patches are applied", async (t) => { + async function testLegacyLibrary(libraryName) { + const ui5Module = new Module({ + id: "legacy-theme-library.e.id", + version: "1.0.0", + modulePath: themeLibraryEPath, + configuration: { + specVersion: "2.6", // should not matter + type: "library", // legacy config for theme-libraries + metadata: { + name: libraryName + } + } + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), libraryName, "Used name from config object"); + t.is(project.getType(), "theme-library", "Project type got patched correctly"); + t.is(extensions.length, 0, "Should return no extensions"); + } + + await Promise.all( + ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"] + .map(testLegacyLibrary)); +}); + +test("Invalid configuration in file", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-error.yaml" + }); + const err = await t.throwsAsync(ui5Module.getSpecifications()); + + t.true(err.message.includes("Invalid ui5.yaml configuration"), "Threw with validation error"); + // Check that config file name is referenced. This validates that the error was not produced by + // the Specification instance but the Module + t.true(err.message.includes("ui5-test-error.yaml"), "Error message references file name"); + t.truthy(err.yaml, "Error object contains yaml information"); +}); + +test("Corrupt configuration in file", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-corrupt.yaml" + }); + const err = await t.throwsAsync(ui5Module.getSpecifications()); + + t.regex(err.message, + new RegExp("^Failed to parse configuration for project application.a.id at 'ui5-test-corrupt.yaml'.*"), + "Threw with parsing error"); +}); + +test("Empty configuration in file", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-empty.yaml" + }); + const res = await ui5Module.getSpecifications(); + + t.deepEqual(res, { + project: null, + extensions: [] + }, "Returned no project or extensions"); +}); + +test("No configuration", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: fixturesPath, // does not contain a ui5.yaml + }); + const res = await ui5Module.getSpecifications(); + + t.deepEqual(res, { + project: null, + extensions: [] + }, "Returned no project or extensions"); +}); + +test("Incorrect config path", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-does-not-exist.yaml" + }); + const err = await t.throwsAsync(ui5Module.getSpecifications()); + + t.is(err.message, + "Failed to read configuration for module application.a.id: " + + "Could not find configuration file in module at path 'ui5-does-not-exist.yaml'", + "Threw with expected error message"); +}); + +test("Incorrect absolute config path", async (t) => { + const configPath = path.join(applicationAPath, "ui5-does-not-exist.yaml"); + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath + }); + const err = await t.throwsAsync(ui5Module.getSpecifications()); + + t.true(err.message.startsWith( + `Failed to read configuration for module application.a.id at '${configPath}'. Error:`), + "Threw with expected error message"); +}); + +test("Module without ui5.yaml is ignored", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationHPath + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.falsy(project, "Should return no project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); diff --git a/packages/project/test/lib/graph/ProjectGraph.js b/packages/project/test/lib/graph/ProjectGraph.js new file mode 100644 index 00000000000..46295f96723 --- /dev/null +++ b/packages/project/test/lib/graph/ProjectGraph.js @@ -0,0 +1,1392 @@ +import path from "node:path"; +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import Specification from "../../../lib/specifications/Specification.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); + +async function createProject(name) { + return await Specification.create({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name} + } + }); +} + +async function createExtension(name) { + return await Specification.create({ + id: "extension.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "extension", + type: "task", + task: {}, + metadata: {name} + } + }); +} + +function traverseBreadthFirst(...args) { + return _traverse(...args, true); +} + +function traverseDepthFirst(...args) { + return _traverse(...args, false); +} + +async function _traverse(t, graph, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await graph.traverseBreadthFirst(callbackStub); + } else { + await graph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); +} + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + warn: sinon.stub(), + verbose: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + isLevelEnabled: () => true + }; + + t.context.ProjectGraph = await esmock.p("../../../lib/graph/ProjectGraph.js", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("graph:ProjectGraph").returns(t.context.log) + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.ProjectGraph); +}); + +test("Instantiate a basic project graph", (t) => { + const {ProjectGraph} = t.context; + t.notThrows(() => { + new ProjectGraph({ + rootProjectName: "my root project" + }); + }, "Should not throw"); +}); + +test("Instantiate a basic project with missing parameter rootProjectName", (t) => { + const {ProjectGraph} = t.context; + const error = t.throws(() => { + new ProjectGraph({}); + }); + t.is(error.message, "Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'", + "Should throw with expected error message"); +}); + +test("getRoot", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "application.a" + }); + const project = await createProject("application.a"); + graph.addProject(project); + const res = graph.getRoot(); + t.is(res, project, "Should return correct root project"); +}); + +test("getRoot: Root not added to graph", (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "application.a" + }); + + const error = t.throws(() => { + graph.getRoot(); + }); + t.is(error.message, + "Unable to find root project with name application.a in project graph", + "Should throw with expected error message"); +}); + +test("add-/getProject", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project = await createProject("application.a"); + graph.addProject(project); + const res = graph.getProject("application.a"); + t.is(res, project, "Should return correct project"); +}); + +test("addProject: Add duplicate", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.a"); + const error = t.throws(() => { + graph.addProject(project2); + }); + t.is(error.message, + "Failed to add project application.a to graph: A project with that name has already been added. " + + "This might be caused by multiple modules containing projects with the same name", + "Should throw with expected error message"); + + const res = graph.getProject("application.a"); + t.is(res, project1, "Should return correct project"); +}); + +test("addProject: Add project with integer-like name", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project = await createProject("1337"); + + const error = t.throws(() => { + graph.addProject(project); + }); + t.is(error.message, + "Failed to add project 1337 to graph: Project name must not be integer-like", + "Should throw with expected error message"); +}); + +test("getProject: Project is not in graph", (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const res = graph.getProject("application.a"); + t.is(res, undefined, "Should return undefined"); +}); + +test("getProjects", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.b"); + graph.addProject(project2); + + const res = graph.getProjects(); + t.deepEqual(Array.from(res), [ + project1, project2 + ], "Should return an iterable for all projects"); +}); + +test("getProjectNames", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.b"); + graph.addProject(project2); + + const res = graph.getProjectNames(); + t.deepEqual(res, [ + "application.a", "application.b" + ], "Should return all project names in a flat array"); +}); + +test("getSize", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.b"); + graph.addProject(project2); + + // Extensions should not influence graph size + const extension1 = await createExtension("extension.a"); + graph.addExtension(extension1); + + t.is(graph.getSize(), 2, "Should return correct project count"); +}); + +test("add-/getExtension", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension = await createExtension("extension.a"); + graph.addExtension(extension); + const res = graph.getExtension("extension.a"); + t.is(res, extension, "Should return correct extension"); +}); + +test("addExtension: Add duplicate", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension1 = await createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = await createExtension("extension.a"); + const error = t.throws(() => { + graph.addExtension(extension2); + }); + t.is(error.message, + "Failed to add extension extension.a to graph: An extension with that name has already been added. " + + "This might be caused by multiple modules containing extensions with the same name", + "Should throw with expected error message"); + + const res = graph.getExtension("extension.a"); + t.is(res, extension1, "Should return correct extension"); +}); + +test("addExtension: Add extension with integer-like name", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension = await createExtension("1337"); + + const error = t.throws(() => { + graph.addExtension(extension); + }); + t.is(error.message, + "Failed to add extension 1337 to graph: Extension name must not be integer-like", + "Should throw with expected error message"); +}); + +test("getExtension: Project is not in graph", (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const res = graph.getExtension("extension.a"); + t.is(res, undefined, "Should return undefined"); +}); + +test("getExtensions", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension1 = await createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = await createExtension("extension.b"); + graph.addExtension(extension2); + const res = graph.getExtensions(); + t.deepEqual(Array.from(res), [ + extension1, extension2 + ], "Should return an iterable for all extensions"); +}); + +test("declareDependency / getDependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + t.deepEqual(graph.getDependencies("library.a"), [ + "library.b" + ], "Should store and return correct dependencies for library.a"); + t.deepEqual(graph.getDependencies("library.b"), [], + "Should store and return correct dependencies for library.b"); + + graph.declareDependency("library.b", "library.a"); + + t.deepEqual(graph.getDependencies("library.a"), [ + "library.b" + ], "Should store and return correct dependencies for library.a"); + t.deepEqual(graph.getDependencies("library.b"), [ + "library.a" + ], "Should store and return correct dependencies for library.b"); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); + + t.is(graph.isOptionalDependency("library.b", "library.a"), false, + "Should declare dependency as non-optional"); +}); + +test("getTransitiveDependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + graph.addProject(await createProject("library.e")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.d", "library.e"); + + t.deepEqual(graph.getTransitiveDependencies("library.a"), [ + "library.b", + "library.c", + "library.d", + "library.e", + ], "Should store and return correct transitive dependencies for library.a"); +}); + +test("getTransitiveDependencies: Unknown project", (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + + const error = t.throws(() => { + graph.getTransitiveDependencies("library.x"); + }); + t.is(error.message, + "Failed to get transitive dependencies for project library.x: Unable to find project in project graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Unknown source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.b")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.b"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.b: Unable " + + "to find depending project with name library.a in project graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Unknown target", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.b"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.b: Unable " + + "to find dependency project with name library.b in project graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Same target as source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.a"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.a: " + + "A project can't depend on itself", + "Should throw with expected error message"); +}); + +test("declareDependency: Already declared", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 1, "log.warn should be called once"); + t.is(log.warn.getCall(0).args[0], + `Dependency has already been declared: library.a depends on library.b`, + "log.warn should be called once with the expected argument"); +}); + +test("declareDependency: Already declared as optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 1, "log.warn should be called once"); + t.is(log.warn.getCall(0).args[0], + `Dependency has already been declared: library.a depends on library.b`, + "log.warn should be called once with the expected argument"); + + t.is(graph.isOptionalDependency("library.a", "library.b"), true, + "Should declare dependency as optional"); +}); + +test("declareDependency: Already declared as non-optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + graph.declareOptionalDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 0, "log.warn should not be called"); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); +}); + +test("declareDependency: Already declared as optional, now non-optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 0, "log.warn should not be called"); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); +}); + +test("getDependencies: Project without dependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + + graph.addProject(await createProject("library.a")); + + t.deepEqual(graph.getDependencies("library.a"), [], + "Should return an empty array for project without dependencies"); +}); + +test("getDependencies: Unknown project", (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + + const error = t.throws(() => { + graph.getDependencies("library.x"); + }); + t.is(error.message, + "Failed to get dependencies for project library.x: Unable to find project in project graph", + "Should throw with expected error message"); +}); + +test("resolveOptionalDependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.d", "library.b"); + graph.declareDependency("library.d", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "library.a should have no optional dependency to library.b anymore"); + + t.is(graph.isOptionalDependency("library.a", "library.c"), false, + "library.a should have no optional dependency to library.c anymore"); + + t.false(graph._hasUnresolvedOptionalDependencies, + "Graph has no unresolved optional dependencies"); + + await traverseDepthFirst(t, graph, [ + "library.b", + "library.c", + "library.d", + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Optional dependency has not been resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.a", "library.d"); + + await graph.resolveOptionalDependencies(); + + t.true(graph.isOptionalDependency("library.a", "library.b"), + "Dependency from library.a to library.b should still be optional"); + + t.true(graph.isOptionalDependency("library.a", "library.c"), + "Dependency from library.a to library.c should still be optional"); + + await traverseDepthFirst(t, graph, [ + "library.d", + "library.a" + ]); + + t.true(graph._hasUnresolvedOptionalDependencies, + "Graph still has unresolved optional dependencies"); + + // Make library.c resolvable through library.d + graph.declareDependency("library.d", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.true(graph.isOptionalDependency("library.a", "library.b"), + "Dependency from library.a to library.b should still be optional"); + + t.false(graph.isOptionalDependency("library.a", "library.c"), + "Dependency from library.a to library.c should be resolved now"); + + t.true(graph._hasUnresolvedOptionalDependencies, + "Graph still has unresolved optional dependencies"); + + await traverseDepthFirst(t, graph, [ + "library.c", + "library.d", + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Dependency of optional dependency has not been resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.b"), true, + "Dependency from library.a to library.b should still be optional"); + + t.is(graph.isOptionalDependency("library.a", "library.c"), true, + "Dependency from library.a to library.c should still be optional"); + + await traverseDepthFirst(t, graph, [ + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Cyclic optional dependency is not resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.c", "library.b"); + graph.declareOptionalDependency("library.b", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.b", "library.c"), true, + "Dependency from library.b to library.c should still be optional"); + + t.true(graph._hasUnresolvedOptionalDependencies, + "Graph still has unresolved optional dependencies"); + + await traverseDepthFirst(t, graph, [ + "library.b", + "library.c", + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Resolves transitive optional dependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + graph.declareOptionalDependency("library.a", "library.d"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.c"), false, + "Dependency from library.a to library.c should not be optional anymore"); + + t.is(graph.isOptionalDependency("library.a", "library.d"), false, + "Dependency from library.a to library.d should not be optional anymore"); + + t.false(graph._hasUnresolvedOptionalDependencies, + "Graph has no unresolved optional dependencies"); + + await traverseDepthFirst(t, graph, [ + "library.d", + "library.c", + "library.b", + "library.a" + ]); +}); + +test("traverseBreadthFirst: Async", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => { + return new Promise((resolve, reject) => { + setTimeout(() => { + t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise"); + resolve(); + }, 100); + }); + }); + await graph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + ], "Traversed graph in correct order, starting with library.a"); +}); + +test("traverseBreadthFirst: Sync", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + const callbackStub = t.context.sinon.stub().returns(); + await graph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + ], "Traversed graph in correct order, starting with library.a"); +}); + +test("traverseBreadthFirst: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); +}); + +test("traverseBreadthFirst: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*", + "Should throw with expected error message"); +}); + +test("traverseBreadthFirst: No cycle when visited breadth first", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.b"); + + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); +}); + +test("traverseBreadthFirst: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + + const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {})); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.a in project graph", + "Should throw with expected error message"); +}); + +test("traverseBreadthFirst: Custom start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseBreadthFirst("library.b", callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.b", + "library.c" + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseBreadthFirst: dependencies parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 3, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + const dependencies = callbackStub.getCalls().map((call) => { + return call.args[0].dependencies; + }); + + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + "library.c" + ], "Traversed graph in correct order"); + + t.deepEqual(dependencies, [ + ["library.b", "library.c"], + ["library.c"], + [] + ], "Provided correct dependencies for each visited project"); +}); + +test("traverseBreadthFirst: Dependency declaration order is followed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + await traverseBreadthFirst(t, graph1, [ + "library.a", + "library.b", + "library.c", + "library.d" + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph2.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.b")); + graph2.addProject(await createProject("library.c")); + graph2.addProject(await createProject("library.d")); + + graph2.declareDependency("library.a", "library.d"); + graph2.declareDependency("library.a", "library.c"); + graph2.declareDependency("library.a", "library.b"); + + await traverseBreadthFirst(t, graph2, [ + "library.a", + "library.d", + "library.c", + "library.b" + ]); +}); + +test("traverseDepthFirst: Async", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => { + return new Promise((resolve, reject) => { + setTimeout(() => { + t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise"); + resolve(); + }, 100); + }); + }); + await graph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.b", + "library.a", + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseDepthFirst: Sync", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + const callbackStub = t.context.sinon.stub().returns(); + await graph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.b", + "library.a", + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseDepthFirst: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await traverseDepthFirst(t, graph, [ + "library.c", + "library.b", + "library.a" + ]); +}); + +test("traverseDepthFirst: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Cycle which does not occur in BFS", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.b"); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: library.a -> *library.b* -> library.c -> *library.b*", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.a in project graph", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Custom start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseDepthFirst("library.b", callbackStub); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.c", + "library.b" + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseDepthFirst: dependencies parameter", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 3, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + const dependencies = callbackStub.getCalls().map((call) => { + return call.args[0].dependencies; + }); + + t.deepEqual(callbackCalls, [ + "library.c", + "library.b", + "library.a", + ], "Traversed graph in correct order"); + + t.deepEqual(dependencies, [ + [], + ["library.c"], + ["library.b", "library.c"], + ], "Provided correct dependencies for each visited project"); +}); + +test("traverseDepthFirst: Dependency declaration order is followed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + await traverseDepthFirst(t, graph1, [ + "library.b", + "library.c", + "library.d", + "library.a", + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph2.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.b")); + graph2.addProject(await createProject("library.c")); + graph2.addProject(await createProject("library.d")); + + graph2.declareDependency("library.a", "library.d"); + graph2.declareDependency("library.a", "library.c"); + graph2.declareDependency("library.a", "library.b"); + + await traverseDepthFirst(t, graph2, [ + "library.d", + "library.c", + "library.b", + "library.a", + ]); +}); + +test("join", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + const extensionA = await createExtension("extension.a"); + graph1.addExtension(extensionA); + + graph2.addProject(await createProject("theme.a")); + graph2.addProject(await createProject("theme.b")); + graph2.addProject(await createProject("theme.c")); + graph2.addProject(await createProject("theme.d")); + graph2.addProject(await createProject("theme.e")); + + graph2.declareDependency("theme.a", "theme.d"); + graph2.declareDependency("theme.a", "theme.c"); + graph2.declareDependency("theme.b", "theme.a"); // This causes theme.b to not appear + graph2.declareOptionalDependency("theme.a", "theme.e"); + + const extensionB = await createExtension("extension.b"); + graph2.addExtension(extensionB); + graph1.join(graph2); + + t.true(graph1._hasUnresolvedOptionalDependencies, + "Graph has unresolved optional dependencies taken over from graph2"); + + graph1.declareDependency("library.d", "theme.a"); + graph1.declareDependency("library.d", "theme.e"); + + graph1.resolveOptionalDependencies(); + + await traverseDepthFirst(t, graph1, [ + "library.b", + "library.c", + "theme.d", + "theme.c", + "theme.e", + "theme.a", + "library.d", + "library.a", + ]); + + t.is(graph1.getExtension("extension.a"), extensionA, "Should return correct extension"); + t.is(graph1.getExtension("extension.b"), extensionB, "Should return correct joined extension"); + + // graph2 remained unmodified + await traverseDepthFirst(t, graph2, [ + "theme.d", + "theme.c", + "theme.a", + ]); +}); + +test("join: Preserves hasUnresolvedOptionalDependencies flag", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.declareOptionalDependency("library.a", "library.b"); + + graph1.join(graph2); + + t.true(graph1._hasUnresolvedOptionalDependencies, + "graph1 still has unresolved optional dependencies"); + t.false(graph2._hasUnresolvedOptionalDependencies, + "graph2 still does not have unresolved optional dependencies"); +}); + +test("join: Seals incoming graph", (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + + + const sealSpy = t.context.sinon.spy(graph2, "seal"); + graph1.join(graph2); + + t.is(sealSpy.callCount, 1, "Should call seal() on incoming graph once"); +}); + +test("join: Incoming graph already sealed", (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + + graph2.seal(); + const sealSpy = t.context.sinon.spy(graph2, "seal"); + graph1.join(graph2); + + t.is(sealSpy.callCount, 0, "Should not call seal() on incoming graph"); +}); + +test("join: Unexpected project intersection", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + `Failed to join project graph with root project 😼 into project graph with root ` + + `project 😹: Failed to merge map: Key 'library.a' already present in target set`, + "Should throw with expected error message"); +}); + +test("join: Unexpected extension intersection", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addExtension(await createExtension("extension.a")); + graph2.addExtension(await createExtension("extension.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + `Failed to join project graph with root project 😼 into project graph with root ` + + `project 😹: Failed to merge map: Key 'extension.a' already present in target set`, + "Should throw with expected error message"); +}); + + +test("Seal/isSealed", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareOptionalDependency("library.c", "library.a"); + + graph.addExtension(await createExtension("extension.a")); + + t.is(graph.isSealed(), false, "Graph should not be sealed"); + // Seal it + graph.seal(); + t.is(graph.isSealed(), true, "Graph should be sealed"); + + const expectedSealMsg = "Project graph with root node library.a has been sealed and is read-only"; + + const libX = await createProject("library.x"); + t.throws(() => { + graph.addProject(libX); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.declareDependency("library.c", "library.b"); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.declareOptionalDependency("library.b", "library.a"); + }, { + message: expectedSealMsg + }); + const extB = await createExtension("extension.b"); + t.throws(() => { + graph.addExtension(extB); + }, { + message: expectedSealMsg + }); + await t.throwsAsync(graph.resolveOptionalDependencies(), { + message: expectedSealMsg + }); + + + const graph2 = new ProjectGraph({ + rootProjectName: "library.x" + }); + t.throws(() => { + graph.join(graph2); + }, { + message: + `Failed to join project graph with root project library.x into project graph ` + + `with root project library.a: ${expectedSealMsg}` + }); + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); + + await traverseDepthFirst(t, graph, [ + "library.c", + "library.b", + "library.a", + ]); + + const project = graph.getProject("library.x"); + t.is(project, undefined, "library.x should not be added"); + + const extension = graph.getExtension("extension.b"); + t.is(extension, undefined, "extension.b should not be added"); +}); diff --git a/packages/project/test/lib/graph/ShimCollection.js b/packages/project/test/lib/graph/ShimCollection.js new file mode 100644 index 00000000000..05e6b881d32 --- /dev/null +++ b/packages/project/test/lib/graph/ShimCollection.js @@ -0,0 +1,81 @@ +import test from "ava"; +import ShimCollection from "../../../lib/graph/ShimCollection.js"; + +test("Add shims", (t) => { + const collection = new ShimCollection(); + collection.addProjectShim({ + getName: () => "shim-1", + getConfigurationShims: () => { + return { + "module-1": "configuration shim 1-1", + "module-2": "configuration shim 2-1" + }; + }, + getDependencyShims: () => { + return { + "module-1": ["dependency shim 1-1"], + "module-2": ["dependency shim 2-1"] + }; + }, + getCollectionShims: () => { + return { + "module-1": "collection shim 1-1", + "module-2": "collection shim 2-1" + }; + }, + }); + collection.addProjectShim({ + getName: () => "shim-2", + getConfigurationShims: () => { + return { + "module-1": "configuration shim 1-2", + "module-2": "configuration shim 2-2" + }; + }, + getDependencyShims: () => { + return { + "module-1": ["dependency shim 1-2"], + "module-2": ["dependency shim 2-2"] + }; + }, + getCollectionShims: () => { + return { + "module-1": "collection shim 1-2", + "module-2": "collection shim 2-2" + }; + }, + }); + + t.deepEqual(collection.getProjectConfigurationShims("module-1"), [{ + name: "shim-1", + shim: "configuration shim 1-1", + }, { + name: "shim-2", + shim: "configuration shim 1-2", + }], "Returns correct project configuration shims for module-1"); + + t.deepEqual(collection.getCollectionShims("module-2"), [{ + name: "shim-1", + shim: "collection shim 2-1", + }, { + name: "shim-2", + shim: "collection shim 2-2", + }], "Returns correct collection shims for module-2"); + + t.deepEqual(collection.getAllDependencyShims(), { + "module-1": [{ + name: "shim-1", + shim: ["dependency shim 1-1"], + }, { + name: "shim-2", + shim: ["dependency shim 1-2"], + }], + "module-2": [{ + name: "shim-1", + shim: ["dependency shim 2-1"], + }, { + name: "shim-2", + shim: ["dependency shim 2-2"], + }] + }, "Returns correct dependency shims"); +}); diff --git a/packages/project/test/lib/graph/Workspace.js b/packages/project/test/lib/graph/Workspace.js new file mode 100644 index 00000000000..6e5b586e138 --- /dev/null +++ b/packages/project/test/lib/graph/Workspace.js @@ -0,0 +1,475 @@ +import path from "node:path"; +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import Module from "../../../lib/graph/Module.js"; + +const __dirname = import.meta.dirname; +const libraryD = path.join(__dirname, "..", "..", "fixtures", "library.d"); +const libraryE = path.join(__dirname, "..", "..", "fixtures", "library.e"); +const collectionLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a"); +const collectionLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b"); +const collectionLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection", "library.c"); +const collectionBLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.a"); +const collectionBLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.b"); +const collectionBLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.c"); + +function createWorkspaceConfig({dependencyManagement}) { + return { + specVersion: "workspace/1.0", + metadata: { + name: "workspace-name" + }, + dependencyManagement + }; +} + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + warn: sinon.stub(), + verbose: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + isLevelEnabled: () => true + }; + + t.context.Workspace = await esmock("../../../lib/graph/Workspace.js", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("graph:Workspace").returns(t.context.log) + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.ProjectGraph); +}); + +test("Basic resolution", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d" + }, { + path: "../../fixtures/library.e" + }] + } + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d", "library.e"], "Correct project name keys"); + + const libE = projectNameMap.get("library.e"); + t.true(libE instanceof Module, "library.e value is instance of Module"); + t.is(libE.getVersion(), "1.0.0", "Correct version for library.e"); + t.is(libE.getPath(), libraryE, "Correct path for library.e"); + + const libD = projectNameMap.get("library.d"); + t.true(libD instanceof Module, "library.d value is instance of Module"); + t.is(libD.getVersion(), "1.0.0", "Correct version for library.d"); + t.is(libD.getPath(), libraryD, "Correct path for library.d"); + + t.is(await workspace.getModuleByProjectName("library.d"), libD, + "getModuleByProjectName returns correct module for library.d"); + t.is(await workspace.getModuleByNodeId("library.d"), libD, + "getModuleByNodeId returns correct module for library.d"); + + const modules = await workspace.getModules(); + t.deepEqual(modules, [libD, libE], "getModules returns modules sorted by module ID"); + + t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.d", "library.e"], "Correct module ID keys"); + moduleIdMap.forEach((value, key) => { + t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`); + }); +}); + +test("Basic resolution: package.json is missing name field", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d" + }, { + path: "../../fixtures/library.e" + }] + } + }) + }); + + t.context.sinon.stub(workspace, "_readPackageJson") + .resolves({ + version: "1.0.0", + }); + + const err = await t.throwsAsync(workspace._getResolvedModules()); + t.is(err.message, + `Failed to resolve workspace dependency resolution path ` + + `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`, + "Threw with expected error message"); +}); + +test("Basic resolution: package.json is missing version field", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d" + }, { + path: "../../fixtures/library.e" + }] + } + }) + }); + + t.context.sinon.stub(workspace, "_readPackageJson") + .resolves({ + name: "Package", + }); + + const err = await t.throwsAsync(workspace._getResolvedModules()); + t.is(err.message, + `Failed to resolve workspace dependency resolution path ` + + `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`, + "Threw with expected error message"); +}); + +test("Package workspace resolution: Static patterns", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/collection" + }] + } + }) + }); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c"], + "Correct project name keys"); + + const libA = projectNameMap.get("library.a"); + t.true(libA instanceof Module, "library.a value is instance of Module"); + t.is(libA.getVersion(), "1.0.0", "Correct version for library.a"); + t.is(libA.getPath(), collectionLibraryA, "Correct path for library.a"); + + const libB = projectNameMap.get("library.b"); + t.true(libB instanceof Module, "library.b value is instance of Module"); + t.is(libB.getVersion(), "1.0.0", "Correct version for library.b"); + t.is(libB.getPath(), collectionLibraryB, "Correct path for library.b"); + + const libC = projectNameMap.get("library.c"); + t.true(libC instanceof Module, "library.c value is instance of Module"); + t.is(libC.getVersion(), "1.0.0", "Correct version for library.c"); + t.is(libC.getPath(), collectionLibraryC, "Correct path for library.c"); + + t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c"], + "Correct module ID keys"); + moduleIdMap.forEach((value, key) => { + t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`); + }); +}); + +test("Package workspace resolution: Dynamic patterns", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/collection.b" + }] + } + }) + }); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"], + "Correct project name keys"); + + const libA = projectNameMap.get("library.a"); + t.true(libA instanceof Module, "library.a value is instance of Module"); + t.is(libA.getVersion(), "1.0.0", "Correct version for library.a"); + t.is(libA.getPath(), collectionBLibraryA, "Correct path for library.a"); + + const libB = projectNameMap.get("library.b"); + t.true(libB instanceof Module, "library.b value is instance of Module"); + t.is(libB.getVersion(), "1.0.0", "Correct version for library.b"); + t.is(libB.getPath(), collectionBLibraryB, "Correct path for library.b"); + + const libC = projectNameMap.get("library.c"); + t.true(libC instanceof Module, "library.c value is instance of Module"); + t.is(libC.getVersion(), "1.0.0", "Correct version for library.c"); + t.is(libC.getPath(), collectionBLibraryC, "Correct path for library.c"); + + const libD = projectNameMap.get("library.d"); + t.true(libD instanceof Module, "library.d value is instance of Module"); + t.is(libD.getVersion(), "1.0.0", "Correct version for library.d"); + t.is(libD.getPath(), libraryD, "Correct path for library.d"); + + t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"], + "Correct module ID keys"); + moduleIdMap.forEach((value, key) => { + t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`); + }); +}); + +test("Package workspace resolution: Nested workspaces", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.xyz" + }] + } + }) + }); + + const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson") + .rejects(new Error("Test does not provide for more package mocks")) + .onCall(0).resolves({ + name: "First Package", + version: "1.0.0", + ui5: { + workspaces: [ + "workspace-a", + "workspace-b" + ] + } + }).onCall(1).resolves({ + name: "Second Package", + version: "1.0.0", + workspaces: [ + "workspace-c", + "workspace-d" + ] + }).onCall(2).resolves({ + name: "Third Package", + version: "1.0.0" + }).onCall(3).resolves({ + name: "Fourth Package", + version: "1.0.0", + }).onCall(4).resolves({ + name: "Fifth Package", + version: "1.0.0", + }); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + // All workspaces. Should not resolve to any module + t.is(readPackageJsonStub.callCount, 5, "readPackageJson got called five times"); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); +}); + +test("Package workspace resolution: Recursive workspaces", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.xyz" + }] + } + }) + }); + + const basePath = path.join(__dirname, "../../fixtures/library.xyz"); + const workspaceAPath = path.join(basePath, "workspace-a"); + + const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson"); + readPackageJsonStub.withArgs(basePath).resolves({ + name: "Base Package", + version: "1.0.0", + workspaces: [ + "workspace-a" + ] + }); + readPackageJsonStub.withArgs(workspaceAPath).resolves({ + name: "Workspace A Package", + version: "1.0.0", + workspaces: [ + ".." + ] + }); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + // All workspaces. Should not resolve to any module + // Recursive workspace definition should not lead to another readPackageJson call + t.is(readPackageJsonStub.callCount, 2, "readPackageJson got called two times"); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); +}); + +test("No resolutions configuration", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: {} + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); + + t.falsy(await workspace.getModuleByProjectName("library.e"), + "getModuleByProjectName yields no result for library.e"); + t.falsy(await workspace.getModuleByNodeId("library.e"), + "getModuleByNodeId yields no result for library.e"); +}); + +test("Empty dependencyManagement configuration", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: {} + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); +}); + +test("Empty resolutions configuration", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [] + } + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); +}); + +test("Missing path in resolution", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{}] + } + }) + }); + + await t.throwsAsync(workspace._getResolvedModules(), { + message: "Missing property 'path' in dependency resolution configuration of workspace workspace-name" + }, "Threw with expected error message"); +}); + +test("Invalid specVersion", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: { + specVersion: "project/1.0", + metadata: { + name: "workspace-name" + } + } + }); + + const err = await t.throwsAsync(workspace._getResolvedModules()); + t.true( + err.message.includes(`Unsupported "specVersion"`), + "Threw with expected error message"); +}); + +test("Invalid resolutions configuration", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/does-not-exist" + }] + } + }) + }); + + const absPath = path.join(__dirname, "../../fixtures/does-not-exist"); + + const err = await t.throwsAsync(workspace._getResolvedModules()); + t.true( + err.message.startsWith(`Failed to resolve workspace dependency resolution path ` + + `../../fixtures/does-not-exist to ${absPath}: ENOENT:`), + "Threw with expected error message"); +}); + +test("Resolves extension only", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/extension.a" + }] + } + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 1, "Added one entry to Module ID to module map"); + t.deepEqual(Array.from(moduleIdMap.keys()), ["extension.a"], + "Expected entry in Module ID to module map"); +}); + +test("Resolution does not lead to a project", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + // Using a directory with a package.json but no ui5.yaml + path: "../../fixtures/init-library" + }] + } + }) + }); + + t.is(workspace.getName(), "workspace-name"); + + const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules(); + t.is(projectNameMap.size, 0, "Project name to module map is empty"); + t.is(moduleIdMap.size, 0, "Module ID to module map is empty"); +}); + +test("Missing parameters", (t) => { + t.throws(() => { + new t.context.Workspace({ + configuration: {metadata: {name: "config-a"}} + }); + }, { + message: "Could not create Workspace: Missing or empty parameter 'cwd'" + }, "Threw with expected error message"); + + t.throws(() => { + new t.context.Workspace({ + cwd: "cwd" + }); + }, { + message: "Could not create Workspace: Missing or empty parameter 'configuration'" + }, "Threw with expected error message"); +}); diff --git a/packages/project/test/lib/graph/graph.integration.js b/packages/project/test/lib/graph/graph.integration.js new file mode 100644 index 00000000000..9b459a0f823 --- /dev/null +++ b/packages/project/test/lib/graph/graph.integration.js @@ -0,0 +1,283 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import Workspace from "../../../lib/graph/Workspace.js"; +import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; +const __dirname = import.meta.dirname; + +const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); +const libraryHPath = path.join(fixturesPath, "library.h"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.npmProviderConstructorStub = sinon.stub(); + class MockNpmProvider { + constructor(params) { + t.context.npmProviderConstructorStub(params); + } + } + + t.context.MockNpmProvider = MockNpmProvider; + + t.context.projectGraphBuilderStub = sinon.stub().resolves("graph"); + t.context.enrichProjectGraphStub = sinon.stub(); + t.context.graph = await esmock.p("../../../lib/graph/graph.js", { + "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider, + "../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub, + "../../../lib/graph/helpers/ui5Framework.js": { + enrichProjectGraph: t.context.enrichProjectGraphStub + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.graph); +}); + +test.serial("graphFromPackageDependencies with workspace object", async (t) => { + const { + npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceConfiguration: { + specVersion: "workspace/1.0", + metadata: { + name: "default" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + }); + + t.is(res, "graph"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace, + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride", + "enrichProjectGraph got called with correct versionOverride parameter"); + t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace, + "enrichProjectGraph got called with correct workspace parameter"); +}); + +test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => { + const { + npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "dolphin", + workspaceConfiguration: { + specVersion: "workspace/1.0", + metadata: { + name: "dolphin" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + }); + + t.is(res, "graph"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace, + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride", + "enrichProjectGraph got called with correct versionOverride parameter"); + t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace, + "enrichProjectGraph got called with correct workspace parameter"); +}); + +test.serial("graphFromPackageDependencies with workspace object not matching workspaceName", async (t) => { + const {graphFromPackageDependencies} = t.context.graph; + + await t.throwsAsync(graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "other", + workspaceConfiguration: { + specVersion: "workspace/1.0", + metadata: { + name: "default" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + }), { + message: "The provided workspace name 'other' does not match the provided workspace configuration 'default'" + }, "Threw with expected error message"); +}); + +test.serial("graphFromPackageDependencies with workspace file", async (t) => { + const { + npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: libraryHPath, + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "default", + }); + + t.is(res, "graph"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: libraryHPath, + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath" + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace, + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride", + "enrichProjectGraph got called with correct versionOverride parameter"); + t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace, + "enrichProjectGraph got called with correct workspace parameter"); +}); + +test.serial("graphFromPackageDependencies with workspace file at custom path", async (t) => { + const { + npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "default", + workspaceConfigPath: path.join(libraryHPath, "ui5-workspace.yaml") + }); + + t.is(res, "graph"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath" + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace, + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride", + "enrichProjectGraph got called with correct versionOverride parameter"); + t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace, + "enrichProjectGraph got called with correct workspace parameter"); +}); + +test.serial("graphFromPackageDependencies with inactive workspace file at custom path", async (t) => { + const { + npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "default", + workspaceConfigPath: path.join(libraryHPath, "custom-ui5-workspace.yaml"), + cacheMode: CacheMode.Force + }); + + t.is(res, "graph"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath" + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.is(projectGraphBuilderStub.getCall(0).args[1], null, + "projectGraphBuilder got called with no workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + workspace: null, + cacheMode: "Force" + }, "enrichProjectGraph got called with correct options"); +}); diff --git a/packages/project/test/lib/graph/graph.js b/packages/project/test/lib/graph/graph.js new file mode 100644 index 00000000000..4cc4c1386c0 --- /dev/null +++ b/packages/project/test/lib/graph/graph.js @@ -0,0 +1,440 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; + +const __dirname = import.meta.dirname; +const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.npmProviderConstructorStub = sinon.stub(); + class MockNpmProvider { + constructor(params) { + t.context.npmProviderConstructorStub(params); + } + } + t.context.createWorkspaceStub = sinon.stub().returns("workspace"); + + t.context.MockNpmProvider = MockNpmProvider; + + t.context.dependencyTreeProviderStub = sinon.stub(); + class DummyDependencyTreeProvider { + constructor(params) { + t.context.dependencyTreeProviderStub(params); + } + } + t.context.DummyDependencyTreeProvider = DummyDependencyTreeProvider; + + t.context.projectGraphBuilderStub = sinon.stub().resolves("graph"); + t.context.enrichProjectGraphStub = sinon.stub(); + t.context.graph = await esmock.p("../../../lib/graph/graph.js", { + "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider, + "../../../lib/graph/providers/DependencyTree.js": t.context.DummyDependencyTreeProvider, + "../../../lib/graph/helpers/createWorkspace.js": t.context.createWorkspaceStub, + "../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub, + "../../../lib/graph/helpers/ui5Framework.js": { + "enrichProjectGraph": t.context.enrichProjectGraphStub + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.graph); +}); + +test.serial("graphFromPackageDependencies", async (t) => { + const { + createWorkspaceStub, npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + cacheMode: CacheMode.Off, + workspaceName: null + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 0, "createWorkspace did not get called"); + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath" + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.is(projectGraphBuilderStub.getCall(0).args[1], undefined, + "projectGraphBuilder got called with an empty workspace"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + workspace: undefined, + cacheMode: "Off" + }, "enrichProjectGraph got called with correct options"); +}); + +test.serial("graphFromPackageDependencies with workspace name", async (t) => { + const { + createWorkspaceStub, npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "dolphin", + cacheMode: CacheMode.Off + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once"); + t.deepEqual(createWorkspaceStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + name: "dolphin", + configPath: "ui5-workspace.yaml", + configObject: undefined, + }, "createWorkspace called with correct parameters"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.is(projectGraphBuilderStub.getCall(0).args[1], "workspace", + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + workspace: "workspace", + cacheMode: "Off" + }, "enrichProjectGraph got called with correct options"); +}); + +test.serial("graphFromPackageDependencies with workspace object", async (t) => { + const { + createWorkspaceStub + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceConfiguration: "workspaceConfiguration", + workspaceName: null + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once"); + t.deepEqual(createWorkspaceStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + configPath: "ui5-workspace.yaml", + name: null, + configObject: "workspaceConfiguration" + }, "createWorkspace called with correct parameters"); +}); + +test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => { + const { + createWorkspaceStub + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "dolphin", + workspaceConfiguration: "workspaceConfiguration" + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once"); + t.deepEqual(createWorkspaceStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + name: "dolphin", + configPath: "ui5-workspace.yaml", + configObject: "workspaceConfiguration" + }, "createWorkspace called with correct parameters"); +}); + +test.serial("graphFromPackageDependencies with workspace path and workspace name", async (t) => { + const { + createWorkspaceStub + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "dolphin", + workspaceConfigPath: "workspaceConfigurationPath" + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once"); + t.deepEqual(createWorkspaceStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + name: "dolphin", + configPath: "workspaceConfigurationPath", + configObject: undefined + }, "createWorkspace called with correct parameters"); +}); + +test.serial("graphFromPackageDependencies with empty workspace", async (t) => { + const { + createWorkspaceStub, npmProviderConstructorStub, + projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider + } = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + // Simulate no workspace config found + createWorkspaceStub.resolves(null); + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + workspaceName: "dolphin", + cacheMode: CacheMode.Off + }); + + t.is(res, "graph"); + + t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once"); + t.deepEqual(createWorkspaceStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + name: "dolphin", + configPath: "ui5-workspace.yaml", + configObject: undefined, + }, "createWorkspace called with correct parameters"); + + t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once"); + t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], { + cwd: path.join(__dirname, "..", "..", "..", "cwd"), + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider, + "projectGraphBuilder got called with correct provider instance"); + t.is(projectGraphBuilderStub.getCall(0).args[1], null, + "projectGraphBuilder got called with correct workspace instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + workspace: null, + cacheMode: "Off" + }, "enrichProjectGraph got called with correct options"); +}); + +test.serial("graphFromPackageDependencies: Do not resolve framework dependencies", async (t) => { + const {enrichProjectGraphStub} = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + resolveFrameworkDependencies: false + }); + + t.is(res, "graph"); + t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called"); +}); + +test.serial("graphFromPackageDependencies: Default workspace name", async (t) => { + const {createWorkspaceStub} = t.context; + const {graphFromPackageDependencies} = t.context.graph; + + const res = await graphFromPackageDependencies({ + cwd: "cwd", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + resolveFrameworkDependencies: false + }); + + t.is(res, "graph"); + t.true(createWorkspaceStub.calledOnce, "createWorkspace is called"); + t.is(createWorkspaceStub.getCall(0).args[0].name, "default", + "createWorkspace is called with 'default' workspace"); +}); + +test.serial("graphFromStaticFile", async (t) => { + const { + dependencyTreeProviderStub, + projectGraphBuilderStub, enrichProjectGraphStub, DummyDependencyTreeProvider + } = t.context; + const {graphFromStaticFile} = t.context.graph; + + const readDependencyConfigFileStub = t.context.sinon.stub(graphFromStaticFile._utils, "readDependencyConfigFile") + .resolves("dependencyTree"); + + const res = await graphFromStaticFile({ + cwd: "cwd", + filePath: "file/path", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + cacheMode: CacheMode.Off + }); + + t.is(res, "graph"); + + t.is(readDependencyConfigFileStub.callCount, 1, "_readDependencyConfigFile got called once"); + t.is(readDependencyConfigFileStub.getCall(0).args[0], path.join(__dirname, "..", "..", "..", "cwd"), + "_readDependencyConfigFile got called with correct directory"); + t.is(readDependencyConfigFileStub.getCall(0).args[1], "file/path", + "_readDependencyConfigFile got called with correct file path"); + + t.is(dependencyTreeProviderStub.callCount, 1, "DependencyTree provider constructor got called once"); + t.deepEqual(dependencyTreeProviderStub.getCall(0).args[0], { + dependencyTree: "dependencyTree", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof DummyDependencyTreeProvider, + "projectGraphBuilder got called with correct provider instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + cacheMode: "Off" + }, "enrichProjectGraph got called with correct options"); +}); + +test.serial("graphFromStaticFile: Do not resolve framework dependencies", async (t) => { + const {enrichProjectGraphStub} = t.context; + const {graphFromStaticFile} = t.context.graph; + + t.context.sinon.stub(graphFromStaticFile._utils, "readDependencyConfigFile") + .resolves("dependencyTree"); + + const res = await graphFromStaticFile({ + cwd: "cwd", + filePath: "filePath", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + resolveFrameworkDependencies: false + }); + + t.is(res, "graph"); + t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called"); +}); + +test.serial("usingObject", async (t) => { + const { + dependencyTreeProviderStub, + projectGraphBuilderStub, enrichProjectGraphStub, DummyDependencyTreeProvider + } = t.context; + const {graphFromObject} = t.context.graph; + + const res = await graphFromObject({ + dependencyTree: "dependencyTree", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + cacheMode: "Off" + }); + + t.is(res, "graph"); + + t.is(dependencyTreeProviderStub.callCount, 1, "DependencyTree provider constructor got called once"); + t.deepEqual(dependencyTreeProviderStub.getCall(0).args[0], { + dependencyTree: "dependencyTree", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + }, "Created NodePackageDependencies provider instance with correct parameters"); + + t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once"); + t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof DummyDependencyTreeProvider, + "projectGraphBuilder got called with correct provider instance"); + + t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once"); + t.is(enrichProjectGraphStub.getCall(0).args[0], "graph", + "enrichProjectGraph got called with graph"); + t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { + versionOverride: "versionOverride", + cacheMode: "Off" + }, "enrichProjectGraph got called with correct options"); +}); + +test.serial("usingObject: Do not resolve framework dependencies", async (t) => { + const {enrichProjectGraphStub} = t.context; + const {graphFromObject} = t.context.graph; + const res = await graphFromObject({ + cwd: "cwd", + filePath: "filePath", + rootConfiguration: "rootConfiguration", + rootConfigPath: "/rootConfigPath", + versionOverride: "versionOverride", + resolveFrameworkDependencies: false + }); + + t.is(res, "graph"); + t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called"); +}); + +test.serial("utils: readDependencyConfigFile", async (t) => { + const {graphFromStaticFile} = t.context.graph; + const res = await graphFromStaticFile._utils.readDependencyConfigFile( + path.join(fixturesPath, "application.h"), "projectDependencies.yaml"); + + t.deepEqual(res, { + id: "static-application.a", + path: path.join(fixturesPath, "application.a"), + version: "0.0.1", + dependencies: [{ + id: "static-library.e", + path: path.join(fixturesPath, "library.e"), + version: "0.0.1", + }], + }, "Returned correct file content"); +}); + diff --git a/packages/project/test/lib/graph/graphFromObject.js b/packages/project/test/lib/graph/graphFromObject.js new file mode 100644 index 00000000000..e331fdb4033 --- /dev/null +++ b/packages/project/test/lib/graph/graphFromObject.js @@ -0,0 +1,1664 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import ValidationError from "../../../lib/validation/ValidationError.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b"); +const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); +const libraryAPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a"); +const libraryBPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b"); +const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); +const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); +const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); + +const legacyLibraryAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.a"); +const legacyLibraryBPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.b"); +const legacyCollectionAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.collection.a"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + error: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + verbose: sinon.stub(), + silly: sinon.stub(), + isLevelEnabled: () => true + }; + + t.context.graph = await esmock.p("../../../lib/graph/graph.js", { + "../../../lib/graph/projectGraphBuilder": await esmock("../../../lib/graph/projectGraphBuilder.js", { + "@ui5/logger": { + getLogger: sinon.stub().withArgs("graph:projectGraphBuilder").returns(t.context.log) + } + }) + }); + t.context.graphFromObject = t.context.graph.graphFromObject; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.graph); +}); + +test("Application A", async (t) => { + const {graphFromObject} = t.context; + const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()}); + const rootProject = projectGraph.getRoot(); + t.is(rootProject.getName(), "application.a", "Returned correct root project"); +}); + +test("Application A: Traverse project graph breadth first", async (t) => { + const {graphFromObject} = t.context; + const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()}); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 5, "Five projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "application.a", + "library.d", + "library.a", + "library.b", + "library.c" + ], "Traversed graph in correct order"); +}); + +test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => { + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: applicationCycleATreeIncDeduped}); + const callbackStub = sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub)); + + t.is(callbackStub.callCount, 4, "Four projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: *application.cycle.a* -> component.cycle.a " + + "-> *application.cycle.a*", + "Threw with expected error message"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.cycle.a", + "component.cycle.a", + "library.cycle.a", + "library.cycle.b", + ], "Traversed graph in correct order"); +}); + +test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => { + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: applicationCycleBTreeIncDeduped}); + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + // TODO: Confirm this behavior with FW. BFS works fine since all modules have already been visited + // before a cycle is entered. DFS fails because it dives into the cycle first. + + t.is(callbackStub.callCount, 3, "Four projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.cycle.b", + "module.d", + "module.e" + ], "Traversed graph in correct order"); +}); + +test("Application A: Traverse project graph depth first", async (t) => { + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()}); + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 5, "Five projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + "library.c", + "library.d", + "application.a", + + ], "Traversed graph in correct order"); +}); + + +test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => { + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: applicationCycleATreeIncDeduped}); + const callbackStub = sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); + + t.is(callbackStub.callCount, 0, "Zero projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: *application.cycle.a* -> component.cycle.a " + + "-> *application.cycle.a*", + "Threw with expected error message"); +}); + +test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => { + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: applicationCycleBTreeIncDeduped}); + const callbackStub = sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); + + t.is(callbackStub.callCount, 0, "Zero projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: application.cycle.b -> *module.d* " + + "-> module.e -> *module.d*", + "Threw with expected error message"); +}); + + +/* ================================================================================================= */ +/* ======= The following tests have been derived from the existing projectPreprocessor tests ======= */ + +function testBasicGraphCreationBfs(...args) { + return _testBasicGraphCreation(...args, true); +} + +function testBasicGraphCreationDfs(...args) { + return _testBasicGraphCreation(...args, false); +} + +async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const {graphFromObject, sinon} = t.context; + const projectGraph = await graphFromObject({dependencyTree: tree}); + const callbackStub = sinon.stub().resolves(); + if (bfs) { + await projectGraph.traverseBreadthFirst(callbackStub); + } else { + await projectGraph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); + return projectGraph; +} + +test("Project with inline configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + } + }; + + await testBasicGraphCreationDfs(t, tree, [ + "xy" + ]); +}); + + +test("Project with inline configuration as array", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }] + }; + + await testBasicGraphCreationDfs(t, tree, [ + "xy" + ]); +}); + +test("Project with inline configuration for two projects", async (t) => { + const {graphFromObject} = t.context; + const tree = { + id: "application.a.id", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, { + specVersion: "2.3", + type: "application", + metadata: { + name: "yz" + } + }] + }; + + await t.throwsAsync(graphFromObject({dependencyTree: tree}), + { + message: + `Found 2 configurations of kind 'project' for module application.a.id. ` + + `There must be only one project per module.` + }, + "Rejected with error"); +}); + +test("Project with configPath", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + configPath: path.join(applicationBPath, "ui5.yaml"), // B, not A - just to have something different + dependencies: [], + version: "1.0.0" + }; + + await testBasicGraphCreationDfs(t, tree, [ + "application.b" + ]); +}); + +test("Project with ui5.yaml at default location", async (t) => { + const tree = { + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }; + + await testBasicGraphCreationDfs(t, tree, [ + "application.a" + ]); +}); + +test("Project with ui5.yaml at default location and some configuration", async (t) => { + const tree = { + id: "application.c", + version: "1.0.0", + path: applicationCPath, + dependencies: [] + }; + + await testBasicGraphCreationDfs(t, tree, [ + "application.c" + ]); +}); + +test("Missing configuration file for root project", async (t) => { + const {graphFromObject} = t.context; + const tree = { + id: "application.a.id", + version: "1.0.0", + path: "/non-existent", + dependencies: [] + }; + await t.throwsAsync(graphFromObject({dependencyTree: tree}), + { + message: + "Failed to create a UI5 project from module application.a.id at /non-existent. " + + "Make sure the path is correct and a project configuration is present or supplied." + }, + "Rejected with error"); +}); + +test("Missing id for root project", async (t) => { + const {graphFromObject} = t.context; + const tree = { + path: path.join(__dirname, "fixtures/application.a"), + dependencies: [] + }; + await t.throwsAsync(graphFromObject({dependencyTree: tree}), + {message: "Could not create Module: Missing or empty parameter 'id'"}, "Rejected with error"); +}); + +test("No type configured for root project", async (t) => { + const {graphFromObject} = t.context; + const tree = { + id: "application.a.id", + version: "1.0.0", + path: path.join(__dirname, "fixtures/application.a"), + dependencies: [], + configuration: { + specVersion: "2.1", + metadata: { + name: "application.a", + namespace: "id1" + } + } + }; + const error = await t.throwsAsync(graphFromObject({dependencyTree: tree})); + + t.is(error.message, `Unable to create Specification instance: Unknown specification type 'undefined'`); +}); + +test("Missing dependencies", async (t) => { + const {graphFromObject} = t.context; + const tree = ({ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath + }); + await t.notThrowsAsync(graphFromObject({dependencyTree: tree}), + "Gracefully accepted project with no dependencies attribute"); +}); + +test("Missing second-level dependencies", async (t) => { + const {graphFromObject} = t.context; + const tree = ({ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d") + }] + }); + await t.notThrowsAsync(graphFromObject({dependencyTree: tree}), + "Gracefully accepted project with no dependencies attribute"); +}); + +test("Single non-root application-project", async (t) => { + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.a" + ]); +}); + +test("Multiple non-root application-projects on same level", async (t) => { + const {log} = t.context; + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }, { + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.a" + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Multiple non-root application-projects on different levels", async (t) => { + const {log} = t.context; + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }, { + id: "library.b", + version: "1.0.0", + path: libraryBPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.b", + "library.a" + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Root- and non-root application-projects", async (t) => { + const {log} = t.context; + const tree = ({ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }] + }); + await testBasicGraphCreationDfs(t, tree, [ + "library.a", + "application.a", + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Ignores additional application-projects", async (t) => { + const {log} = t.context; + const tree = ({ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }); + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Inconsistent dependencies with same ID", async (t) => { + // The one closer to the root should win + const tree = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryBPath, // B, not A - inconsistency! + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.XY", + } + }, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [] + } + ] + }; + await testBasicGraphCreationDfs(t, tree, [ + // "library.XY" is ignored since the ID has already been processed and resolved to library A + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with inline configs depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithInlineConfigs, [ + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with configPaths depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithConfigPaths, [ + "library.a", + "library.d", + "application.a" + + ]); +}); + +test("Project tree A with default YAMLs depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithDefaultYamls, [ + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with inline configs breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithInlineConfigs, [ + "application.a", + "library.d", + "library.a", + ]); +}); + +test("Project tree A with configPaths breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithConfigPaths, [ + "application.a", + "library.d", + "library.a" + + ]); +}); + +test("Project tree A with default YAMLs breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithDefaultYamls, [ + "application.a", + "library.d", + "library.a" + ]); +}); + +test("Project tree B with inline configs", async (t) => { + // Tree B depends on Library B which has a dependency to Library D + await testBasicGraphCreationDfs(t, applicationBTreeWithInlineConfigs, [ + "library.a", + "library.d", + "library.b", + "application.b" + ]); +}); + +test("Project with nested invalid dependencies", async (t) => { + await testBasicGraphCreationDfs(t, treeWithInvalidModules, [ + "library.a", + "library.b", + "application.a" + ]); +}); + +/* ========================= */ +/* ======= Test data ======= */ + +function getApplicationATree() { + return { + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [ + { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [] + }, + { + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, + { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + } + ] + } + ] + }; +} + + +const applicationCycleATreeIncDeduped = { + id: "@ui5-internal/application.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.a"), + dependencies: [ + { + id: "@ui5-internal/component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"), + dependencies: [ + { + id: "@ui5-internal/library.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "library.cycle.a"), + dependencies: [ + { + id: "@ui5-internal/component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"), + dependencies: [], + deduped: true + } + ] + }, + { + id: "@ui5-internal/library.cycle.b", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "library.cycle.b"), + dependencies: [ + { + id: "@ui5-internal/component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"), + dependencies: [], + deduped: true + } + ] + }, + { + id: "@ui5-internal/application.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.a"), + dependencies: [], + deduped: true + } + ] + } + ] +}; + +const applicationCycleBTreeIncDeduped = { + id: "@ui5-internal/application.cycle.b", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.b"), + dependencies: [ + { + id: "@ui5-internal/module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"), + dependencies: [ + { + id: "@ui5-internal/module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"), + dependencies: [ + { + id: "@ui5-internal/module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"), + dependencies: [], + deduped: true + } + ] + } + ] + }, + { + id: "@ui5-internal/module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"), + dependencies: [ + { + id: "@ui5-internal/module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"), + dependencies: [ + { + id: "@ui5-internal/module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"), + dependencies: [], + deduped: true + } + ] + } + ] + } + ] +}; + + +/* === Tree A === */ +const applicationATreeWithInlineConfigs = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + }, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a", + }, + }, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + }, + }, + dependencies: [] + } + ] +}; + +const applicationATreeWithConfigPaths = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + configPath: path.join(applicationAPath, "ui5.yaml"), + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configPath: path.join(libraryDPath, "ui5.yaml"), + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configPath: path.join(libraryAPath, "ui5.yaml"), + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configPath: path.join(libraryAPath, "ui5.yaml"), + dependencies: [] + } + ] +}; + +const applicationATreeWithDefaultYamls = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [] + } + ] +}; + +/* === Tree B === */ +const applicationBTreeWithInlineConfigs = { + id: "application.b", + version: "1.0.0", + path: applicationBPath, + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.b" + } + }, + dependencies: [ + { + id: "library.b", + version: "1.0.0", + path: libraryBPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.b", + } + }, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + } + }, + dependencies: [] + } + ] + } + ] + }, + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + } + }, + dependencies: [] + } + ] + } + ] +}; + +/* === Invalid Modules */ +const treeWithInvalidModules = { + id: "application.a", + path: applicationAPath, + dependencies: [ + // A + { + id: "library.a", + path: libraryAPath, + dependencies: [ + { + // C - invalid - should be missing in preprocessed tree + id: "module.c", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + }, + { + // D - invalid - should be missing in preprocessed tree + id: "module.d", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "library", + metadata: {name: "library.a"} + } + }, + // B + { + id: "library.b", + path: libraryBPath, + dependencies: [ + { + // C - invalid - should be missing in preprocessed tree + id: "module.c", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + }, + { + // D - invalid - should be missing in preprocessed tree + id: "module.d", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "library", + metadata: {name: "library.b"} + } + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + } +}; + +/* ======================================================================================= */ +/* ======= The following tests have been derived from the existing extension tests ======= */ + +/* The following scenario is supported by the projectPreprocessor but not by projectGraphFromTree + * A shim extension located in a project's dependencies can't influence other dependencies of that project anymore + * TODO: Check whether the above is fine for us + +test("Legacy: Project with project-shim extension with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + dependencies: [], + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + } + }, { + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); +});*/ + +test("Project with project-shim extension with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + }], + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); +}); + +test("Project with project-shim extension dependency with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + }, + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }], + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); + +test("Project with project-shim extension with invalid dependency configuration", async (t) => { + const {graphFromObject} = t.context; + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library" + } + } + } + }], + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + const validationError = await t.throwsAsync(graphFromObject({dependencyTree: tree}), { + instanceOf: ValidationError + }); + t.true(validationError.message.includes("Configuration must have required property 'metadata'"), + "ValidationError should contain error about missing metadata configuration"); +}); + +test("Project with project-shim extension with dependency declaration and configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + }, + "legacy.library.b.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.b", + } + } + }, + dependencies: { + "legacy.library.a.id": [ + "legacy.library.b.id" + ] + } + } + }, + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }, { + id: "legacy.library.b.id", + version: "1.0.0", + path: legacyLibraryBPath, + dependencies: [] + }], + }] + }; + // application.a and legacy.library.a will both have a dependency to legacy.library.b + // (one because it's the actual dependency and one because it's a shimmed dependency) + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.b", + "legacy.library.a", + "application.a", + ]); + t.deepEqual(graph.getDependencies("legacy.library.a"), [ + "legacy.library.b" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); + +test("Project with project-shim extension with collection", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.x.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.x", + } + }, + "legacy.library.y.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.y", + } + } + }, + dependencies: { + "application.a.id": [ + "legacy.library.x.id", + "legacy.library.y.id" + ], + "legacy.library.x.id": [ + "legacy.library.y.id" + ] + }, + collections: { + "legacy.collection.a": { + modules: { + "legacy.library.x.id": "src/legacy.library.x", + "legacy.library.y.id": "src/legacy.library.y" + } + } + } + } + }, + dependencies: [{ + id: "legacy.collection.a", + version: "1.0.0", + path: legacyCollectionAPath, + dependencies: [] + }] + }] + }; + + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.y", + "legacy.library.x", + "application.a", + ]); + t.deepEqual(graph.getDependencies("application.a"), [ + "legacy.library.x", + "legacy.library.y" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); + +// TODO: Fixme +// eslint-disable-next-line ava/no-skip-test +test.skip("Project with project-shim extension with self-containing collection shim", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "legacy.collection.a", + path: legacyCollectionAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "library", + metadata: { + name: "my.fe" + }, + framework: { + name: "OpenUI5" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.x.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.x", + } + }, + "legacy.library.y.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.y", + } + } + }, + dependencies: { + "legacy.library.x.id": [ + "legacy.library.y.id" + ] + }, + collections: { + "legacy.collection.a": { + modules: { + "legacy.library.x.id": "src/legacy.library.x", + "legacy.library.y.id": "src/legacy.library.y" + } + } + } + } + }], + dependencies: [] + }] + }; + + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.y", + "legacy.library.x", + "application.a", + ]); + t.deepEqual(graph.getDependencies("application.a"), [ + "legacy.library.x", + "legacy.library.y" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); + + const libraryY = graph.getProject("legacy.library.y"); + t.deepEqual(libraryY.getFrameworkName(), { + name: "OpenUI5" + }, "Configuration from collection project should be taken over into shimmed project"); +}); + +test("Project with unknown extension dependency inline configuration", async (t) => { + const {graphFromObject} = t.context; + const tree = { + id: "application.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, + dependencies: [{ + id: "extension.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "phony-pony", + metadata: { + name: "pinky.pie" + } + }, + dependencies: [], + }], + }; + const validationError = await t.throwsAsync(graphFromObject({dependencyTree: tree})); + t.is(validationError.message, + `Unable to create Specification instance: Unknown specification type 'phony-pony'`, + "Should throw with expected error message"); +}); + +test("Project with task extension dependency", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "ext.task.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "task", + metadata: { + name: "task.a" + }, + task: { + path: "task.a.js" + } + }, + dependencies: [], + }] + }; + const graph = await testBasicGraphCreationDfs(t, tree, [ + "application.a" + ]); + t.truthy(graph.getExtension("task.a"), "Extension should be added to the graph"); +}); + +test("Project with middleware extension dependency", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "ext.middleware.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "server-middleware", + metadata: { + name: "middleware.a" + }, + middleware: { + path: "middleware.a.js" + } + }, + dependencies: [], + }], + }; + const graph = await testBasicGraphCreationDfs(t, tree, [ + "application.a" + ]); + t.truthy(graph.getExtension("middleware.a"), "Extension should be added to the graph"); +}); + +test("rootConfiguration", async (t) => { + const {graphFromObject} = t.context; + const projectGraph = await graphFromObject({ + dependencyTree: getApplicationATree(), + rootConfiguration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + rootConfigurationTest: true + } + } + }); + + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigurationTest: true + }); +}); + +test("rootConfig", async (t) => { + const {graphFromObject} = t.context; + const projectGraph = await graphFromObject({ + dependencyTree: getApplicationATree(), + cwd: applicationAPath, + rootConfigPath: "ui5-test-configPath.yaml", + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + configPathTest: true + }); +}); diff --git a/packages/project/test/lib/graph/graphFromPackageDependencies.js b/packages/project/test/lib/graph/graphFromPackageDependencies.js new file mode 100644 index 00000000000..491cc442ed5 --- /dev/null +++ b/packages/project/test/lib/graph/graphFromPackageDependencies.js @@ -0,0 +1,41 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import {graphFromPackageDependencies} from "../../../lib/graph/graph.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Application A", async (t) => { + const projectGraph = await graphFromPackageDependencies({cwd: applicationAPath}); + const rootProject = projectGraph.getRoot(); + t.is(rootProject.getName(), "application.a", "Returned correct root project"); +}); + +test("Application A: Traverse project graph breadth first", async (t) => { + const projectGraph = await graphFromPackageDependencies({cwd: applicationAPath}); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 5, "Five projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "application.a", + "library.d", + "library.a", + "library.b", + "library.c" + ], "Traversed graph in correct order"); +}); + +// More integration tests for package.json dependencies in graph/providers/NodePackageDependencies.integration.js diff --git a/packages/project/test/lib/graph/graphFromStaticFile.js b/packages/project/test/lib/graph/graphFromStaticFile.js new file mode 100644 index 00000000000..acff5761ab3 --- /dev/null +++ b/packages/project/test/lib/graph/graphFromStaticFile.js @@ -0,0 +1,112 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; + +import {graphFromStaticFile} from "../../../lib/graph/graph.js"; + +const __dirname = import.meta.dirname; + +const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h"); +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const notExistingPath = path.join(__dirname, "..", "..", "fixtures", "does_not_exist"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Application H: Traverse project graph breadth first", async (t) => { + const projectGraph = await graphFromStaticFile({ + cwd: applicationHPath + }); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Two projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "application.a", + "library.e", + ], "Traversed graph in correct order"); +}); + +test("Throws error if file not found", async (t) => { + const err = await t.throwsAsync(graphFromStaticFile({ + cwd: notExistingPath + })); + t.is(err.message, + `Failed to load dependency tree configuration from path ` + + `${path.join(notExistingPath, "projectDependencies.yaml")}: ` + + `ENOENT: no such file or directory, open '${path.join(notExistingPath, "projectDependencies.yaml")}'`, + "Correct error message"); +}); + +test("Throws for missing id", async (t) => { + const err = await t.throwsAsync(graphFromStaticFile({ + cwd: applicationHPath, + filePath: "projectDependencies-missing-id.yaml" + })); + t.is(err.message, + `Failed to load dependency tree configuration from path ` + + `${path.join(applicationHPath, "projectDependencies-missing-id.yaml")}: ` + + `Missing or empty attribute 'id' for project with path ${applicationAPath}`, + "Correct error message"); +}); + +test("Throws for missing version", async (t) => { + const err = await t.throwsAsync(graphFromStaticFile({ + cwd: applicationHPath, + filePath: "projectDependencies-missing-version.yaml" + })); + t.is(err.message, + `Failed to load dependency tree configuration from path ` + + `${path.join(applicationHPath, "projectDependencies-missing-version.yaml")}: ` + + `Missing or empty attribute 'version' for project static-application.a`, + "Correct error message"); +}); + +test("Throws for missing path", async (t) => { + const err = await t.throwsAsync(graphFromStaticFile({ + cwd: applicationHPath, + filePath: "projectDependencies-missing-path.yaml" + })); + t.is(err.message, + `Failed to load dependency tree configuration from path ` + + `${path.join(applicationHPath, "projectDependencies-missing-path.yaml")}: ` + + `Missing or empty attribute 'path' for project static-library.e`, + "Correct error message"); +}); + +test("rootConfiguration", async (t) => { + const projectGraph = await graphFromStaticFile({ + cwd: applicationHPath, + rootConfiguration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + rootConfigurationTest: true + } + } + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigurationTest: true + }); +}); + +test("rootConfig", async (t) => { + const projectGraph = await graphFromStaticFile({ + cwd: applicationHPath, + rootConfigPath: "../application.a/ui5-test-configPath.yaml" + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + configPathTest: true + }); +}); diff --git a/packages/project/test/lib/graph/helpers/createWorkspace.js b/packages/project/test/lib/graph/helpers/createWorkspace.js new file mode 100644 index 00000000000..b73a7c4a395 --- /dev/null +++ b/packages/project/test/lib/graph/helpers/createWorkspace.js @@ -0,0 +1,332 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +const __dirname = import.meta.dirname; +const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures"); +const libraryHPath = path.join(fixturesPath, "library.h"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.workspaceConstructorStub = sinon.stub(); + class MockWorkspace { + constructor(params) { + t.context.workspaceConstructorStub(params); + } + } + t.context.MockWorkspace = MockWorkspace; + + t.context.createWorkspace = await esmock("../../../../lib/graph/helpers/createWorkspace", { + "../../../../lib/graph/Workspace.js": t.context.MockWorkspace + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("createWorkspace: Missing parameter 'configObject' or 'configPath'", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: "cwd", + })); + t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'", + "Threw with expected error message"); +}); + +test("createWorkspace: Missing parameter 'cwd'", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml") + })); + t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'", + "Threw with expected error message"); +}); + +test("createWorkspace: Missing parameter 'name' if 'configPath' is set", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: "cwd", + configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml") + })); + t.is(err.message, "createWorkspace: Parameter 'configPath' implies parameter 'name', but it's empty", + "Threw with expected error message"); +}); + +test("createWorkspace: Using object", async (t) => { + const { + workspaceConstructorStub, + MockWorkspace, + createWorkspace + } = t.context; + + const res = await createWorkspace({ + cwd: "cwd", + configObject: { + specVersion: "workspace/1.0", + metadata: { + name: "default" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + }); + + t.true(res instanceof MockWorkspace, "Returned instance of Workspace"); + + t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once"); + t.deepEqual(workspaceConstructorStub.getCall(0).args[0], { + cwd: "cwd", + configuration: { + specVersion: "workspace/1.0", + metadata: { + name: "default" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + }, "Created Workspace instance with correct parameters"); +}); + +test("createWorkspace: Using invalid object", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: "cwd", + configObject: { + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + })); + t.is(err.message, "Invalid workspace configuration: Missing or empty property 'metadata.name'", + "Threw with validation error"); +}); + +test("createWorkspace: Using name and object with different workspace name", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: "cwd", + name: "my-workspace", + configObject: { + metadata: { + name: "my-other-workspace" + }, + dependencyManagement: { + resolutions: [{ + path: "resolution/path" + }] + } + } + })); + t.is(err.message, + `The provided workspace name 'my-workspace' does not match ` + + `the provided workspace configuration 'my-other-workspace'`, + "Threw with validation error"); +}); + +test("createWorkspace: Using file", async (t) => { + const {createWorkspace, MockWorkspace, workspaceConstructorStub} = t.context; + + const res = await createWorkspace({ + cwd: "cwd", + name: "default", + configPath: path.join(libraryHPath, "ui5-workspace.yaml") + }); + + t.true(res instanceof MockWorkspace, "Returned instance of Workspace"); + + t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once"); + t.deepEqual(workspaceConstructorStub.getCall(0).args[0], { + cwd: libraryHPath, + configuration: { + specVersion: "workspace/1.0", + metadata: { + name: "default" + }, + dependencyManagement: { + resolutions: [{ + path: "../library.d" + }] + } + } + }, "Created Workspace instance with correct parameters"); +}); + +test("createWorkspace: Using invalid file", async (t) => { + const {createWorkspace} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: "cwd", + name: "default", + configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml") + })); + + t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error"); +}); + +test("createWorkspace: Using missing file", async (t) => { + const {createWorkspace, workspaceConstructorStub} = t.context; + + const res = await createWorkspace({ + cwd: path.join(fixturesPath, "library.d"), + name: "default", + configPath: "ui5-workspace.yaml" + }); + + t.is(res, null, "Returned no workspace"); + + t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called"); +}); + +test("createWorkspace: Missing default workspace in file", async (t) => { + const {createWorkspace, workspaceConstructorStub} = t.context; + + const res = await createWorkspace({ + cwd: path.join(fixturesPath, "library.h"), + name: "default", + configPath: path.join(fixturesPath, "library.h", "custom-ui5-workspace.yaml") + }); + + t.is(res, null, "Returned no workspace"); + + t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called"); +}); + +test("createWorkspace: Using missing file and non-default name", async (t) => { + const {createWorkspace, workspaceConstructorStub} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: path.join(fixturesPath, "library.d"), + name: "special", + configPath: "ui5-workspace.yaml" + })); + + const filePath = path.join(fixturesPath, "library.d", "ui5-workspace.yaml"); + t.true(err.message.startsWith( + `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message"); + + t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called"); +}); + +test("createWorkspace: Using non-default file and non-default name", async (t) => { + const {createWorkspace, workspaceConstructorStub, MockWorkspace} = t.context; + + const res = await createWorkspace({ + cwd: path.join(fixturesPath, "library.h"), + name: "library-d", + configPath: "custom-ui5-workspace.yaml" + }); + + t.true(res instanceof MockWorkspace, "Returned instance of Workspace"); + + t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once"); + t.deepEqual(workspaceConstructorStub.getCall(0).args[0], { + cwd: libraryHPath, + configuration: { + specVersion: "workspace/1.0", + metadata: { + name: "library-d" + }, + dependencyManagement: { + resolutions: [{ + path: "../library.d" + }] + } + } + }, "Created Workspace instance with correct parameters"); +}); + +test("createWorkspace: Using non-default file and non-default name which is not in file", async (t) => { + const {createWorkspace, workspaceConstructorStub} = t.context; + + const err = await t.throwsAsync(createWorkspace({ + cwd: path.join(fixturesPath, "library.h"), + name: "special", + configPath: "custom-ui5-workspace.yaml" + })); + + t.is(err.message, `Could not find a workspace named 'special' in custom-ui5-workspace.yaml`, + "Threw with expected error message"); + + t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called"); +}); + +test("readWorkspaceConfigFile", async (t) => { + const {createWorkspace} = t.context; + const res = await createWorkspace._readWorkspaceConfigFile( + path.join(libraryHPath, "ui5-workspace.yaml"), false); + t.deepEqual(res, + [{ + specVersion: "workspace/1.0", + metadata: { + name: "default", + }, + dependencyManagement: { + resolutions: [{ + path: "../library.d", + }] + }, + }, { + specVersion: "workspace/1.0", + metadata: { + name: "all-libraries", + }, + dependencyManagement: { + resolutions: [{ + path: "../library.d", + }, { + path: "../library.e", + }, { + path: "../library.f", + }], + }, + }], "Read workspace configuration file correctly"); +}); + +test("readWorkspaceConfigFile: Throws for missing file", async (t) => { + const {createWorkspace} = t.context; + const filePath = path.join(fixturesPath, "library.d", "other-ui5-workspace.yaml"); + const err = + await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath)); + t.true(err.message.startsWith( + `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message"); +}); + +test("readWorkspaceConfigFile: Validation errors", async (t) => { + const {createWorkspace} = t.context; + const filePath = path.join(libraryHPath, "invalid-ui5-workspace.yaml"); + const err = + await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true)); + t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error"); +}); + +test("readWorkspaceConfigFile: Not a YAML", async (t) => { + const {createWorkspace} = t.context; + const filePath = path.join(libraryHPath, "corrupt-ui5-workspace.yaml"); + const err = + await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true)); + t.true(err.message.includes(`Failed to parse workspace configuration at ${filePath}`), + "Threw with parsing error"); +}); + +test("readWorkspaceConfigFile: Empty file", async (t) => { + const {createWorkspace} = t.context; + const filePath = path.join(libraryHPath, "empty-ui5-workspace.yaml"); + const res = await createWorkspace._readWorkspaceConfigFile(filePath, true); + t.deepEqual(res, [], "No workspace configuration returned"); +}); diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.integration.js b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js new file mode 100644 index 00000000000..93096d50109 --- /dev/null +++ b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js @@ -0,0 +1,925 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js"; + +const __dirname = import.meta.dirname; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); +const ui5FrameworkBaseDir = path.join(fakeBaseDir, "homedir", ".ui5", "framework"); +const ui5PackagesBaseDir = path.join(ui5FrameworkBaseDir, "packages"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.logStub = { + info: sinon.stub(), + verbose: sinon.stub(), + silly: sinon.stub(), + isLevelEnabled: sinon.stub().returns(false), + _getLogger: sinon.stub() + }; + const ui5Logger = { + getLogger: sinon.stub().returns(t.context.logStub) + }; + + t.context.pacote = { + extract: sinon.stub(), + manifest: sinon.stub() + }; + + class Config { + static get typeDefs() { + return {}; + } + + async load() {} + + get flat() { + return {}; + } + } + sinon.stub(Config.prototype, "flat").value({ + registry: "https://registry.fake", + proxy: "" + }); + + t.context.Registry = await esmock.p("../../../../lib/ui5Framework/npm/Registry.js", { + "@ui5/logger": ui5Logger, + "pacote": t.context.pacote, + "@npmcli/config": { + "default": Config + } + }); + + const AbstractInstaller = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "lockfile": { + lock: sinon.stub().yieldsAsync(), + unlock: sinon.stub().yieldsAsync() + } + }); + + t.context.Installer = await esmock.p("../../../../lib/ui5Framework/npm/Installer.js", { + "@ui5/logger": ui5Logger, + "graceful-fs": { + rename: sinon.stub().yieldsAsync(), + }, + "../../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "../../../../lib/ui5Framework/npm/Registry.js": t.context.Registry, + "../../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller + }); + + t.context.AbstractResolver = await esmock.p("../../../../lib/ui5Framework/AbstractResolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + }, + }); + + t.context.Openui5Resolver = await esmock.p("../../../../lib/ui5Framework/Openui5Resolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + }, + "../../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver, + "../../../../lib/ui5Framework/npm/Installer.js": t.context.Installer + }); + + t.context.Sapui5Resolver = await esmock.p("../../../../lib/ui5Framework/Sapui5Resolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + }, + "../../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver, + "../../../../lib/ui5Framework/npm/Installer.js": t.context.Installer + }); + + t.context.Application = await esmock.p("../../../../lib/specifications/types/Application.js"); + t.context.Library = await esmock.p("../../../../lib/specifications/types/Library.js"); + + // Stub specification internal checks since none of the projects actually exist on disk + sinon.stub(t.context.Application.prototype, "_configureAndValidatePaths").resolves(); + sinon.stub(t.context.Library.prototype, "_configureAndValidatePaths").resolves(); + sinon.stub(t.context.Application.prototype, "_parseConfiguration").resolves(); + sinon.stub(t.context.Library.prototype, "_parseConfiguration").resolves(); + + t.context.Specification = await esmock.p("../../../../lib/specifications/Specification.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/specifications/types/Application.js": t.context.Application, + "../../../../lib/specifications/types/Library.js": t.context.Library + }); + + t.context.Module = await esmock.p("../../../../lib/graph/Module.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/specifications/Specification.js": t.context.Specification + }); + + // Stub os homedir to prevent the actual ~/.ui5rc from being used in tests + t.context.Configuration = await esmock.p("../../../../lib/config/Configuration.js", { + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + } + }); + + t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/graph/Module.js": t.context.Module, + "../../../../lib/ui5Framework/Openui5Resolver.js": t.context.Openui5Resolver, + "../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5Resolver, + "../../../../lib/config/Configuration.js": t.context.Configuration + }); + + t.context.projectGraphBuilder = await esmock.p("../../../../lib/graph/projectGraphBuilder.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/graph/Module.js": t.context.Module + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Registry); + esmock.purge(t.context.Installer); + esmock.purge(t.context.AbstractResolver); + esmock.purge(t.context.Openui5Resolver); + esmock.purge(t.context.Sapui5Resolver); + esmock.purge(t.context.Application); + esmock.purge(t.context.Library); + esmock.purge(t.context.Specification); + esmock.purge(t.context.Module); + esmock.purge(t.context.ui5Framework); + esmock.purge(t.context.projectGraphBuilder); +}); + +function defineTest(testName, { + frameworkName, + verbose = false, + librariesInWorkspace = null +}) { + const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; + + const distributionMetadata = { + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + }, + "sap.ui.lib8": { + npmPackageName: "@sapui5/sap.ui.lib8", + version: "1.75.8", + dependencies: [], + optionalDependencies: [] + } + } + }; + + test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + const {sinon, ui5Framework, Installer, projectGraphBuilder, Module, pacote, logStub} = t.context; + + // Enable verbose logging + if (verbose) { + logStub.isLevelEnabled.withArgs("verbose").returns(true); + } + + const testDependency = { + id: "test-dependency-id", + version: "4.5.6", + path: path.join(fakeBaseDir, "project-test-dependency"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency" + }, + framework: { + version: "1.99.0", + name: frameworkName, + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + }, + { + name: "sap.ui.lib5", + optional: true + }, + { + name: "sap.ui.lib6", + development: true + }, + { + name: "sap.ui.lib8", + // optional dependency gets resolved by dev-dependency of root project + optional: true + } + ] + } + } + }; + const dependencyTree = { + id: "test-application-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "project-test-application"), + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-application" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + }, + { + name: "sap.ui.lib8", + development: true + } + ] + } + }, + dependencies: [ + testDependency, + { + id: "test-dependency-no-framework-id", + version: "7.8.9", + path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency-no-framework" + } + }, + dependencies: [ + testDependency + ] + } + ] + }; + + sinon.stub(Module.prototype, "_readConfigFile") + .callsFake(async function() { + // eslint-disable-next-line no-invalid-this + switch (this.getPath()) { + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", + frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib1" + }, + framework: { + name: frameworkName, + libraries: [] + } + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", + frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib2" + }, + framework: { + name: frameworkName, + libraries: [] + } + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", + frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib3" + }, + framework: { + name: frameworkName, + libraries: [] + } + }]; + case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", + frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib4" + }, + framework: { + name: frameworkName, + libraries: [] + } + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", + frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib8" + }, + framework: { + name: frameworkName, + libraries: [] + } + }]; + default: + throw new Error( + "Module#_readConfigFile stub called with unknown project: " + + // eslint-disable-next-line no-invalid-this + (this.getId()) + ); + } + }); + + pacote.extract.resolves(); + + if (frameworkName === "OpenUI5") { + pacote.manifest + .callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }) + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib2@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib3": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib3@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib3", + version: "1.75.0", + devDependencies: { + "@openui5/sap.ui.lib4": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib1": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib8@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib8", + version: "1.75.0", + dependencies: {} + }); + } else if (frameworkName === "SAPUI5") { + sinon.stub(Installer.prototype, "readJson") + .callsFake(async (path) => { + throw new Error("Installer#readJson stub called with unknown path: " + path); + }) + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves(distributionMetadata); + } + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + if (librariesInWorkspace) { + const projectNameMap = new Map(); + const moduleIdMap = new Map(); + librariesInWorkspace.forEach((libName) => { + const libraryDistMetadata = distributionMetadata.libraries[libName]; + const module = { + getSpecifications: sinon.stub().resolves({ + project: { + getName: sinon.stub().returns(libName), + getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"), + getRootPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)), + isFrameworkProject: sinon.stub().returns(true), + getId: sinon.stub().returns(libraryDistMetadata.npmPackageName), + getRootReader: sinon.stub().returns({ + byPath: sinon.stub().resolves({ + getString: sinon.stub().resolves(JSON.stringify({dependencies: {}})) + }) + }), + getFrameworkDependencies: sinon.stub().callsFake(() => { + const deps = []; + libraryDistMetadata.dependencies.forEach((dep) => { + deps.push({name: dep}); + }); + libraryDistMetadata.optionalDependencies.forEach((optDep) => { + deps.push({name: optDep, optional: true}); + }); + return deps; + }), + isDeprecated: sinon.stub().returns(false), + isSapInternal: sinon.stub().returns(false), + getAllowSapInternal: sinon.stub().returns(false), + } + }), + getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"), + getPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)), + }; + projectNameMap.set(libName, module); + moduleIdMap.set(libraryDistMetadata.npmPackageName, module); + }); + + const getModuleByProjectName = sinon.stub().callsFake( + async (projectName) => projectNameMap.get(projectName) + ); + const getModules = sinon.stub().callsFake( + async () => { + const sortedMap = new Map([...moduleIdMap].sort((a, b) => String(a[0]).localeCompare(b[0]))); + return Array.from(sortedMap.values()); + } + ); + + const workspace = { + getName: sinon.stub().returns("test"), + getModules, + getModuleByProjectName + }; + + await ui5Framework.enrichProjectGraph(projectGraph, {workspace}); + } else { + await ui5Framework.enrichProjectGraph(projectGraph); + } + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 8, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "sap.ui.lib1", + "sap.ui.lib8", + "sap.ui.lib4", + "sap.ui.lib3", + "sap.ui.lib2", + "test-dependency", + "test-dependency-no-framework", + "test-application", + ], "Traversed graph in correct order"); + + t.deepEqual(projectGraph.getDependencies("test-application"), [ + "test-dependency", + "test-dependency-no-framework", + "sap.ui.lib1", + "sap.ui.lib4", + "sap.ui.lib8", + ], `Non-framework dependency has correct dependencies`); + + t.deepEqual(projectGraph.getDependencies("test-dependency"), [ + "sap.ui.lib1", + "sap.ui.lib2", + "sap.ui.lib8", + ], `Non-framework dependency has correct dependencies`); + + const frameworkLibAlreadyAddedInfoLogged = (logStub.info.getCalls() + .map(($) => $.firstArg) + .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); + t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); + }); +} + +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5" +}); +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5", + verbose: true +}); +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5" +}); +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5", + verbose: true +}); + +defineTest("ui5Framework helper should not cause install of libraries within workspace", { + frameworkName: "SAPUI5", + librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"] +}); +defineTest("ui5Framework helper should not cause install of libraries within workspace", { + frameworkName: "OpenUI5", + librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"] +}); + +function defineErrorTest(testName, { + frameworkName, + failExtract = false, + failMetadata = false, + expectedErrorMessage +}) { + test.serial(testName, async (t) => { + const {sinon, ui5Framework, Installer, projectGraphBuilder, pacote} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] + } + } + }; + + pacote.extract.callsFake(async (spec) => { + throw new Error("pacote.extract stub called with unknown spec: " + spec); + }); + + pacote.manifest.callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }); + + if (frameworkName === "SAPUI5") { + if (failExtract) { + pacote.extract + .withArgs("@sapui5/sap.ui.lib1@1.75.1") + .rejects(new Error("404 - @sapui5/sap.ui.lib1")) + .withArgs("@openui5/sap.ui.lib4@1.75.4") + .rejects(new Error("404 - @openui5/sap.ui.lib4")); + } else { + pacote.extract + .withArgs("@sapui5/sap.ui.lib1@1.75.1").resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.4").resolves(); + } + if (failMetadata) { + pacote.extract + .withArgs("@sapui5/distribution-metadata@1.75.0") + .rejects(new Error("404 - @sapui5/distribution-metadata")); + } else { + pacote.extract + .withArgs("@sapui5/distribution-metadata@1.75.0") + .resolves(); + sinon.stub(Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + } + } else if (frameworkName === "OpenUI5") { + if (failExtract) { + pacote.extract + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("404 - @openui5/sap.ui.lib1")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("404 - @openui5/sap.ui.lib4")); + } else { + pacote.extract + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves(); + } + if (failMetadata) { + pacote.manifest + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib1@1.75.0")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib4@1.75.0")); + } else { + pacote.manifest + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0" + }); + } + } + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + await t.throwsAsync(async () => { + await ui5Framework.enrichProjectGraph(projectGraph); + }, {message: expectedErrorMessage}); + }); +} + +defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when metadata request fails", { + frameworkName: "SAPUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata + 2. Failed to resolve library sap.ui.lib4: Error already logged` +}); +defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when package extraction fails", { + frameworkName: "SAPUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/sap.ui.lib1@1.75.1: ` + +`404 - @sapui5/sap.ui.lib1 + 2. Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.4: ` + +`404 - @openui5/sap.ui.lib4` +}); +defineErrorTest( + "SAPUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { + frameworkName: "SAPUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata + 2. Failed to resolve library sap.ui.lib4: Error already logged` + }); + + +defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when metadata request fails", { + frameworkName: "OpenUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 + 2. Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` +}); +defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when package extraction fails", { + frameworkName: "OpenUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to extract package @openui5/sap.ui.lib1@1.75.0: ` + +`404 - @openui5/sap.ui.lib1 + 2. Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.0: ` + +`404 - @openui5/sap.ui.lib4` +}); +defineErrorTest( + "OpenUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { + frameworkName: "OpenUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 + 2. Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` + }); + +test.serial("ui5Framework helper should not fail when no framework configuration is given", async (t) => { + const {ui5Framework, projectGraphBuilder} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(projectGraph, projectGraph, "Returned same graph without error"); +}); + +test.serial("ui5Framework translator should not try to install anything when no library is referenced", async (t) => { + const {ui5Framework, projectGraphBuilder, pacote} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(pacote.extract.callCount, 0, "No package should be extracted"); + t.is(pacote.manifest.callCount, 0, "No manifest should be requested"); +}); + +test.serial("ui5Framework helper shouldn't throw when framework version and libraries are not provided", async (t) => { + const {ui5Framework, projectGraphBuilder, logStub} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(logStub.verbose.callCount, 5); + t.deepEqual(logStub.verbose.getCall(0).args, [ + "Configuration for module test-id has been supplied directly" + ]); + t.deepEqual(logStub.verbose.getCall(1).args, [ + "Module test-id contains project test-project" + ]); + t.deepEqual(logStub.verbose.getCall(2).args, [ + "Root project test-project qualified as application project for project graph" + ]); + t.deepEqual(logStub.verbose.getCall(3).args, [ + "Project test-project has no framework dependencies" + ]); + t.deepEqual(logStub.verbose.getCall(4).args, [ + "No SAPUI5 libraries referenced in project test-project or in any of its dependencies" + ]); +}); + +test.serial( + "SAPUI5: ui5Framework helper should throw error when using a library that is not part of the dist metadata", + async (t) => { + const {sinon, ui5Framework, Installer, projectGraphBuilder} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0", + libraries: [ + {name: "sap.ui.lib1"}, + {name: "does.not.exist"}, + {name: "sap.ui.lib4"}, + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + sinon.stub(Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + + await t.throwsAsync(async () => { + await ui5Framework.enrichProjectGraph(projectGraph); + }, { + message: `Failed to resolve library does.not.exist: Could not find library "does.not.exist"`}); + }); + +// TODO test: Should not download packages again in case they are already installed + +// TODO test: Should ignore framework libraries in dependencies diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.js b/packages/project/test/lib/graph/helpers/ui5Framework.js new file mode 100644 index 00000000000..ceae8c52e54 --- /dev/null +++ b/packages/project/test/lib/graph/helpers/ui5Framework.js @@ -0,0 +1,2517 @@ +import path from "node:path"; +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js"; +import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js"; +import Specification from "../../../../lib/specifications/Specification.js"; +import CacheMode from "../../../../lib/ui5Framework/maven/CacheMode.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); +const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f"); + +test.beforeEach(async (t) => { + // Tests either rely on not having UI5_DATA_DIR defined, or explicitly define it + t.context.originalUi5DataDirEnv = process.env.UI5_DATA_DIR; + delete process.env.UI5_DATA_DIR; + + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + info: sinon.stub(), + warn: sinon.stub(), + verbose: sinon.stub(), + isLevelEnabled: sinon.stub().returns(false), + _getLogger: sinon.stub() + }; + const ui5Logger = { + getLogger: sinon.stub().returns(t.context.log) + }; + + t.context.Openui5ResolverStub = sinon.stub(); + + t.context.Sapui5ResolverStub = sinon.stub(); + t.context.Sapui5ResolverInstallStub = sinon.stub(); + t.context.Sapui5ResolverStub.callsFake(() => { + return { + install: t.context.Sapui5ResolverInstallStub + }; + }); + t.context.Sapui5ResolverResolveVersionStub = sinon.stub(); + t.context.Sapui5ResolverStub.resolveVersion = t.context.Sapui5ResolverResolveVersionStub; + + t.context.Sapui5MavenSnapshotResolverInstallStub = sinon.stub(); + t.context.Sapui5MavenSnapshotResolverStub = sinon.stub() + .callsFake(() => { + return { + install: t.context.Sapui5MavenSnapshotResolverInstallStub + }; + }); + t.context.Sapui5MavenSnapshotResolverResolveVersionStub = sinon.stub(); + t.context.Sapui5MavenSnapshotResolverStub.resolveVersion = t.context.Sapui5MavenSnapshotResolverResolveVersionStub; + + t.context.getUi5DataDirStub = sinon.stub().returns(undefined); + + t.context.ConfigurationStub = { + fromFile: sinon.stub().resolves({ + getUi5DataDir: t.context.getUi5DataDirStub + }) + }; + + t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", { + "@ui5/logger": ui5Logger, + "../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5ResolverStub, + "../../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js": t.context.Sapui5MavenSnapshotResolverStub, + "../../../../lib/config/Configuration.js": t.context.ConfigurationStub, + }); + t.context.utils = t.context.ui5Framework._utils; +}); + +test.afterEach.always((t) => { + // Reset UI5_DATA_DIR env + if (typeof t.context.originalUi5DataDirEnv === "undefined") { + delete process.env.UI5_DATA_DIR; + } else { + process.env.UI5_DATA_DIR = t.context.originalUi5DataDirEnv; + } + t.context.sinon.restore(); + esmock.purge(t.context.ui5Framework); +}); + +test.serial("enrichProjectGraph", async (t) => { + const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + const getFrameworkLibrariesFromGraphStub = sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + + const addProjectToGraphStub = sinon.stub(); + const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: dependencyTree.configuration.framework.version, + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); + + t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once"); + t.deepEqual(t.context.Sapui5ResolverInstallStub.getCall(0).args, [ + referencedLibraries + ], "Sapui5Resolver#install should be called with expected args"); + + t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); + const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0]; + t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata, + "Correct libraryMetadata provided to ProjectProcessor"); + t.is(projectProcessorConstructorArgs.graph._rootProjectName, + "fake-root-of-application.a-framework-dependency-graph", + "Correct graph provided to ProjectProcessor"); + t.falsy(projectProcessorConstructorArgs.workspace, + "No workspace provided to ProjectProcessor"); + + t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); + t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 1)"); + t.deepEqual(addProjectToGraphStub.getCall(1).args[0], referencedLibraries[1], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 2)"); + t.deepEqual(addProjectToGraphStub.getCall(2).args[0], referencedLibraries[2], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 3)"); + + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 1, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.a" + ], "Traversed graph in correct order"); +}); + +test.serial("enrichProjectGraph: without framework configuration", async (t) => { + const {ui5Framework, log} = t.context; + const dependencyTree = { + id: "application.a", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + } + } + }; + + t.is(log.verbose.callCount, 0); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged"); + t.is(log.verbose.callCount, 1); + t.deepEqual(log.verbose.getCall(0).args, [ + "Root project application.a has no framework configuration. Nothing to do here" + ]); +}); + +test.serial("enrichProjectGraph SNAPSHOT", async (t) => { + const {sinon, ui5Framework, utils, Sapui5MavenSnapshotResolverInstallStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0-SNAPSHOT" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {libraryMetadata: {fake: "metadata"}}; + + const getFrameworkLibrariesFromGraphStub = sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5MavenSnapshotResolverInstallStub.resolves(libraryMetadata); + + const addProjectToGraphStub = sinon.stub(); + const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, { + cacheMode: CacheMode.Force + }); + + t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); + + t.is(t.context.Sapui5MavenSnapshotResolverInstallStub.callCount, + 1, "Sapui5MavenSnapshotResolverInstallStub#constructor should be called once"); + t.deepEqual( + t.context.Sapui5MavenSnapshotResolverInstallStub.getCall(0).args, + [["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]], + "Sapui5MavenSnapshotResolverInstallStub#constructor should be called with expected args" + ); + + t.is(t.context.Sapui5MavenSnapshotResolverInstallStub.callCount, 1, + "Sapui5MavenSnapshotResolverInstallStub#install should be called once"); + t.deepEqual(t.context.Sapui5MavenSnapshotResolverInstallStub.getCall(0).args, [ + referencedLibraries + ], "Sapui5MavenSnapshotResolverInstallStub#install should be called with expected args"); + + t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); + const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0]; + t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata.libraryMetadata, + "Correct libraryMetadata provided to ProjectProcessor"); + t.is(projectProcessorConstructorArgs.graph._rootProjectName, + "fake-root-of-application.a-framework-dependency-graph", + "Correct graph provided to ProjectProcessor"); + t.falsy(projectProcessorConstructorArgs.workspace, + "No workspace provided to ProjectProcessor"); + + t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); + t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 1)"); + t.deepEqual(addProjectToGraphStub.getCall(1).args[0], referencedLibraries[1], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 2)"); + t.deepEqual(addProjectToGraphStub.getCall(2).args[0], referencedLibraries[2], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 3)"); + + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 1, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.a" + ], "Traversed graph in correct order"); +}); + +test.serial("enrichProjectGraph: With versionOverride", async (t) => { + const { + sinon, ui5Framework, utils, + Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub + } = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + Sapui5ResolverResolveVersionStub.resolves("1.99.9"); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "1.99"}); + + t.is(Sapui5ResolverResolveVersionStub.callCount, 1); + t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["1.99", { + cwd: dependencyTree.path, + ui5DataDir: undefined, + }]); + + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.99.9", + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph: With versionOverride containing snapshot version", async (t) => { + const { + sinon, ui5Framework, utils, + Sapui5MavenSnapshotResolverStub, Sapui5MavenSnapshotResolverResolveVersionStub, + Sapui5MavenSnapshotResolverInstallStub + } = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5MavenSnapshotResolverInstallStub.resolves({libraryMetadata}); + + Sapui5MavenSnapshotResolverResolveVersionStub.resolves("1.99.9-SNAPSHOT"); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "1.99-SNAPSHOT"}); + + t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1); + t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["1.99-SNAPSHOT", { + cwd: dependencyTree.path, + ui5DataDir: undefined, + }]); + + t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, + "Sapui5MavenSnapshotResolverStub#constructor should be called once"); + t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.99.9-SNAPSHOT", + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot", async (t) => { + const { + sinon, ui5Framework, utils, + Sapui5MavenSnapshotResolverStub, Sapui5MavenSnapshotResolverResolveVersionStub, + Sapui5MavenSnapshotResolverInstallStub + } = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5MavenSnapshotResolverInstallStub.resolves({libraryMetadata}); + + Sapui5MavenSnapshotResolverResolveVersionStub.resolves("1.99.9-SNAPSHOT"); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "latest-snapshot"}); + + t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1); + t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["latest-snapshot", { + cwd: dependencyTree.path, + ui5DataDir: undefined, + }]); + + t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, + "Sapui5MavenSnapshotResolverStub#constructor should be called once"); + t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.99.9-SNAPSHOT", + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph shouldn't throw when no framework version and no libraries are provided", async (t) => { + const {ui5Framework, log, Sapui5ResolverResolveVersionStub} = t.context; + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5" + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + // Framework override is fine, even if no framework version is configured + await ui5Framework.enrichProjectGraph(projectGraph, { + versionOverride: "1.75.0" + }); + + t.is(Sapui5ResolverResolveVersionStub.callCount, 0, + "resolveVersion should not be called when no libraries are provided"); + + t.is(log.verbose.callCount, 2); + t.deepEqual(log.verbose.getCall(0).args, [ + "Project application.a has no framework dependencies" + ]); + t.deepEqual(log.verbose.getCall(1).args, [ + "No SAPUI5 libraries referenced in project application.a or in any of its dependencies" + ]); +}); + +test.serial("enrichProjectGraph should skip framework project without version", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged"); +}); + +test.serial("enrichProjectGraph should resolve framework project with version and framework config", async (t) => { + // Framework projects should not specify framework versions, but they might do so in dedicated configuration files + // In this case the graph is generated the usual way for the root-project. However, framework projects on + // other levels of the graph are ignored + const { + sinon, ui5Framework, utils, + Sapui5ResolverStub, Sapui5ResolverInstallStub + } = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.2.3", + libraries: [ + { + name: "lib1", + optional: true + } + ] + } + }, + dependencies: [{ + id: "@openui5/test1", // Will not be scanned + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "library.d" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib2" + }] + } + } + }, { + id: "@openui5/lib1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib1" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib3" + }] + } + } + }] + }; + const referencedLibraries = ["lib1"]; + const libraryMetadata = {fake: "metadata"}; + + const getFrameworkLibrariesFromGraphStub = + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getSize(), 3, "Project graph should remain unchanged"); + + t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once"); + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.2.3", + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph should resolve framework project " + + "with framework config and version override", async (t) => { + // Framework projects should not specify framework versions, but they might do so in dedicated configuration files + // In this case the graph is generated the usual way for the root-project. However, framework projects on + // other levels of the graph are ignored + const { + sinon, ui5Framework, utils, + Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub + } = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + libraries: [ + { + name: "lib1", + optional: true + } + ] + } + }, + dependencies: [{ + id: "@openui5/test1", // Will not be scanned + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "library.d" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib2" + }] + } + } + }, { + id: "@openui5/lib1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib1" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib3" + }] + } + } + }] + }; + const referencedLibraries = ["lib1"]; + const libraryMetadata = {fake: "metadata"}; + + const getFrameworkLibrariesFromGraphStub = + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + Sapui5ResolverResolveVersionStub.resolves("1.99.9"); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "3.4.5"}); + t.is(projectGraph.getSize(), 3, "Project graph should remain unchanged"); + + t.is(Sapui5ResolverResolveVersionStub.callCount, 1); + t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["3.4.5", { + cwd: dependencyTree.path, + ui5DataDir: undefined, + }]); + + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.99.9", + ui5DataDir: undefined, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph should skip framework project when all dependencies are in graph", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + libraries: [ + {name: "lib1"} + ] + } + }, + dependencies: [{ + id: "@openui5/lib1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib1" + } + } + }] + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getSize(), 2, "Project graph should remain unchanged"); +}); + +test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => { + const {ui5Framework, Sapui5ResolverInstallStub} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + libraries: [ + { + name: "lib1" + } + ] + } + } + }; + + const installError = new Error("Resolution of framework libraries failed with errors: TEST ERROR"); + + Sapui5ResolverInstallStub.rejects(installError); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph)); + t.is(err.message, installError.message); +}); + +test.serial("enrichProjectGraph should throw for incorrect framework name", async (t) => { + const {ui5Framework, sinon} = t.context; + const dependencyTree = { + id: "project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.2.3", + libraries: [ + { + name: "lib1", + optional: true + } + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + sinon.stub(projectGraph.getRoot(), "getFrameworkName").returns("Pony5"); + const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph)); + t.is(err.message, `Unknown framework.name "Pony5" for project application.a. Must be "OpenUI5" or "SAPUI5"`, + "Threw with expected error message"); +}); + +test.serial("enrichProjectGraph should ignore root project without framework configuration", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged"); +}); + +test.serial("enrichProjectGraph should throw error when projectGraph contains a framework library project " + +"that is also defined in framework configuration", async (t) => { + const { + sinon, ui5Framework, utils, + Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub + } = t.context; + const dependencyTree = { + id: "application.a", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [{ + name: "sap.ui.core" + }] + } + }, + dependencies: [{ + id: "@openui5/sap.ui.core", + version: "1.99.0", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "sap.ui.core" + } + } + }] + }; + + const referencedLibraries = ["sap.ui.core"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + Sapui5ResolverResolveVersionStub.resolves("1.100.0"); + + sinon.stub(utils, "ProjectProcessor") + .callsFake(({graph}) => { + return { + async addProjectToGraph() { + const fakeCoreProject = await Specification.create({ + id: "@openui5/sap.ui.core", + version: "1.100.0", + modulePath: libraryEPath, + configuration: { + specVersion: "3.1", + kind: "project", + type: "library", + metadata: { + name: "sap.ui.core" + } + } + }); + graph.addProject(fakeCoreProject); + } + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph), { + message: `Duplicate framework dependency definition(s) found for project application.a: sap.ui.core.\n` + + `Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, ` + + `nor any of its dependencies should include them as direct dependencies (e.g. via package.json).` + }); +}); + +test.serial("enrichProjectGraph should use framework library metadata from workspace", async (t) => { + const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.111.1", + libraries: [ + {name: "lib1"}, + {name: "lib2"} + ] + } + } + }; + + const workspace = { + getName: sinon.stub().resolves("default") + }; + + const workspaceFrameworkLibraryMetadata = {}; + const libraryMetadata = {}; + + sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata); + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves(); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider, {workspace}); + + await ui5Framework.enrichProjectGraph(projectGraph, {workspace}); + + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: "1.111.1", + ui5DataDir: undefined, + providedLibraryMetadata: workspaceFrameworkLibraryMetadata + }], "Sapui5Resolver#constructor should be called with expected args"); + t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata); +}); + +test.serial("enrichProjectGraph should allow omitting framework version in case " + + "all framework libraries come from the workspace", async (t) => { + const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + libraries: [ + {name: "lib1"}, + {name: "lib2"} + ] + } + } + }; + + const workspace = { + getName: sinon.stub().resolves("default") + }; + + const workspaceFrameworkLibraryMetadata = {}; + const libraryMetadata = {}; + + sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata); + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves(); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider, {workspace}); + + await ui5Framework.enrichProjectGraph(projectGraph, {workspace}); + + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + ui5DataDir: undefined, + version: undefined, + providedLibraryMetadata: workspaceFrameworkLibraryMetadata + }], "Sapui5Resolver#constructor should be called with expected args"); + t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata); +}); + +test.serial("enrichProjectGraph should use UI5 data dir from env var", async (t) => { + const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + process.env.UI5_DATA_DIR = "./ui5-data-dir-from-env-var"; + + const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-env-var"); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: dependencyTree.configuration.framework.version, + ui5DataDir: expectedUi5DataDir, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph should use UI5 data dir from configuration", async (t) => { + const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + getUi5DataDirStub.returns("./ui5-data-dir-from-config"); + + const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-config"); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: dependencyTree.configuration.framework.version, + ui5DataDir: expectedUi5DataDir, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("enrichProjectGraph should use absolute UI5 data dir from configuration", async (t) => { + const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + getUi5DataDirStub.returns("/absolute-ui5-data-dir-from-config"); + + const expectedUi5DataDir = path.resolve("/absolute-ui5-data-dir-from-config"); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ + cacheMode: undefined, + cwd: dependencyTree.path, + version: dependencyTree.configuration.framework.version, + ui5DataDir: expectedUi5DataDir, + providedLibraryMetadata: undefined + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("utils.shouldIncludeDependency", (t) => { + const {utils} = t.context; + // root project dependency should always be included + t.true(utils.shouldIncludeDependency({}, true)); + t.true(utils.shouldIncludeDependency({optional: true}, true)); + t.true(utils.shouldIncludeDependency({optional: false}, true)); + t.true(utils.shouldIncludeDependency({optional: null}, true)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, true)); + t.true(utils.shouldIncludeDependency({development: true}, true)); + t.true(utils.shouldIncludeDependency({development: false}, true)); + t.true(utils.shouldIncludeDependency({development: null}, true)); + t.true(utils.shouldIncludeDependency({development: "abc"}, true)); + t.true(utils.shouldIncludeDependency({foo: true}, true)); + + t.true(utils.shouldIncludeDependency({}, false)); + t.false(utils.shouldIncludeDependency({optional: true}, false)); + t.true(utils.shouldIncludeDependency({optional: false}, false)); + t.true(utils.shouldIncludeDependency({optional: null}, false)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, false)); + t.false(utils.shouldIncludeDependency({development: true}, false)); + t.true(utils.shouldIncludeDependency({development: false}, false)); + t.true(utils.shouldIncludeDependency({development: null}, false)); + t.true(utils.shouldIncludeDependency({development: "abc"}, false)); + t.true(utils.shouldIncludeDependency({foo: true}, false)); + + // Having both optional and development should not be the case, but that should be validated beforehand + t.true(utils.shouldIncludeDependency({optional: true, development: true}, true)); + t.false(utils.shouldIncludeDependency({optional: true, development: true}, false)); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies", async (t) => { + const {utils} = t.context; + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [] + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, []); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Framework project with framework dependency", async (t) => { + // Only root-level framework projects are scanned + const {utils} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [ + { + name: "lib1" + } + ] + } + }, + dependencies: [{ + id: "@openui5/test1", // Will not be scanned + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "library.d" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib2" + }] + } + } + }] + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, ["lib1"]); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", async (t) => { + const {utils} = t.context; + const dependencyTree = { + id: "test-project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [{ + name: "lib1" + }, { + name: "lib2", + optional: true + }, { + name: "lib6", + development: true + }] + } + }, + dependencies: [{ + id: "test2", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test2" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib3" + }, { + name: "lib4", + optional: true + }] + } + }, + dependencies: [{ + id: "test3", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test3" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib5" + }, { + name: "lib7", + optional: true + }] + } + } + }] + }, { + id: "@openui5/lib8", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib8" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "should.be.ignored" + }] + } + } + }, { + id: "@openui5/lib9", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib9" + } + } + }] + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]); +}); + +test.serial("utils.declareFrameworkDependenciesInGraph", async (t) => { + const {utils, sinon, log} = t.context; + const projectTree = { + id: "test-project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [{ + name: "lib1" + }, { + name: "lib2", + optional: true + }, { + name: "lib3", + development: true + }, { + name: "lib5", + optional: true + }] + } + }, + dependencies: [{ + id: "library.a", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "library.a" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib2" + }, { + name: "lib3", + optional: true + }, { + name: "lib4", + development: true + }, { + name: "lib5", + optional: true + }, { + name: "lib6", + optional: true + }] + } + }, + dependencies: [] + }] + }; + const frameworkTree = { + id: "dummy-framework-tree-root", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "dummy-framework-tree-root" + } + }, + dependencies: [{ + id: "@openui5/lib1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib1", + deprecated: true + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "should.be.ignored" + }] + } + } + }, { + id: "@openui5/lib2", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib2", + sapInternal: true + } + } + }, { + id: "@openui5/lib3", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib3", + deprecated: true + } + } + }, { + id: "@openui5/lib5", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib5" + } + } + }] + }; + const projectGraph = await projectGraphBuilder(new DependencyTreeProvider({ + dependencyTree: projectTree + })); + const frameworkGraph = await projectGraphBuilder(new DependencyTreeProvider({ + dependencyTree: frameworkTree + })); + projectGraph.join(frameworkGraph); + + const declareDependencySpy = sinon.spy(projectGraph, "declareDependency"); + const declareOptionalDependencySpy = sinon.spy(projectGraph, "declareOptionalDependency"); + const resolveOptionalDependenciesSpy = sinon.spy(projectGraph, "resolveOptionalDependencies"); + await utils.declareFrameworkDependenciesInGraph(projectGraph); + + t.is(declareDependencySpy.callCount, 7, "declareDependency got called seven times"); + t.deepEqual(declareDependencySpy.getCall(0).args, ["application.a", "lib1"], + "declareDependency got called with correct arguments on first call"); + t.deepEqual(declareDependencySpy.getCall(1).args, ["application.a", "lib2"], + "declareDependency got called with correct arguments on second call"); + t.deepEqual(declareDependencySpy.getCall(2).args, ["application.a", "lib3"], + "declareDependency got called with correct arguments on third call"); + t.deepEqual(declareDependencySpy.getCall(3).args, ["application.a", "lib5"], + "declareDependency got called with correct arguments on fourth call"); + t.deepEqual(declareDependencySpy.getCall(4).args, ["library.a", "lib2"], + "declareDependency got called with correct arguments on fifth call"); + t.deepEqual(declareDependencySpy.getCall(5).args, ["library.a", "lib3"], + "declareDependency got called with correct arguments on sixth call"); + t.deepEqual(declareDependencySpy.getCall(6).args, ["library.a", "lib5"], + "declareDependency got called with correct arguments on seventh call"); + t.is(declareOptionalDependencySpy.callCount, 2, "declareOptionalDependency got called "); + t.deepEqual(declareOptionalDependencySpy.getCall(0).args, ["library.a", "lib3"], + "declareOptionalDependency got called with correct arguments on first call"); + t.deepEqual(declareOptionalDependencySpy.getCall(1).args, ["library.a", "lib5"], + "declareOptionalDependency got called with correct arguments on second call"); + t.is(resolveOptionalDependenciesSpy.callCount, 1, + "resolveOptionalDependenciesSpy got called once"); + + t.is(log.warn.callCount, 3, + "Three warnings got logged"); + t.is(log.warn.getCall(0).args[0], "Dependency lib1 is deprecated and should not be used for new projects!", + "Expected first warning logged"); + t.is(log.warn.getCall(1).args[0], + `Dependency lib2 is restricted for use by SAP internal projects only! If the project application.a is ` + + `an SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`, + "Expected first warning logged"); + t.is(log.warn.getCall(2).args[0], "Dependency lib3 is deprecated and should not be used for new projects!", + "Expected first warning logged"); + + t.deepEqual(projectGraph.getDependencies("application.a"), [ + "library.a", + "lib1", + "lib2", + "lib3", + "lib5" + ], `Root project has correct dependencies`); + + t.deepEqual(projectGraph.getDependencies("library.a"), [ + "lib2", + "lib3", + "lib5" + ], `Non-framework dependency has correct dependencies`); +}); + +test.serial("utils.declareFrameworkDependenciesInGraph: No deprecation warnings for testsuite projects", async (t) => { + const {utils, log} = t.context; + + const projectTree = { + id: "test-project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "testsuite" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [{ + name: "lib1" + }, { + name: "lib2", + optional: true + }, { + name: "lib3", + development: true + }] + } + }, + dependencies: [] + }; + const frameworkTree = { + id: "dummy-framework-tree-root", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "dummy-framework-tree-root" + } + }, + dependencies: [{ + id: "@openui5/lib1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib1", + deprecated: true + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "should.be.ignored" + }] + } + } + }, { + id: "@openui5/lib2", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib2", + sapInternal: true + } + } + }, { + id: "@openui5/lib3", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib3", + deprecated: true + } + } + }] + }; + const projectGraph = await projectGraphBuilder(new DependencyTreeProvider({ + dependencyTree: projectTree + })); + const frameworkGraph = await projectGraphBuilder(new DependencyTreeProvider({ + dependencyTree: frameworkTree + })); + projectGraph.join(frameworkGraph); + + await utils.declareFrameworkDependenciesInGraph(projectGraph); + + t.is(log.warn.callCount, 1, + "One warning got logged"); + + t.is(log.warn.getCall(0).args[0], + `Dependency lib2 is restricted for use by SAP internal projects only! If the project testsuite is ` + + `an SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`, + "Expected first warning logged"); + + t.deepEqual(projectGraph.getDependencies("testsuite"), [ + "lib1", + "lib2", + "lib3" + ], `Root project has correct dependencies`); +}); + +test("utils.checkForDuplicateFrameworkProjects: No duplicates", (t) => { + const {utils, sinon} = t.context; + + const projectGraph = { + getRoot: sinon.stub().returns({ + getName: sinon.stub().returns("root-project") + }), + getProjectNames: sinon.stub().returns(["lib1", "lib2", "lib3"]) + }; + const frameworkGraph = { + getProjectNames: sinon.stub().returns(["sap.ui.core"]) + }; + + t.notThrows(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph)); +}); + +test("utils.checkForDuplicateFrameworkProjects: One duplicate", (t) => { + const {utils, sinon} = t.context; + + const projectGraph = { + getRoot: sinon.stub().returns({ + getName: sinon.stub().returns("root-project") + }), + getProjectNames: sinon.stub().returns(["lib1", "sap.ui.core", "lib2", "lib3"]) + }; + const frameworkGraph = { + getProjectNames: sinon.stub().returns(["sap.ui.core"]) + }; + + t.throws(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph), { + message: "Duplicate framework dependency definition(s) found for project root-project: " + + "sap.ui.core.\n" + + "Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, " + + "nor any of its dependencies should include them as direct dependencies (e.g. via package.json)." + }); +}); + +test("utils.checkForDuplicateFrameworkProjects: Two duplicates", (t) => { + const {utils, sinon} = t.context; + + const projectGraph = { + getRoot: sinon.stub().returns({ + getName: sinon.stub().returns("root-project") + }), + getProjectNames: sinon.stub().returns(["lib1", "sap.ui.core", "lib2", "sap.ui.layout", "lib3"]) + }; + const frameworkGraph = { + getProjectNames: sinon.stub().returns(["sap.ui.core", "sap.ui.layout", "sap.m"]) + }; + + t.throws(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph), { + message: "Duplicate framework dependency definition(s) found for project root-project: " + + "sap.ui.core, sap.ui.layout.\n" + + "Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, " + + "nor any of its dependencies should include them as direct dependencies (e.g. via package.json)." + }); +}); + +test("utils.getFrameworkLibraryDependencies: OpenUI5 library", async (t) => { + const {utils, sinon} = t.context; + + const project = { + getId: sinon.stub().returns("@openui5/sap.ui.lib1"), + getRootReader: sinon.stub().returns({ + byPath: sinon.stub().withArgs("/package.json").resolves({ + getString: sinon.stub().resolves(JSON.stringify({ + dependencies: { + "@openui5/sap.ui.lib2": "*" + }, + devDependencies: { + "@openui5/themelib_fancy": "*" + } + })) + }) + }), + }; + + const result = await utils.getFrameworkLibraryDependencies(project); + t.deepEqual(result, { + dependencies: ["sap.ui.lib2"], + optionalDependencies: ["themelib_fancy"] + }); +}); + +test("utils.getFrameworkLibraryDependencies: SAPUI5 library", async (t) => { + const {utils, sinon} = t.context; + + const project = { + getId: sinon.stub().returns("@sapui5/sap.ui.lib1"), + getFrameworkDependencies: sinon.stub().returns([ + { + name: "sap.ui.lib2" + }, + { + name: "themelib_fancy", + optional: true + }, + { + name: "sap.ui.lib3", + development: true + } + ]) + }; + + const result = await utils.getFrameworkLibraryDependencies(project); + t.deepEqual(result, { + dependencies: ["sap.ui.lib2"], + optionalDependencies: ["themelib_fancy"] + }); +}); + +test("utils.getFrameworkLibraryDependencies: OpenUI5 library - no dependencies", async (t) => { + const {utils, sinon} = t.context; + + const project = { + getId: sinon.stub().returns("@openui5/sap.ui.lib1"), + getRootReader: sinon.stub().returns({ + byPath: sinon.stub().withArgs("/package.json").resolves({ + getString: sinon.stub().resolves(JSON.stringify({})) + }) + }), + }; + + const result = await utils.getFrameworkLibraryDependencies(project); + t.deepEqual(result, { + dependencies: [], + optionalDependencies: [] + }); +}); + +test("utils.getFrameworkLibraryDependencies: No framework library", async (t) => { + const {utils, sinon} = t.context; + + const project = { + getId: sinon.stub().returns("foo") + }; + + const result = await utils.getFrameworkLibraryDependencies(project); + t.deepEqual(result, { + dependencies: [], + optionalDependencies: [] + }); +}); + +test("utils.getWorkspaceFrameworkLibraryMetadata: No workspace modules", async (t) => { + const {utils, sinon} = t.context; + + const workspace = { + getModules: sinon.stub().resolves([]) + }; + + const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph: {}}); + + t.deepEqual(libraryMetadata, {}); +}); + +test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace modules", async (t) => { + const {utils, sinon} = t.context; + + const workspace = { + getModules: sinon.stub().resolves([ + { + // Extensions don't have projects, should be ignored + getSpecifications: sinon.stub().resolves({ + project: null + }) + }, + { + getSpecifications: sinon.stub().resolves({ + project: { + // some types don't have a "isFrameworkProject" method + } + }) + }, + { + getSpecifications: sinon.stub().resolves({ + project: { + isFrameworkProject: sinon.stub().returns(false) + } + }) + }, + { + getSpecifications: sinon.stub().resolves({ + project: { + isFrameworkProject: sinon.stub().returns(true), + getName: sinon.stub().returns("sap.ui.lib1"), + getId: sinon.stub().returns("@openui5/sap.ui.lib1"), + getRootPath: sinon.stub().returns("/rootPath"), + getRootReader: sinon.stub().returns({ + byPath: sinon.stub().withArgs("/package.json").resolves({ + getString: sinon.stub().resolves(JSON.stringify({ + dependencies: { + "@openui5/sap.ui.lib2": "*" + }, + devDependencies: { + "@openui5/themelib_fancy": "*" + } + })) + }) + }), + getVersion: sinon.stub().returns("1.0.0"), + } + }) + }, + { + getSpecifications: sinon.stub().resolves({ + project: { + isFrameworkProject: sinon.stub().returns(true), + getName: sinon.stub().returns("sap.ui.lib3"), + getId: sinon.stub().returns("@sapui5/sap.ui.lib3"), + getRootPath: sinon.stub().returns("/rootPath"), + getVersion: sinon.stub().returns("1.0.0"), + getFrameworkDependencies: sinon.stub().returns([ + { + name: "sap.ui.lib4" + }, + { + name: "sap.ui.lib5", + optional: true + }, + { + name: "sap.ui.lib6", + development: true + } + ]) + } + }) + } + ]) + }; + + const getProject = sinon.stub(); + getProject.withArgs("sap.ui.lib1").returns(undefined); + const projectGraph = { + getProject + }; + + const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph}); + + t.deepEqual(libraryMetadata, { + "sap.ui.lib1": { + dependencies: [ + "sap.ui.lib2" + ], + id: "@openui5/sap.ui.lib1", + optionalDependencies: [ + "themelib_fancy" + ], + path: "/rootPath", + version: "1.0.0" + }, + "sap.ui.lib3": { + dependencies: [ + "sap.ui.lib4" + ], + id: "@sapui5/sap.ui.lib3", + optionalDependencies: [ + "sap.ui.lib5" + ], + path: "/rootPath", + version: "1.0.0" + }, + }); +}); + +test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace module within projectGraph", async (t) => { + const {utils, sinon} = t.context; + + const workspace = { + getModules: sinon.stub().resolves([ + { + getSpecifications: sinon.stub().resolves({ + project: { + isFrameworkProject: sinon.stub().returns(true), + getName: sinon.stub().returns("sap.ui.lib1") + } + }) + } + ]) + }; + + const getProject = sinon.stub(); + getProject.withArgs("sap.ui.lib1").returns({}); + const projectGraph = { + getProject + }; + + const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph}); + + t.deepEqual(libraryMetadata, {}); +}); + +test.serial("ProjectProcessor: Add project to graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got called with the correct project"); +}); + +test.serial("ProjectProcessor: Add same project twice", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got called with the correct project"); +}); + +test.serial("ProjectProcessor: Project already in graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns("project"), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 0, "graph#addProject never got called"); +}); + +test.serial("ProjectProcessor: Add project with dependencies to graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called twice"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d", + "graph#addProject got called with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e", + "graph#addProject got called with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got called with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getVersion: () => "1.0.0", + getPath: () => path.join("module", "path"), + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryDProjectMock + }) + .onSecondCall().resolves({ + project: libraryEProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d", + "graph#addProject got called with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e", + "graph#addProject got called with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got called with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with additional dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getVersion: () => "1.0.0", + getPath: () => path.join("module", "path"), + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }) + .onSecondCall().resolves({ + project: libraryDProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], // Dependency to library.d is only declared in workspace-resolved library.e + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got called with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.d", + "graph#addProject got called with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got called with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with additional, unknown dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.xyz" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getVersion: () => "1.0.0", + getPath: () => path.join("module", "path"), + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }) + .onSecondCall().resolves({ + project: libraryDProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), { + message: + "Unable to find dependency library.xyz, required by project library.e " + + "(resolved via workspace name workspace) " + + "in current set of libraries. Try adding it temporarily to the root project's dependencies" + }, "Threw with expected error message"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with cyclic dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.e" // Cyclic dependency in workspace project + }]) + }; + const moduleMock = { + getVersion: () => "1.0.0", + getPath: () => path.join("module", "path"), + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }) + .onSecondCall().resolves({ + project: libraryDProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), { + message: + "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " + + "library.e -> *library.d* -> *library.d*" + }, "Threw with expected error message"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with distant cyclic dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.f" + }]) + }; + const libraryFProjectMock = { + getName: () => "library.f", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.e" // Cyclic dependency in workspace project + }]) + }; + const moduleMock = { + getVersion: () => "1.0.0", + getPath: () => path.join("module", "path"), + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }) + .onSecondCall().resolves({ + project: libraryDProjectMock + }) + .onThirdCall().resolves({ + project: libraryFProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: ["library.f"], + optionalDependencies: [] + }, + "library.f": { + id: "lib.f.id", + version: "1.0.0", + path: libraryFPath, + dependencies: [], + optionalDependencies: [] + }, + }, + graph: graphMock, + workspace: workspaceMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), { + message: + "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " + + "library.e -> *library.d* -> library.f -> *library.d*" + }, "Threw with expected error message"); +}); + +test.serial("ProjectProcessor: Project missing in metadata", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "lib.x": {} + }, + graph: graphMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("lib.a"), { + message: "Failed to find library lib.a in dist packages metadata.json" + }, "Threw with expected error message"); +}); diff --git a/packages/project/test/lib/graph/projectGraphBuilder.js b/packages/project/test/lib/graph/projectGraphBuilder.js new file mode 100644 index 00000000000..d6bfdd392a0 --- /dev/null +++ b/packages/project/test/lib/graph/projectGraphBuilder.js @@ -0,0 +1,967 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import projectGraphBuilder from "../../../lib/graph/projectGraphBuilder.js"; + +const __dirname = import.meta.dirname; + +const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); +const libraryFPath = path.join(__dirname, "..", "..", "fixtures", "library.f"); +const libraryGPath = path.join(__dirname, "..", "..", "fixtures", "library.g"); +const collectionPath = path.join(__dirname, "..", "..", "fixtures", "collection"); +const nonExistingPath = path.join(__dirname, "..", "..", "fixtures", "does-not-exist"); + +function createNode({id, name, version = "1.0.0", modulePath, optional, configuration}) { + if (!Array.isArray(configuration)) { + configuration = Object.assign({ + specVersion: "2.6", + type: "library", + metadata: { + name: name || id + } + }, configuration); + } + return { + id, + version, + path: modulePath || libraryEPath, + optional, + configuration + }; +} + +function traverseBreadthFirst(...args) { + return _traverse(...args, true); +} + +async function _traverse(t, graph, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await graph.traverseBreadthFirst(callbackStub); + } else { + await graph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); +} + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.getRootNode = t.context.sinon.stub(); + t.context.getDependencies = t.context.sinon.stub().resolves([]); + + t.context.provider = { + getRootNode: t.context.getRootNode, + getDependencies: t.context.getDependencies, + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Basic graph creation", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1" + })); + const graph = await projectGraphBuilder(t.context.provider); + + await traverseBreadthFirst(t, graph, [ + "id1" + ]); + + const p = graph.getProject("id1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + + t.is(t.context.getRootNode.callCount, 1, "NodeProvider#getRoodNode got called once"); + t.is(t.context.getDependencies.callCount, 1, "NodeProvider#getDependencies got called once"); +}); + +test("Basic graph with dependencies", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + })]); + t.context.getDependencies.onSecondCall().resolves([createNode({ + id: "id3", + name: "project-3" + })]); + const graph = await projectGraphBuilder(t.context.provider); + + await traverseBreadthFirst(t, graph, [ + "project-1", + "project-2", + "project-3" + ]); + + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + + t.is(t.context.getRootNode.callCount, 1, "NodeProvider#getRoodNode got called once"); + t.is(t.context.getDependencies.callCount, 3, "NodeProvider#getDependencies got called once"); +}); + +test.serial("Correct warnings logged", async (t) => { + const {sinon, getRootNode, getDependencies, provider} = t.context; + const logWarnStub = sinon.stub(); + + const projectGraphBuilder = await esmock("../../../lib/graph/projectGraphBuilder.js", { + "@ui5/logger": { + getLogger: sinon.stub() + .withArgs("graph:projectGraphBuilder").returns({ + warn: logWarnStub, + verbose: () => "", + silly: () => "", + }) + } + }); + + getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + const node2 = createNode({ + id: "id2", + name: "project-2" + }); + node2.configuration.metadata.deprecated = true; + node2.configuration.metadata.sapInternal = true; + getDependencies.onFirstCall().resolves([node2]); + const node3 = createNode({ + id: "id3", + name: "project-3" + }); + node3.configuration.metadata.deprecated = true; + node3.configuration.metadata.sapInternal = true; + getDependencies.onSecondCall().resolves([node3]); + const graph = await projectGraphBuilder(provider); + + await traverseBreadthFirst(t, graph, [ + "project-1", + "project-2", + "project-3" + ]); + + t.is(logWarnStub.callCount, 2, "Two warnings logged"); + t.is(logWarnStub.getCall(0).args[0], "Dependency project-2 is deprecated and should not be used for new projects!", + "Correct deprecation warning logged"); + t.is(logWarnStub.getCall(1).args[0], + `Dependency project-2 is restricted for use by SAP internal projects only! If the project project-1 is an ` + + `SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`, + "Correct SAP-internal project warning logged"); +}); + +test.serial("No warnings logged", async (t) => { + const {sinon, getRootNode, getDependencies} = t.context; + const logWarnStub = sinon.stub(); + + const projectGraphBuilder = await esmock("../../../lib/graph/projectGraphBuilder.js", { + "@ui5/logger": { + getLogger: sinon.stub() + .withArgs("graph:projectGraphBuilder").returns({ + warn: logWarnStub, + verbose: () => "", + silly: () => "", + }) + } + }); + + const node1 = createNode({ + id: "id1", + name: "testsuite" // "testsuite" name should suppress deprecation warnings + }); + node1.configuration.metadata.allowSapInternal = true; + getRootNode.resolves(node1); + const node2 = createNode({ + id: "id2", + name: "project-2" + }); + node2.configuration.metadata.deprecated = true; + node2.configuration.metadata.sapInternal = true; + getDependencies.onFirstCall().resolves([node2]); + const node3 = createNode({ + id: "id3", + name: "project-3" + }); + node3.configuration.metadata.deprecated = true; + node3.configuration.metadata.sapInternal = true; + getDependencies.onSecondCall().resolves([node3]); + const graph = await projectGraphBuilder(t.context.provider); + + await traverseBreadthFirst(t, graph, [ + "testsuite", + "project-2", + "project-3" + ]); + + t.is(logWarnStub.callCount, 0, "No warnings logged"); +}); + +test("Legacy node with specVersion attribute as root", async (t) => { + const node = createNode({ + id: "id1" + }); + node.specVersion = "1.0"; + t.context.getRootNode.resolves(node); + const err = await t.throwsAsync(projectGraphBuilder(t.context.provider)); + + t.is(err.message, + "Provided node with ID id1 contains a top-level 'specVersion' property. With UI5 CLI 3.0, " + + "project configuration needs to be provided in a dedicated 'configuration' object", + "Threw with expected error message"); +}); + +test("Legacy node with metadata attribute in dependencies", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1" + })); + const node = createNode({ + id: "id2" + }); + node.metadata = {name: "id2"}; + t.context.getDependencies.resolves([node]); + const err = await t.throwsAsync(projectGraphBuilder(t.context.provider)); + + t.is(err.message, + "Provided node with ID id2 contains a top-level 'metadata' property. With UI5 CLI 3.0, " + + "project configuration needs to be provided in a dedicated 'configuration' object", + "Threw with expected error message"); +}); + +test("Node depends on itself", async (t) => { + const node = createNode({ + id: "id1", + name: "project-1" + }); + t.context.getRootNode.resolves(node); + t.context.getDependencies.resolves([node]); + const err = await t.throwsAsync(projectGraphBuilder(t.context.provider)); + + t.is(err.message, + "Failed to declare dependency from project project-1 to project-1: A project can't depend on itself", + "Threw with expected error message"); +}); + +test("Cyclic dependencies", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies + .onFirstCall().resolves([ + createNode({ + id: "id2", + name: "project-2" + }), + ]) + .onSecondCall().resolves([ + createNode({ + id: "id1", + name: "project-1" + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.deepEqual(graph.getDependencies("project-1"), ["project-2"], "Cyclic dependency has been added"); + t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added"); +}); + +test("Nested node with same id is processed correctly", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + name: "project-2" + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + createNode({ + id: "id1", + name: "project-3" // name will be ignored, since the first "id1" node is being used + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.falsy(graph.getProject("project-3"), "Configuration of project with same ID has been ignored"); + t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added"); +}); + +test("Nested node with different id but same project is processed correctly", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + name: "project-2", + modulePath: libraryFPath + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + createNode({ + id: "id3", + name: "project-1", // Project is already in the graph and won't be added again + modulePath: libraryGPath + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added"); +}); + +test("Unresolved optional dependency", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + // Deps of id1 + createNode({ + id: "id2", + name: "project-2", + optional: true + }), + createNode({ + id: "id3", + name: "project-3" + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + name: "project-4" + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + id: "id2", + name: "project-2", + optional: true + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.deepEqual(graph.getDependencies("project-1"), ["project-3"], "Correct dependencies for project-1"); + t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2"); + t.deepEqual(graph.getDependencies("project-3"), [], "Correct dependencies for project-3"); +}); + +test("Nested node with same project resolves optional dependency", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + // Deps of id1 + createNode({ + id: "id2", + name: "project-2", + optional: true + }), + createNode({ + id: "id3", + name: "project-3" + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + name: "project-4" + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + // non-optional dependency to id2/project-2 + id: "id2", + name: "project-2", + modulePath: libraryGPath // Different path but same module id should be ignored (first module is reused) + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.deepEqual(graph.getDependencies("project-1"), ["project-3", "project-2"], "Correct dependencies for project-1"); + t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2"); + t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Correct dependencies for project-3"); +}); + +test("Nested node with different id but same project resolves optional dependency", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + // Deps of id1 + createNode({ + id: "id2", + name: "project-2", + optional: true + }), + createNode({ + id: "id3", + name: "project-3" + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + name: "project-4" + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + // non-optional dependency to project-2 + id: "id5", // Different module but same project should still resolve the optional dependency + name: "project-2", + modulePath: libraryGPath + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.deepEqual(graph.getDependencies("project-1"), ["project-3", "project-2"], "Correct dependencies for project-1"); + t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2"); + t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Correct dependencies for project-3"); +}); + +test("Root node must provide a project", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1", + modulePath: collectionPath, + configuration: { + kind: "extension", + type: "project-shim", + shims: { + collections: { + "id1": { + modules: { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c", + } + } + } + } + } + })); + const err = await t.throwsAsync(projectGraphBuilder(t.context.provider)); + t.is(err.message, + `Failed to create a UI5 project from module id1 at ${collectionPath}. ` + + `Make sure the path is correct and a project configuration is present or supplied.`, + "Threw with expected error message"); +}); + +test("Dependency is a collection", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + name: "shim-1", + modulePath: collectionPath, + configuration: { + kind: "extension", + type: "project-shim", + shims: { + collections: { + "id2": { + modules: { + "lib.a": "./library.a", + "lib.b": "./library.b", + "lib.c": "./library.c", + } + } + }, + dependencies: { + "lib.a": ["lib.b"], + } + } + } + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + await traverseBreadthFirst(t, graph, [ + "project-1", + "library.a", + "library.b", + "library.c" + ]); + const p = graph.getProject("project-1"); + t.is(p.getRootPath(), libraryEPath, "Project returned correct path"); + t.deepEqual(graph.getDependencies("project-1"), [ + "library.a", "library.b", "library.c" + ], "Correct dependencies for root node maintained"); + t.deepEqual(graph.getDependencies("library.a"), [ + "library.b" + ], "Correct dependencies for library.a maintained"); +}); + +test("Shim in root defines collection", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + configuration: [{ + specVersion: "2.6", + type: "library", + metadata: { + name: "project-1" + } + }, { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim" + }, + shims: { + collections: { + "id2": { + modules: { + "lib.a": "./library.a", + "lib.b": "./library.b", + "lib.c": "./library.c", + } + } + }, + dependencies: { + "lib.a": ["lib.b"], + }, + configurations: { + "lib.a": { + customConfiguration: { + someConfig: true + } + } + } + } + }] + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + name: "shim-1", + modulePath: collectionPath, + configuration: [] + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + await traverseBreadthFirst(t, graph, [ + "project-1", + "library.a", + "library.b", + "library.c" + ]); + const p = graph.getProject("library.a"); + t.deepEqual(p.getCustomConfiguration(), { + someConfig: true + }, "Custom configuration from shim has been applied"); +}); + +test("Project defining a collection shim for itself should be ignored", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1", + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + name: "shim-1", + modulePath: collectionPath, + configuration: [{ + specVersion: "2.6", + type: "library", + metadata: { + name: "collection-library" // will be ignored + }, + customConfiguration: { + someConfig: true + } + }, { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim" + }, + shims: { + collections: { + "id2": { + modules: { + "lib.a": "./library.a", + "lib.b": "./library.b", + "lib.c": "./library.c", + } + } + }, + dependencies: { + "lib.a": ["lib.b"], + } + } + }] + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + await traverseBreadthFirst(t, graph, [ + "project-1", + "library.a", + "library.b", + "library.c" + ]); + const p = graph.getProject("library.a"); + t.is(p.getCustomConfiguration(), undefined, + "No configuration from collection project has been applied"); +}); + +test("Dependencies defined through shim", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "ext1", + configuration: { + kind: "extension", + type: "project-shim", + shims: { + dependencies: { + "id3": ["id2"], + } + } + } + }), + ]); + t.context.getDependencies.onSecondCall().resolves([ + createNode({ + id: "id2", + name: "project-2" + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + createNode({ + id: "id3", + name: "project-3" + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Shimmed dependency has been defined"); +}); + +test("Define external dependency as shims in sub-module", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "app", + version: "1.0.0", + path: "/app" + })); + + t.context.getDependencies.onCall(0).resolves([ + createNode({ + id: "lib", + version: "1.0.0", + path: "/lib" + }), + { + id: "external-thirdparty", + version: "1.0.0", + path: "/app/node_modules/external-thirdparty" + }, + createNode({ + id: "external-thirdparty-shim", + configuration: { + kind: "extension", + type: "project-shim", + shims: { + configurations: { + "external-thirdparty": { + specVersion: "3.1", + type: "module", + metadata: {name: "external-thirdparty"}, + resources: { + configuration: { + paths: {"/resources/": ""}, + }, + }, + }, + }, + }, + } + }) + ]); + + t.context.getDependencies.onCall(1).resolves([ + createNode({ + id: "external-thirdparty", + version: "1.0.0", + path: "/app/node_modules/external-thirdparty", + optional: false + }) + ]); + + const graph = await projectGraphBuilder(t.context.provider); + + t.deepEqual(graph.getDependencies("app"), ["lib"], "'app' depends on 'lib'"); + t.deepEqual(graph.getDependencies("lib"), ["external-thirdparty"], "'lib' depends on 'external-thirdparty'"); +}); + +test("Extension in dependencies", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + modulePath: libraryEPath, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph"); +}); + +test("Extension is an optional dependency of the root project", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([ + createNode({ + id: "id2", + modulePath: libraryEPath, + optional: true, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph"); +}); + +test("Extension is an optional dependency of a non-root project", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + })]); + t.context.getDependencies.onSecondCall().resolves([ + createNode({ + id: "id3", + modulePath: libraryEPath, + optional: true, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.falsy(graph.getExtension("task-a"), "Extension has not been added to the graph"); +}); + +test("Extension is an optional dependency of a non-root project and is not available", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + })]); + t.context.getDependencies.onSecondCall().resolves([ + createNode({ + id: "id3", + modulePath: nonExistingPath, // Module is not installed (transitive devDependency) + optional: true, + configuration: [] + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.falsy(graph.getExtension("task-a"), "Extension has not been added to the graph"); +}); + +test("Extension is a partially optional dependency of a non-root project", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + }), createNode({ + id: "id3", + name: "project-3" + })]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + modulePath: libraryEPath, + optional: true, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + id: "id4", // Will reuse the already visited id4 module + optional: false, // Will cause the extension to be added + modulePath: libraryEPath + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph"); +}); + +test("Multiple dependencies to same module containing an extension", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + }), createNode({ + id: "id3", + name: "project-3" + })]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + modulePath: libraryEPath, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + id: "id4", // Will reuse the already visited id4 module + modulePath: libraryEPath + }), + ]); + const graph = await projectGraphBuilder(t.context.provider); + t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph"); +}); + +test("Multiple dependencies to different module containing the same extension", async (t) => { + t.context.getRootNode.resolves(createNode({ + id: "id1", + name: "project-1" + })); + t.context.getDependencies.onFirstCall().resolves([createNode({ + id: "id2", + name: "project-2" + }), createNode({ + id: "id3", + name: "project-3" + })]); + t.context.getDependencies.onSecondCall().resolves([ + // Deps of id2 + createNode({ + id: "id4", + modulePath: libraryEPath, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + t.context.getDependencies.onThirdCall().resolves([ + // Deps of id3 + createNode({ + id: "id5", + modulePath: libraryEPath, + configuration: { + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "task-a.js" + } + } + }), + ]); + await t.throwsAsync(projectGraphBuilder(t.context.provider), { + message: + "Failed to add extension task-a to graph: An extension with that name has already been added. " + + "This might be caused by multiple modules containing extensions with the same name" + }); +}); diff --git a/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js b/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js new file mode 100644 index 00000000000..104b6c70402 --- /dev/null +++ b/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js @@ -0,0 +1,272 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const applicationAAliasesPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a.aliases"); +const applicationCPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.c"); +const applicationC2Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c2"); +const applicationC3Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c3"); +const applicationDPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.d"); +const applicationFPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.f"); +const applicationGPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.g"); +const errApplicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "err.application.a"); +const cycleDepsBasePath = path.join(__dirname, "..", "..", "..", "fixtures", "cyclic-deps", "node_modules"); +const libraryDOverridePath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d-adtl-deps"); + +import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js"; +import NodePackageDependenciesProvider from "../../../../lib/graph/providers/NodePackageDependencies.js"; + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +function testGraphCreationBfs(...args) { + return _testGraphCreation(true, ...args); +} + +function testGraphCreationDfs(...args) { + return _testGraphCreation(false, ...args); +} + +async function _testGraphCreation(bfs, t, npmProvider, expectedOrder, workspace) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const projectGraph = await projectGraphBuilder(npmProvider, workspace); + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await projectGraph.traverseBreadthFirst(callbackStub); + } else { + await projectGraph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); + return projectGraph; +} + +test("AppA: project with collection dependency", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.a", + "library.b", + "library.c", + "application.a", + ]); +}); + +test("AppA: project with an alias dependency", async (t) => { + const workspace = { + getName: () => "workspace name", + getModuleByNodeId: t.context.sinon.stub().resolves(undefined).onFirstCall().resolves({ + getPath: () => path.join(applicationAAliasesPath, "node_modules", "extension.a.esm.alias"), + getVersion: () => "1.0.0", + }) + }; + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationAAliasesPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "extension.a.esm.alias", + "application.a.aliases", + ], workspace); +}); + +test("AppA: project with workspace overrides", async (t) => { + const workspace = { + getName: () => "workspace name", + getModuleByNodeId: t.context.sinon.stub().resolves(undefined).onFirstCall().resolves({ + // This version of library.d has an additional dependency to library.f, + // which in turn has a dependency to library.g + getPath: () => libraryDOverridePath, + getVersion: () => "1.0.0", + }) + }; + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationAPath + }); + const graph = await testGraphCreationDfs(t, npmProvider, [ + "library.g", // Added through workspace override of library.d + "library.a", + "library.b", + "library.c", + "library.f", // Added through workspace override of library.d + "library.d", + "application.a", + ], workspace); + + t.is(workspace.getModuleByNodeId.callCount, 2, "Workspace#getModuleByNodeId got called twice"); + t.is(workspace.getModuleByNodeId.getCall(0).args[0], "library.d", + "Workspace#getModuleByNodeId got called with correct argument on first call"); + t.is(workspace.getModuleByNodeId.getCall(1).args[0], "collection", + "Workspace#getModuleByNodeId got called with correct argument on second call"); + t.is(graph.getProject("library.d").getVersion(), "2.0.0", "Version from override is used"); +}); + +test("AppC: project with dependency with optional dependency resolved through root project", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "application.c", + ]); +}); + +test("AppC2: project with dependency with optional dependency resolved through other project", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationC2Path + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "library.d-depender", + "application.c2" + ]); +}); + +test("AppC3: project with dependency with optional dependency resolved " + + "through other project (but got hoisted)", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationC3Path + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "library.d-depender", + "application.c3" + ]); +}); + +test("AppD: project with dependency with unresolved optional dependency", async (t) => { + // application.d`s dependency "library.e" has an optional dependency to "library.d" + // which is already present in the node_modules directory of library.e + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationDPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.e", + "application.d" + ]); +}); + +test("AppF: UI5-dependencies in package.json are ignored", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationFPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "application.f" + ]); +}); + +test("AppG: project with npm 'optionalDependencies' should not fail if optional dependency cannot be resolved", + async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationGPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "application.g" + ]); + }); + +test("AppCycleA: cyclic dev deps", async (t) => { + const applicationCycleAPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.a"); + + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.cycle.a", + "library.cycle.b", + "component.cycle.a", + "application.cycle.a" + ]); +}); + +test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { + const applicationCycleBPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.b"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleBPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.e", + "module.d", + "application.cycle.b" + ]); +}); + +test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => { + const applicationCycleCPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.c"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.f", + "module.g", + "application.cycle.c" + ]); + await testGraphCreationBfs(t, npmProvider, [ + "application.cycle.c", + "module.f", + "module.g", + ]); +}); + +test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { + const applicationCycleDPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.d"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleDPath + }); + + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, + `Detected cyclic dependency chain: application.cycle.d -> *module.h* -> module.i -> module.k -> *module.h*`); +}); + +test("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { + const applicationCycleEPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.e"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleEPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.l", + "module.m", + "application.cycle.e" + ]); +}); + +test("Error: missing package.json", async (t) => { + const dir = path.parse(__dirname).root; + const npmProvider = new NodePackageDependenciesProvider({ + cwd: dir + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, `Failed to locate package.json for directory ${dir}`); +}); + +test("Error: missing dependency", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: errApplicationAPath + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, + `Unable to locate module library.xx via resolve logic: Cannot find module 'library.xx/package.json' from ` + + `'${errApplicationAPath}'`); +}); diff --git a/packages/project/test/lib/graph/providers/NodePackageDependencies.js b/packages/project/test/lib/graph/providers/NodePackageDependencies.js new file mode 100644 index 00000000000..5a7b2e6554d --- /dev/null +++ b/packages/project/test/lib/graph/providers/NodePackageDependencies.js @@ -0,0 +1,54 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.readPackageUp = sinon.stub(); + + t.context.NodePackageDependencies = await esmock("../../../../lib/graph/providers/NodePackageDependencies.js", { + "read-package-up": { + readPackageUp: t.context.readPackageUp + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("getRootNode should reject with error when 'name' is empty/missing in package.json", async (t) => { + const {NodePackageDependencies, readPackageUp} = t.context; + + const resolver = new NodePackageDependencies({cwd: "cwd"}); + + readPackageUp.resolves({ + path: "/path/to/root/package.json", + packageJson: { + name: "" + } + }); + + await t.throwsAsync(() => resolver.getRootNode(), { + message: "Missing or empty 'name' attribute in package.json at /path/to/root" + }); +}); + +test("getRootNode should reject with error when 'version' is empty/missing in package.json", async (t) => { + const {NodePackageDependencies, readPackageUp} = t.context; + + const resolver = new NodePackageDependencies({cwd: "cwd"}); + + readPackageUp.resolves({ + path: "/path/to/root/package.json", + packageJson: { + name: "test-package-name", + version: "" + } + }); + + await t.throwsAsync(() => resolver.getRootNode(), { + message: "Missing or empty 'version' attribute in package.json at /path/to/root" + }); +}); diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js new file mode 100644 index 00000000000..305cdbb04b1 --- /dev/null +++ b/packages/project/test/lib/package-exports.js @@ -0,0 +1,50 @@ +import test from "ava"; +import {createRequire} from "node:module"; + +// Using CommonsJS require since JSON module imports are still experimental +const require = createRequire(import.meta.url); + +// package.json should be exported to allow reading version (e.g. from @ui5/cli) +test("export of package.json", (t) => { + const packageJson = require("@ui5/project/package.json"); + t.truthy(packageJson.version); +}); + +// Check number of definied exports +test("check number of exports", (t) => { + const packageJson = require("@ui5/project/package.json"); + t.is(Object.keys(packageJson.exports).length, 13); +}); + +// Public API contract (exported modules) +[ + "config/Configuration", + "specifications/Specification", + "specifications/SpecificationVersion", + "ui5Framework/Openui5Resolver", + "ui5Framework/Sapui5Resolver", + "ui5Framework/Sapui5MavenSnapshotResolver", + "ui5Framework/maven/CacheMode", + "validation/validator", + "validation/ValidationError", + "graph/ProjectGraph", + "graph/projectGraphBuilder", + {exportedSpecifier: "graph", mappedModule: "../../lib/graph/graph.js"}, +].forEach((v) => { + let exportedSpecifier; let mappedModule; + if (typeof v === "string") { + exportedSpecifier = v; + } else { + exportedSpecifier = v.exportedSpecifier; + mappedModule = v.mappedModule; + } + if (!mappedModule) { + mappedModule = `../../lib/${exportedSpecifier}.js`; + } + const spec = `@ui5/project/${exportedSpecifier}`; + test(`${spec}`, async (t) => { + const actual = await import(spec); + const expected = await import(mappedModule); + t.is(actual, expected, "Correct module exported"); + }); +}); diff --git a/packages/project/test/lib/specifications/ComponentProject.js b/packages/project/test/lib/specifications/ComponentProject.js new file mode 100644 index 00000000000..1c662e3b4b5 --- /dev/null +++ b/packages/project/test/lib/specifications/ComponentProject.js @@ -0,0 +1,233 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import Specification from "../../../lib/specifications/Specification.js"; + +function clone(o) { + return JSON.parse(JSON.stringify(o)); +} + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Default getters", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", + "Returned correct default propertiesFileSourceEncoding configuration"); + t.is(project.getCopyright(), undefined, + "Returned correct default copyright configuration"); + t.deepEqual(project.getComponentPreloadPaths(), [], + "Returned correct default componentPreloadPaths configuration"); + t.deepEqual(project.getComponentPreloadNamespaces(), [], + "Returned correct default componentPreloadNamespaces configuration"); + t.deepEqual(project.getComponentPreloadExcludes(), [], + "Returned correct default componentPreloadExcludes configuration"); + t.deepEqual(project.getMinificationExcludes(), [], + "Returned correct default minificationExcludes configuration"); + t.deepEqual(project.getBundles(), [], + "Returned correct default bundles configuration"); +}); + +test("getPropertiesFileSourceEncoding", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources = { + configuration: { + propertiesFileSourceEncoding: "ISO-8859-1" + } + }; + const project = await Specification.create(customProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", + "Returned correct propertiesFileSourceEncoding configuration"); +}); + +test("getCopyright", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.metadata.copyright = "copyright"; + const project = await Specification.create(customProjectInput); + t.is(project.getCopyright(), "copyright", + "Returned correct copyright configuration"); +}); + +test("getComponentPreloadPaths", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + componentPreload: { + paths: ["paths"] + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getComponentPreloadPaths(), ["paths"], + "Returned correct componentPreloadPaths configuration"); +}); + +test("getComponentPreloadNamespaces", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + componentPreload: { + namespaces: ["namespaces"] + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getComponentPreloadNamespaces(), ["namespaces"], + "Returned correct componentPreloadNamespaces configuration"); +}); + +test("getComponentPreloadExcludes", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + componentPreload: { + excludes: ["excludes"] + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getComponentPreloadExcludes(), ["excludes"], + "Returned correct componentPreloadExcludes configuration"); +}); + +test("getMinificationExcludes", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + minification: { + excludes: ["excludes"] + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getMinificationExcludes(), ["excludes"], + "Returned correct minificationExcludes configuration"); +}); + +test("getBundles", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + bundles: [{bundleDefinition: {name: "bundle"}}] + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getBundles(), [{bundleDefinition: {name: "bundle"}}], + "Returned correct bundles configuration"); +}); + +test("hasMavenPlaceholder: has maven placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + const res = project._hasMavenPlaceholder("${mvn-pony}"); + t.true(res, "String has maven placeholder"); +}); + +test("hasMavenPlaceholder: has no maven placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + + const res = project._hasMavenPlaceholder("$mvn-pony}"); + t.false(res, "String has no maven placeholder"); +}); + +test("_resolveMavenPlaceholder: resolves maven placeholder from first POM level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({ + project: { + properties: { + "mvn-pony": "unicorn" + } + } + }); + + const res = await project._resolveMavenPlaceholder("${mvn-pony}"); + t.is(res, "unicorn", "Resolved placeholder correctly"); +}); + +test("_resolveMavenPlaceholder: resolves maven placeholder from deeper POM level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({ + "mvn-pony": { + some: { + id: "unicorn" + } + } + }); + + const res = await project._resolveMavenPlaceholder("${mvn-pony.some.id}"); + t.is(res, "unicorn", "Resolved placeholder correctly"); +}); + +test("_resolveMavenPlaceholder: can't resolve from POM", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({}); + + const err = await t.throwsAsync(project._resolveMavenPlaceholder("${mvn-pony}")); + t.deepEqual(err.message, + `"\${mvn-pony}" couldn't be resolved from maven property "mvn-pony" ` + + `of pom.xml of project application.a`, + "Rejected with correct error message"); +}); + +test("_resolveMavenPlaceholder: provided value is no placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + + const err = await t.throwsAsync(project._resolveMavenPlaceholder("My ${mvn-pony}")); + t.is(err.message, + `"My \${mvn-pony}" is not a maven placeholder`, + "Rejected with correct error message"); +}); + +test("_getPom: reads correctly", async (t) => { + const projectInput = clone(basicProjectInput); + // Application H contains a pom.xml + const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h"); + projectInput.modulePath = applicationHPath; + projectInput.configuration.metadata.name = "application.h"; + const project = await Specification.create(projectInput); + + const res = await project._getPom(); + t.is(res.project.modelVersion, "4.0.0", "pom.xml content has been read"); +}); + +test.serial("_getPom: fs read error", async (t) => { + const project = await Specification.create(basicProjectInput); + project.getRootReader = () => { + return { + byPath: async () => { + throw new Error("EPON: Pony Error"); + } + }; + }; + const error = await t.throwsAsync(project._getPom()); + t.deepEqual(error.message, + "Failed to read pom.xml for project application.a: " + + "EPON: Pony Error", + "Rejected with correct error message"); +}); + +test.serial("_getPom: result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => `no unicorn` + }); + + project.getRootReader = () => { + return { + byPath: byPathStub + }; + }; + + let res = await project._getPom(); + t.deepEqual(res, {pony: "no unicorn"}, "Correct result on first call"); + res = await project._getPom(); + t.deepEqual(res, {pony: "no unicorn"}, "Correct result on second call"); + + t.is(byPathStub.callCount, 1, "getRootReader().byPath got called exactly once (and then cached)"); +}); diff --git a/packages/project/test/lib/specifications/Project.js b/packages/project/test/lib/specifications/Project.js new file mode 100644 index 00000000000..bb5aec17529 --- /dev/null +++ b/packages/project/test/lib/specifications/Project.js @@ -0,0 +1,187 @@ +import test from "ava"; +import path from "node:path"; +import chalk from "chalk"; +import Specification from "../../../lib/specifications/Specification.js"; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test("Invalid configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources = { + configuration: { + propertiesFileSourceEncoding: "Ponycode" + } + }; + const error = await t.throwsAsync(Specification.create(customProjectInput)); + t.is(error.message, `${chalk.red("Invalid ui5.yaml configuration for project application.a.id")} + +Configuration \ +${chalk.underline(chalk.red("resources/configuration/propertiesFileSourceEncoding"))} \ +must be equal to one of the allowed values +Allowed values: UTF-8, ISO-8859-1`, "Threw with validation error"); +}); + +test("getCustomTasks", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + customTasks: [{ + name: "myTask", + beforeTask: "minify", + configuration: { + color: "orange" + } + }] + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getCustomTasks(), [{ + name: "myTask", + beforeTask: "minify", + configuration: { + color: "orange" + } + }], "Returned correct custom task configuration"); +}); + +test("getCustomMiddleware", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.server = { + customMiddleware: [{ + name: "myMiddleware", + mountPath: "/app", + afterMiddleware: "compression", + configuration: { + color: "orange" + } + }] + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getCustomMiddleware(), [{ + name: "myMiddleware", + mountPath: "/app", + afterMiddleware: "compression", + configuration: { + color: "orange" + } + }], "Returned correct custom middleware configuration"); +}); + +test("getCustomTasks/getCustomMiddleware defaults", async (t) => { + const customProjectInput = clone(basicProjectInput); + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getCustomTasks(), [], + "Returned correct default value for custom task configuration"); + t.deepEqual(project.getCustomMiddleware(), [], + "Returned correct default value for custom middleware configuration"); +}); + +test("getFramework*: Defaults", async (t) => { + const customProjectInput = clone(basicProjectInput); + const project = await Specification.create(customProjectInput); + t.is(project.getFrameworkName(), undefined, "Returned correct framework name"); + t.is(project.getFrameworkVersion(), undefined, "Returned correct framework version"); + t.deepEqual(project.getFrameworkDependencies(), [], "Returned correct framework dependencies"); + t.false(project.isFrameworkProject(), "Is not a framework project"); +}); + +test("getFramework* configurations", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.framework = { + name: "OpenUI5", + version: "1.111.1", + libraries: [ + {name: "lib-1"}, + {name: "lib-2"}, + ] + }; + customProjectInput.id = "@openui5/" + customProjectInput.id; + const project = await Specification.create(customProjectInput); + t.is(project.getFrameworkName(), "OpenUI5", "Returned correct framework name"); + t.is(project.getFrameworkVersion(), "1.111.1", "Returned correct framework version"); + t.deepEqual(project.getFrameworkDependencies(), [ + {name: "lib-1"}, + {name: "lib-2"} + ], "Returned correct framework dependencies"); + t.true(project.isFrameworkProject(), "Is a framework project"); +}); + +test("isFrameworkProject: sapui5", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.id = "@sapui5/" + customProjectInput.id; + const project = await Specification.create(customProjectInput); + t.true(project.isFrameworkProject(), "Is a framework project"); +}); + +test("isDeprecated/isSapInternal: Defaults", async (t) => { + const customProjectInput = clone(basicProjectInput); + + const project = await Specification.create(customProjectInput); + t.false(project.isDeprecated(), "Is not deprecated"); + t.false(project.isSapInternal(), "Is not SAP-internal"); + t.false(project.getAllowSapInternal(), "Does not allow SAP-internal"); +}); + +test("isDeprecated/isSapInternal: True", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.metadata.deprecated = true; + customProjectInput.configuration.metadata.sapInternal = true; + customProjectInput.configuration.metadata.allowSapInternal = true; + const project = await Specification.create(customProjectInput); + t.true(project.isDeprecated(), "Is deprecated"); + t.true(project.isSapInternal(), "Is SAP-internal"); + t.true(project.getAllowSapInternal(), "Does allow SAP-internal"); +}); + +test("getServerSettings", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.server = { + settings: { + httpPort: 1337 + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getServerSettings(), { + httpPort: 1337 + }, "Returned correct server settings"); +}); + +test("getBuilderSettings", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + settings: { + includeDependency: ["my-lib"] + } + }; + const project = await Specification.create(customProjectInput); + t.deepEqual(project.getBuilderSettings(), { + includeDependency: ["my-lib"] + }, "Returned correct build settings"); +}); + +test("getBuildManifest", async (t) => { + const projectWithoutBuildManifest = await Specification.create(clone(basicProjectInput)); + t.is(projectWithoutBuildManifest.getBuildManifest(), null, "Project has a no build manifest"); + + const customProjectInput = clone(basicProjectInput); + customProjectInput.buildManifest = "buildManifest"; + const project = await Specification.create(customProjectInput); + t.is(project.getBuildManifest(), "buildManifest", "Returned correct build manifest"); +}); + +// == Most functionality is tested in the specific types diff --git a/packages/project/test/lib/specifications/Specification.js b/packages/project/test/lib/specifications/Specification.js new file mode 100644 index 00000000000..ab449809182 --- /dev/null +++ b/packages/project/test/lib/specifications/Specification.js @@ -0,0 +1,428 @@ +import test from "ava"; +import esmock from "esmock"; +import path from "node:path"; +import sinon from "sinon"; +import Specification from "../../../lib/specifications/Specification.js"; +import Application from "../../../lib/specifications/types/Application.js"; +import Library from "../../../lib/specifications/types/Library.js"; +import ThemeLibrary from "../../../lib/specifications/types/ThemeLibrary.js"; +import Module from "../../../lib/specifications/types/Module.js"; +import Task from "../../../lib/specifications/extensions/Task.js"; +import ProjectShim from "../../../lib/specifications/extensions/ProjectShim.js"; +import ServerMiddleware from "../../../lib/specifications/extensions/ServerMiddleware.js"; + +const __dirname = import.meta.dirname; + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const libraryHPath = path.join(__dirname, "..", "..", "fixtures", "library.h"); +const themeLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "theme.library.e"); +const genericExtensionPath = path.join(__dirname, "..", "..", "fixtures", "extension.a"); +const moduleAPath = path.join(__dirname, "..", "..", "fixtures", "module.a"); + +function createSubclass(Specification) { + class MockSpecification extends Specification { + getRootPath() { + return "path"; + } + getType() { + return "type"; + } + getKind() { + return "kind"; + } + getName() { + return "name"; + } + } + return MockSpecification; +} + +test.beforeEach((t) => { + t.context.basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } + }; +}); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Specification can't be instantiated", (t) => { + t.throws(() => { + new Specification(); + }, { + message: "Class 'Specification' is abstract. Please use one of the 'types' subclasses" + }); +}); + +test("Instantiate a basic project", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + t.is(project.getId(), "application.a.id", "Returned correct ID"); + t.is(project.getName(), "application.a", "Returned correct name"); + t.is(project.getVersion(), "1.0.0", "Returned correct version"); + t.is(project.getRootPath(), applicationAPath, "Returned correct project path"); +}); + +test("init: Missing id", async (t) => { + delete t.context.basicProjectInput.id; + await t.throwsAsync(Specification.create(t.context.basicProjectInput), { + message: "Could not create Specification: Missing or empty parameter 'id'" + }, "Threw with expected error message"); +}); + +test("init: Missing version", async (t) => { + delete t.context.basicProjectInput.version; + await t.throwsAsync(Specification.create(t.context.basicProjectInput), { + message: "Could not create Specification: Missing or empty parameter 'version'" + }, "Threw with expected error message"); +}); + +test("init: Missing modulePath", async (t) => { + delete t.context.basicProjectInput.modulePath; + await t.throwsAsync(Specification.create(t.context.basicProjectInput), { + message: "Could not create Specification: Missing or empty parameter 'modulePath'" + }, "Threw with expected error message"); +}); + +test("init: Missing configuration", async (t) => { + delete t.context.basicProjectInput.configuration; + const project = new Application(); + + await t.throwsAsync(project.init(t.context.basicProjectInput), { + message: "Could not create Specification: Missing or empty parameter 'configuration'" + }, "Threw with expected error message"); +}); + +test("init: Invalid constructor name", async (t) => { + const MockSpecification = createSubclass(Specification); + const project = new MockSpecification(); + + await t.throwsAsync(project.init(t.context.basicProjectInput), { + message: "Configuration mismatch: Supplied configuration of type 'application' " + + "does not match with specification class MockSpecification" + }, "Threw with expected error message"); +}); + +test("Configurations", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + t.is(project.getKind(), "project", "Returned correct kind configuration"); + t.is(project.getType(), "application", "Returned correct type configuration"); + t.is(project.getSpecVersion().toString(), "2.3", "Returned correct specification version"); + t.is(project.getSpecVersion().major(), 2, + "SpecVersionComparator returned correct major version"); +}); + +test("Access project root resources via reader", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + const rootReader = await project.getRootReader(); + const packageJsonResource = await rootReader.byPath("/package.json"); + t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); +}); + +test("_dirExists: Directory exists", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + const bExists = await project._dirExists("/webapp"); + t.true(bExists, "directory exists"); +}); + +test("_dirExists: Missing leading slash", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + + await t.throwsAsync(project._dirExists("webapp"), { + message: "Failed to resolve virtual path 'webapp': Path must be absolute" + }); +}); + +test("_dirExists: Trailing slash is ok", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + const bExists = await project._dirExists("/webapp/"); + t.true(bExists, "directory exists"); +}); + +test("_dirExists: Directory is a file", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + + await t.throwsAsync(project._dirExists("webapp/index.html"), { + message: "Failed to resolve virtual path 'webapp/index.html': Path must be absolute" + }); +}); + +test("_dirExists: Directory does not exist", async (t) => { + const project = await Specification.create(t.context.basicProjectInput); + + const bExists = await project._dirExists("/w"); + t.false(bExists, "directory does not exist"); +}); + +test("Project with incorrect name", async (t) => { + const project = await Specification.create({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application a"} + } + }); + t.is(project.getName(), "application a", "Returned correct name"); + t.is(project.getVersion(), "1.0.0", "Returned correct version"); + t.is(project.getRootPath(), applicationAPath, "Returned correct project path"); +}); + +test("Migrate legacy project", async (t) => { + t.context.basicProjectInput.configuration.specVersion = "1.0"; + const project = await Specification.create(t.context.basicProjectInput); + + t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion"); +}); + +test("Migrate legacy project unexpected configuration", async (t) => { + t.context.basicProjectInput.configuration.specVersion = "1.0"; + t.context.basicProjectInput.configuration.someCustomSetting = "Pineapple"; + const err = await t.throwsAsync(Specification.create(t.context.basicProjectInput)); + + t.is(err.message, + "project application.a defines unsupported Specification Version 1.0. Please manually upgrade to 3.0 or " + + "higher. For details see https://ui5.github.io/cli/pages/Configuration/#specification-versions - " + + "An attempted migration to a supported specification version failed, likely due to unrecognized " + + "configuration. Check verbose log for details.", + "Threw with expected error message"); +}); + +test("Migrate legacy module: specVersion 1.0", async (t) => { + const project = await Specification.create({ + id: "my.task", + version: "3.4.7-beta", + modulePath: genericExtensionPath, + configuration: { + specVersion: "1.0", + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "lib/extensionModule.js" + } + } + }); + + t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion"); +}); + +test("Migrate legacy module: specVersion 0.1", async (t) => { + const project = await Specification.create({ + id: "my.task", + version: "3.4.7-beta", + modulePath: genericExtensionPath, + configuration: { + specVersion: "0.1", + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "lib/extensionModule.js" + } + } + }); + + t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion"); +}); + +test("Migrate legacy extension", async (t) => { + const project = await Specification.create({ + id: "module.a.id", + version: "1.0.0", + modulePath: moduleAPath, + configuration: { + specVersion: "1.1", + kind: "project", + type: "module", + metadata: { + name: "module.a", + copyright: "Some fancy copyright" // allowed but ignored + }, + resources: { + configuration: { + paths: { + "/": "dist", + "/dev/": "dev" + } + } + } + } + }); + + t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion"); +}); + +[{ + kind: "project", + type: "application", + modulePath: applicationAPath, + SpecificationClass: Application +}, { + kind: "project", + type: "library", + modulePath: libraryHPath, + SpecificationClass: Library +}, { + kind: "project", + type: "theme-library", + modulePath: themeLibraryEPath, + SpecificationClass: ThemeLibrary +}, { + kind: "project", + type: "module", + modulePath: moduleAPath, + SpecificationClass: Module +}, { + kind: "extension", + type: "task", + modulePath: genericExtensionPath, + SpecificationClass: Task +}, { + kind: "extension", + type: "project-shim", + modulePath: genericExtensionPath, + SpecificationClass: ProjectShim +}, { + kind: "extension", + type: "server-middleware", + modulePath: genericExtensionPath, + SpecificationClass: ServerMiddleware +}].forEach(({kind, type, modulePath, SpecificationClass}) => { + test(`create: kind '${kind}', type '${type}'`, async (t) => { + const additionalConfiguration = {}; + if (type === "task") { + additionalConfiguration.task = {path: "lib/middleware.js"}; + } else if (type === "server-middleware") { + additionalConfiguration.middleware = {path: "lib/middleware.js"}; + } else if (type === "project-shim") { + additionalConfiguration.shims = {}; + } + const project = await Specification.create({ + id: `${type}.a.id`, + version: "1.0.0", + modulePath, + configuration: { + specVersion: "2.6", + kind, + type, + metadata: { + name: `${type}.a` + }, + ...additionalConfiguration + } + }); + t.true(project instanceof SpecificationClass); + }); +}); + +test("create: Missing configuration", async (t) => { + await t.throwsAsync(Specification.create({ + id: "application.a.id", + version: "1.0.0", + }), { + message: "Unable to create Specification instance: Missing configuration parameter" + }); +}); + +test("create: Unknown kind", async (t) => { + await t.throwsAsync(Specification.create({ + configuration: { + kind: "foo", + } + }), { + message: "Unable to create Specification instance: Unknown kind 'foo'" + }); +}); + +test("create: Unknown type", async (t) => { + await t.throwsAsync(Specification.create({ + configuration: { + kind: "project", + type: "foo" + } + }), { + message: "Unable to create Specification instance: Unknown specification type 'foo'" + }); +}); + +test("Invalid specVersion", async (t) => { + t.context.basicProjectInput.configuration.specVersion = "0.5"; + await t.throwsAsync(Specification.create(t.context.basicProjectInput), { + message: + "Unsupported Specification Version 0.5 defined. Your UI5 CLI installation might be outdated. " + + "For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions" + }, "Threw with expected error message"); +}); + +test("getRootReader: Default parameters", async (t) => { + // Since Specification#create instantiates a far-away subclass, it would be a mess to mock + // every class up to "Specification.js" just to stub the resourceFactory's createReader method + // Therefore we just come up with our own subclass that can be instantiated right away: + + const createReaderStub = sinon.stub(); + const Specification = await esmock("../../../lib/specifications/Specification.js", { + "@ui5/fs/resourceFactory": { + createReader: createReaderStub + } + }); + + const MockSpecification = createSubclass(Specification); + const spec = new MockSpecification(); + await spec.getRootReader(); + + t.is(createReaderStub.callCount, 1, "createReader got called once"); + t.deepEqual(createReaderStub.getCall(0).args[0], { + fsBasePath: "path", + name: "Root reader for type kind name", + useGitignore: true, + virBasePath: "/", + }, "createReader got called with expected arguments"); +}); + +test("getRootReader: Custom parameters", async (t) => { + const createReaderStub = sinon.stub(); + const Specification = await esmock("../../../lib/specifications/Specification.js", { + "@ui5/fs/resourceFactory": { + createReader: createReaderStub + } + }); + + const MockSpecification = createSubclass(Specification); + const spec = new MockSpecification(); + await spec.getRootReader({}); + await spec.getRootReader({ + useGitignore: false + }); + + + t.is(createReaderStub.callCount, 2, "createReader got called twice"); + t.deepEqual(createReaderStub.getCall(0).args[0], { + fsBasePath: "path", + name: "Root reader for type kind name", + useGitignore: true, + virBasePath: "/", + }, "createReader got called with expected arguments on first call"); + + t.deepEqual(createReaderStub.getCall(1).args[0], { + fsBasePath: "path", + name: "Root reader for type kind name", + useGitignore: false, + virBasePath: "/", + }, "createReader got called with expected arguments on second call"); +}); diff --git a/packages/project/test/lib/specifications/SpecificationVersion.js b/packages/project/test/lib/specifications/SpecificationVersion.js new file mode 100644 index 00000000000..45d90a39fe1 --- /dev/null +++ b/packages/project/test/lib/specifications/SpecificationVersion.js @@ -0,0 +1,294 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import SpecificationVersion from "../../../lib/specifications/SpecificationVersion.js"; +import {__localFunctions__} from "../../../lib/specifications/SpecificationVersion.js"; + +const unsupportedSpecVersionText = (specVersion) => + `Unsupported Specification Version ${specVersion} defined. Your UI5 CLI installation might be outdated. ` + + `For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions`; + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test.serial("Invalid specVersion", (t) => { + const {sinon} = t.context; + const isSupportedSpecVersionStub = + sinon.stub(SpecificationVersion, "isSupportedSpecVersion").returns(false); + + t.throws(() => { + new SpecificationVersion("2.5"); + }, { + message: unsupportedSpecVersionText("2.5") + }, "Threw with expected error message"); + + t.is(isSupportedSpecVersionStub.callCount, 1, "Static isSupportedSpecVersionStub has been called once"); + t.deepEqual(isSupportedSpecVersionStub.getCall(0).args, ["2.5"], + "Static isSupportedSpecVersionStub has been called with expected arguments"); +}); + +test("(instance) toString", (t) => { + t.is(new SpecificationVersion("0.1").toString(), "0.1"); + t.is(new SpecificationVersion("1.1").toString(), "1.1"); +}); + +test("(instance) major", (t) => { + t.is(new SpecificationVersion("0.1").major(), 0); + t.is(new SpecificationVersion("1.1").major(), 1); + t.is(new SpecificationVersion("2.1").major(), 2); + + t.is(t.throws(() => { + new SpecificationVersion("0.2").major(); + }).message, unsupportedSpecVersionText("0.2")); +}); + +test("(instance) minor", (t) => { + t.is(new SpecificationVersion("2.1").minor(), 1); + t.is(new SpecificationVersion("2.2").minor(), 2); + t.is(new SpecificationVersion("2.3").minor(), 3); + + t.is(t.throws(() => { + new SpecificationVersion("1.2").minor(); + }).message, unsupportedSpecVersionText("1.2")); +}); + +test("(instance) satisfies", (t) => { + // range: 1.x + t.is(new SpecificationVersion("1.0").satisfies("1.x"), true); + t.is(new SpecificationVersion("1.1").satisfies("1.x"), true); + t.is(new SpecificationVersion("2.0").satisfies("1.x"), false); + + // range: ^2.2 + t.is(new SpecificationVersion("2.1").satisfies("^2.2"), false); + t.is(new SpecificationVersion("2.2").satisfies("^2.2"), true); + t.is(new SpecificationVersion("2.3").satisfies("^2.2"), true); + + // range: >=2.2 + t.is(new SpecificationVersion("2.1").satisfies(">=2.2"), false); + t.is(new SpecificationVersion("2.2").satisfies(">=2.2"), true); + t.is(new SpecificationVersion("2.3").satisfies(">=2.2"), true); + t.is(new SpecificationVersion("3.1").satisfies(">=2.2"), true); + t.is(new SpecificationVersion("4.0").satisfies(">=2.2"), true); + + // range: > 1.0 + t.is(new SpecificationVersion("1.0").satisfies("> 1.0"), false); + t.is(new SpecificationVersion("1.1").satisfies("> 1.0"), true); + t.is(new SpecificationVersion("2.2").satisfies("> 1.0"), true); + + // range: 2.2 - 2.4 + t.is(new SpecificationVersion("2.1").satisfies("2.2 - 2.4"), false); + t.is(new SpecificationVersion("2.2").satisfies("2.2 - 2.4"), true); + t.is(new SpecificationVersion("2.3").satisfies("2.2 - 2.4"), true); + t.is(new SpecificationVersion("2.4").satisfies("2.2 - 2.4"), true); + t.is(new SpecificationVersion("2.5").satisfies("2.2 - 2.4"), false); + + // range: 0.1 || 1.0 - 1.1 || ^2.5 + t.is(new SpecificationVersion("0.1").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(new SpecificationVersion("1.0").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(new SpecificationVersion("1.1").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(new SpecificationVersion("2.4").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), false); + t.is(new SpecificationVersion("2.5").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(new SpecificationVersion("2.6").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true); + + // unsupported spec version + t.is(t.throws(() => { + new SpecificationVersion("0.2").satisfies("1.x"); + }).message, unsupportedSpecVersionText("0.2")); +}); + +test("(instance) low level comparator", (t) => { + t.is(new SpecificationVersion("2.1").gt("2.2"), false); + t.is(new SpecificationVersion("2.2").gt("2.2"), false); + t.is(new SpecificationVersion("2.3").gt("2.2"), true); + + t.is(new SpecificationVersion("2.1").gte("2.2"), false); + t.is(new SpecificationVersion("2.2").gte("2.2"), true); + t.is(new SpecificationVersion("2.3").gte("2.2"), true); + + t.is(new SpecificationVersion("2.1").lt("2.2"), true); + t.is(new SpecificationVersion("2.2").lt("2.2"), false); + t.is(new SpecificationVersion("2.3").lt("2.2"), false); + + t.is(new SpecificationVersion("2.1").lte("2.2"), true); + t.is(new SpecificationVersion("2.2").lte("2.2"), true); + t.is(new SpecificationVersion("2.3").lte("2.2"), false); + + t.is(new SpecificationVersion("2.0").eq("2.2"), false); + t.is(new SpecificationVersion("2.2").eq("2.2"), true); + + t.is(new SpecificationVersion("2.0").neq("2.2"), true); + t.is(new SpecificationVersion("2.2").neq("2.2"), false); +}); + +test("(static) isSupportedSpecVersion", (t) => { + t.is(SpecificationVersion.isSupportedSpecVersion("0.1"), true); + t.is(SpecificationVersion.isSupportedSpecVersion("1.0"), true); + t.is(SpecificationVersion.isSupportedSpecVersion("1.1"), true); + t.is(SpecificationVersion.isSupportedSpecVersion("2.0"), true); + t.is(SpecificationVersion.isSupportedSpecVersion("2.4"), true); + t.is(SpecificationVersion.isSupportedSpecVersion("0.2"), false); + t.is(SpecificationVersion.isSupportedSpecVersion("1.2"), false); + t.is(SpecificationVersion.isSupportedSpecVersion(1.1), false); + t.is(SpecificationVersion.isSupportedSpecVersion("foo"), false); + t.is(SpecificationVersion.isSupportedSpecVersion(""), false); + t.is(SpecificationVersion.isSupportedSpecVersion(), false); +}); + +test("(static) major", (t) => { + t.is(SpecificationVersion.major("0.1"), 0); + t.is(SpecificationVersion.major("1.1"), 1); + t.is(SpecificationVersion.major("2.1"), 2); + + t.is(t.throws(() => { + SpecificationVersion.major("0.2"); + }).message, unsupportedSpecVersionText("0.2")); +}); + +test("(static) minor", (t) => { + t.is(SpecificationVersion.minor("2.1"), 1); + t.is(SpecificationVersion.minor("2.2"), 2); + t.is(SpecificationVersion.minor("2.3"), 3); + + t.is(t.throws(() => { + SpecificationVersion.minor("1.2"); + }).message, unsupportedSpecVersionText("1.2")); +}); + +test("(static) satisfies", (t) => { + // range: 1.x + t.is(SpecificationVersion.satisfies("1.0", "1.x"), true); + t.is(SpecificationVersion.satisfies("1.1", "1.x"), true); + t.is(SpecificationVersion.satisfies("2.0", "1.x"), false); + + // range: ^2.2 + t.is(SpecificationVersion.satisfies("2.1", "^2.2"), false); + t.is(SpecificationVersion.satisfies("2.2", "^2.2"), true); + t.is(SpecificationVersion.satisfies("2.3", "^2.2"), true); + + // range: >=2.2 + t.is(SpecificationVersion.satisfies("2.1", ">=2.2"), false); + t.is(SpecificationVersion.satisfies("2.2", ">=2.2"), true); + t.is(SpecificationVersion.satisfies("2.3", ">=2.2"), true); + t.is(SpecificationVersion.satisfies("3.1", ">=2.2"), true); + + // range: > 1.0 + t.is(SpecificationVersion.satisfies("1.0", "> 1.0"), false); + t.is(SpecificationVersion.satisfies("1.1", "> 1.0"), true); + t.is(SpecificationVersion.satisfies("2.2", "> 1.0"), true); + + // range: 2.2 - 2.4 + t.is(SpecificationVersion.satisfies("2.1", "2.2 - 2.4"), false); + t.is(SpecificationVersion.satisfies("2.2", "2.2 - 2.4"), true); + t.is(SpecificationVersion.satisfies("2.3", "2.2 - 2.4"), true); + t.is(SpecificationVersion.satisfies("2.4", "2.2 - 2.4"), true); + t.is(SpecificationVersion.satisfies("2.5", "2.2 - 2.4"), false); + + // range: 0.1 || 1.0 - 1.1 || ^2.5 + t.is(SpecificationVersion.satisfies("0.1", "0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(SpecificationVersion.satisfies("1.0", "0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(SpecificationVersion.satisfies("1.1", "0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(SpecificationVersion.satisfies("2.4", "0.1 || 1.0 - 1.1 || ^2.5"), false); + t.is(SpecificationVersion.satisfies("2.5", "0.1 || 1.0 - 1.1 || ^2.5"), true); + t.is(SpecificationVersion.satisfies("2.6", "0.1 || 1.0 - 1.1 || ^2.5"), true); + + // unsupported spec version + t.is(t.throws(() => { + SpecificationVersion.satisfies("0.2", "1.x"); + }).message, unsupportedSpecVersionText("0.2")); +}); + +test("(static) low level comparator", (t) => { + t.is(SpecificationVersion.gt("2.1", "2.2"), false); + t.is(SpecificationVersion.gt("2.2", "2.2"), false); + t.is(SpecificationVersion.gt("2.3", "2.2"), true); + + t.is(SpecificationVersion.gte("2.1", "2.2"), false); + t.is(SpecificationVersion.gte("2.2", "2.2"), true); + t.is(SpecificationVersion.gte("2.3", "2.2"), true); + + t.is(SpecificationVersion.lt("2.1", "2.2"), true); + t.is(SpecificationVersion.lt("2.2", "2.2"), false); + t.is(SpecificationVersion.lt("2.3", "2.2"), false); + + t.is(SpecificationVersion.lte("2.1", "2.2"), true); + t.is(SpecificationVersion.lte("2.2", "2.2"), true); + t.is(SpecificationVersion.lte("2.3", "2.2"), false); + + t.is(SpecificationVersion.eq("2.0", "2.2"), false); + t.is(SpecificationVersion.eq("2.2", "2.2"), true); + + t.is(SpecificationVersion.neq("2.0", "2.2"), true); + t.is(SpecificationVersion.neq("2.2", "2.2"), false); +}); + +test("(static) getVersionsForRange", (t) => { + // range: 1.x + t.deepEqual(SpecificationVersion.getVersionsForRange("1.x"), [ + "1.0", "1.1" + ]); + + // range: ^2.2 + t.deepEqual(SpecificationVersion.getVersionsForRange("^2.2"), [ + "2.2", "2.3", "2.4", "2.5", "2.6" + ]); + + // range: >=2.2 + t.deepEqual(SpecificationVersion.getVersionsForRange(">=2.2"), [ + "2.2", "2.3", "2.4", "2.5", "2.6", + "3.0", "3.1", "3.2", "4.0" + ]); + + // range: > 1.0 + t.deepEqual(SpecificationVersion.getVersionsForRange("> 1.0"), [ + "1.1", + "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", + "3.0", "3.1", "3.2", "4.0" + ]); + + // range: 2.2 - 2.4 + t.deepEqual(SpecificationVersion.getVersionsForRange("2.2 - 2.4"), [ + "2.2", "2.3", "2.4" + ]); + + // range: 0.1 || 1.0 - 1.1 || ^2.5 + t.deepEqual(SpecificationVersion.getVersionsForRange("0.1 || 1.0 - 1.1 || ^2.5"), [ + "0.1", "1.0", "1.1", + "2.5", "2.6" + ]); + + // Incorrect range returns empty array + t.deepEqual(SpecificationVersion.getVersionsForRange("not a range"), []); +}); + +test("getSemverCompatibleVersion", (t) => { + t.is(__localFunctions__.getSemverCompatibleVersion("0.1"), "0.1.0"); + t.is(__localFunctions__.getSemverCompatibleVersion("1.1"), "1.1.0"); + t.is(__localFunctions__.getSemverCompatibleVersion("2.0"), "2.0.0"); + + t.is(t.throws(() => { + __localFunctions__.getSemverCompatibleVersion("1.2.3"); + }).message, unsupportedSpecVersionText("1.2.3")); + t.is(t.throws(() => { + __localFunctions__.getSemverCompatibleVersion("0.99"); + }).message, unsupportedSpecVersionText("0.99")); + t.is(t.throws(() => { + __localFunctions__.getSemverCompatibleVersion("foo"); + }).message, unsupportedSpecVersionText("foo")); + t.is(t.throws(() => { + __localFunctions__.getSemverCompatibleVersion(); + }).message, unsupportedSpecVersionText("undefined")); +}); + +test("handleSemverComparator", (t) => { + const comparatorStub = t.context.sinon.stub().returns("foobar"); + t.is(__localFunctions__.handleSemverComparator(comparatorStub, "1.1.0", "2.2"), "foobar"); + t.deepEqual(comparatorStub.getCall(0).args, ["1.1.0", "2.2.0"]); + + t.is(t.throws(() => { + __localFunctions__.handleSemverComparator(undefined, undefined, "a.b"); + }).message, "Invalid spec version expectation given in comparator: a.b"); +}); diff --git a/packages/project/test/lib/specifications/extensions/ProjectShim.js b/packages/project/test/lib/specifications/extensions/ProjectShim.js new file mode 100644 index 00000000000..f9afa782b47 --- /dev/null +++ b/packages/project/test/lib/specifications/extensions/ProjectShim.js @@ -0,0 +1,89 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; +import ProjectShim from "../../../../lib/specifications/extensions/ProjectShim.js"; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const __dirname = import.meta.dirname; + +const nonExistingPath = path.join(__dirname, "..", "..", "..", "fixtures", "does-not-exist"); +const basicProjectShimInput = { + id: "shim.a", + version: "1.0.0", + modulePath: nonExistingPath, // should not matter + configuration: { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "project-shim-a" + }, + shims: { + dependencies: { + "module.a": ["dependencies"] + }, + configurations: { + "module.b": { + configuration: "configuration" + } + }, + collections: { + "module.c": { + modules: { + "module.x": "some/path" + } + } + } + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Correct class", async (t) => { + const extension = await Specification.create(clone(basicProjectShimInput)); + t.true(extension instanceof ProjectShim, `Is an instance of the ProjectShim class`); +}); + +test("Defaults", async (t) => { + const projectShimInput = clone(basicProjectShimInput); + projectShimInput.configuration.shims = {}; + + const extension = await Specification.create(projectShimInput); + t.deepEqual(extension.getDependencyShims(), {}, "Returned correct default value for dependencies"); + t.deepEqual(extension.getConfigurationShims(), {}, "Returned correct default value for configuration"); + t.deepEqual(extension.getCollectionShims(), {}, "Returned correct default value for collection"); +}); + +test("getDependencyShims", async (t) => { + const extension = await Specification.create(clone(basicProjectShimInput)); + t.deepEqual(extension.getDependencyShims(), { + "module.a": ["dependencies"] + }, "Returned correct value for dependencies shim configuration"); +}); + +test("getConfigurationShims", async (t) => { + const extension = await Specification.create(clone(basicProjectShimInput)); + t.deepEqual(extension.getConfigurationShims(), { + "module.b": { + configuration: "configuration" + } + }, "Returned correct value for configuration shim configuration"); +}); + +test("getCollectionShims", async (t) => { + const extension = await Specification.create(clone(basicProjectShimInput)); + t.deepEqual(extension.getCollectionShims(), { + "module.c": { + modules: { + "module.x": "some/path" + } + } + }, "Returned correct value for collection shim configuration"); +}); diff --git a/packages/project/test/lib/specifications/extensions/ServerMiddleware.js b/packages/project/test/lib/specifications/extensions/ServerMiddleware.js new file mode 100644 index 00000000000..d6c25d9bcee --- /dev/null +++ b/packages/project/test/lib/specifications/extensions/ServerMiddleware.js @@ -0,0 +1,83 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; +import ServerMiddleware from "../../../../lib/specifications/extensions/ServerMiddleware.js"; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const __dirname = import.meta.dirname; + +const genericCjsExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a"); +const genericEsmExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a.esm"); +const basicCjsServerMiddlewareInput = { + id: "server.middleware.a", + version: "1.0.0", + modulePath: genericCjsExtensionPath, + configuration: { + specVersion: "2.6", + kind: "extension", + type: "server-middleware", + metadata: { + name: "middleware-a" + }, + middleware: { + path: "lib/extensionModule.js" + } + } +}; +const basicEsmServerMiddlewareInput = { + id: "server.middleware.a", + version: "1.0.0", + modulePath: genericEsmExtensionPath, + configuration: { + specVersion: "2.6", + kind: "extension", + type: "server-middleware", + metadata: { + name: "middleware-a" + }, + middleware: { + path: "lib/extensionModule.js" + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Correct class (CJS)", async (t) => { + const extension = await Specification.create(clone(basicCjsServerMiddlewareInput)); + t.true(extension instanceof ServerMiddleware, `Is an instance of the ServerMiddleware class`); +}); +test("Correct class (ESM)", async (t) => { + const extension = await Specification.create(clone(basicEsmServerMiddlewareInput)); + t.true(extension instanceof ServerMiddleware, `Is an instance of the ServerMiddleware class`); +}); + +test("getMiddleware (CJS)", async (t) => { + const extension = await Specification.create(clone(basicCjsServerMiddlewareInput)); + const middleware = await extension.getMiddleware(); + t.is(middleware(), "extension module", + "Returned correct module"); +}); + +test("getMiddleware (ESM)", async (t) => { + const extension = await Specification.create(clone(basicEsmServerMiddlewareInput)); + const middleware = await extension.getMiddleware(); + t.is(middleware(), "extension module", + "Returned correct module"); +}); + +test("Middleware with illegal suffix", async (t) => { + const serverMiddlewareInput = clone(basicCjsServerMiddlewareInput); + serverMiddlewareInput.configuration.metadata.name += "--1"; + const err = await t.throwsAsync(Specification.create(serverMiddlewareInput)); + t.is(err.message, + "Failed to validate configuration of server-middleware extension middleware-a--1: " + + "Server middleware name must not end with '--'", + "Threw with expected error message"); +}); diff --git a/packages/project/test/lib/specifications/extensions/Task.js b/packages/project/test/lib/specifications/extensions/Task.js new file mode 100644 index 00000000000..24cf306523a --- /dev/null +++ b/packages/project/test/lib/specifications/extensions/Task.js @@ -0,0 +1,100 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; +import Task from "../../../../lib/specifications/extensions/Task.js"; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const __dirname = import.meta.dirname; + +const genericCjsExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a"); +const genericEsmExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a.esm"); + +const basicCjsTaskInput = { + id: "task.a", + version: "1.0.0", + modulePath: genericCjsExtensionPath, + configuration: { + specVersion: "2.6", + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "lib/extensionModule.js" + } + } +}; + +const basicEsmTaskInput = { + id: "task.a", + version: "1.0.0", + modulePath: genericEsmExtensionPath, + configuration: { + specVersion: "2.6", + kind: "extension", + type: "task", + metadata: { + name: "task-a" + }, + task: { + path: "lib/extensionModule.js" + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("Correct class (CJS)", async (t) => { + const extension = await Specification.create(clone(basicCjsTaskInput)); + t.true(extension instanceof Task, `Is an instance of the Task class`); +}); + +test("Correct class (ESM)", async (t) => { + const extension = await Specification.create(clone(basicEsmTaskInput)); + t.true(extension instanceof Task, `Is an instance of the Task class`); +}); + +test("getTask (CJS)", async (t) => { + const extension = await Specification.create(clone(basicCjsTaskInput)); + const task = await extension.getTask(); + t.is(task(), "extension module", + "Returned correct module"); +}); + +test("getTask (ESM)", async (t) => { + const extension = await Specification.create(clone(basicEsmTaskInput)); + const task = await extension.getTask(); + t.is(task(), "extension module", + "Returned correct module"); +}); + +test("getRequiredDependenciesCallback (CJS)", async (t) => { + const extension = await Specification.create(clone(basicCjsTaskInput)); + const requiredDependenciesCallback = await extension.getRequiredDependenciesCallback(); + t.is(requiredDependenciesCallback(), "required dependencies function", + "Returned correct module"); +}); + +test("getRequiredDependenciesCallback (ESM)", async (t) => { + const extension = await Specification.create(clone(basicEsmTaskInput)); + const requiredDependenciesCallback = await extension.getRequiredDependenciesCallback(); + t.is(requiredDependenciesCallback(), "required dependencies function", + "Returned correct module"); +}); + +test("Task with illegal suffix", async (t) => { + const TaskInput = clone(basicCjsTaskInput); + TaskInput.configuration.metadata.name += "--1"; + const err = await t.throwsAsync(Specification.create(TaskInput)); + t.is(err.message, + "Failed to validate configuration of task extension task-a--1: " + + "Task name must not end with '--'", + "Threw with expected error message"); +}); diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js new file mode 100644 index 00000000000..0a53ae309b0 --- /dev/null +++ b/packages/project/test/lib/specifications/types/Application.js @@ -0,0 +1,681 @@ +import test from "ava"; +import path from "node:path"; +import {createResource} from "@ui5/fs/resourceFactory"; +import sinonGlobal from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; +import Application from "../../../../lib/specifications/types/Application.js"; + +const __dirname = import.meta.dirname; +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.projectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } + }; + + t.context.applicationHInput = { + id: "application.h.id", + version: "1.0.0", + modulePath: applicationHPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.h"}, + resources: { + configuration: { + paths: { + webapp: "webapp" + } + } + } + } + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Correct class", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.true(project instanceof Application, `Is an instance of the Application class`); +}); + +test("getNamespace", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getNamespace(), "id1", + "Returned correct namespace"); +}); + +test("getSourcePath", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getSourcePath(), path.join(applicationAPath, "webapp"), + "Returned correct source path"); +}); + +test("getCachebusterSignatureType: Default", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getCachebusterSignatureType(), "time", + "Returned correct default cachebuster signature type configuration"); +}); + +test("getCachebusterSignatureType: Configuration", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.builder = { + cachebuster: { + signatureType: "hash" + } + }; + const project = await Specification.create(projectInput); + t.is(project.getCachebusterSignatureType(), "hash", + "Returned correct default cachebuster signature type configuration"); +}); + +test("Access project resources via reader: buildtime style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource = await reader.byPath("/resources/id1/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/id1/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader({style: "flat"}); + const resource = await reader.byPath("/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader: runtime style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader({style: "runtime"}); + const resource = await reader.byPath("/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); +}); + +test("Access project resources via workspace w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ absolute builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["/resources/id1/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ relative builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ legacy builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Modify project resources via workspace and access via flat and runtime readers", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/id1/index.html"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("Application A", "Some Name"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const flatReader = project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/index.html"); + t.truthy(flatReaderResource, "Found the requested resource byPath"); + t.is(flatReaderResource.getPath(), "/index.html", "Resource (byPath) has correct path"); + t.is(await flatReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const flatGlobResult = await flatReader.byGlob("**/index.html"); + t.is(flatGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(flatGlobResult[0].getPath(), "/index.html", "Resource (byGlob) has correct path"); + t.is(await flatGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); + + const runtimeReader = project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/index.html"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath"); + t.is(runtimeReaderResource.getPath(), "/index.html", "Resource (byPath) has correct path"); + t.is(await runtimeReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/index.html"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(runtimeGlobResult[0].getPath(), "/index.html", "Resource (byGlob) has correct path"); + t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); +}); + + +test("Read and write resources outside of app namespace", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + + await workspace.write(createResource({ + path: "/resources/my-custom-bundle.js" + })); + + const buildtimeReader = project.getReader({style: "buildtime"}); + const buildtimeReaderResource = await buildtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(buildtimeReaderResource, "Found the requested resource byPath (buildtime)"); + t.is(buildtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (buildtime)"); + + const buildtimeGlobResult = await buildtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(buildtimeGlobResult.length, 1, "Found the requested resource byGlob (buildtime)"); + t.is(buildtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (buildtime)"); + + const flatReader = project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/resources/my-custom-bundle.js"); + t.falsy(flatReaderResource, "Resource outside of app namespace can't be read using flat reader"); + + const flatGlobResult = await flatReader.byGlob("**/my-custom-bundle.js"); + t.is(flatGlobResult.length, 0, "Resource outside of app namespace can't be found using flat reader"); + + const runtimeReader = project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)"); + t.is(runtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (runtime)"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)"); + t.is(runtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (runtime)"); +}); + +test("_configureAndValidatePaths: Default paths", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + t.is(project._webappPath, "webapp", "Correct default path"); +}); + +test("_configureAndValidatePaths: Custom webapp directory", async (t) => { + const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h"); + const projectInput = { + id: "application.h.id", + version: "1.0.0", + modulePath: applicationHPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.h"}, + resources: { + configuration: { + paths: { + webapp: "webapp-properties.componentName" + } + } + } + } + }; + + const project = await Specification.create(projectInput); + + t.is(project._webappPath, "webapp-properties.componentName", "Correct path for src"); +}); + +test("_configureAndValidatePaths: Webapp directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = { + configuration: { + paths: { + webapp: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find source directory 'does/not/exist' in application project application.a"); +}); + +test("_getNamespaceFromManifestJson: No 'sap.app' configuration found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.is(error.message, "No sap.app/id configuration found in manifest.json of project application.a", + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestJson: No application id in 'sap.app' configuration found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {}}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.is(error.message, "No sap.app/id configuration found in manifest.json of project application.a"); +}); + +test("_getNamespaceFromManifestJson: set namespace to id", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {id: "my.id"}}); + + const namespace = await project._getNamespaceFromManifestJson(); + t.is(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespaceFromManifestAppDescVariant: No 'id' property found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestAppDescVariant()); + t.is(error.message, `No "id" property found in manifest.appdescr_variant of project application.a`, + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestAppDescVariant: set namespace to id", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({id: "my.id"}); + + const namespace = await project._getNamespaceFromManifestAppDescVariant(); + t.is(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespace: Correct fallback to manifest.appdescr_variant if manifest.json is missing", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().resolves({id: "my.id"}); + + const namespace = await project._getNamespace(); + t.is(namespace, "my/id", "Returned correct namespace"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant failed", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.is(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant is not possible", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({message: "No such stable or directory: manifest.json", code: "ENOENT"}) + .onSecondCall().rejects({code: "ENOENT"}); // both files are missing + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, + "Could not find required manifest.json for project application.a: " + + "No such stable or directory: manifest.json" + + "\n\n" + + "If you are about to start a new project, please refer to:\n" + + "https://ui5.github.io/cli/v4/pages/GettingStarted/#starting-a-new-project", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: No fallback if manifest.json is present but failed to parse", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.is(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 1, "_getManifest called exactly once"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json only"); +}); + +test("_getManifest: reads correctly", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + const content = await project._getManifest("/manifest.json"); + t.is(content._version, "1.1.0", "manifest.json content has been read"); +}); + +test("_getManifest: invalid JSON", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => "no json" + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.regex(error.message, /^Failed to read \/some-manifest\.json for project application\.a: /, + "Rejected with correct error message"); + t.is(byPathStub.callCount, 1, "byPath got called once"); + t.is(byPathStub.getCall(0).args[0], "/some-manifest.json", "byPath got called with the correct argument"); +}); + +test.serial("_getManifest: File does not exist", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + const error = await t.throwsAsync(project._getManifest("/does-not-exist.json")); + t.is(error.message, + "Could not find resource /does-not-exist.json in project application.a", + "Rejected with correct error message"); + t.is(error.code, "ENOENT"); +}); + +test.serial("_getManifest: result is cached", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => `{"pony": "no unicorn"}` + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const content = await project._getManifest("/some-manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on first call"); + + const content2 = await project._getManifest("/some-other-manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "Correct result on second call"); + + t.is(byPathStub.callCount, 2, "byPath got called exactly twice (and then cached)"); +}); + +test.serial("_getManifest: Caches successes and failures", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const getStringStub = sinon.stub() + .onFirstCall().rejects(new Error("EPON: Pony Error")) + .onSecondCall().resolves(`{"pony": "no unicorn"}`); + const byPathStub = sinon.stub().resolves({ + getString: getStringStub + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error.message, + "Failed to read /some-manifest.json for project application.a: " + + "EPON: Pony Error", + "Rejected with correct error message"); + + const content = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on second call"); + + const error2 = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error2.message, + "Failed to read /some-manifest.json for project application.a: " + + "EPON: Pony Error", + "From cache: Rejected with correct error message"); + + const content2 = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "From cache: Correct result on first call"); + + t.is(byPathStub.callCount, 2, + "byPath got called exactly twice (and then cached)"); +}); + +test("namespace: detect namespace from pom.xml via ${project.artifactId}", async (t) => { + const {applicationHInput} = t.context; + applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-project.artifactId"; + const project = await Specification.create(applicationHInput); + + t.is(project.getNamespace(), "application/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${componentName} from properties", async (t) => { + const {applicationHInput} = t.context; + applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-properties.componentName"; + const project = await Specification.create(applicationHInput); + + t.is(project.getNamespace(), "application/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${appId} from properties", async (t) => { + const {applicationHInput} = t.context; + applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-properties.appId"; + + const error = await t.throwsAsync(Specification.create(applicationHInput)); + t.deepEqual(error.message, "Failed to resolve namespace of project application.h: \"${appId}\"" + + " couldn't be resolved from maven property \"appId\" of pom.xml of project application.h"); +}); diff --git a/packages/project/test/lib/specifications/types/Library.js b/packages/project/test/lib/specifications/types/Library.js new file mode 100644 index 00000000000..aaeed466701 --- /dev/null +++ b/packages/project/test/lib/specifications/types/Library.js @@ -0,0 +1,1569 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import Library from "../../../../lib/specifications/types/Library.js"; + +const __dirname = import.meta.dirname; +const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const libraryHPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.h"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.projectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryDPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "main/src", + test: "main/test" + } + } + }, + } + }; + t.context.flatProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryHPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "library", + metadata: { + name: "library.h", + } + } + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("getNamespace", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + t.is(project.getNamespace(), "library/d", + "Returned correct namespace"); +}); + +test("getSourcePath", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + t.is(project.getSourcePath(), path.join(libraryDPath, "main", "src"), + "Returned correct source path"); +}); + +test("getPropertiesFileSourceEncoding: Default", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("getPropertiesFileSourceEncoding: Configuration", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + const project = await (new Library().init(projectInput)); + t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("getJsdocExcludes", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.builder = { + jsdoc: { + excludes: ["excludes"] + } + }; + const project = await (new Library().init(projectInput)); + t.deepEqual(project.getJsdocExcludes(), ["excludes"], + "Returned correct jsdocExcludes configuration"); +}); + +test("getJsdocExcludes: default", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + t.deepEqual(project.getJsdocExcludes(), [], + "Returned correct jsdocExcludes configuration"); +}); + +test("Access project resources via reader: buildtime style", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + const reader = project.getReader(); + const resource = await reader.byPath("/resources/library/d/.library"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/library/d/.library", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + const reader = project.getReader({style: "flat"}); + const resource = await reader.byPath("/.library"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/.library", "Resource has correct path"); +}); + +test("Access project test-resources via reader: buildtime style", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + const reader = project.getReader({style: "buildtime"}); + const resource = await reader.byPath("/test-resources/library/d/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); +}); + +test("Access project test-resources via reader: runtime style", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + const reader = project.getReader({style: "runtime"}); + const resource = await reader.byPath("/test-resources/library/d/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); +}); + +test("Access project resources via reader w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/.library"] + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found excluded resource for runtime style"); +}); + +test("Access project resources via workspace w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/.library"] + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getWorkspace().byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources via reader and workspace w/ absolute builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["/resources/library/d/.library"] + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources via reader and workspace w/ incorrect builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["/.library"] // Absolute path does not match base path + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 1, + "Found resource in project with incorrect exclude for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1, + "Found resource in project with incorrect exclude for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1, + "Found resource in project with incorrect exclude for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1, + "Can not read any test-resources for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1, + "Can not read any test-resources for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1, + "Found resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/.library")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 1, + "Found resource in project with incorrect exclude for default style"); +}); + +test("Access project test-resources via reader and workspace w/ absolute builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["/test-resources/library/d/Test.html"] + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/Test.html")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 0, + "Did not find excluded resource for dist style"); + + // Test resources are not available in flat reader + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0, + "Can not read any test-resources for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0, + "Can not read any test-resources for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/Test.html")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project test-resources via reader and workspace w/ relative builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await (new Library().init(projectInput)); + + projectInput.configuration.builder = { + resources: { + excludes: ["Test.html"] // Has no effect since library excludes must be absolute or use wildcards + } + }; + const excludesProject = await (new Library().init(projectInput)); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/Test.html")).length, 1, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1, + "Did not find excluded resource for dist style"); + + // Test resources are not available in flat reader + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0, + "Can not read any test-resources for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0, + "Can not read any test-resources for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/Test.html")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/Test.html")).length, 1, + "Did not find excluded resource for default style"); +}); + +test("Modify project resources via workspace and access via flat and runtime reader", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/library/d/.library"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const flatReader = project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/.library"); + t.truthy(flatReaderResource, "Found the requested resource byPath (flat)"); + t.is(flatReaderResource.getPath(), "/.library", "Resource (byPath) has correct path (flat)"); + t.is(await flatReaderResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content (flat)"); + + const flatGlobResult = await flatReader.byGlob("**/.library"); + t.is(flatGlobResult.length, 1, "Found the requested resource byGlob (flat)"); + t.is(flatGlobResult[0].getPath(), "/.library", "Resource (byGlob) has correct path (flat)"); + t.is(await flatGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content (flat)"); + + const runtimeReader = project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/library/d/.library"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)"); + t.is(runtimeReaderResource.getPath(), "/resources/library/d/.library", + "Resource (byPath) has correct path (runtime)"); + t.is(await runtimeReaderResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content (runtime)"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/.library"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)"); + t.is(runtimeGlobResult[0].getPath(), "/resources/library/d/.library", + "Resource (byGlob) has correct path (runtime)"); + t.is(await runtimeGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content (runtime)"); +}); + +test("Access flat project resources via reader: buildtime style", async (t) => { + const {flatProjectInput} = t.context; + const project = await (new Library().init(flatProjectInput)); + const reader = project.getReader({style: "buildtime"}); + const resource = await reader.byPath("/resources/library/h/some.js"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/library/h/some.js", "Resource has correct path"); +}); + +test("_configureAndValidatePaths: Default paths", async (t) => { + const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); + const projectInput = { + id: "library.e.id", + version: "1.0.0", + modulePath: libraryEPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "library", + metadata: { + name: "library.e", + } + } + }; + + const project = await (new Library().init(projectInput)); + + t.is(project._srcPath, "src", "Correct default path for src"); + t.is(project._testPath, "test", "Correct default path for test"); + t.true(project._testPathExists, "Test path detected as existing"); +}); + +test("_configureAndValidatePaths: Test directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources.configuration.paths.test = "does/not/exist"; + const project = await (new Library().init(projectInput)); + + t.is(project._srcPath, "main/src", "Correct path for src"); + t.is(project._testPath, "does/not/exist", "Correct path for test"); + t.false(project._testPathExists, "Test path detected as non-existent"); +}); + +test("_configureAndValidatePaths: Source directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources.configuration.paths.src = "does/not/exist"; + const err = await t.throwsAsync(new Library().init(projectInput)); + + t.is(err.message, "Unable to find source directory 'does/not/exist' in library project library.d"); +}); + +test("_parseConfiguration: Get copyright", async (t) => { + const {projectInput} = t.context; + const project = await (new Library().init(projectInput)); + + t.is(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); +}); + +test("_parseConfiguration: Copyright already configured", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.metadata.copyright = "My copyright"; + const project = await (new Library().init(projectInput)); + + t.is(project.getCopyright(), "My copyright", "Copyright was not altered"); +}); + +test.serial("_parseConfiguration: Copyright retrieval fails", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "_getCopyrightFromDotLibrary").resolves(null); + const project = await (new Library().init(projectInput)); + + t.is(project.getCopyright(), undefined, "Copyright was not altered"); +}); + +test.serial("_parseConfiguration: Preload excludes from .library", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]); + + const project = new Library(); + + const loggerVerboseSpy = sinon.spy(project._log, "verbose"); + + await project.init(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test("_parseConfiguration: Preload excludes from project configuration (non-framework library)", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.builder = { + libraryPreload: { + excludes: ["test/exclude/**"] + } + }; + const project = await (new Library().init(projectInput)); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); +}); + +test.serial("_parseConfiguration: Preload exclude fallback to .library (framework libraries only)", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]); + + const project = new Library(); + + const loggerVerboseSpy = sinon.spy(project._log, "verbose"); + + await project.init(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test.serial("_parseConfiguration: No preload excludes from .library", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(null); + + const project = new Library(); + + const loggerVerboseSpy = sinon.spy(project._log, "verbose"); + + await project.init(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), [], + "No library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test.serial("_parseConfiguration: Preload excludes from project configuration (framework library)", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + const getPreloadExcludesFromDotLibraryStub = + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves([]); + + projectInput.configuration.builder = { + libraryPreload: { + excludes: ["test/exclude/**"] + } + }; + const project = new Library(); + + const loggerVerboseSpy = sinon.spy(project._log, "verbose"); + + await project.init(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "Using preload excludes for framework library library.d from project configuration" + ]); + + t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called"); +}); + +test.serial("_parseConfiguration: No preload exclude fallback for non-framework libraries", async (t) => { + const {projectInput, sinon} = t.context; + + sinon.stub(Library.prototype, "isFrameworkProject").returns(false); + const getPreloadExcludesFromDotLibraryStub = sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary") + .resolves(["test/exclude/**"]); + const project = await (new Library().init(projectInput)); + + t.deepEqual(project.getLibraryPreloadExcludes(), [], + "No library preload excludes have been set"); + t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called"); +}); + +test("_getManifest: Reads correctly", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const {content, filePath} = await project._getManifest(); + t.is(content.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getManifest: No manifest.json", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, + "Could not find manifest.json file for project library.d", + "Rejected with correct error message"); +}); + +test("_getManifest: Invalid JSON", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `no pony`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.regex(error.message, /^Failed to read some path for project library\.d: /, + "Rejected with correct error message"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getManifest: Propagates exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getManifest: Multiple manifest.json files", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }, { + getString: async () => `{"pony": "no shark"}`, + getPath: () => "some other path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, "Found multiple (2) manifest.json files for project library.d", + "Rejected with correct error message"); +}); + +test("_getManifest: Result is cached", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const {content: content1, filePath: filePath1} = await project._getManifest(); + t.is(content1.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath1, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); + const {content: content2, filePath: filePath2} = await project._getManifest(); + + t.is(content2.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: Reads correctly", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const {content, filePath} = await project._getDotLibrary(); + t.deepEqual(content, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: No .library file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "Could not find .library file for project library.d", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Invalid XML", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `no pony`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "Failed to read some path for project library.d: " + + "Non-whitespace before first tag.\nLine: 0\nColumn: 1\nChar: n", + "Rejected with correct error message"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: Propagates exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Multiple .library files", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }, { + getString: async () => `Hungry`, + getPath: () => "some other path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, "Found multiple (2) .library files for project library.d", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Result is cached", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const {content: content1, filePath: filePath1} = await project._getDotLibrary(); + t.deepEqual(content1, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath1, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); + const {content: content2, filePath: filePath2} = await project._getDotLibrary(); + + t.deepEqual(content2, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getLibraryJsPath: Reads correctly", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const filePath = await project._getLibraryJsPath(); + t.is(filePath, "some path", "Expected library.js path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); +}); + +test("_getLibraryJsPath: No library.js file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.is(error.message, + "Could not find library.js file for project library.d", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Propagates exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Multiple library.js files", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }, { + getPath: () => "some other path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.is(error.message, "Found multiple (2) library.js files for project library.d", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Result is cached", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }]); + + project._getRawSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const filePath1 = await project._getLibraryJsPath(); + t.is(filePath1, "some path", "Expected library.js path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); + + const filePath2 = await project._getLibraryJsPath(); + t.is(filePath2, "some path", "Expected library.js path"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); +}); + +test.serial("_getNamespace: namespace resolution fails", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + const loggerVerboseSpy = sinon.stub(project._log, "verbose"); + + sinon.stub(project, "_getNamespaceFromManifest").resolves({}); + sinon.stub(project, "_getNamespaceFromDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").rejects(new Error("pony error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, "Failed to detect namespace or namespace is empty for project library.d." + + " Check verbose log for details."); + + t.is(loggerVerboseSpy.callCount, 2, "2 calls to log.verbose should be done"); + const logVerboseCalls = loggerVerboseSpy.getCalls().map((call) => call.args[0]); + + t.true(logVerboseCalls.includes( + "Failed to resolve namespace of project library.d from manifest.json or .library file. " + + "Falling back to library.js file path..."), + "should contain message for missing manifest.json"); + + t.true(logVerboseCalls.includes( + "Namespace resolution from library.js file path failed for project library.d: pony error"), + "should contain message for missing library.js"); +}); + +test("_getNamespace: from manifest.json with .library on same level", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/mani-pony/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/mani-pony/.library" + }); + const res = await project._getNamespace(); + t.is(res, "mani-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from manifest.json for flat project", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "mani-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); +}); + +test("_getNamespace: from .library for flat project", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "dot-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); +}); + +test("_getNamespace: from manifest.json with .library on same level but different directory", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/mani-pony/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/different-pony/.library" + }); + + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + `Failed to detect namespace for project library.d: Found a manifest.json on the same directory level ` + + `but in a different directory than the .library file. They should be in the same directory.\n` + + ` manifest.json path: /mani-pony/manifest.json\n` + + ` is different to\n` + + ` .library path: /different-pony/.library`, + "Rejected with correct error message"); +}); + +test("_getNamespace: from manifest.json with not matching file path", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/different/namespace/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/different/namespace/.library" + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, `Detected namespace "mani-pony" does not match detected directory structure ` + + `"different/namespace" for project library.d`, "Rejected with correct error message"); +}); + +test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + const manifestPath = "/different/namespace/manifest.json"; + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + } + }, + filePath: manifestPath + }); + sinon.stub(project, "_getDotLibrary").resolves({}); + + const loggerStub = sinon.stub(project._log, "verbose"); + + const err = await t.throwsAsync(project._getNamespace()); + + t.is(err.message, + `Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.`, + "Rejected with correct error message"); + t.is(loggerStub.callCount, 4, "calls to verbose"); + + + t.is(loggerStub.getCall(0).args[0], + `Namespace resolution from manifest.json failed for project library.d: ` + + `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`, + "correct verbose message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from .library", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/dot-pony/.library" + }); + const res = await project._getNamespace(); + t.is(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from .library with ignored manifest.json on lower level", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/namespace/somedir/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/dot-pony/.library" + }); + const res = await project._getNamespace(); + t.is(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: manifest.json on higher level than .library", async (t) => { + const {projectInput, sinon} = t.context; + + const manifestFsPath = "/namespace/manifest.json"; + const dotLibraryFsPath = "/namespace/morenamespace/.library"; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: manifestFsPath + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: dotLibraryFsPath + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + `Failed to detect namespace for project library.d: ` + + `Found a manifest.json on a higher directory level than the .library file. ` + + `It should be on the same or a lower level. ` + + `Note that a manifest.json on a lower level will be ignored.\n` + + ` manifest.json path: ${manifestFsPath}\n` + + ` is higher than\n` + + ` .library path: ${dotLibraryFsPath}`, + "Rejected with correct error message"); +}); + +test("_getNamespace: from .library with maven placeholder", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "${mvn-pony}"}} + }, + filePath: "/mvn-unicorn/.library" + }); + const resolveMavenPlaceholderStub = + sinon.stub(project, "_resolveMavenPlaceholder").resolves("mvn-unicorn"); + const res = await project._getNamespace(); + + t.is(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", + "resolveMavenPlaceholder called with correct argument"); + t.is(res, "mvn-unicorn", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from .library with not matching file path", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "mvn-pony"}} + }, + filePath: "/different/namespace/.library" + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, `Detected namespace "mvn-pony" does not match detected directory structure ` + + `"different/namespace" for project library.d`, + "Rejected with correct error message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from library.js", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({}); // Empty result or exception should not matter + sinon.stub(project, "_getDotLibrary").rejects(new Error("Because bird")); + sinon.stub(project, "_getLibraryJsPath").resolves("/my/namespace/library.js"); + const res = await project._getNamespace(); + t.is(res, "my/namespace", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from project root level library.js", async (t) => { + const {projectInput, sinon} = t.context; + + const project = new Library(); + + const loggerStub = sinon.stub(project._log, "verbose"); + + await project.init(projectInput); + + sinon.stub(project, "_getManifest").resolves({}); + sinon.stub(project, "_getDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").resolves("/library.js"); + const err = await t.throwsAsync(project._getNamespace()); + + t.is(err.message, + "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.", + "Rejected with correct error message"); + + const logCalls = loggerStub.getCalls().map((call) => call.args[0]); + t.true(logCalls.includes( + "Namespace resolution from library.js file path failed for project library.d: " + + "Found library.js file in root directory. " + + "Expected it to be in namespace directory."), + "should contain message for root level library.js"); +}); + +test("_getNamespace: neither manifest nor .library or library.js path contain it", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({}); + sinon.stub(project, "_getDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").rejects(new Error("Not found bla")); + const err = await t.throwsAsync(project._getNamespace()); + t.is(err.message, + "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.", + "Rejected with correct error message"); +}); + +test("_getNamespace: maven placeholder resolution fails", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "${mvn-pony}" + } + }, + filePath: "/not/used" + }); + sinon.stub(project, "_getDotLibrary").resolves({}); + const resolveMavenPlaceholderStub = + sinon.stub(project, "_resolveMavenPlaceholder") + .rejects(new Error("because squirrel")); + const err = await t.throwsAsync(project._getNamespace()); + t.is(err.message, + "Failed to resolve namespace maven placeholder of project library.d: because squirrel", + "Rejected with correct error message"); + t.is(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", + "resolveMavenPlaceholder called with correct argument"); +}); + +test("_getCopyrightFromDotLibrary", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + copyright: { + _: "copyleft" + } + } + } + }); + const copyright = await project._getCopyrightFromDotLibrary(); + t.is(copyright, "copyleft", "Returned correct copyright"); +}); + +test("_getCopyrightFromDotLibrary: No copyright in .library file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const copyright = await project._getCopyrightFromDotLibrary(); + t.is(copyright, null, "No copyright returned"); +}); + +test("_getCopyrightFromDotLibrary: Does not propagate exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const res = await project._getCopyrightFromDotLibrary(); + t.is(res, null, "Returned with null"); +}); + +test("_getPreloadExcludesFromDotLibrary: Single exclude", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + appData: { + packaging: { + "all-in-one": { + exclude: { + $: { + name: "test/exclude/**" + } + } + } + } + } + } + } + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.deepEqual(excludes, [ + "test/exclude/**", + ], "_getPreloadExcludesFromDotLibrary should return array with excludes"); +}); + +test("_getPreloadExcludesFromDotLibrary: Multiple excludes", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + appData: { + packaging: { + "all-in-one": { + exclude: [ + { + $: { + name: "test/exclude1/**" + } + }, + { + $: { + name: "test/exclude2/**" + } + }, + { + $: { + name: "test/exclude3/**" + } + } + ] + } + } + } + } + } + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.deepEqual(excludes, [ + "test/exclude1/**", + "test/exclude2/**", + "test/exclude3/**" + ], "_getPreloadExcludesFromDotLibrary should return array with excludes"); +}); + +test("_getPreloadExcludesFromDotLibrary: No excludes in .library file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.is(excludes, null, "No excludes returned"); +}); + +test("_getPreloadExcludesFromDotLibrary: Propagates exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const err = await t.throwsAsync(project._getPreloadExcludesFromDotLibrary()); + t.is(err.message, "because shark", + "Threw with excepted error message"); +}); + +test("_getNamespaceFromManifest", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "library namespace" + } + }, + filePath: "some path" + }); + const {namespace, filePath} = await project._getNamespaceFromManifest(); + t.is(namespace, "library namespace", "Returned correct namespace"); + t.is(filePath, "some path", "Returned correct file path"); +}); + +test("_getNamespaceFromManifest: No ID in manifest.json file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": {} + }, + filePath: "some path" + }); + const res = await project._getNamespaceFromManifest(); + t.deepEqual(res, {}, "Empty object returned"); +}); + +test("_getNamespaceFromManifest: Does not propagate exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + sinon.stub(project, "_getManifest").rejects(new Error("because shark")); + const res = await project._getNamespaceFromManifest(); + t.deepEqual(res, {}, "Empty object returned"); +}); + +test("_getNamespaceFromDotLibrary", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + name: { + _: "library namespace" + } + } + }, + filePath: "some path" + }); + const {namespace, filePath} = await project._getNamespaceFromDotLibrary(); + t.is(namespace, "library namespace", + "Returned correct namespace"); + t.is(filePath, "some path", + "Returned correct file path"); +}); + +test("_getNamespaceFromDotLibrary: No library name in .library file", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const res = await project._getNamespaceFromDotLibrary(); + t.deepEqual(res, {}, "Empty object returned"); +}); + +test("_getNamespaceFromDotLibrary: Does not propagate exception", async (t) => { + const {projectInput, sinon} = t.context; + + const project = await (new Library().init(projectInput)); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const res = await project._getNamespaceFromDotLibrary(); + t.deepEqual(res, {}, "Empty object returned"); +}); diff --git a/packages/project/test/lib/specifications/types/Module.js b/packages/project/test/lib/specifications/types/Module.js new file mode 100644 index 00000000000..deff4f7fdb8 --- /dev/null +++ b/packages/project/test/lib/specifications/types/Module.js @@ -0,0 +1,313 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; + +const __dirname = import.meta.dirname; +const moduleAPath = path.join(__dirname, "..", "..", "..", "fixtures", "module.a"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.projectInput = { + id: "module.a.id", + version: "1.0.0", + modulePath: moduleAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "module", + metadata: { + name: "module.a", + copyright: "Some fancy copyright" // allowed but ignored + }, + resources: { + configuration: { + paths: { + "/": "dist", + "/dev/": "dev" + } + } + } + } + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Correct class", async (t) => { + const {projectInput} = t.context; + const {default: Module} = await import("../../../../lib/specifications/types/Module.js"); + const project = await Specification.create(projectInput); + t.true(project instanceof Module, `Is an instance of the Module class`); +}); + +test("getSourcePath: Throws", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const err = t.throws(() => { + project.getSourcePath(); + }); + t.is(err.message, "Projects of type module have more than one source path", + "Threw with expected error message"); +}); + +test("getNamespace", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getNamespace(), null, + "Returned no namespace"); +}); + +test("Access project resources via reader", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.throws(() => { + project.getSourcePath(); + }, { + message: "Projects of type module have more than one source path" + }, "Threw with expected error message"); +}); + +test("Access project resources via reader (multiple mappings)", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource1 = await reader.byPath("/dev/devTools.js"); + t.truthy(resource1, "Found the requested resource"); + t.is(resource1.getPath(), "/dev/devTools.js", "Resource has correct path"); + + const resource2 = await reader.byPath("/index.js"); + t.truthy(resource2, "Found the requested resource"); + t.is(resource2.getPath(), "/index.js", "Resource has correct path"); +}); + +test("Access project resources via reader (one mapping)", async (t) => { + const {projectInput} = t.context; + delete projectInput.configuration.resources.configuration.paths["/"]; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource1 = await reader.byPath("/dev/devTools.js"); + t.truthy(resource1, "Found the requested resource"); + t.is(resource1.getPath(), "/dev/devTools.js", "Resource has correct path"); + + const resource2 = await reader.byPath("/index.js"); + t.falsy(resource2, "Could not find resource in unmapped path"); +}); + +test("Access project resources via reader w/ builder excludes", async (t) => { + const {projectInput, sinon} = t.context; + const baselineProject = await Specification.create(projectInput); + const excludesProject = await Specification.create(projectInput); + + // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration. + // Hence modules can't practically be configured with builder excludes. + // We still simply stub the respective API call to test the code and be prepared + // + // projectInput.configuration.builder = { + // resources: { + // excludes: ["**/devTools.js"] + // } + // }; + // So stub instead: + sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["**/devTools.js"]); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1, + "Found excluded resource for runtime style"); +}); + +test("Access project resources via workspace w/ builder excludes", async (t) => { + const {projectInput, sinon} = t.context; + const baselineProject = await Specification.create(projectInput); + const excludesProject = await Specification.create(projectInput); + + // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration. + // Hence modules can't practically be configured with builder excludes. + // We still simply stub the respective API call to test the code and be prepared + // + // projectInput.configuration.builder = { + // resources: { + // excludes: ["**/devTools.js"] + // } + // }; + // So stub instead: + sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["**/devTools.js"]); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getWorkspace().byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources via reader w/ absolute builder excludes", async (t) => { + const {projectInput, sinon} = t.context; + const baselineProject = await Specification.create(projectInput); + const excludesProject = await Specification.create(projectInput); + + // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration. + // Hence modules can't practically be configured with builder excludes. + // We still simply stub the respective API call to test the code and be prepared + // + // projectInput.configuration.builder = { + // resources: { + // excludes: ["/dev/devTools.js"] + // } + // }; + // So stub instead: + sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["/dev/devTools.js"]); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/devTools.js")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/devTools.js")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Modify project resources via workspace and access via reader", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/dev/devTools.js"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace(/dev/g, "duck duck"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = project.getReader(); + const readerResource = await reader.byPath("/dev/devTools.js"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/dev/devTools.js", "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const gGlobResult = await reader.byGlob("**/devTools.js"); + t.is(gGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(gGlobResult[0].getPath(), "/dev/devTools.js", "Resource (byGlob) has correct path"); + t.is(await gGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("Modify project resources via workspace and access via reader for other path mapping", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/index.js"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("world", "duck"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = project.getReader(); + const readerResource = await reader.byPath("/index.js"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/index.js", "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const gGlobResult = await reader.byGlob("**/index.js"); + t.is(gGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(gGlobResult[0].getPath(), "/index.js", "Resource (byGlob) has correct path"); + t.is(await gGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("_configureAndValidatePaths: Default path mapping", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = {}; + const project = await Specification.create(projectInput); + + t.is(project._paths.length, 1, "One default path mapping"); + t.is(project._paths[0].virBasePath, "/", "Default path mapping for /"); + t.is(project._paths[0].fsBasePath, projectInput.modulePath, "Correct fs path"); +}); + +test("_configureAndValidatePaths: Configured path mapping", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + t.is(project._paths.length, 2, "Two path mappings"); + t.is(project._paths[0].virBasePath, "/", "Correct virtual base path for /"); + t.is(project._paths[0].fsBasePath, path.join(projectInput.modulePath, "dist"), "Correct fs path"); + t.is(project._paths[1].virBasePath, "/dev/", "Correct virtual base path for /dev/"); + t.is(project._paths[1].fsBasePath, path.join(projectInput.modulePath, "dev"), "Correct fs path"); +}); + +test("_configureAndValidatePaths: Default directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = {}; + projectInput.modulePath = "/does/not/exist"; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find root directory of module project module.a"); +}); + +test("_configureAndValidatePaths: Directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources.configuration.paths.doesNotExist = "does/not/exist"; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find source directory 'does/not/exist' in module project module.a"); +}); diff --git a/packages/project/test/lib/specifications/types/ThemeLibrary.js b/packages/project/test/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 00000000000..10559e29c3e --- /dev/null +++ b/packages/project/test/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,250 @@ +import test from "ava"; +import path from "node:path"; +import sinonGlobal from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; + +const __dirname = import.meta.dirname; +const themeLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "theme.library.e"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.projectInput = { + id: "theme.library.e.id", + version: "1.0.0", + modulePath: themeLibraryEPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "theme-library", + metadata: { + name: "theme.library.e", + copyright: "Some fancy copyright" + } + } + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Correct class", async (t) => { + const {projectInput} = t.context; + const {default: ThemeLibrary} = await import("../../../../lib/specifications/types/ThemeLibrary.js"); + const project = await Specification.create(projectInput); + t.true(project instanceof ThemeLibrary, `Is an instance of the ThemeLibrary class`); +}); + +test("getCopyright", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + t.is(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); +}); + +test("getSourcePath", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getSourcePath(), path.join(themeLibraryEPath, "src"), "Correct source path"); +}); + +test("getNamespace", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getNamespace(), null, + "Returned no namespace"); +}); + +test("Access project resources via reader", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource = await reader.byPath("/resources/theme/library/e/themes/my_theme/.theme"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/theme/library/e/themes/my_theme/.theme", "Resource has correct path"); +}); + +test("Access project test-resources via reader", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource = await reader.byPath("/test-resources/theme/library/e/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/theme/library/e/Test.html", "Resource has correct path"); +}); + +test("Access project resources via reader w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/.theme"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.theme")).length, 1, + "Found excluded resource for runtime style"); +}); + +test("Access project resources via workspace w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/.theme"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getWorkspace().byGlob("**/.theme")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/.theme")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources via reader w/ absolute builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["/resources/theme/library/e/themes/my_theme/.theme"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.theme")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.theme")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/.theme")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/.theme")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Modify project resources via workspace and access via flat and runtime reader", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = project.getReader(); + const readerResource = await reader.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const globResult = await reader.byGlob("**/library.source.less"); + t.is(globResult.length, 1, "Found the requested resource byGlob"); + t.is(globResult[0].getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byGlob) has correct path"); + t.is(await globResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("_configureAndValidatePaths: Default paths", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct default path for src"); + t.is(project._testPath, "test", "Correct default path for test"); + t.true(project._testPathExists, "Test path detected as existing"); +}); + +test("_configureAndValidatePaths: Test directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = { + configuration: { + paths: { + test: "does/not/exist" + } + } + }; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct path for src"); + t.is(project._testPath, "does/not/exist", "Correct path for test"); + t.false(project._testPathExists, "Test path detected as non-existent"); +}); + +test("_configureAndValidatePaths: Source directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = { + configuration: { + paths: { + src: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find source directory 'does/not/exist' in theme-library project theme.library.e"); +}); diff --git a/packages/project/test/lib/ui5framework/AbstractInstaller.js b/packages/project/test/lib/ui5framework/AbstractInstaller.js new file mode 100644 index 00000000000..691e40984a5 --- /dev/null +++ b/packages/project/test/lib/ui5framework/AbstractInstaller.js @@ -0,0 +1,11 @@ +import test from "ava"; +import AbstractInstaller from "../../../lib/ui5Framework/AbstractInstaller.js"; + +test("AbstractInstaller: constructor throws an error", (t) => { + t.throws(() => { + new AbstractInstaller(); + }, { + instanceOf: TypeError, + message: "Class 'AbstractInstaller' is abstract" + }); +}); diff --git a/packages/project/test/lib/ui5framework/AbstractResolver.js b/packages/project/test/lib/ui5framework/AbstractResolver.js new file mode 100644 index 00000000000..7156df0a545 --- /dev/null +++ b/packages/project/test/lib/ui5framework/AbstractResolver.js @@ -0,0 +1,1206 @@ +import test from "ava"; +import sinon from "sinon"; +import path from "node:path"; +import os from "node:os"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + t.context.osHomeDirStub = sinon.stub().callsFake(() => os.homedir()); + t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", { + "node:os": { + homedir: t.context.osHomeDirStub + } + }); + + class MyResolver extends t.context.AbstractResolver { + static async fetchAllVersions() {} + } + + t.context.MyResolver = MyResolver; +}); + +test.afterEach.always((t) => { + delete process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES; + esmock.purge(t.context.AbstractResolver); + sinon.restore(); +}); + +test("AbstractResolver: abstract constructor should throw", async (t) => { + const {AbstractResolver} = t.context; + await t.throwsAsync(async () => { + new AbstractResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + }, {message: `Class 'AbstractResolver' is abstract`}); +}); + +test("AbstractResolver: constructor", (t) => { + const {MyResolver, AbstractResolver} = t.context; + const providedLibraryMetadata = {"test": "data"}; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0", + providedLibraryMetadata, + sources: true + }); + t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class"); + t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class"); + t.is(resolver._version, "1.75.0"); + t.true(resolver._sources, "Correct value for 'sources' flag"); +}); + +test("AbstractResolver: constructor overwrites sources with env variable", (t) => { + const {MyResolver, AbstractResolver} = t.context; + + process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES = true; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0", + sources: false // Environment variable overrules parameter + }); + t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class"); + t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class"); + t.is(resolver._version, "1.75.0"); + t.true(resolver._sources, "Correct value for 'sources' flag"); +}); + +test("AbstractResolver: constructor without version", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/" + }); + t.is(resolver._version, undefined); +}); + +test("AbstractResolver: Set absolute 'cwd'", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + cwd: "/my-cwd" + }); + t.is(resolver._cwd, path.resolve("/my-cwd"), "Should be resolved 'cwd'"); +}); + +test("AbstractResolver: Set relative 'cwd'", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + cwd: "./my-cwd" + }); + t.is(resolver._cwd, path.resolve("./my-cwd"), "Should be resolved 'cwd'"); +}); + +test("AbstractResolver: Defaults 'cwd' to process.cwd()", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + ui5DataDir: "/ui5data" + }); + t.is(resolver._cwd, process.cwd(), "Should default to process.cwd()"); +}); + +test("AbstractResolver: Set absolute 'ui5DataDir'", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + ui5DataDir: "/my-ui5DataDir" + }); + t.is(resolver._ui5DataDir, path.resolve("/my-ui5DataDir"), "Should be resolved 'ui5DataDir'"); +}); + +test("AbstractResolver: Set relative 'ui5DataDir'", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + ui5DataDir: "./my-ui5DataDir" + }); + t.is(resolver._ui5DataDir, path.resolve("./my-ui5DataDir"), "Should be resolved 'ui5DataDir'"); +}); + +test("AbstractResolver: 'ui5DataDir' overriden os.homedir()", (t) => { + const {MyResolver, osHomeDirStub} = t.context; + + osHomeDirStub.returns("./"); + + const resolver = new MyResolver({ + version: "1.75.0" + }); + t.is(resolver._ui5DataDir, path.resolve("./.ui5"), "Should be resolved 'ui5DataDir'"); +}); + +test("AbstractResolver: Defaults 'ui5DataDir' to ~/.ui5", (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + version: "1.75.0", + cwd: "/test-project/" + }); + t.is(resolver._ui5DataDir, path.join(os.homedir(), ".ui5"), "Should default to ~/.ui5"); +}); + +test("AbstractResolver: getLibraryMetadata should throw an Error when not implemented", async (t) => { + const {MyResolver} = t.context; + await t.throwsAsync(async () => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + await resolver.getLibraryMetadata(); + }, {message: `AbstractResolver: getLibraryMetadata must be implemented!`}); +}); + +test("AbstractResolver: handleLibrary should throw an Error when not implemented", async (t) => { + const {MyResolver} = t.context; + await t.throwsAsync(async () => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + await resolver.handleLibrary(); + }, {message: `AbstractResolver: handleLibrary must be implemented!`}); +}); + +test("AbstractResolver: install", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const metadata = { + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, + "sap.ui.lib2": { + "npmPackageName": "@openui5/sap.ui.lib2", + "version": "1.75.0", + "dependencies": [ + "sap.ui.lib3" + ], + "optionalDependencies": [] + }, + "sap.ui.lib3": { + "npmPackageName": "@openui5/sap.ui.lib3", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + "npmPackageName": "@openui5/sap.ui.lib4", + "version": "1.75.0", + "dependencies": [ + "sap.ui.lib1" + ], + "optionalDependencies": [] + } + } + }; + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib1"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"}) + }) + .withArgs("sap.ui.lib3").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"}) + }) + .withArgs("sap.ui.lib4").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib4"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib4"}) + }); + + const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib4"]); + + t.is(handleLibraryStub.callCount, 4, "Each library should be handled once"); + t.deepEqual(result, { + libraryMetadata: { + "sap.ui.lib1": { + dependencies: [], + npmPackageName: "@openui5/sap.ui.lib1", + optionalDependencies: [], + path: "/foo/sap.ui.lib1", + version: "1.75.0", + }, + "sap.ui.lib2": { + dependencies: [ + "sap.ui.lib3", + ], + npmPackageName: "@openui5/sap.ui.lib2", + optionalDependencies: [], + path: "/foo/sap.ui.lib2", + version: "1.75.0", + }, + "sap.ui.lib3": { + dependencies: [], + npmPackageName: "@openui5/sap.ui.lib3", + optionalDependencies: [ + "sap.ui.lib4", + ], + path: "/foo/sap.ui.lib3", + version: "1.75.0", + }, + "sap.ui.lib4": { + dependencies: [ + "sap.ui.lib1", + ], + npmPackageName: "@openui5/sap.ui.lib4", + optionalDependencies: [], + path: "/foo/sap.ui.lib4", + version: "1.75.0", + }, + } + }); +}); + +test("AbstractResolver: install (with providedLibraryMetadata)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0", + providedLibraryMetadata: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0-workspace", + "dependencies": [ + "sap.ui.lib3" + ], + "optionalDependencies": [], + "path": "/workspace/sap.ui.lib1" + }, + "sap.ui.lib4": { + "npmPackageName": "@openui5/sap.ui.lib4", + "version": "1.75.0-workspace", + "dependencies": [ + "sap.ui.lib5" + ], + "optionalDependencies": [], + "path": "/workspace/sap.ui.lib4" + }, + "sap.ui.lib5": { + "npmPackageName": "@openui5/sap.ui.lib5", + "version": "1.75.0-workspace", + "dependencies": [], + "optionalDependencies": [], + "path": "/workspace/sap.ui.lib5" + }, + } + }); + + const metadata = { + libraries: { + "sap.ui.lib2": { + "npmPackageName": "@openui5/sap.ui.lib2", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, + "sap.ui.lib3": { + "npmPackageName": "@openui5/sap.ui.lib3", + "version": "1.75.0", + "dependencies": [ + "sap.ui.lib4" + ], + "optionalDependencies": [] + }, + } + }; + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"}) + }) + .withArgs("sap.ui.lib3").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"}) + }); + + const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]); + + t.is(handleLibraryStub.callCount, 2, "Each library not part of providedLibraryMetadata should be handled once"); + t.deepEqual(result, { + libraryMetadata: { + "sap.ui.lib1": { + dependencies: ["sap.ui.lib3"], + npmPackageName: "@openui5/sap.ui.lib1", + optionalDependencies: [], + path: "/workspace/sap.ui.lib1", + version: "1.75.0-workspace", + }, + "sap.ui.lib2": { + dependencies: [], + npmPackageName: "@openui5/sap.ui.lib2", + optionalDependencies: [], + path: "/foo/sap.ui.lib2", + version: "1.75.0", + }, + "sap.ui.lib3": { + dependencies: ["sap.ui.lib4",], + npmPackageName: "@openui5/sap.ui.lib3", + optionalDependencies: [], + path: "/foo/sap.ui.lib3", + version: "1.75.0", + }, + "sap.ui.lib4": { + dependencies: [ + "sap.ui.lib5", + ], + npmPackageName: "@openui5/sap.ui.lib4", + optionalDependencies: [], + path: "/workspace/sap.ui.lib4", + version: "1.75.0-workspace", + }, + "sap.ui.lib5": { + dependencies: [], + npmPackageName: "@openui5/sap.ui.lib5", + optionalDependencies: [], + path: "/workspace/sap.ui.lib5", + version: "1.75.0-workspace", + }, + } + }); +}); + +test("AbstractResolver: install error handling (rejection of metadata/install)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib1")), + install: Promise.reject(new Error("Error installing sap.ui.lib1")) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")), + install: Promise.reject(new Error("Error installing sap.ui.lib2")) + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]); + }, {message: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Error installing sap.ui.lib1 + 2. Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`}); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (rejection of dependency metadata/install)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib2"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")), + install: Promise.reject(new Error("Error installing sap.ui.lib2")) + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1"]); + }, {message: `Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`}); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (rejection of dependency install)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib2"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").callsFake(() => { + return { + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib3"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }; + }) + .withArgs("sap.ui.lib3").callsFake(() => { + return { + metadata: Promise.resolve({ + dependencies: [] + }), + install: Promise.reject(new Error("Error installing sap.ui.lib3")) + }; + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1"]); + }, {message: `Failed to resolve library sap.ui.lib3: Error installing sap.ui.lib3`}); + + t.is(handleLibraryStub.callCount, 3, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (handleLibrary throws error)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Error within handleLibrary: ${libraryName}`); + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]); + }, {message: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Error within handleLibrary: sap.ui.lib1 + 2. Failed to resolve library sap.ui.lib2: Error within handleLibrary: sap.ui.lib2`}); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling " + +"(no version, no providedLibraryMetadata)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + + await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), { + message: `Resolution of framework libraries failed with errors: + 1. Failed to resolve library sap.ui.lib1: Unable to install library sap.ui.lib1. No framework version provided. + 2. Failed to resolve library sap.ui.lib2: Unable to install library sap.ui.lib2. No framework version provided.` + }); + + t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available"); +}); + +test("AbstractResolver: install error handling " + +"(no version, one lib not part of providedLibraryMetadata)", async (t) => { + const {MyResolver} = t.context; + const resolver = new MyResolver({ + cwd: "/test-project/", + providedLibraryMetadata: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [] + } + } + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + + await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), { + message: + "Failed to resolve library sap.ui.lib2:" + + " Unable to install library sap.ui.lib2. No framework version provided.", + }); + + t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available"); +}); + +test("AbstractResolver: static fetchAllVersions should throw an Error when not implemented", async (t) => { + const {AbstractResolver} = t.context; + await t.throwsAsync(async () => { + await AbstractResolver.fetchAllVersions(); + }, {message: `AbstractResolver: static fetchAllVersions must be implemented!`}); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'latest'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const version = await MyResolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.76.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const version = await MyResolver.resolveVersion("1", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.76.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR-SNAPSHOT'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("1-SNAPSHOT", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const version = await MyResolver.resolveVersion("1.75", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.75.1", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR-SNAPSHOT'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("1.79-SNAPSHOT", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR.PATCH'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const version = await MyResolver.resolveVersion("1.75.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.75.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR.PATCH-SNAPSHOT'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("1.79.0-SNAPSHOT", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion does not include prereleases for 'latest' version", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.76.0", "1.77.0", "1.78.0", "1.79.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.78.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'latest-snapshot'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT", "1.76.1-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("latest-snapshot", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.76.1-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion includes non-prereleases for 'latest-snapshot'", async (t) => { + // Realistically this should never happen, since the Sapui5MavenSnapshotResolver would never return + // non-snapshot versions. This test therefore simply illustrates the current behavior for this theoretic case + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.76.0", "1.77.0", "1.78.0", "1.79.0-SNAPSHOT", "1.79.1"]); + + const version = await MyResolver.resolveVersion("latest-snapshot", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.79.1", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion without options", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0"]); + + await MyResolver.resolveVersion("1.75.0"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: undefined, + ui5DataDir: undefined + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion throws error for 'lts'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions"); + + const error = await t.throwsAsync(MyResolver.resolveVersion("lts", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Framework version specifier "lts" is incorrect or not supported`); + + t.is(fetchAllVersionsStub.callCount, 0, "fetchAllVersions should not be called"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '1.x'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]); + + const version = await MyResolver.resolveVersion("1.x", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.76.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '1.75.x'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]); + + const version = await MyResolver.resolveVersion("1.75.x", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.75.1", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '^1.75.0'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]); + + const version = await MyResolver.resolveVersion("^1.75.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.76.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '~1.75.0'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]); + + const version = await MyResolver.resolveVersion("~1.75.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.75.1", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '> 1.75.0 < 1.75.3'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.75.2", "1.75.3"]); + + const version = await MyResolver.resolveVersion("> 1.75.0 < 1.75.3", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "1.75.2", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'x.x.x-SNAPSHOT'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0-SNAPSHOT", "1.76.0-SNAPSHOT", "1.77.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("x.x.x-SNAPSHOT", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + // All ranges ending with -SNAPSHOT should use "includePrerelease" in order to + // properly match prerelease (i.e. -SNAPSHOT) versions. + t.is(version, "1.77.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '^2.0.0-SNAPSHOT'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["2.0.0-SNAPSHOT", "2.0.1-SNAPSHOT", "2.1.0-SNAPSHOT"]); + + const version = await MyResolver.resolveVersion("^2.0.0-SNAPSHOT", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + // All ranges ending with -SNAPSHOT should use "includePrerelease" in order to + // properly match prerelease (i.e. -SNAPSHOT) versions. + t.is(version, "2.1.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves '2.x.x-alpha'", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["2.0.0-alpha", "2.0.1-alpha", "2.1.0-alpha"]); + + const version = await MyResolver.resolveVersion("^2.0.0-alpha", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + // Prerelease ranges other than -SNAPSHOT should not use "includePrerelease" + // and therefore not match pre-releases like normal versions + t.is(version, "2.0.0-alpha", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'next' using tags", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.0.0", "2.0.0"]); + const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags") + .resolves({ + "latest": "1.0.0", + "next": "2.0.0" + }); + + const version = await MyResolver.resolveVersion("next", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "2.0.0", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); + t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllTags should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion resolves 'next' to a pre-release using tags", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.0.0", "2.0.0-SNAPSHOT"]); + const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags") + .resolves({ + "latest": "1.0.0", + "next": "2.0.0-SNAPSHOT" + }); + + const version = await MyResolver.resolveVersion("next", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + + t.is(version, "2.0.0-SNAPSHOT", "Resolved version should be correct"); + + t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllVersions should be called with expected arguments"); + t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllTags should be called with expected arguments"); +}); + + +test.serial("AbstractResolver: Static resolveVersion resolves 'latest' using tags only " + +"when the resolver supports them", async (t) => { + const {MyResolver} = t.context; + const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]); + const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags") + .resolves(null); + + // Resolver does not support tags (resolves with "null" instead of an object) + // 'latest' should resolve to the highest version available + const version1 = await MyResolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + t.is(version1, "2.0.0", "Resolved version should be correct"); + + t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once"); + t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllTags should be called with expected arguments"); + + // Change behavior of Resolver to support tags, so that version should be used now + // instead of the highest version + fetchAllTagsStub.resolves({ + "latest": "1.76.0" + }); + const version2 = await MyResolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }); + t.is(version2, "1.76.0", "Resolved version should be correct"); + + t.is(fetchAllTagsStub.callCount, 2, "fetchAllTagsStub should be called twice"); + t.deepEqual(fetchAllVersionsStub.getCall(1).args, [{ + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }], "fetchAllTags should be called with expected arguments"); +}); + +test.serial("AbstractResolver: Static resolveVersion throws error for empty string", async (t) => { + const {MyResolver} = t.context; + sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const error = await t.throwsAsync(MyResolver.resolveVersion("", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Framework version specifier "" is incorrect or not supported`); +}); + +test.serial("AbstractResolver: Static resolveVersion throws error for invalid tag name", async (t) => { + const {MyResolver} = t.context; + sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const error = await t.throwsAsync(MyResolver.resolveVersion("%20", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Framework version specifier "%20" is incorrect or not supported`); +}); + +test.serial("AbstractResolver: Static resolveVersion throws error for non-existing tag", async (t) => { + const {MyResolver} = t.context; + sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + sinon.stub(MyResolver, "fetchAllTags") + .resolves({"latest": "1.76.0"}); + + const error = await t.throwsAsync(MyResolver.resolveVersion("this-tag-does-not-exist", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version via tag 'this-tag-does-not-exist'. ` + + `Make sure the tag is available in the configured registry.` + ); +}); + +test.serial("AbstractResolver: Static resolveVersion throws error for version not found", async (t) => { + const {MyResolver} = t.context; + sinon.stub(MyResolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const error = await t.throwsAsync(MyResolver.resolveVersion("1.74.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version 1.74.0. ` + + `Make sure the version is valid and available in the configured registry.`); +}); + +test.serial( + "AbstractResolver: Static resolveVersion throws error for version lower than lowest OpenUI5 version", async (t) => { + const {AbstractResolver} = t.context; + class Openui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Openui5Resolver, "fetchAllVersions") + .returns(["1.75.0", "1.75.1", "1.76.0"]); + + const error = await t.throwsAsync(Openui5Resolver.resolveVersion("1.50.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, + `Could not resolve framework version 1.50.0. Note that OpenUI5 framework libraries can only be ` + + `consumed by the UI5 CLI starting with OpenUI5 v1.52.5`); + }); + +test.serial( + "AbstractResolver: Static resolveVersion throws error for version lower than lowest SAPUI5 version", async (t) => { + const {AbstractResolver} = t.context; + class Sapui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Sapui5Resolver, "fetchAllVersions") + .returns(["1.76.0", "1.76.1", "1.90.0"]); + + const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("1.75.0", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, + `Could not resolve framework version 1.75.0. Note that SAPUI5 framework libraries can only be ` + + `consumed by the UI5 CLI starting with SAPUI5 v1.76.0`); + }); + +test.serial( + "AbstractResolver: Static resolveVersion throws error when latest OpenUI5 version cannot be found", async (t) => { + const {AbstractResolver} = t.context; + class Openui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Openui5Resolver, "fetchAllVersions") + .returns([]); + + const error = await t.throwsAsync(Openui5Resolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version latest. ` + + `Make sure the version is valid and available in the configured registry.`); + }); + +test.serial( + "AbstractResolver: Static resolveVersion throws error when latest SAPUI5 version cannot be found", async (t) => { + const {AbstractResolver} = t.context; + class Sapui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Sapui5Resolver, "fetchAllVersions") + .returns([]); + + const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("latest", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version latest. ` + + `Make sure the version is valid and available in the configured registry.`); + }); + +test.serial( + "AbstractResolver: Static resolveVersion throws error when OpenUI5 version range cannot be resolved", async (t) => { + const {AbstractResolver} = t.context; + class Openui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Openui5Resolver, "fetchAllVersions") + .returns([]); + + const error = await t.throwsAsync(Openui5Resolver.resolveVersion("1.99", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version 1.99. ` + + `Make sure the version is valid and available in the configured registry.`); + }); + +test.serial( + "AbstractResolver: Static resolveVersion throws error when SAPUI5 version range cannot be resolved", async (t) => { + const {AbstractResolver} = t.context; + class Sapui5Resolver extends AbstractResolver { + static async fetchAllVersions() {} + } + + sinon.stub(Sapui5Resolver, "fetchAllVersions") + .returns([]); + + const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("1.99", { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + })); + + t.is(error.message, `Could not resolve framework version 1.99. ` + + `Make sure the version is valid and available in the configured registry.`); + }); diff --git a/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js b/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js new file mode 100644 index 00000000000..e8049f0a412 --- /dev/null +++ b/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js @@ -0,0 +1,200 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import path from "node:path"; + +const __dirname = import.meta.dirname; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.logStub = { + info: sinon.stub(), + verbose: sinon.stub(), + silly: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + isLevelEnabled: sinon.stub().returns(false), + _getLogger: sinon.stub() + }; + const ui5Logger = { + getLogger: sinon.stub().returns(t.context.logStub) + }; + + t.context.pacote = { + packument: sinon.stub().callsFake(async (...args) => { + throw new Error(`pacote.packument stub called with unknown args: ${args}`); + }) + }; + + t.context.NpmcliConfig = sinon.stub().returns({ + load: sinon.stub().resolves(), + flat: { + registry: "https://registry.fake" + } + }); + + t.context.Registry = await esmock.p("../../../lib/ui5Framework/npm/Registry.js", { + "@ui5/logger": ui5Logger, + "pacote": t.context.pacote, + "@npmcli/config": { + "default": t.context.NpmcliConfig + } + }); + + const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", { + "@ui5/logger": ui5Logger, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "lockfile": { + lock: sinon.stub().yieldsAsync(), + unlock: sinon.stub().yieldsAsync() + } + }); + + t.context.Installer = await esmock.p("../../../lib/ui5Framework/npm/Installer.js", { + "@ui5/logger": ui5Logger, + "graceful-fs": { + rename: sinon.stub().yieldsAsync(), + }, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "../../../lib/ui5Framework/npm/Registry.js": t.context.Registry, + "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller + }); + + t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir")) + }, + }); + + t.context.Openui5Resolver = await esmock.p("../../../lib/ui5Framework/Openui5Resolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir")) + }, + "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver, + "../../../lib/ui5Framework/npm/Installer.js": t.context.Installer + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Registry); + esmock.purge(t.context.Installer); + esmock.purge(t.context.AbstractResolver); + esmock.purge(t.context.Openui5Resolver); +}); + +test.serial("resolveVersion", async (t) => { + const {Openui5Resolver, pacote, logStub, NpmcliConfig} = t.context; + + pacote.packument + .withArgs("@openui5/sap.ui.core") + .resolves({ + "versions": { + "1.120.1": "", + "1.120.0": "", + "1.119.0": "", + "1.118.0": "", + "2.0.0-rc.1": "", + "1.123.4-SNAPSHOT": "" + }, + "dist-tags": { + // NOTE: latest does not correspond to highest version in order to verify + // that this tag is used instead of picking the highest version + "latest": "1.120.0", + + "next": "2.0.0-rc.1", + + // NOTE: Tag ends with "-snapshot" in order to verify that the special handling + // of that + "not-a-snapshot": "1.118.0" + } + }); + + const defaultCwd = process.cwd(); + const defaultUi5DataDir = path.join(fakeBaseDir, "datadir", ".ui5"); + + // Generic testing without and with options argument + const optionsArguments = [ + undefined, + { + cwd: path.join(fakeBaseDir, "custom-cwd"), + ui5DataDir: path.join(fakeBaseDir, "custom-datadir", ".ui5") + } + ]; + for (const options of optionsArguments) { + // Reset calls to be able to check them per for-loop run + NpmcliConfig.resetHistory(); + pacote.packument.resetHistory(); + + // Ranges + t.is(await Openui5Resolver.resolveVersion("1", options), "1.120.1"); + t.is(await Openui5Resolver.resolveVersion("1.120", options), "1.120.1"); + t.is(await Openui5Resolver.resolveVersion("1.x", options), "1.120.1"); + t.is(await Openui5Resolver.resolveVersion("1.x.x", options), "1.120.1"); + t.is(await Openui5Resolver.resolveVersion("^1", options), "1.120.1"); + t.is(await Openui5Resolver.resolveVersion("*", options), "1.120.1"); + + // Tags + t.is(await Openui5Resolver.resolveVersion("latest", options), "1.120.0"); + t.is(await Openui5Resolver.resolveVersion("next", options), "2.0.0-rc.1"); + t.is(await Openui5Resolver.resolveVersion("not-a-snapshot", options), "1.118.0"); + + // Exact versions + t.is(await Openui5Resolver.resolveVersion("1.118.0", options), "1.118.0"); + t.is(await Openui5Resolver.resolveVersion("2.0.0-rc.1", options), "2.0.0-rc.1"); + t.is(await Openui5Resolver.resolveVersion("1.123.4-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + + // SNAPSHOT ranges + t.is(await Openui5Resolver.resolveVersion("1-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + t.is(await Openui5Resolver.resolveVersion("1.123-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + + // Error cases + await t.throwsAsync(Openui5Resolver.resolveVersion("", options), { + message: `Framework version specifier "" is incorrect or not supported` + }); + await t.throwsAsync(Openui5Resolver.resolveVersion("tag-does-not-exist", options), { + message: `Could not resolve framework version via tag 'tag-does-not-exist'. ` + + `Make sure the tag is available in the configured registry.` + }); + await t.throwsAsync(Openui5Resolver.resolveVersion("invalid-tag-%20", options), { + message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported` + }); + + await t.throwsAsync(Openui5Resolver.resolveVersion("1.999.9", options), { + message: `Could not resolve framework version 1.999.9. ` + + `Make sure the version is valid and available in the configured registry.` + }); + await t.throwsAsync(Openui5Resolver.resolveVersion("1.0.0", options), { + message: `Could not resolve framework version 1.0.0. ` + + `Note that OpenUI5 framework libraries can only be consumed by the UI5 CLI ` + + `starting with OpenUI5 v1.52.5` + }); + await t.throwsAsync(Openui5Resolver.resolveVersion("^999", options), { + message: `Could not resolve framework version ^999. ` + + `Make sure the version is valid and available in the configured registry.` + }); + + // Check whether options have been passed as expected + t.true(NpmcliConfig.alwaysCalledWithNew()); + t.true(NpmcliConfig.alwaysCalledWithMatch(sinonGlobal.match({ + cwd: options?.cwd ?? defaultCwd + }))); + t.true(pacote.packument.alwaysCalledWithMatch("@openui5/sap.ui.core", { + cache: path.join(options?.ui5DataDir ?? defaultUi5DataDir, "framework", "cacache") + })); + } + + t.is(logStub.warn.callCount, 0); + t.is(logStub.error.callCount, 0); +}); diff --git a/packages/project/test/lib/ui5framework/Openui5Resolver.js b/packages/project/test/lib/ui5framework/Openui5Resolver.js new file mode 100644 index 00000000000..34d756987da --- /dev/null +++ b/packages/project/test/lib/ui5framework/Openui5Resolver.js @@ -0,0 +1,217 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import os from "node:os"; + +test.beforeEach(async (t) => { + t.context.InstallerStub = sinon.stub(); + t.context.fetchPackageDistTags = sinon.stub(); + t.context.fetchPackageManifestStub = sinon.stub(); + t.context.fetchPackageVersionsStub = sinon.stub(); + t.context.installPackageStub = sinon.stub(); + t.context.InstallerStub.callsFake(() => { + return { + fetchPackageDistTags: t.context.fetchPackageDistTags, + fetchPackageManifest: t.context.fetchPackageManifestStub, + fetchPackageVersions: t.context.fetchPackageVersionsStub, + installPackage: t.context.installPackageStub + }; + }); + + t.context.Openui5Resolver = await esmock("../../../lib/ui5Framework/Openui5Resolver.js", { + "../../../lib/ui5Framework/npm/Installer": t.context.InstallerStub + }); +}); + +test.afterEach.always(() => { + sinon.restore(); +}); + +test.serial("Openui5Resolver: _getNpmPackageName", (t) => { + const {Openui5Resolver} = t.context; + t.is(Openui5Resolver._getNpmPackageName("foo"), "@openui5/foo"); +}); + +test.serial("Openui5Resolver: _getLibaryName", (t) => { + const {Openui5Resolver} = t.context; + t.is(Openui5Resolver._getLibaryName("@openui5/foo"), "foo"); + t.is(Openui5Resolver._getLibaryName("@something/else"), "@something/else"); +}); + +test.serial("Openui5Resolver: getLibraryMetadata", async (t) => { + const {Openui5Resolver} = t.context; + + const resolver = new Openui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + t.context.fetchPackageManifestStub + .callsFake(async ({pkgName}) => { + throw new Error(`Unknown install call: ${pkgName}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({}) + .withArgs({pkgName: "@openui5/sap.ui.lib2", version: "1.75.0"}).resolves({ + dependencies: { + "sap.ui.lib3": "1.2.3" + }, + devDependencies: { + "sap.ui.lib4": "4.5.6" + } + }); + + async function assert(libraryName, expectedMetadata) { + const pLibraryMetadata = resolver.getLibraryMetadata(libraryName); + const pLibraryMetadata2 = resolver.getLibraryMetadata(libraryName); + + const libraryMetadata = await pLibraryMetadata; + t.deepEqual(libraryMetadata, expectedMetadata, + libraryName + ": First call should resolve with expected metadata"); + const libraryMetadata2 = await pLibraryMetadata2; + t.deepEqual(libraryMetadata2, expectedMetadata, + libraryName + ": Second call should also resolve with expected metadata"); + + const libraryMetadata3 = await resolver.getLibraryMetadata(libraryName); + + t.deepEqual(libraryMetadata3, expectedMetadata, + libraryName + ": Third call should still return the same metadata"); + } + + await assert("sap.ui.lib1", { + id: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: [], + optionalDependencies: [] + }); + + await assert("sap.ui.lib2", { + id: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [ + "sap.ui.lib4" + ] + }); + + t.is(t.context.fetchPackageManifestStub.callCount, 2, "fetchPackageManifest should be called twice"); +}); + +test.serial("Openui5Resolver: handleLibrary", async (t) => { + const {Openui5Resolver} = t.context; + + const resolver = new Openui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const getLibraryMetadataStub = sinon.stub(resolver, "getLibraryMetadata"); + getLibraryMetadataStub + .callsFake(async (libraryName) => { + throw new Error("getLibraryMetadata stub called with unknown libraryName: " + libraryName); + }) + .withArgs("sap.ui.lib1").resolves({ + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"}); + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); +}); + +test.serial("Openui5Resolver: Static _getInstaller", (t) => { + const {Openui5Resolver} = t.context; + + const options = { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }; + + const installer = Openui5Resolver._getInstaller(options); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: path.resolve("/cwd"), + ui5DataDir: path.resolve("/ui5DataDir") + }], "Installer should be called with expected arguments"); +}); + +test.serial("Openui5Resolver: Static _getInstaller without options", (t) => { + const {Openui5Resolver} = t.context; + + const installer = Openui5Resolver._getInstaller(); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: process.cwd(), + ui5DataDir: path.join(os.homedir(), ".ui5") + }], "Installer should be called with expected arguments"); +}); + +test.serial("Openui5Resolver: Static fetchAllVersions", async (t) => { + const {Openui5Resolver} = t.context; + + const expectedVersions = ["1.75.0", "1.75.1", "1.76.0"]; + + t.context.fetchPackageVersionsStub.returns(expectedVersions); + + const getInstallerSpy = sinon.spy(Openui5Resolver, "_getInstaller"); + + const versions = await Openui5Resolver.fetchAllVersions(); + + t.deepEqual(versions, expectedVersions, "Fetched versions should be correct"); + + t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args, [{pkgName: "@openui5/sap.ui.core"}], + "fetchPackageVersions should be called with expected arguments"); + + t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once"); + t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options"); +}); + +test.serial("Openui5Resolver: Static fetchAllTags", async (t) => { + const {Openui5Resolver} = t.context; + + const expectedTags = ["latest", "latest-1.71", "latest-1"]; + + t.context.fetchPackageDistTags.returns(expectedTags); + + const getInstallerSpy = sinon.spy(Openui5Resolver, "_getInstaller"); + + const tags = await Openui5Resolver.fetchAllTags(); + + t.deepEqual(tags, expectedTags, "Fetched tags should be correct"); + + t.is(t.context.fetchPackageDistTags.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual(t.context.fetchPackageDistTags.getCall(0).args, [{pkgName: "@openui5/sap.ui.core"}], + "fetchPackageVersions should be called with expected arguments"); + + t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once"); + t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options"); +}); diff --git a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js new file mode 100644 index 00000000000..c82fc42ab9a --- /dev/null +++ b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js @@ -0,0 +1,157 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import path from "node:path"; + +const __dirname = import.meta.dirname; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL = "_SNAPSHOT_URL_"; + + t.context.logStub = { + info: sinon.stub(), + verbose: sinon.stub(), + silly: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + isLevelEnabled: sinon.stub().returns(false), + _getLogger: sinon.stub() + }; + const ui5Logger = { + getLogger: sinon.stub().returns(t.context.logStub) + }; + + t.context.makeFetchHappen = sinon.stub(); + + t.context.gracefulFs = { + stat: sinon.stub().yieldsAsync(), + readFile: sinon.stub().yieldsAsync(), + writeFile: sinon.stub().yieldsAsync(), + rename: sinon.stub().yieldsAsync(), + rm: sinon.stub().yieldsAsync(), + createWriteStream: sinon.stub() + }; + + t.context.Registry = await esmock.p("../../../lib/ui5Framework/maven/Registry.js", { + "@ui5/logger": ui5Logger, + "graceful-fs": t.context.gracefulFs, + "make-fetch-happen": t.context.makeFetchHappen, + }); + + const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", { + "@ui5/logger": ui5Logger, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "lockfile": { + lock: sinon.stub().yieldsAsync(), + unlock: sinon.stub().yieldsAsync() + } + }); + + t.context.Installer = await esmock.p("../../../lib/ui5Framework/maven/Installer.js", { + "@ui5/logger": ui5Logger, + "graceful-fs": t.context.gracefulFs, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "../../../lib/ui5Framework/maven/Registry.js": t.context.Registry, + "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller + }); + + t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + }, + }); + + t.context.Sapui5MavenSnapshotResolver = await esmock.p("../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir")) + }, + "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver, + "../../../lib/ui5Framework/maven/Installer.js": t.context.Installer + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Registry); + esmock.purge(t.context.Installer); + esmock.purge(t.context.AbstractResolver); + esmock.purge(t.context.Sapui5MavenSnapshotResolver); + delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; +}); + +test.serial("resolveVersion", async (t) => { + const {Sapui5MavenSnapshotResolver, makeFetchHappen, logStub, sinon} = t.context; + + makeFetchHappen.withArgs("_SNAPSHOT_URL_/com/sap/ui5/dist/sapui5-sdk-dist/maven-metadata.xml") + .resolves({ + ok: true, + buffer: sinon.stub().resolves(` + + + + + 1.120.1 + 2.0.0-rc.1 + 1.120.1-SNAPSHOT + 1.123.4-SNAPSHOT + 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT + 2.1.2-SNAPSHOT + + + + `) + }); + + + // Exact SNAPSHOT versions + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1.123.4-SNAPSHOT"), "1.123.4-SNAPSHOT"); + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("2.0.1-SNAPSHOT"), "2.0.1-SNAPSHOT"); + + // latest-snapshot + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("latest-snapshot"), "2.1.2-SNAPSHOT"); + + // SNAPSHOT ranges + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1-SNAPSHOT"), "1.123.4-SNAPSHOT"); + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("2-SNAPSHOT"), "2.1.2-SNAPSHOT"); + t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1.123-SNAPSHOT"), "1.123.4-SNAPSHOT"); + + // Error cases + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion(""), { + message: `Framework version specifier "" is incorrect or not supported` + }); + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("tag-does-not-exist"), { + message: `Framework version specifier "tag-does-not-exist" is incorrect or not supported` + }); + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("invalid-tag-%20"), { + message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported` + }); + + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("1.999.9"), { + message: `Could not resolve framework version 1.999.9. ` + + `Make sure the version is valid and available in the configured registry.` + }); + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("1.0.0-SNAPSHOT"), { + message: `Could not resolve framework version 1.0.0-SNAPSHOT. ` + + `Make sure the version is valid and available in the configured registry.` + }); + await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("3-SNAPSHOT"), { + message: `Could not resolve framework version 3-SNAPSHOT. ` + + `Make sure the version is valid and available in the configured registry.` + }); + + t.is(logStub.warn.callCount, 0); + t.is(logStub.error.callCount, 0); +}); diff --git a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js new file mode 100644 index 00000000000..ed9a4de6cd9 --- /dev/null +++ b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js @@ -0,0 +1,648 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import os from "node:os"; + +test.beforeEach(async (t) => { + t.context.InstallerStub = sinon.stub(); + t.context.fetchPackageVersionsStub = sinon.stub(); + t.context.installPackageStub = sinon.stub(); + t.context.readJsonStub = sinon.stub(); + t.context.InstallerStub.callsFake(() => { + return { + fetchPackageVersions: t.context.fetchPackageVersionsStub, + installPackage: t.context.installPackageStub, + readJson: t.context.readJsonStub + }; + }); + + process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL = "_SNAPSHOT_URL_"; + + t.context.yesnoStub = sinon.stub(); + t.context.promisifyStub = sinon.stub(); + t.context.loggerVerbose = sinon.stub(); + t.context.loggerWarn = sinon.stub(); + t.context.loggerInfo = sinon.stub(); + + t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", {}); + t.context.configFromFile = sinon.stub(t.context.Configuration, "fromFile") + .resolves(new t.context.Configuration({})); + t.context.configToFile = sinon.stub(t.context.Configuration, "toFile").resolves(); + + t.context.Sapui5MavenSnapshotResolver = await esmock.p("../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js", { + "../../../lib/ui5Framework/maven/Installer": t.context.InstallerStub, + "yesno": t.context.yesnoStub, + "node:util": { + "promisify": t.context.promisifyStub + }, + "@ui5/logger": { + getLogger: () => ({ + verbose: t.context.loggerVerbose, + warning: t.context.loggerWarn, + info: t.context.loggerInfo, + }) + }, + "../../../lib/config/Configuration": t.context.Configuration + }); + + t.context.originalIsTty = process.stdout.isTTY; +}); + +test.afterEach.always((t) => { + process.stdout.isTTY = t.context.originalIsTty; + delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; + esmock.purge(t.context.Sapui5MavenSnapshotResolver); + sinon.restore(); +}); + +test.serial( + "Sapui5MavenSnapshotResolver: loadDistMetadata loads metadata "+ + "once from @sapui5/distribution-metadata package", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const expectedMetadata = { + libraries: { + "sap.ui.foo": { + "npmPackageName": "@openui5/sap.ui.foo", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + } + } + }; + + t.context.installPackageStub + .withArgs({ + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "zip", + }) + .resolves({pkgPath: "/path/to/distribution-metadata/1.75.0"}); + + t.context.readJsonStub + .withArgs(path.join("/path", "to", "distribution-metadata", "1.75.0", "metadata.json")) + .resolves(expectedMetadata); + + let distMetadata = await resolver.loadDistMetadata(); + t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "loadDistMetadata should resolve with expected metadata"); + + // Calling loadDistMetadata again should not load package again + distMetadata = await resolver.loadDistMetadata(); + + t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should still be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "Metadata should still be the expected metadata after calling loadDistMetadata again"); + + const libraryMetadata = await resolver.getLibraryMetadata("sap.ui.foo"); + t.deepEqual(libraryMetadata, expectedMetadata.libraries["sap.ui.foo"], + "getLibraryMetadata returns metadata for one library"); + }); + +test.serial("Sapui5MavenSnapshotResolver: getLibraryMetadata throws", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: {} + }); + + await t.throwsAsync(resolver.getLibraryMetadata("sap.ui.foo"), { + message: "Could not find library \"sap.ui.foo\"", + }); +}); + +test.serial("Sapui5MavenSnapshotResolver: handleLibrary", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.116.0-SNAPSHOT" + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.116.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [], + "gav": "x:y:z" + } + } + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({ + pkgName: "@openui5/sap.ui.lib1-prebuilt", + groupId: "x", + artifactId: "y", + version: "1.116.0-SNAPSHOT", + classifier: "npm-dist", + extension: "zip", + }) + .resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1-prebuilt", + "version": "1.116.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); + + +test.serial("Sapui5MavenSnapshotResolver: handleLibrary - legacy version", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.75.0-SNAPSHOT" + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [], + "gav": "x:y:z" + } + } + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({ + pkgName: "@openui5/sap.ui.lib1-prebuilt", + groupId: "x", + artifactId: "y", + version: "1.75.0-SNAPSHOT", + classifier: null, + extension: "jar", + }) + .resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1-prebuilt", + "version": "1.75.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); + +test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.116.0-SNAPSHOT", + sources: true + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.116.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [], + "gav": "x:y:z" + } + } + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({ + pkgName: "@openui5/sap.ui.lib1", + groupId: "x", + artifactId: "y", + version: "1.116.0-SNAPSHOT", + classifier: "npm-sources", + extension: "zip", + }) + .resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.116.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); + +test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested with legacy version", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.75.0-SNAPSHOT", + sources: true + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [], + "gav": "x:y:z" + } + } + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({ + pkgName: "@openui5/sap.ui.lib1", + groupId: "x", + artifactId: "y", + version: "1.75.0-SNAPSHOT", + classifier: "npm-sources", + extension: "zip", + }) + .resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0-SNAPSHOT", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); + +test.serial("Sapui5MavenSnapshotResolver: handleLibrary throws", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const resolver = new Sapui5MavenSnapshotResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + sinon.stub(resolver, "getLibraryMetadata").resolves({}); + + await t.throwsAsync(resolver.handleLibrary("sap.ui.lib1"), { + message: + "Metadata is missing GAV (group, artifact and version) information. "+ + "This might indicate an unsupported SNAPSHOT version.", + }); +}); + +test.serial("Sapui5MavenSnapshotResolver: Static fetchAllVersions", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const expectedVersions = ["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT"]; + const options = { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }; + + t.context.fetchPackageVersionsStub.returns(expectedVersions); + sinon.stub(Sapui5MavenSnapshotResolver, "_createSnapshotEndpointUrlCallback") + .returns("snapshotEndpointUrlCallback"); + + const versions = await Sapui5MavenSnapshotResolver.fetchAllVersions(options); + + t.deepEqual(versions, expectedVersions, "Fetched versions should be correct"); + + t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual( + t.context.fetchPackageVersionsStub.getCall(0).args, + [{artifactId: "sapui5-sdk-dist", groupId: "com.sap.ui5.dist"}], + "fetchPackageVersions should be called with expected arguments" + ); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: path.resolve("/cwd"), + snapshotEndpointUrlCb: "snapshotEndpointUrlCallback", + ui5DataDir: path.resolve("/ui5DataDir") + }], "Installer should be called with expected arguments"); +}); + +test.serial("Sapui5MavenSnapshotResolver: Static fetchAllVersions without options", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + + const expectedVersions = ["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT"]; + + t.context.fetchPackageVersionsStub.returns(expectedVersions); + sinon.stub(Sapui5MavenSnapshotResolver, "_createSnapshotEndpointUrlCallback") + .returns("snapshotEndpointUrlCallback"); + + const versions = await Sapui5MavenSnapshotResolver.fetchAllVersions(); + + t.deepEqual(versions, expectedVersions, "Fetched versions should be correct"); + + t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args, + [{artifactId: "sapui5-sdk-dist", groupId: "com.sap.ui5.dist"}], + "fetchPackageVersions should be called with expected arguments"); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: process.cwd(), + snapshotEndpointUrlCb: "snapshotEndpointUrlCallback", + ui5DataDir: path.join(os.homedir(), ".ui5") + }], "Installer should be called with expected arguments"); +}); + +test.serial("_createSnapshotEndpointUrlCallback: Environment variable", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback; + + const endpointCallback = await createSnapshotEndpointUrlCallback("my url"); + + t.is(await endpointCallback(), "_SNAPSHOT_URL_", + "Returned a callback resolving to value of env variable"); +}); + +test.serial("_createSnapshotEndpointUrlCallback: Parameter", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback; + + delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; // Delete env variable for this test + const endpointCallback = await createSnapshotEndpointUrlCallback("my url"); + + t.is(await endpointCallback(), "my url", + "Returned a callback resolving to value of env variable"); +}); + +test.serial("_createSnapshotEndpointUrlCallback: Fallback to configuration files", async (t) => { + const {Sapui5MavenSnapshotResolver} = t.context; + const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback; + + delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; // Delete env variable for this test + const resolveUrlStub = sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrl").resolves("🐱"); + + const endpointCallback = await createSnapshotEndpointUrlCallback(); + + t.is(endpointCallback, resolveUrlStub, "Returned correct callback"); + t.is(await endpointCallback(), "🐱", "Callback can be executed correctly"); +}); + +test.serial("_resolveSnapshotEndpointUrl: From configuration", async (t) => { + const {configFromFile, configToFile, Configuration, Sapui5MavenSnapshotResolver} = t.context; + const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl; + + configFromFile.resolves(new Configuration({mavenSnapshotEndpointUrl: "config-url"})); + const fromMavenStub = sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves(); + + const endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, "config-url", "Returned URL extracted from UI5 CLI configuration"); + t.is(configFromFile.callCount, 1, "Configuration has been read once"); + t.is(configToFile.callCount, 0, "Configuration has not been written"); + t.is(fromMavenStub.callCount, 0, "Maven configuration has not been requested"); +}); + +test.serial("_resolveSnapshotEndpointUrl: Maven fallback with config update", async (t) => { + const {configFromFile, configToFile, Sapui5MavenSnapshotResolver} = t.context; + const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl; + + sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves("maven-url"); + + const endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, "maven-url", "Returned URL extracted from Maven settings.xml"); + t.is(configFromFile.callCount, 1, "Configuration has been read once"); + t.is(configToFile.callCount, 1, "Configuration has been written once"); + t.deepEqual(configToFile.firstCall.firstArg.toJson(), { + mavenSnapshotEndpointUrl: "maven-url", + ui5DataDir: undefined + }, "Correct configuration has been written"); +}); + +test.serial("_resolveSnapshotEndpointUrl: Maven fallback without config update", async (t) => { + const {configFromFile, configToFile, Sapui5MavenSnapshotResolver} = t.context; + const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl; + + // Resolving with null + sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves(null); + + const endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, null, "No URL resolved"); + t.is(configFromFile.callCount, 1, "Configuration has been read once"); + t.is(configToFile.callCount, 0, "Configuration has not been written"); +}); + +test.serial("_resolveSnapshotEndpointUrlFromMaven", async (t) => { + const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven; + const {promisifyStub, yesnoStub} = t.context; + + process.stdout.isTTY = true; + + const readStub = sinon.stub().resolves(` + + + snapshot.build + + + artifactory + /build-snapshots/ + + + + + `); + promisifyStub.callsFake(() => readStub); + yesnoStub.resolves(true); + + const endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, "/build-snapshots/", "URL Extracted from settings.xml"); +}); + +test.serial("_resolveSnapshotEndpointUrlFromMaven: No snapshot.build attribute", async (t) => { + const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven; + const {promisifyStub, yesnoStub} = t.context; + + process.stdout.isTTY = true; + + const readStub = sinon.stub().resolves(` + + + deploy.build + + + artifactory + /build-snapshots/ + + + + + `); + promisifyStub.callsFake(() => readStub); + yesnoStub.resolves(true); + + const endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, null, "No URL Extracted from settings.xml"); +}); + +test.serial("_resolveSnapshotEndpointUrlFromMaven fails", async (t) => { + const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven; + const {promisifyStub, yesnoStub, loggerVerbose, loggerWarn} = t.context; + + process.stdout.isTTY = true; + + const readStub = sinon.stub() + .onFirstCall().throws({code: "ENOENT"}) + .onSecondCall().throws(new Error("Error")) + .resolves(` + + + snapshot.build + + + artifactory + /build-snapshots/ + + + + + `); + promisifyStub.callsFake(() => readStub); + + let endpoint; + endpoint = await resolveSnapshotEndpointUrl(".m2/settings.xml"); + t.is(endpoint, null, "No endpoint resolved"); + t.is( + loggerVerbose.getCall(0).args[0], + "Attempting to resolve snapshot endpoint URL from Maven configuration file at .m2/settings.xml..." + ); + t.is( + loggerVerbose.getCall(1).args[0], + `File does not exist: .m2/settings.xml` + ); + + loggerVerbose.reset(); + loggerWarn.reset(); + endpoint = await resolveSnapshotEndpointUrl("settings.xml"); + t.is(endpoint, null, "No endpoint resolved"); + t.is( + loggerVerbose.getCall(0).args[0], + "Attempting to resolve snapshot endpoint URL from Maven configuration file at settings.xml..." + ); + t.is( + loggerWarn.getCall(0).args[0], + "Failed to read Maven configuration file from settings.xml: Error" + ); + + loggerVerbose.reset(); + loggerWarn.reset(); + yesnoStub.resolves(false); + endpoint = await resolveSnapshotEndpointUrl(); + + t.is(endpoint, null, "URL is not extracted after user rejection"); + t.is( + loggerVerbose.getCall(1).args[0], + "User rejected usage of the resolved URL" + ); +}); + +test.serial("_resolveSnapshotEndpointUrlFromMaven no TTY", async (t) => { + const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven; + const {promisifyStub, yesnoStub} = t.context; + + process.stdout.isTTY = false; + + const readStub = sinon.stub().resolves(` + + + snapshot.build + + + artifactory + /build-snapshots/ + + + + + `); + promisifyStub.callsFake(() => readStub); + yesnoStub.resolves(true); + + const endpoint = await resolveSnapshotEndpointUrl(".m2/settings.xml"); + + t.is(readStub.callCount, 0, "read did not get called"); + t.is(yesnoStub.callCount, 0, "yesno did not get called"); + t.is(endpoint, null, "No URL got extracted"); +}); diff --git a/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js b/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js new file mode 100644 index 00000000000..3b04805ad42 --- /dev/null +++ b/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js @@ -0,0 +1,200 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; +import path from "node:path"; + +const __dirname = import.meta.dirname; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.logStub = { + info: sinon.stub(), + verbose: sinon.stub(), + silly: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + isLevelEnabled: sinon.stub().returns(false), + _getLogger: sinon.stub() + }; + const ui5Logger = { + getLogger: sinon.stub().returns(t.context.logStub) + }; + + t.context.pacote = { + packument: sinon.stub().callsFake(async (...args) => { + throw new Error(`pacote.packument stub called with unknown args: ${args}`); + }) + }; + + t.context.NpmcliConfig = sinon.stub().returns({ + load: sinon.stub().resolves(), + flat: { + registry: "https://registry.fake" + } + }); + + t.context.Registry = await esmock.p("../../../lib/ui5Framework/npm/Registry.js", { + "@ui5/logger": ui5Logger, + "pacote": t.context.pacote, + "@npmcli/config": { + "default": t.context.NpmcliConfig + } + }); + + const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", { + "@ui5/logger": ui5Logger, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "lockfile": { + lock: sinon.stub().yieldsAsync(), + unlock: sinon.stub().yieldsAsync() + } + }); + + t.context.Installer = await esmock.p("../../../lib/ui5Framework/npm/Installer.js", { + "@ui5/logger": ui5Logger, + "graceful-fs": { + rename: sinon.stub().yieldsAsync(), + }, + "../../../lib/utils/fs.js": { + mkdirp: sinon.stub().resolves() + }, + "../../../lib/ui5Framework/npm/Registry.js": t.context.Registry, + "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller + }); + + t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir")) + }, + }); + + t.context.Sapui5Resolver = await esmock.p("../../../lib/ui5Framework/Sapui5Resolver.js", { + "@ui5/logger": ui5Logger, + "node:os": { + homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir")) + }, + "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver, + "../../../lib/ui5Framework/npm/Installer.js": t.context.Installer + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Registry); + esmock.purge(t.context.Installer); + esmock.purge(t.context.AbstractResolver); + esmock.purge(t.context.Sapui5Resolver); +}); + +test.serial("resolveVersion", async (t) => { + const {Sapui5Resolver, pacote, logStub, NpmcliConfig} = t.context; + + pacote.packument + .withArgs("@sapui5/distribution-metadata") + .resolves({ + "versions": { + "1.120.1": "", + "1.120.0": "", + "1.119.0": "", + "1.118.0": "", + "2.0.0-rc.1": "", + "1.123.4-SNAPSHOT": "" + }, + "dist-tags": { + // NOTE: latest does not correspond to highest version in order to verify + // that this tag is used instead of picking the highest version + "latest": "1.120.0", + + "next": "2.0.0-rc.1", + + // NOTE: Tag ends with "-snapshot" in order to verify that the special handling + // of that + "not-a-snapshot": "1.118.0" + } + }); + + const defaultCwd = process.cwd(); + const defaultUi5DataDir = path.join(fakeBaseDir, "datadir", ".ui5"); + + // Generic testing without and with options argument + const optionsArguments = [ + undefined, + { + cwd: path.join(fakeBaseDir, "custom-cwd"), + ui5DataDir: path.join(fakeBaseDir, "custom-datadir", ".ui5") + } + ]; + for (const options of optionsArguments) { + // Reset calls to be able to check them per for-loop run + NpmcliConfig.resetHistory(); + pacote.packument.resetHistory(); + + // Ranges + t.is(await Sapui5Resolver.resolveVersion("1", options), "1.120.1"); + t.is(await Sapui5Resolver.resolveVersion("1.120", options), "1.120.1"); + t.is(await Sapui5Resolver.resolveVersion("1.x", options), "1.120.1"); + t.is(await Sapui5Resolver.resolveVersion("1.x.x", options), "1.120.1"); + t.is(await Sapui5Resolver.resolveVersion("^1", options), "1.120.1"); + t.is(await Sapui5Resolver.resolveVersion("*", options), "1.120.1"); + + // Tags + t.is(await Sapui5Resolver.resolveVersion("latest", options), "1.120.0"); + t.is(await Sapui5Resolver.resolveVersion("next", options), "2.0.0-rc.1"); + t.is(await Sapui5Resolver.resolveVersion("not-a-snapshot", options), "1.118.0"); + + // Exact versions + t.is(await Sapui5Resolver.resolveVersion("1.118.0", options), "1.118.0"); + t.is(await Sapui5Resolver.resolveVersion("2.0.0-rc.1", options), "2.0.0-rc.1"); + t.is(await Sapui5Resolver.resolveVersion("1.123.4-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + + // SNAPSHOT ranges + t.is(await Sapui5Resolver.resolveVersion("1-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + t.is(await Sapui5Resolver.resolveVersion("1.123-SNAPSHOT", options), "1.123.4-SNAPSHOT"); + + // Error cases + await t.throwsAsync(Sapui5Resolver.resolveVersion("", options), { + message: `Framework version specifier "" is incorrect or not supported` + }); + await t.throwsAsync(Sapui5Resolver.resolveVersion("tag-does-not-exist", options), { + message: `Could not resolve framework version via tag 'tag-does-not-exist'. ` + + `Make sure the tag is available in the configured registry.` + }); + await t.throwsAsync(Sapui5Resolver.resolveVersion("invalid-tag-%20", options), { + message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported` + }); + + await t.throwsAsync(Sapui5Resolver.resolveVersion("1.999.9", options), { + message: `Could not resolve framework version 1.999.9. ` + + `Make sure the version is valid and available in the configured registry.` + }); + await t.throwsAsync(Sapui5Resolver.resolveVersion("1.0.0", options), { + message: `Could not resolve framework version 1.0.0. ` + + `Note that SAPUI5 framework libraries can only be consumed by the UI5 CLI ` + + `starting with SAPUI5 v1.76.0` + }); + await t.throwsAsync(Sapui5Resolver.resolveVersion("^999", options), { + message: `Could not resolve framework version ^999. ` + + `Make sure the version is valid and available in the configured registry.` + }); + + // Check whether options have been passed as expected + t.true(NpmcliConfig.alwaysCalledWithNew()); + t.true(NpmcliConfig.alwaysCalledWithMatch(sinonGlobal.match({ + cwd: options?.cwd ?? defaultCwd + }))); + t.true(pacote.packument.alwaysCalledWithMatch("@sapui5/distribution-metadata", { + cache: path.join(options?.ui5DataDir ?? defaultUi5DataDir, "framework", "cacache") + })); + } + + t.is(logStub.warn.callCount, 0); + t.is(logStub.error.callCount, 0); +}); diff --git a/packages/project/test/lib/ui5framework/Sapui5Resolver.js b/packages/project/test/lib/ui5framework/Sapui5Resolver.js new file mode 100644 index 00000000000..63288d356ed --- /dev/null +++ b/packages/project/test/lib/ui5framework/Sapui5Resolver.js @@ -0,0 +1,257 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import os from "node:os"; +import Openui5Resolver from "../../../lib/ui5Framework/Openui5Resolver.js"; + +test.beforeEach(async (t) => { + t.context.InstallerStub = sinon.stub(); + t.context.fetchPackageDistTags = sinon.stub(); + t.context.fetchPackageVersionsStub = sinon.stub(); + t.context.installPackageStub = sinon.stub(); + t.context.getTargetDirForPackageStub = sinon.stub(); + t.context.readJsonStub = sinon.stub(); + t.context.InstallerStub.callsFake(() => { + return { + fetchPackageDistTags: t.context.fetchPackageDistTags, + fetchPackageVersions: t.context.fetchPackageVersionsStub, + installPackage: t.context.installPackageStub, + getTargetDirForPackage: t.context.getTargetDirForPackageStub, + readJson: t.context.readJsonStub + }; + }); + + t.context.Sapui5Resolver = await esmock("../../../lib/ui5Framework/Sapui5Resolver.js", { + "../../../lib/ui5Framework/npm/Installer": t.context.InstallerStub + }); +}); + +test.afterEach.always(() => { + sinon.restore(); +}); + +test.serial( + "Sapui5Resolver: loadDistMetadata loads metadata once from @sapui5/distribution-metadata package", async (t) => { + const {Sapui5Resolver} = t.context; + + const resolver = new Sapui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + t.context.getTargetDirForPackageStub.callsFake(({pkgName, version}) => { + throw new Error( + `getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`); + }).withArgs({ + pkgName: "@sapui5/distribution-metadata", + version: "1.75.0" + }).returns(path.join("/path", "to", "distribution-metadata", "1.75.0")); + t.context.installPackageStub.withArgs({ + pkgName: "@sapui5/distribution-metadata", + version: "1.75.0" + }).resolves({pkgPath: path.join("/path", "to", "distribution-metadata", "1.75.0")}); + + const expectedMetadata = { + libraries: { + "sap.ui.foo": { + "npmPackageName": "@openui5/sap.ui.foo", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + } + } + }; + t.context.readJsonStub + .withArgs(path.join("/path", "to", "distribution-metadata", "1.75.0", "metadata.json")) + .resolves(expectedMetadata); + + let distMetadata = await resolver.loadDistMetadata(); + t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "loadDistMetadata should resolve with expected metadata"); + + // Calling loadDistMetadata again should not load package again + distMetadata = await resolver.loadDistMetadata(); + + t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should still be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "Metadata should still be the expected metadata after calling loadDistMetadata again"); + + const libraryMetadata = await resolver.getLibraryMetadata("sap.ui.foo"); + t.deepEqual(libraryMetadata, expectedMetadata.libraries["sap.ui.foo"], + "getLibraryMetadata returns metadata for one library"); + }); + +test.serial("Sapui5Resolver: handleLibrary", async (t) => { + const {Sapui5Resolver} = t.context; + + const resolver = new Sapui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + } + } + }); + + t.context.installPackageStub + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); + +test.serial("Sapui5Resolver: Static _getInstaller", (t) => { + const {Sapui5Resolver} = t.context; + + const options = { + cwd: "/cwd", + ui5DataDir: "/ui5DataDir" + }; + + const installer = Sapui5Resolver._getInstaller(options); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: path.resolve("/cwd"), + ui5DataDir: path.resolve("/ui5DataDir") + }], "Installer should be called with expected arguments"); +}); + +test.serial("Sapui5Resolver: Static _getInstaller without options", (t) => { + const {Sapui5Resolver} = t.context; + + const installer = Sapui5Resolver._getInstaller(); + + t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once"); + t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new"); + t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned"); + t.deepEqual(t.context.InstallerStub.getCall(0).args, [{ + cwd: process.cwd(), + ui5DataDir: path.join(os.homedir(), ".ui5") + }], "Installer should be called with expected arguments"); +}); + +test.serial("Sapui5Resolver: Static fetchAllVersions", async (t) => { + const {Sapui5Resolver} = t.context; + + const expectedVersions = ["1.75.0", "1.75.1", "1.76.0"]; + + t.context.fetchPackageVersionsStub.returns(expectedVersions); + + const getInstallerSpy = sinon.spy(Sapui5Resolver, "_getInstaller"); + + const versions = await Sapui5Resolver.fetchAllVersions(); + + t.deepEqual(versions, expectedVersions, "Fetched versions should be correct"); + + t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args, [{pkgName: "@sapui5/distribution-metadata"}], + "fetchPackageVersions should be called with expected arguments"); + t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once"); + t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options"); +}); + +test.serial("Sapui5Resolver: Static fetchAllTags", async (t) => { + const {Sapui5Resolver} = t.context; + + const expectedTags = ["latest", "latest-1.71", "latest-1"]; + + t.context.fetchPackageDistTags.returns(expectedTags); + + const getInstallerSpy = sinon.spy(Sapui5Resolver, "_getInstaller"); + + const tags = await Sapui5Resolver.fetchAllTags(); + + t.deepEqual(tags, expectedTags, "Fetched tags should be correct"); + + t.is(t.context.fetchPackageDistTags.callCount, 1, "fetchPackageVersions should be called once"); + t.deepEqual(t.context.fetchPackageDistTags.getCall(0).args, [{pkgName: "@sapui5/distribution-metadata"}], + "fetchPackageVersions should be called with expected arguments"); + + t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once"); + t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options"); +}); + +test.serial( + "Sapui5Resolver: getLibraryMetadata should use Openui5Resolver for @openui5/ modules in 1.77.x", async (t) => { + const {Sapui5Resolver} = t.context; + + const resolver = new Sapui5Resolver({ + cwd: "/test-project/", + version: "1.77.7" + }); + + const openui5LibraryMetadata = { + "id": "@openui5/sap.ui.lib3", + "version": "1.77.4", + "dependencies": [ + "@openui5/sap.ui.lib1" + ], + "optionalDependencies": [ + "@openui5/sap.ui.lib2" + ] + }; + const expectedMetadata = { + "npmPackageName": "@openui5/sap.ui.lib3", + "version": "1.77.4", + "dependencies": [ + "@openui5/sap.ui.lib1" + ], + "optionalDependencies": [ + "@openui5/sap.ui.lib2" + ] + }; + + const openui5GetLibraryMetadataStub = sinon.stub(Openui5Resolver.prototype, "getLibraryMetadata"); + openui5GetLibraryMetadataStub.resolves(openui5LibraryMetadata); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.77.4", + "dependencies": [], + "optionalDependencies": [] + } + } + }); + + const metadata = await resolver.getLibraryMetadata("sap.ui.lib1"); + t.deepEqual(metadata, expectedMetadata, "Metadata should be equal to expected OpenUI5 metadata"); + + t.is(openui5GetLibraryMetadataStub.callCount, 1, "Openui5Resolver#getLibraryMetadata should be called once"); + t.deepEqual(openui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"], + "Openui5Resolver#getLibraryMetadata should be called with library name"); + t.is(openui5GetLibraryMetadataStub.getCall(0).thisValue._version, "1.77.4", + "Openui5Resolver should be created with @openui5 library version"); + }); diff --git a/packages/project/test/lib/ui5framework/maven/Installer.js b/packages/project/test/lib/ui5framework/maven/Installer.js new file mode 100644 index 00000000000..86b00754cdb --- /dev/null +++ b/packages/project/test/lib/ui5framework/maven/Installer.js @@ -0,0 +1,1084 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; +import fs from "graceful-fs"; + +test.beforeEach(async (t) => { + t.context.mkdirpStub = sinon.stub().resolves(); + t.context.rmrfStub = sinon.stub().resolves(); + t.context.readFileStub = sinon.stub(); + t.context.writeFileStub = sinon.stub(); + t.context.renameStub = sinon.stub().returns(); + t.context.rmStub = sinon.stub().returns(); + t.context.statStub = sinon.stub().returns(); + + t.context.promisifyStub = sinon.stub(); + t.context.promisifyStub.withArgs(fs.readFile).callsFake(() => t.context.readFileStub); + t.context.promisifyStub.withArgs(fs.writeFile).callsFake(() => t.context.writeFileStub); + t.context.promisifyStub.withArgs(fs.rename).callsFake(() => t.context.renameStub); + t.context.promisifyStub.withArgs(fs.rm).callsFake(() => t.context.rmStub); + t.context.promisifyStub.withArgs(fs.stat).callsFake(() => t.context.statStub); + + t.context.lockStub = sinon.stub(); + t.context.unlockStub = sinon.stub(); + t.context.zipStub = class StreamZipStub { + extract = sinon.stub().resolves(); + close = sinon.stub().resolves(); + }; + + t.context.registryRequestMavenMetadataStub = sinon.stub().resolves(); + t.context.registryRequestArtifactStub = sinon.stub().resolves(); + + t.context.RegistryConstructorStub = sinon.stub().returns({ + requestMavenMetadata: t.context.registryRequestMavenMetadataStub, + requestArtifact: t.context.registryRequestArtifactStub + }); + + t.context.AbstractInstaller = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", { + "../../../../lib/utils/fs.js": { + mkdirp: t.context.mkdirpStub, + rmrf: t.context.rmrfStub + }, + "lockfile": { + lock: t.context.lockStub, + unlock: t.context.unlockStub + } + }); + + t.context.Installer = await esmock.p("../../../../lib/ui5Framework/maven/Installer.js", { + "../../../../lib/ui5Framework/maven/Registry.js": t.context.RegistryConstructorStub, + "../../../../lib/ui5Framework/AbstractInstaller.js": t.context.AbstractInstaller, + "../../../../lib/utils/fs.js": { + mkdirp: t.context.mkdirpStub + }, + "node:util": { + "promisify": t.context.promisifyStub, + }, + "node-stream-zip": { + "async": t.context.zipStub + } + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); + esmock.purge(t.context.AbstractInstaller); + esmock.purge(t.context.Installer); +}); + +test.serial("constructor", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + t.true(installer instanceof Installer, "Constructor returns instance of class"); + t.is(installer._artifactsDir, path.join("/ui5Data/", "framework", "artifacts")); + t.is(installer._packagesDir, path.join("/ui5Data/", "framework", "packages")); + t.is(installer._stagingDir, path.join("/ui5Data/", "framework", "staging")); + t.is(installer._metadataDir, path.join("/ui5Data/", "framework", "metadata")); + t.is(installer._lockDir, path.join("/ui5Data/", "framework", "locks")); +}); + +test.serial("constructor requires 'ui5DataDir'", (t) => { + const {Installer} = t.context; + + t.throws(() => { + new Installer({ + cwd: "/cwd/" + }); + }, {message: `Installer: Missing parameter "ui5DataDir"`}); +}); + +test.serial("constructor requires 'snapshotEndpointUrlCb'", (t) => { + const {Installer} = t.context; + + t.throws(() => { + new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data" + }); + }, {message: `Installer: Missing Snapshot-Endpoint URL callback parameter`}); +}); + +test.serial("getRegistry", async (t) => { + const {Installer, RegistryConstructorStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + const registry1 = await installer.getRegistry(); + + t.truthy(registry1, "Created registry"); + t.is(RegistryConstructorStub.callCount, 1, "Registry constructor called once"); + t.deepEqual(RegistryConstructorStub.firstCall.firstArg, { + endpointUrl: "endpoint-url" + }, "Registry constructor called with correct endpoint URL"); + + const registry2 = await installer.getRegistry(); + t.is(registry2, registry1, "Registry instance is cached"); + t.is(RegistryConstructorStub.callCount, 1, "Registry constructor still only called once"); +}); + +test.serial("getRegistry: Missing endpoint URL", async (t) => { + const {Installer, RegistryConstructorStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve(null) + }); + + const err = await t.throwsAsync(installer.getRegistry()); + t.is(err.message, "Installer: Missing or empty Maven repository URL for snapshot consumption. " + + "This URL is required for consuming snapshot versions of UI5 libraries. " + + "Please configure the correct URL using the following command: " + + "'ui5 config set mavenSnapshotEndpointUrl '", + "Threw with expected error message"); + + t.is(RegistryConstructorStub.callCount, 0, "Registry constructor did not get called"); +}); + +test.serial("fetchPackageVersions", async (t) => { + const {Installer, registryRequestMavenMetadataStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + registryRequestMavenMetadataStub + .resolves({ + versioning: { + versions: { + version: ["1.0.0", "2.0.0", "2.0.0-SNAPSHOT", "3.0.0", "5.0.0-SNAPSHOT"] + } + } + }); + + const packageVersions = await installer.fetchPackageVersions({groupId: "ui5.corp", artifactId: "great-thing"}); + + t.deepEqual(packageVersions, ["2.0.0-SNAPSHOT", "5.0.0-SNAPSHOT"], "Should resolve with expected versions"); + + t.is(registryRequestMavenMetadataStub.callCount, 1, "requestPackagePackument should be called once"); + t.deepEqual(registryRequestMavenMetadataStub.getCall(0).args[0], {groupId: "ui5.corp", artifactId: "great-thing"}, + "requestMavenMetadata was called with correct arguments"); +}); + +test.serial("fetchPackageVersions throws", async (t) => { + const {Installer, registryRequestMavenMetadataStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + registryRequestMavenMetadataStub.resolves({}); + + await t.throwsAsync( + installer.fetchPackageVersions({ + groupId: "ui5.corp", + artifactId: "great-thing", + }), + {message: "Missing Maven metadata for artifact ui5.corp:great-thing"} + ); +}); + +test.serial("_getLockPath", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const lockPath = installer._getLockPath("package-@openui5/sap.ui.lib1@1.2.3-SNAPSHOT"); + + t.is(lockPath, path.join("/ui5Data/", "framework", "locks", "package-@openui5-sap.ui.lib1@1.2.3-SNAPSHOT.lock")); +}); + +test.serial("readJson", async (t) => { + const jsonStub = {json: "response"}; + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + t.context.readFileStub.resolves(JSON.stringify(jsonStub)); + + const jsonResponse = await installer.readJson("package-@openui5/sap.ui.lib1@1.2.3-SNAPSHOT"); + + t.deepEqual(jsonResponse, jsonStub); +}); + +test.serial("installPackage", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const removeArtifactStub = sinon.stub().resolves(); + const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists").resolves(false); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + const installArtifactStub = sinon.stub(installer, "installArtifact").resolves({ + artifactPath: "/ui5Data/framework/artifacts/com_sap_ui5_dist-sapui5-sdk-dist/5/npm-sources.zip", + removeArtifact: removeArtifactStub + }); + + const installedPackage = await installer.installPackage({ + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "zip", + }); + + t.deepEqual( + installedPackage, + {pkgPath: + path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")}, + "Install the correct package" + ); + + t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once"); + t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, { + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "zip", + }, "fetchArtifactMetadataStub got called with expected arguments"); + + t.is(installArtifactStub.callCount, 1, "installArtifact got called once"); + t.deepEqual(installArtifactStub.firstCall.firstArg, { + revision: "5", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "zip", + }, "installArtifact got called with the expected parameters"); + t.is(removeArtifactStub.callCount, 1, "removeArtifact got called once"); +}); + +test.serial("installPackage: No classifier", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const removeArtifactStub = sinon.stub().resolves(); + const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists").resolves(false); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + const installArtifactStub = sinon.stub(installer, "installArtifact").resolves({ + artifactPath: "/ui5Data/framework/artifacts/com_sap_ui5_dist-sapui5-sdk-dist/5/npm-sources.zip", + removeArtifact: removeArtifactStub + }); + + const installedPackage = await installer.installPackage({ + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: null, + extension: "jar", + }); + + t.deepEqual( + installedPackage, + {pkgPath: + path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")}, + "Install the correct package" + ); + + t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once"); + t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, { + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: null, + extension: "jar", + }, "fetchArtifactMetadataStub got called with expected arguments"); + + t.is(installArtifactStub.callCount, 1, "installArtifact got called once"); + t.deepEqual(installArtifactStub.firstCall.firstArg, { + revision: "5", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: null, + extension: "jar", + }, "installArtifact got called with the expected parameters"); + t.is(removeArtifactStub.callCount, 1, "removeArtifact got called once"); +}); + +test.serial("installPackage: Already installed", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_projectExists").resolves(true); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + const installArtifactStub = sinon.stub(installer, "installArtifact"); + + const installedPackage = await installer.installPackage({ + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "jar", + }); + + t.deepEqual( + installedPackage, + {pkgPath: + path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")}, + "Install the correct package" + ); + + t.is(installArtifactStub.callCount, 0, "installArtifact did not get called"); +}); + +test.serial("installPackage: Already installed only after lock acquired", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_projectExists") + .onFirstCall().resolves(false) + .onSecondCall().resolves(true); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + const installArtifactStub = sinon.stub(installer, "installArtifact"); + + const installedPackage = await installer.installPackage({ + pkgName: "@sapui5/distribution-metadata", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: "npm-sources", + extension: "jar", + }); + + t.deepEqual( + installedPackage, + {pkgPath: + path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")}, + "Install the correct package" + ); + + t.is(installArtifactStub.callCount, 0, "installArtifact did not get called"); +}); + +test.serial("installArtifact", async (t) => { + const {Installer, rmStub, registryRequestArtifactStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: async () => "url" + }); + + const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists").resolves(false); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + + const installedArtifact = await installer.installArtifact({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + classifier: null + }); + + const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar"); + t.is( + installedArtifact.artifactPath, + expectedPath, + "artifactPath correctly resolved" + ); + + t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once"); + t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, { + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + classifier: null, + extension: "jar", + }, "fetchArtifactMetadataStub got called with expected arguments"); + + t.is(registryRequestArtifactStub.callCount, 1, "Registry#requestArtifact got called once"); + t.deepEqual(registryRequestArtifactStub.firstCall.firstArg, { + revision: "5", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + classifier: null, + version: "1.75.0", + extension: "jar", + }, "Registry#requestArtifact got called with expected coordinates"); + t.is(registryRequestArtifactStub.firstCall.args[1], + path.join("/ui5Data/", "framework", "staging", "com.sap.ui5.dist_sapui5-sdk-dist_5_jar"), + "Registry#requestArtifact got called with expected target directory"); + + t.is( + typeof installedArtifact.removeArtifact, + "function", + "removeArtifact method" + ); + rmStub.resetHistory(); + await installedArtifact.removeArtifact(); + t.is(rmStub.callCount, 1, "fs.rm got called once"); + t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument"); +}); + + +test.serial("installArtifact: Target revision provided", async (t) => { + const {Installer, rmStub, registryRequestArtifactStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: async () => "url" + }); + + const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists").resolves(false); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + + const installedArtifact = await installer.installArtifact({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "zip", + classifier: "npm-sources", + revision: "16" + }); + + const expectedPath = path.join("/ui5Data/", "framework", "artifacts", + "com_sap_ui5_dist-sapui5-sdk-dist", "16", "npm-sources.zip"); + t.is( + installedArtifact.artifactPath, + expectedPath, + "artifactPath correctly resolved" + ); + + t.is(fetchArtifactMetadataStub.callCount, 0, "fetchArtifactMetadataStub did not get called"); + + t.is(registryRequestArtifactStub.callCount, 1, "Registry#requestArtifact got called once"); + t.deepEqual(registryRequestArtifactStub.firstCall.firstArg, { + revision: "16", + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + classifier: "npm-sources", + version: "1.75.0", + extension: "zip", + }, "Registry#requestArtifact got called with expected coordinates"); + t.is(registryRequestArtifactStub.firstCall.args[1], + path.join("/ui5Data/", "framework", "staging", "com.sap.ui5.dist_sapui5-sdk-dist_16_npm-sources.zip"), + "Registry#requestArtifact got called with expected target directory"); + + t.is( + typeof installedArtifact.removeArtifact, + "function", + "removeArtifact method" + ); + rmStub.resetHistory(); + await installedArtifact.removeArtifact(); + t.is(rmStub.callCount, 1, "fs.rm got called once"); + t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument"); +}); + +test.serial("installArtifact: Already installed", async (t) => { + const {Installer, rmStub, registryRequestArtifactStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists").resolves(true); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + + const installedArtifact = await installer.installArtifact({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar"); + t.is( + installedArtifact.artifactPath, + expectedPath, + "artifactPath correctly resolved" + ); + + t.is(registryRequestArtifactStub.callCount, 0, "Registry#requestArtifact did not get called"); + + t.is( + typeof installedArtifact.removeArtifact, + "function", + "removeArtifact method" + ); + rmStub.resetHistory(); + await installedArtifact.removeArtifact(); + t.is(rmStub.callCount, 1, "fs.rm got called once"); + t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument"); +}); + +test.serial("installArtifact: Already installed only after lock acquired", async (t) => { + const {Installer, rmStub, registryRequestArtifactStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"}); + sinon.stub(installer, "_pathExists") + .onFirstCall().resolves(false) + .onSecondCall().resolves(true); + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + + const installedArtifact = await installer.installArtifact({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar"); + t.is( + installedArtifact.artifactPath, + expectedPath, + "artifactPath correctly resolved" + ); + + t.is(registryRequestArtifactStub.callCount, 0, "Registry#requestArtifact did not get called"); + + t.is( + typeof installedArtifact.removeArtifact, + "function", + "removeArtifact method" + ); + rmStub.resetHistory(); + await installedArtifact.removeArtifact(); + t.is(rmStub.callCount, 1, "fs.rm got called once"); + t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument"); +}); + +test.serial("_fetchArtifactMetadata", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + sinon.stub(installer, "_getLocalArtifactMetadata") + .resolves({ + lastCheck: 0, + lastUpdate: 0, + revision: "2", + staleRevisions: [], + }); + + const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata") + .resolves({revision: "5", lastUpdate: 0}); + sinon.stub(installer, "_removeStaleRevisions").resolves(); + sinon.stub(installer, "_writeLocalArtifactMetadata").resolves(); + + const artifactMetadata = await installer._fetchArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck"); + t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate"); + t.is(artifactMetadata.revision, "5", "Proper metadata: revision"); + + t.is(getRemoteArtifactMetadataStub.callCount, 1, "getRemoteArtifactMetadata got called once"); +}); + +test.serial("_fetchArtifactMetadata: Cached", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {}, + }); + + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + sinon.stub(installer, "_getLocalArtifactMetadata") + .resolves({ + lastCheck: new Date().getTime(), + lastUpdate: 0, + revision: "2", + staleRevisions: [], + }); + const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata") + .resolves({revision: "5", lastUpdate: 0}); + sinon.stub(installer, "_removeStaleRevisions").resolves(); + sinon.stub(installer, "_writeLocalArtifactMetadata").resolves(); + + const artifactMetadata = await installer._fetchArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck"); + t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate"); + t.is(artifactMetadata.revision, "2", "Proper metadata: revision"); + + t.is(getRemoteArtifactMetadataStub.callCount, 0, "getRemoteArtifactMetadata did not get called"); +}); + +test.serial("_fetchArtifactMetadata: Cache available but disabled", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {}, + cacheMode: "Off" + }); + + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + sinon.stub(installer, "_getLocalArtifactMetadata") + .resolves({ + lastCheck: new Date().getTime(), + lastUpdate: 0, + revision: "2", + staleRevisions: [], + }); + const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata") + .resolves({revision: "5", lastUpdate: 0}); + sinon.stub(installer, "_removeStaleRevisions").resolves(); + sinon.stub(installer, "_writeLocalArtifactMetadata").resolves(); + + const artifactMetadata = await installer._fetchArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck"); + t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate"); + t.is(artifactMetadata.revision, "5", "Proper metadata: revision"); + t.is(getRemoteArtifactMetadataStub.callCount, 1, "getRemoteArtifactMetadata got called once"); +}); + +test.serial("_fetchArtifactMetadata: Cache outdated but enforced", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {}, + cacheMode: "Force" + }); + + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + sinon.stub(installer, "_getLocalArtifactMetadata") + .resolves({ + lastCheck: 1, // first millisecond to indicate a cache is present but outdated + lastUpdate: 0, + revision: "2", + staleRevisions: [], + }); + const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata") + .resolves({revision: "5", lastUpdate: 0}); + sinon.stub(installer, "_removeStaleRevisions").resolves(); + sinon.stub(installer, "_writeLocalArtifactMetadata").resolves(); + + const artifactMetadata = await installer._fetchArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck"); + t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate"); + t.is(artifactMetadata.revision, "2", "Proper metadata: revision"); + + t.is(getRemoteArtifactMetadataStub.callCount, 0, "getRemoteArtifactMetadata did not get called"); +}); + +test.serial("_fetchArtifactMetadata throws", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {}, + cacheMode: "Force" + }); + + sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); + sinon.stub(installer, "_getLocalArtifactMetadata").resolves({}); + + await t.throwsAsync(installer._fetchArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }), { + message: + "Could not find artifact com.sap.ui5.dist:sapui5-sdk-dist:1.75.0:jar in local cache", + }); +}); + +test.serial("_getRemoteArtifactMetadata", async (t) => { + const {Installer, registryRequestMavenMetadataStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + registryRequestMavenMetadataStub + .resolves({ + versioning: { + snapshotVersions: { + snapshotVersion: [{"extension": "jar", "updated": "20220828080910", "value": "5.0.0-SNAPSHOT"}] + } + } + }); + + const remoteArtifactMetadata = await installer._getRemoteArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }); + + t.truthy(remoteArtifactMetadata.lastUpdate, "Proper metadata: lastUpdate"); + t.is(remoteArtifactMetadata.revision, "5.0.0-SNAPSHOT", "Proper metadata: revision"); + + t.is(registryRequestMavenMetadataStub.callCount, 1, "requestPackagePackument should be called once"); + t.deepEqual(registryRequestMavenMetadataStub.getCall(0).args[0], + {groupId: "com.sap.ui5.dist", artifactId: "sapui5-sdk-dist", version: "1.75.0"}, + "requestMavenMetadata was called with correct arguments"); +}); + +test.serial("_getRemoteArtifactMetadata throws", async (t) => { + const {Installer, registryRequestMavenMetadataStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + registryRequestMavenMetadataStub.resolves({}); + + await t.throwsAsync(installer._getRemoteArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "jar", + }), {message: "Missing Maven snapshot metadata for artifact com.sap.ui5.dist:sapui5-sdk-dist:1.75.0"}); +}); + +test.serial("_getRemoteArtifactMetadata throws missing deployment metadata", async (t) => { + const {Installer, registryRequestMavenMetadataStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url") + }); + + registryRequestMavenMetadataStub + .resolves({ + versioning: { + snapshotVersions: { + snapshotVersion: [ + {"extension": "jar", "updated": "20220828080910", "value": "5.0.0-SNAPSHOT"}, + { + "classifier": "pony-sources", "extension": "zip", "updated": "20220828080910", + "value": "5.0.0-SNAPSHOT" + } + ] + } + } + }); + + await t.throwsAsync(installer._getRemoteArtifactMetadata({ + groupId: "com.sap.ui5.dist", + artifactId: "sapui5-sdk-dist", + version: "1.75.0", + extension: "zip", + classifier: "npm-sources", + }), { + message: "Could not find npm-sources.zip deployment for artifact " + + "com.sap.ui5.dist:sapui5-sdk-dist:1.75.0 in snapshot metadata:\n" + + `[{"extension":"jar","updated":"20220828080910","value":"5.0.0-SNAPSHOT"},` + + `{"classifier":"pony-sources","extension":"zip","updated":"20220828080910","value":"5.0.0-SNAPSHOT"}]` + }); +}); + +test.serial("_getLocalArtifactMetadata", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "readJson").resolves({foo: "bar"}); + const localArtifactMetadata = await installer._getLocalArtifactMetadata(); + + t.deepEqual(localArtifactMetadata, {foo: "bar"}, "Returns the correct metadata"); +}); + +test.serial("_getLocalArtifactMetadata file not found", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "readJson").throws({code: "ENOENT"}); + const localArtifactMetadata = await installer._getLocalArtifactMetadata(); + + t.deepEqual( + localArtifactMetadata, + {lastCheck: 0, lastUpdate: 0, revision: null, staleRevisions: []}, + "Returns an 'empty' localArtifactMetadata" + ); +}); + +test.serial("_getLocalArtifactMetadata throws", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + sinon.stub(installer, "readJson").throws(() => { + throw new Error("Error message"); + }); + + await t.throwsAsync(installer._getLocalArtifactMetadata(), { + message: "Error message", + }); +}); + + +test.serial("_writeLocalArtifactMetadata", async (t) => { + const {Installer, writeFileStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + // const writeJsonStub = sinon.stub(installer, "_writeJson").resolves("/path/to/file"); + writeFileStub.resolves("/path/to/file"); + + const fsWriteRsource = await installer._writeLocalArtifactMetadata("Id", {foo: "bar"}); + + t.is(fsWriteRsource, "/path/to/file"); + t.is(writeFileStub.callCount, 1, "_writeJson called"); + t.deepEqual( + writeFileStub.args, + [[path.join("/ui5Data/", "framework", "metadata", "Id.json"), "{\"foo\":\"bar\"}"]], + "_writeJson called with correct arguments" + ); +}); + +test.serial("_removeStaleRevisions", async (t) => { + const {Installer, rmStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const pathForArtifact = sinon.stub(installer, "_getTargetPathForArtifact") + .onCall(0).resolves("/path/to/artifact/1") + .onCall(1).resolves("/path/to/artifact/2"); + + let metadata = { + staleRevisions: ["1", "2", "3", "4", "5"], + }; + + await installer._removeStaleRevisions("Id", metadata, {pkgName: "myPkg"}); + + t.is(metadata.staleRevisions.length, 3, "Metadata's staleRevisions cut"); + t.is(pathForArtifact.callCount, 2, "requested path for 2 artifacts"); + t.is(pathForArtifact.getCall(0).args[0].revision, "1", "Resolved revison 1"); + t.is(pathForArtifact.getCall(1).args[0].revision, "2", "Resolved revison 2"); + + t.is(await rmStub.getCall(0).args[0], "/path/to/artifact/1", "Rm artifact 1"); + t.is(await rmStub.getCall(1).args[0], "/path/to/artifact/2", "Rm artifact 2"); + + metadata = { + staleRevisions: ["1"], + }; + await installer._removeStaleRevisions("Id", metadata, {pkgName: "myPkg"}); + + t.deepEqual(metadata, {staleRevisions: ["1"]}, "Stale revisions stay untouched if 1 or less"); +}); + +test.serial("_pathExists", async (t) => { + const {Installer, statStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + statStub.resolves(); + const pathExists = await installer._pathExists("/target/path/"); + + t.is(pathExists, true, "Target path exists"); + t.is(statStub.callCount, 1, "stat got called once"); + t.is(statStub.firstCall.firstArg, "/target/path/", "stat got called with expected argument"); +}); + +test.serial("_pathExists file not found", async (t) => { + const {Installer, statStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + statStub.throws({code: "ENOENT"}); + const pathExists = await installer._pathExists("/target/path/"); + + t.is(pathExists, false, "Target path does not exist"); +}); + +test.serial("_pathExists throws", async (t) => { + const {Installer, statStub} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + statStub.throws(() => { + throw new Error("Error message"); + }); + + await t.throwsAsync(installer._pathExists("/target/path/"), { + message: "Error message", + }, "Threw with expected error message"); +}); + +test.serial("_projectExists", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(true); + const projectExists = await installer._projectExists("/target/path/"); + + t.is(projectExists, true, "Resolves the target path"); + t.is(pathExistsStub.callCount, 1, "_pathExists got called once"); + t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"), + "_pathExists got called with expected argument"); +}); + +test.serial("_projectExists: Does not exist", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false); + const projectExists = await installer._projectExists("/target/path/"); + + t.is(projectExists, false, "Resolves the target path"); + t.is(pathExistsStub.callCount, 1, "_pathExists got called once"); + t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"), + "_pathExists got called with expected argument"); +}); + +test.serial("_projectExists: Throws", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/", + snapshotEndpointUrlCb: () => {} + }); + + const pathExistsStub = sinon.stub(installer, "_pathExists").throws(() => { + throw new Error("Error message"); + }); + + await t.throwsAsync(installer._projectExists("/target/path/"), { + message: "Error message", + }, "Threw with expected error message"); + + t.is(pathExistsStub.callCount, 1, "_pathExists got called once"); + t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"), + "_pathExists got called with expected argument"); +}); diff --git a/packages/project/test/lib/ui5framework/maven/Registry.js b/packages/project/test/lib/ui5framework/maven/Registry.js new file mode 100644 index 00000000000..51a63e10f5d --- /dev/null +++ b/packages/project/test/lib/ui5framework/maven/Registry.js @@ -0,0 +1,240 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import {promisify} from "node:util"; + +test.beforeEach(async (t) => { + t.context.pipelineStub = sinon.stub().resolves(); + t.context.streamPipelineStub = sinon.stub().resolves(); + + t.context.promisifyStub = sinon.stub(); + + t.context.fetchStub = sinon.stub().resolves({ + ok: true, + buffer: sinon.stub().resolves("Some metadata") + }); + + t.context.fsCreateWriteStreamStub = sinon.stub().resolves(); + + t.context.Registry = await esmock.p("../../../../lib/ui5Framework/maven/Registry.js", { + "make-fetch-happen": t.context.fetchStub, + "node:stream/promises": { + "pipeline": t.context.streamPipelineStub + }, + "node:util": { + "promisify": t.context.promisifyStub + }, + "graceful-fs": { + "createWriteStream": t.context.fsCreateWriteStreamStub + } + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); + esmock.purge(t.context.Registry); +}); + +test.serial("Registry: constructor", (t) => { + const {Registry} = t.context; + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + t.true(reg instanceof Registry, "Constructor returns instance of class"); + t.is(reg._endpointUrl, "some-url/"); +}); + +test.serial("Registry: constructor requires 'endpointUrl'", (t) => { + const {Registry} = t.context; + + t.throws(() => { + new Registry({cwd: "/"}); + }, {message: `Registry: Missing parameter "endpointUrl"`}); +}); + +test.serial("Registry: requestMavenMetadata", async (t) => { + const {Registry, promisifyStub} = t.context; + + promisifyStub.callsFake((fn) => promisify(fn)); // Use the native promisify + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + const resolvedMetadata = await reg.requestMavenMetadata({ + groupId: "ui5.corp", + artifactId: "great-thing", + version: "1.75.0-SNAPSHOT", + }); + + t.is(resolvedMetadata, "Some metadata"); +}); + +test.serial("Registry: requestMavenMetadata bad request", async (t) => { + const {Registry, fetchStub} = t.context; + + fetchStub.resolves({status: "500", statusText: "Bad request"}); + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + await t.throwsAsync( + reg.requestMavenMetadata({ + groupId: "ui5.corp", + artifactId: "great-thing", + version: "1.75.0-SNAPSHOT", + }), + { + message: + "Failed to retrieve maven-metadata.xml for ui5.corp:great-thing:1.75.0-SNAPSHOT:" + + " [HTTP Error] 500 Bad request", + } + ); +}); + +test.serial("Registry: requestMavenMetadata not found", async (t) => { + const {Registry, fetchStub} = t.context; + + fetchStub.throws({code: "ENOTFOUND"}); + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + await t.throwsAsync( + reg.requestMavenMetadata({ + groupId: "ui5.corp", + artifactId: "great-thing" + }), + { + message: + "Failed to connect to Maven registry at some-url/. " + + "Please check the correct endpoint URL is maintained and can be reached. "+ + "You can change the configured URL " + + "using the following command: 'ui5 config set mavenSnapshotEndpointUrl '" + } + ); +}); + +test.serial("Registry: requestMavenMetadata No metadata/bad xml", async (t) => { + const {Registry, fetchStub, promisifyStub} = t.context; + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + promisifyStub.callsFake((fn) => promisify(fn)); // Use the native promisify + + fetchStub.resolves({ + ok: true, + buffer: sinon.stub().resolves("") + }); + + await t.throwsAsync( + reg.requestMavenMetadata({ + groupId: "ui5.corp", + artifactId: "great-thing", + version: "1.75.0-SNAPSHOT", + }), + { + message: + "Failed to retrieve maven-metadata.xml for ui5.corp:great-thing:1.75.0-SNAPSHOT: " + + "Empty or unexpected response body:\n" + + "\n" + + "Parsed as:\n" + + "{\"metadata\":\"\"}" + } + ); +}); + +test.serial("Registry: requestArtifact", async (t) => { + const {Registry, fetchStub, streamPipelineStub, fsCreateWriteStreamStub} = t.context; + + fetchStub.resolves({ + ok: true, + body: "content body" + }); + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + await reg.requestArtifact({ + groupId: "ui5.corp", + artifactId: "great-thing", + revision: "2", + extension: "jar" + }, "/target/path/"); + + t.is(streamPipelineStub.callCount, 1, "Pipeline is called"); + t.is(streamPipelineStub.args[0][0], "content body", "Pipeline called with response body as argument"); + t.is(fsCreateWriteStreamStub.callCount, 1, "writeStream called"); + t.deepEqual(fsCreateWriteStreamStub.args[0], ["/target/path/"], "writeStream called with the target path"); +}); + +test.serial("Registry: requestArtifact bad request", async (t) => { + const {Registry, fetchStub} = t.context; + + fetchStub.resolves({status: "500", statusText: "Bad request"}); + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + await t.throwsAsync( + reg.requestArtifact({ + groupId: "ui5.corp", + artifactId: "great-thing", + revision: "2", + version: "2", + classifier: "classifier", + extension: "jar" + }, "/target/path/"), + { + message: + "Failed to retrieve artifact ui5.corp:great-thing:2:classifier:jar" + + " [HTTP Error] 500 Bad request", + } + ); +}); + +test.serial("Registry: requestArtifact not found", async (t) => { + const {Registry, fetchStub} = t.context; + + fetchStub.throws({code: "ENOTFOUND"}); + + const reg = new Registry({ + cwd: "/cwd/", + endpointUrl: "some-url" + }); + + await t.throwsAsync( + reg.requestArtifact( + { + groupId: "ui5.corp", + artifactId: "great-thing", + revision: "2", + version: "2", + classifier: "", + extension: "jar", + }, + "/target/path/" + ), + { + message: + "Failed to connect to Maven registry at some-url/. " + + "Please check the correct endpoint URL is maintained and can be reached. "+ + "You can change the configured URL " + + "using the following command: 'ui5 config set mavenSnapshotEndpointUrl '" + } + ); +}); diff --git a/packages/project/test/lib/ui5framework/npm/Installer.js b/packages/project/test/lib/ui5framework/npm/Installer.js new file mode 100644 index 00000000000..c06b36ae33d --- /dev/null +++ b/packages/project/test/lib/ui5framework/npm/Installer.js @@ -0,0 +1,801 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +import path from "node:path"; + +const __dirname = import.meta.dirname; + +test.beforeEach(async (t) => { + t.context.mkdirpStub = sinon.stub().resolves(); + t.context.rmrfStub = sinon.stub().resolves(); + + t.context.lockStub = sinon.stub(); + t.context.unlockStub = sinon.stub(); + t.context.renameStub = sinon.stub().yieldsAsync(); + t.context.statStub = sinon.stub().yieldsAsync(); + + t.context.AbstractResolver = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", { + "../../../../lib/utils/fs.js": { + mkdirp: t.context.mkdirpStub, + rmrf: t.context.rmrfStub + }, + "lockfile": { + lock: t.context.lockStub, + unlock: t.context.unlockStub + } + }); + t.context.Installer = await esmock.p("../../../../lib/ui5Framework/npm/Installer.js", { + "../../../../lib/ui5Framework/AbstractInstaller.js": t.context.AbstractResolver, + "../../../../lib/utils/fs.js": { + mkdirp: t.context.mkdirpStub, + rmrf: t.context.rmrfStub + }, + "graceful-fs": { + rename: t.context.renameStub, + stat: t.context.statStub + } + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); + esmock.purge(t.context.AbstractResolver); + esmock.purge(t.context.Installer); +}); + +test.serial("Installer: constructor", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + t.true(installer instanceof Installer, "Constructor returns instance of class"); + t.is(installer._packagesDir, path.join("/ui5Data/", "framework", "packages")); + t.is(installer._lockDir, path.join("/ui5Data/", "framework", "locks")); + t.is(installer._stagingDir, path.join("/ui5Data/", "framework", "staging")); +}); + +test.serial("Installer: constructor requires 'cwd'", (t) => { + const {Installer} = t.context; + + t.throws(() => { + new Installer({ + ui5DataDir: "/ui5Data/" + }); + }, {message: `Installer: Missing parameter "cwd"`}); +}); + +test.serial("Installer: constructor requires 'ui5DataDir'", (t) => { + const {Installer} = t.context; + + t.throws(() => { + new Installer({ + cwd: "/cwd/" + }); + }, {message: `Installer: Missing parameter "ui5DataDir"`}); +}); + +test.serial("Installer: fetchPackageVersions", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const registry = installer.getRegistry(); + const requestPackagePackumentStub = sinon.stub().resolves({ + versions: { + "1.0.0": {}, + "2.0.0": {}, + "3.0.0": {}, + }, + }); + sinon + .stub(registry, "_getPacote") + .resolves({ + pacote: { + packument: requestPackagePackumentStub + }, + pacoteOptions: {}, + }); + + const packageVersions = await installer.fetchPackageVersions({pkgName: "@openui5/sap.ui.lib1"}); + + t.deepEqual(packageVersions, ["1.0.0", "2.0.0", "3.0.0"], "Should resolve with expected versions"); + + t.is(requestPackagePackumentStub.callCount, 1, "requestPackagePackument should be called once"); + t.is(requestPackagePackumentStub.getCall(0).args[0], "@openui5/sap.ui.lib1", + "requestPackagePackument should be called with pkgName"); +}); + +test.serial("Installer: _getLockPath", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const lockPath = installer._getLockPath("lo/ck-n@me"); + + t.is(lockPath, path.join("/ui5Data/", "framework", "locks", "lo-ck-n@me.lock")); +}); + +test.serial("Installer: _getLockPath with illegal characters", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.throws(() => installer._getLockPath("lock.näme"), { + message: "Illegal file name: lock.näme" + }); + t.throws(() => installer._getLockPath(".lock.name"), { + message: "Illegal file name: .lock.name" + }); +}); + +test.serial("Installer: fetchPackageManifest (without existing package.json)", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const mockedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + }, + foo: "bar" + }; + + const expectedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + } + }; + + const registry = installer.getRegistry(); + const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest") + .callsFake((pkgName, version) => { + throw new Error( + "_cachedRegistry.requestPackageManifest stub called with unknown arguments " + + `pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs("myPackage", "1.2.3").resolves(mockedManifest); + + const readJsonStub = sinon.stub(installer, "readJson") + .callsFake((path) => { + throw new Error( + `readJson stub called with unknown path: ${path}` + ); + }) + .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json")) + .callsFake(async (path) => { + const error = new Error(`ENOENT: no such file or directory, open '${path}'`); + error.code = "ENOENT"; + throw error; + }); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + + t.deepEqual(manifest, expectedManifest, "Should return expected manifest object"); + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 1, "requestPackageManifest should be called once"); +}); + +test.serial("Installer: fetchPackageManifest (with existing package.json)", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const mockedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + }, + foo: "bar" + }; + + const expectedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + } + }; + + const registry = installer.getRegistry(); + const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest") + .rejects(new Error("Unexpected call")); + + const readJsonStub = sinon.stub(installer, "readJson") + .callsFake((path) => { + throw new Error( + `readJson stub called with unknown path: ${path}` + ); + }) + .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json")) + .resolves(mockedManifest); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + + t.deepEqual(manifest, expectedManifest, "Should return expected manifest object"); + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called"); +}); + +test.serial("Installer: fetchPackageManifest (readJson throws error)", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const registry = installer.getRegistry(); + const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest") + .rejects(new Error("Unexpected call")); + + const readJsonStub = sinon.stub(installer, "readJson") + .rejects(new Error("Error from readJson")); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + await t.throwsAsync(async () => { + await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + }, {message: "Error from readJson"}); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called"); +}); + +test.serial("Installer: _synchronize", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const getLockPathStub = sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().resolves(); + + await installer._synchronize("lock/name", callback); + + t.is(getLockPathStub.callCount, 1, "_getLockPath should be called once"); + t.is(getLockPathStub.getCall(0).args[0], "lock/name", + "_getLockPath should be called with expected args"); + + t.is(t.context.mkdirpStub.callCount, 1, "_mkdirp should be called once"); + t.deepEqual(t.context.mkdirpStub.getCall(0).args, [path.join("/ui5Data/", "framework", "locks")], + "_mkdirp should be called with expected args"); + + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.lockStub.getCall(0).args[0], "/locks/lockfile.lock", + "lock should be called with expected path"); + t.deepEqual(t.context.lockStub.getCall(0).args[1], {wait: 10000, stale: 60000, retries: 10}, + "lock should be called with expected options"); + + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); + t.is(t.context.unlockStub.getCall(0).args[0], "/locks/lockfile.lock", + "unlock should be called with expected path"); + + t.is(callback.callCount, 1, "callback should be called once"); + + t.true(t.context.lockStub.calledBefore(callback), "Lock should be called before invoking the callback"); + t.true(t.context.unlockStub.calledAfter(callback), "Unlock should be called after invoking the callback"); +}); + +test.serial("Installer: _synchronize should unlock when callback promise has resolved", async (t) => { + const {Installer} = t.context; + + t.plan(4); + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().callsFake(async () => { + t.is(t.context.lockStub.callCount, 1, "lock should have been called when the callback is invoked"); + await Promise.resolve(); + t.is(t.context.unlockStub.callCount, 0, + "unlock should not be called when the callback did not fully resolve, yet"); + }); + + await installer._synchronize("lock/name", callback); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called after _synchronize has resolved"); +}); + +test.serial("Installer: _synchronize should throw when locking fails", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(new Error("Locking error")); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub(); + + await t.throwsAsync(async () => { + await installer._synchronize("lock/name", callback); + }, {message: "Locking error"}); + + t.is(callback.callCount, 0, "callback should not be called"); + t.is(t.context.unlockStub.callCount, 0, "unlock should not be called"); +}); + +test.serial("Installer: _synchronize should still unlock when callback throws an error", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().throws(new Error("Callback throws error")); + + await t.throwsAsync(async () => { + await installer._synchronize("lock/name", callback); + }, {message: "Callback throws error"}); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); +}); + +test.serial("Installer: _synchronize should still unlock when callback rejects with error", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().rejects(new Error("Callback rejects with error")); + + await t.throwsAsync(async () => { + await installer._synchronize("lock/name", callback); + }, {message: "Callback rejects with error"}); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); +}); + +test.serial("Installer: installPackage with new package", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const targetDir = path.join("my", "package", "dir"); + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .returns(targetDir); + + const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(false); + const synchronizeSpy = sinon.spy(installer, "_synchronize"); + + const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage") + .returns("staging-dir-path"); + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false); + + const registry = installer.getRegistry(); + const extractPackageStub = sinon.stub(registry, "extractPackage").resolves(); + + const res = await installer.installPackage({ + pkgName: "myPackage", + version: "1.2.3" + }); + + t.deepEqual(res, { + pkgPath: targetDir + }, "Should return correct values"); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getTargetDirForPackage should be called with the correct arguments"); + + t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice"); + t.is(packageJsonExistsStub.getCall(0).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments on first call"); + t.is(packageJsonExistsStub.getCall(1).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments on second call"); + + t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once"); + t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3", + "_synchronize should be called with the correct first argument"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); + + t.is(getStagingDirForPackageStub.callCount, 1, "_getStagingDirForPackage should be called once"); + t.deepEqual(getStagingDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getStagingDirForPackage should be called with the correct arguments"); + + t.is(pathExistsStub.callCount, 2, "_pathExists should be called twice"); + t.is(pathExistsStub.getCall(0).args[0], "staging-dir-path", + "_packageJsonExists should be called with the correct arguments"); + t.is(pathExistsStub.getCall(1).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments"); + t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called"); + + t.is(extractPackageStub.callCount, 1, "_extractPackage should be called once"); + + t.is(t.context.mkdirpStub.callCount, 2, "mkdirp should be called twice"); + t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"), + "mkdirp should be called with the correct arguments on first call"); + t.is(t.context.mkdirpStub.getCall(1).args[0], path.join("my", "package"), + "mkdirp should be called with the correct arguments on second call"); + + t.is(t.context.renameStub.callCount, 1, "fs.rename should be called once"); + t.is(t.context.renameStub.getCall(0).args[0], "staging-dir-path", + "fs.rename should be called with the correct first argument"); + t.is(t.context.renameStub.getCall(0).args[1], targetDir, + "fs.rename should be called with the correct second argument"); +}); + +test.serial("Installer: installPackage with already installed package", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .returns("package-dir-path"); + + const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(true); + const synchronizeSpy = sinon.spy(installer, "_synchronize"); + + const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage") + .returns("staging-dir-path"); + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false); + + const registry = installer.getRegistry(); + const extractPackageStub = sinon.stub(registry, "extractPackage").resolves(); + + const res = await installer.installPackage({ + pkgName: "myPackage", + version: "1.2.3" + }); + + t.deepEqual(res, { + pkgPath: "package-dir-path" + }, "Should return correct values"); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getTargetDirForPackage should be called with the correct arguments"); + + t.is(packageJsonExistsStub.callCount, 1, "_packageJsonExists should be called once"); + t.is(packageJsonExistsStub.getCall(0).args[0], "package-dir-path", + "_packageJsonExists should be called with the correct arguments on first call"); + + t.is(synchronizeSpy.callCount, 0, "_synchronize should never be called"); + t.is(t.context.lockStub.callCount, 0, "lock should never be called"); + t.is(t.context.unlockStub.callCount, 0, "unlock should never be called"); + t.is(getStagingDirForPackageStub.callCount, 0, "_getStagingDirForPackage should never be called"); + t.is(pathExistsStub.callCount, 0, "_pathExists should never be called"); + t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called"); + t.is(extractPackageStub.callCount, 0, "_extractPackage should never be called"); + t.is(t.context.mkdirpStub.callCount, 0, "mkdirp should never be called"); + t.is(t.context.renameStub.callCount, 0, "fs.rename should never be called"); +}); + +test.serial("Installer: installPackage with install already in progress", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .returns("package-dir-path"); + + const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists") + .onFirstCall().resolves(false) + .onSecondCall().resolves(true); // After lock got acquired, package has been installed + + const synchronizeSpy = sinon.spy(installer, "_synchronize"); + + const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage") + .returns("staging-dir-path"); + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false); + + const registry = installer.getRegistry(); + const extractPackageStub = sinon.stub(registry, "extractPackage").resolves(); + + await installer.installPackage({ + pkgName: "myPackage", + version: "1.2.3" + }); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getTargetDirForPackage should be called with the correct arguments"); + + t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice"); + t.is(packageJsonExistsStub.getCall(0).args[0], "package-dir-path", + "_packageJsonExists should be called with the correct arguments on first call"); + t.is(packageJsonExistsStub.getCall(1).args[0], "package-dir-path", + "_packageJsonExists should be called with the correct arguments on second call"); + + t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once"); + t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3", + "_synchronize should be called with the correct first argument"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); + + t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called"); + + t.is(t.context.mkdirpStub.callCount, 1, "mkdirp should be called once"); + t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"), + "mkdirp should be called with the correct arguments"); + + t.is(getStagingDirForPackageStub.callCount, 0, "_getStagingDirForPackage should never be called"); + t.is(pathExistsStub.callCount, 0, "_pathExists should never be called"); + t.is(extractPackageStub.callCount, 0, "_extractPackage should never be called"); + t.is(t.context.renameStub.callCount, 0, "fs.rename should never be called"); +}); + +test.serial("Installer: installPackage with new package and existing target and staging", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const targetDir = path.join("my", "package", "dir"); + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .returns(targetDir); + + const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(false); + const synchronizeSpy = sinon.spy(installer, "_synchronize"); + + const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage") + .returns("staging-dir-path"); + const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(true); // Staging dir exists + + const registry = installer.getRegistry(); + const extractPackageStub = sinon.stub(registry, "extractPackage").resolves(); + + const res = await installer.installPackage({ + pkgName: "myPackage", + version: "1.2.3" + }); + + t.deepEqual(res, { + pkgPath: targetDir + }, "Should return correct values"); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getTargetDirForPackage should be called with the correct arguments"); + + t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice"); + t.is(packageJsonExistsStub.getCall(0).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments on first call"); + t.is(packageJsonExistsStub.getCall(1).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments on second call"); + + t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once"); + t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3", + "_synchronize should be called with the correct first argument"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); + + t.is(getStagingDirForPackageStub.callCount, 1, "_getStagingDirForPackage should be called once"); + t.deepEqual(getStagingDirForPackageStub.getCall(0).args[0], { + pkgName: "myPackage", + version: "1.2.3" + }, "_getStagingDirForPackage should be called with the correct arguments"); + + t.is(pathExistsStub.callCount, 2, "_pathExists should be called twice"); + t.is(pathExistsStub.getCall(0).args[0], "staging-dir-path", + "_packageJsonExists should be called with the correct arguments"); + t.is(pathExistsStub.getCall(1).args[0], targetDir, + "_packageJsonExists should be called with the correct arguments"); + + t.is(t.context.rmrfStub.callCount, 2, "rmrf should be called twice"); + t.is(t.context.rmrfStub.getCall(0).args[0], "staging-dir-path", + "rmrf should be called with the correct arguments"); + t.is(t.context.rmrfStub.getCall(1).args[0], targetDir, + "rmrf should be called with the correct arguments"); + + t.is(extractPackageStub.callCount, 1, "_extractPackage should be called once"); + + t.is(t.context.mkdirpStub.callCount, 2, "mkdirp should be called twice"); + t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"), + "mkdirp should be called with the correct arguments on first call"); + t.is(t.context.mkdirpStub.getCall(1).args[0], path.join("my", "package"), + "mkdirp should be called with the correct arguments on second call"); + + t.is(t.context.renameStub.callCount, 1, "fs.rename should be called once"); + t.is(t.context.renameStub.getCall(0).args[0], "staging-dir-path", + "fs.rename should be called with the correct first argument"); + t.is(t.context.renameStub.getCall(0).args[1], targetDir, + "fs.rename should be called with the correct second argument"); +}); + +test.serial("Installer: _pathExists - exists", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const res = await installer._pathExists(__dirname); + + t.is(res, true, "Path should exist"); + t.is(t.context.statStub.getCall(0).args[0], __dirname, + "fs.stat should be called with correct arguments"); +}); + +test.serial("Installer: _pathExists - does not exist", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const notFoundError = new Error("Not found"); + notFoundError.code = "ENOENT"; + t.context.statStub.yieldsAsync(notFoundError); + + const res = await installer._pathExists("my-path"); + + t.is(res, false, "Path should not exist"); + t.is(t.context.statStub.getCall(0).args[0], "my-path", + "fs.stat should be called with correct arguments"); +}); + +test.serial("Installer: _pathExists - re-throws unexpected errors", async (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + const notFoundError = new Error("Pony Error"); + notFoundError.code = "PONY"; + t.context.statStub.yieldsAsync(notFoundError); + + const err = await t.throwsAsync(installer._pathExists("my-path")); + + t.is(err, notFoundError, "Should throw with expected exception"); + t.is(t.context.statStub.getCall(0).args[0], "my-path", + "fs.stat should be called with correct arguments"); +}); + + +test.serial("Installer: Registry throws", (t) => { + const {Installer} = t.context; + + const installer = new Installer({ + cwd: "/cwd/", + ui5DataDir: "/ui5Data/" + }); + + installer._cwd = null; + t.throws(() => installer.getRegistry(), { + message: "Registry: Missing parameter \"cwd\"", + }, "Registry requires cwd"); + + installer._cwd = "/cwd/"; + installer._caCacheDir = null; + t.throws(() => installer.getRegistry(), { + message: "Registry: Missing parameter \"cacheDir\"", + }, "Registry requires cahceDir"); +}); diff --git a/packages/project/test/lib/ui5framework/npm/Registry.js b/packages/project/test/lib/ui5framework/npm/Registry.js new file mode 100644 index 00000000000..2a3465e0860 --- /dev/null +++ b/packages/project/test/lib/ui5framework/npm/Registry.js @@ -0,0 +1,190 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = (t.context.sinon = sinonGlobal.createSandbox()); + + t.context.pacote = { + packument: sinon.stub(), + manifest: sinon.stub(), + extract: sinon.stub(), + }; + + class Config { + constructor(...args) { + t.context.npmConfigConstructor(...args); + } + + static get typeDefs() { + return {path: "string"}; + } + + async load() {} + + get flat() { + return {}; + } + } + + t.context.npmConfigConstructor = sinon.stub(); + t.context.npmConfigFlat = sinon.stub(Config.prototype, "flat"); + t.context.Registry = await esmock.p("../../../../lib/ui5Framework/npm/Registry.js", { + "pacote": { + "default": t.context.pacote + }, + "@npmcli/config": { + "default": Config + }, + "@npmcli/config/lib/definitions/index.js": { + default: { + flatten: "flatten", + definitions: "definitions", + shorthands: "shorthands", + defaults: "defaults", + } + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Registry); +}); + +test.serial("Constructor", (t) => { + const {Registry} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + t.true(registry instanceof Registry); +}); + +test.serial("_getPacoteOptions", async (t) => { + const {Registry, npmConfigFlat, npmConfigConstructor} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + const npmConfig = { + "fake": "config" + }; + + const expectedPacoteOptions = { + fake: "config", + cache: "cacheDir" + }; + npmConfigFlat.value(npmConfig); + + const pacoteOptions = await registry._getPacoteOptions(); + + t.is(npmConfigConstructor.callCount, 1); + t.deepEqual(npmConfigConstructor.firstCall.firstArg, { + cwd: "cwd", + npmPath: "cwd", + flatten: "flatten", + definitions: "definitions", + shorthands: "shorthands", + defaults: "defaults", + }); + + t.deepEqual(pacoteOptions, expectedPacoteOptions); +}); + +test.serial("_getPacoteOptions (proxy config set)", async (t) => { + const {Registry, npmConfigFlat, npmConfigConstructor} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + const npmConfig = { + "proxy": "http://localhost:9999" + }; + + const expectedPacoteOptions = { + proxy: "http://localhost:9999", + cache: "cacheDir" + }; + + npmConfigFlat.value(npmConfig); + + const pacoteOptions = await registry._getPacoteOptions(); + + t.is(npmConfigConstructor.callCount, 1); + + t.deepEqual(pacoteOptions, expectedPacoteOptions); +}); + +test.serial("_getPacoteOptions (https-proxy config set)", async (t) => { + const {Registry, npmConfigFlat, npmConfigConstructor} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + const npmConfig = { + "httpsProxy": "http://localhost:9999" + }; + + const expectedPacoteOptions = { + httpsProxy: "http://localhost:9999", + cache: "cacheDir" + }; + + npmConfigFlat.value(npmConfig); + + const pacoteOptions = await registry._getPacoteOptions(); + + t.is(npmConfigConstructor.callCount, 1); + + t.deepEqual(pacoteOptions, expectedPacoteOptions); +}); + +test.serial("_getPacote", async (t) => { + const {Registry, sinon} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + const expectedPacoteOptions = {"fake": "options"}; + + sinon.stub(registry, "_getPacoteOptions").resolves(expectedPacoteOptions); + + const {pacote, pacoteOptions} = await registry._getPacote(); + + t.is(pacote, t.context.pacote); + t.is(pacoteOptions, expectedPacoteOptions); +}); + +test.serial("_getPacote caching", async (t) => { + const {Registry, sinon} = t.context; + + const registry = new Registry({ + cwd: "cwd", + cacheDir: "cacheDir" + }); + + const expectedPacoteOptions = {"fake": "options"}; + + const getPacoteOptionsStub = sinon.stub(registry, "_getPacoteOptions").resolves(expectedPacoteOptions); + + const {pacote, pacoteOptions} = await registry._getPacote(); + + t.is(pacote, t.context.pacote); + t.is(pacoteOptions, expectedPacoteOptions); + + await registry._getPacote(); + await registry._getPacote(); + + t.is(getPacoteOptionsStub.callCount, 1, "_getPacoteOptions got called once"); +}); diff --git a/packages/project/test/lib/utils/fs.js b/packages/project/test/lib/utils/fs.js new file mode 100644 index 00000000000..322beff2fcd --- /dev/null +++ b/packages/project/test/lib/utils/fs.js @@ -0,0 +1,13 @@ +import test from "ava"; +import path from "node:path"; +import {stat} from "node:fs/promises"; +import {mkdirp} from "../../../lib/utils/fs.js"; + +const __dirname = import.meta.dirname; + +test("mkdirp: Create directory hierarchy", async (t) => { + const targetPath = path.join(__dirname, "..", "..", "tmp", "mkdir-test", "this", "is", "a", "directory"); + await mkdirp(targetPath); + const res = await stat(targetPath); + t.truthy(res, "Target directory has been created"); +}); diff --git a/packages/project/test/lib/validation/ValidationError.js b/packages/project/test/lib/validation/ValidationError.js new file mode 100644 index 00000000000..dd32292ef8f --- /dev/null +++ b/packages/project/test/lib/validation/ValidationError.js @@ -0,0 +1,939 @@ +import test from "ava"; +import sinon from "sinon"; +import chalk from "chalk"; +import ValidationError from "../../../lib/validation/ValidationError.js"; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("ValidationError constructor", (t) => { + const errors = [ + {dataPath: "", keyword: "", message: "error1", params: {}}, + {dataPath: "", keyword: "", message: "error2", params: {}} + ]; + const project = {id: "id"}; + const schema = {schema: "schema"}; + const data = {data: "data"}; + const yaml = {path: "path", source: "source", documentIndex: 0}; + + const filteredErrors = [{dataPath: "", keyword: "", message: "error1", params: {}}]; + + const filterErrorsStub = sinon.stub(ValidationError, "filterErrors"); + filterErrorsStub.returns(filteredErrors); + + const formatErrorsStub = sinon.stub(ValidationError.prototype, "formatErrors"); + formatErrorsStub.returns("Formatted Message"); + + const validationError = new ValidationError({errors, schema, data, project, yaml}); + + t.true(validationError instanceof ValidationError, "ValidationError constructor returns instance"); + t.true(validationError instanceof Error, "ValidationError inherits from Error"); + t.is(validationError.name, "ValidationError", "ValidationError should have 'name' property"); + + t.deepEqual(validationError.errors, filteredErrors, + "ValidationError should have 'errors' property with filtered errors"); + t.deepEqual(validationError.project, project, "ValidationError should have 'project' property"); + t.deepEqual(validationError.yaml, yaml, "≈ should have 'yaml' property"); + t.is(validationError.message, "Formatted Message", "ValidationError should have 'message' property"); + + t.is(filterErrorsStub.callCount, 1, "ValidationError.filterErrors should be called once"); + t.deepEqual(filterErrorsStub.getCall(0).args, [errors], + "ValidationError.filterErrors should be called with errors, project and yaml"); + + t.is(formatErrorsStub.callCount, 1, "ValidationError#formatErrors should be called once"); + t.deepEqual(formatErrorsStub.getCall(0).args, [], + "ValidationError.formatErrors should be called without args"); +}); + +test.serial("ValidationError.filterErrors", (t) => { + const allErrors = [ + { + keyword: "if" + }, + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword2" + }, + { + dataPath: "dataPath3", + keyword: "keyword2" + }, + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword1", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath4", + keyword: "keyword5", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "foo" + } + ] + } + } + ]; + + const expectedErrors = [ + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword2" + }, + { + dataPath: "dataPath3", + keyword: "keyword2" + }, + { + dataPath: "dataPath1", + keyword: "keyword1", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath4", + keyword: "keyword5", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "foo" + } + ] + } + } + ]; + + const filteredErrors = ValidationError.filterErrors(allErrors); + + t.deepEqual(filteredErrors, expectedErrors, "filterErrors should return expected errors"); +}); + +test.serial("ValidationError.formatErrors", (t) => { + const fakeValidationErrorInstance = { + errors: [{}, {}], + project: {id: "my-project"} + }; + + const formatErrorStub = sinon.stub(); + formatErrorStub.onFirstCall().returns("Error message 1"); + formatErrorStub.onSecondCall().returns("Error message 2"); + fakeValidationErrorInstance.formatError = formatErrorStub; + + const message = ValidationError.prototype.formatErrors.apply(fakeValidationErrorInstance); + + const expectedMessage = +`${chalk.red("Invalid ui5.yaml configuration for project my-project")} + +Error message 1 + +${process.stdout.isTTY ? chalk.grey.dim("─".repeat(process.stdout.columns || 80)) : ""} + +Error message 2`; + + t.is(message, expectedMessage); + + t.is(formatErrorStub.callCount, 2, "formatErrorStub should be called twice"); + t.deepEqual(formatErrorStub.getCall(0).args, [ + fakeValidationErrorInstance.errors[0] + ], "formatErrorStub should be called with first error"); + t.deepEqual(formatErrorStub.getCall(1).args, [ + fakeValidationErrorInstance.errors[1] + ], "formatErrorStub should be called with second error"); +}); + +test.serial("ValidationError.formatError (with yaml)", (t) => { + const fakeValidationErrorInstance = { + yaml: { + path: "/path", + source: "source" + } + }; + const error = {"error": true}; + + const formatMessageStub = sinon.stub(ValidationError, "formatMessage"); + formatMessageStub.returns("First line\nSecond line\nThird line"); + + const getYamlExtractStub = sinon.stub(ValidationError, "getYamlExtract"); + getYamlExtractStub.returns("YAML"); + + const message = ValidationError.prototype.formatError.call(fakeValidationErrorInstance, error); + + const expectedMessage = +`First line + +YAML +Second line +Third line`; + + t.is(message, expectedMessage); + + t.is(formatMessageStub.callCount, 1, "formatMessageStub should be called once"); + t.deepEqual(formatMessageStub.getCall(0).args, [error], "formatMessageStub should be called with error"); + + t.is(getYamlExtractStub.callCount, 1, "getYamlExtractStub should be called once"); + t.deepEqual(getYamlExtractStub.getCall(0).args, [ + {error, yaml: fakeValidationErrorInstance.yaml}], + "getYamlExtractStub should be called with error and yaml"); +}); + +test.serial("ValidationError.getYamlExtract", (t) => { + const error = {}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: value2 +property3: value3 +property4: value4 +property5: value5 +`, + documentIndex: 0 + }; + + const analyzeYamlErrorStub = sinon.stub(ValidationError, "analyzeYamlError"); + analyzeYamlErrorStub.returns({line: 3, column: 12}); + + const expectedYamlExtract = + chalk.grey("/my-project/ui5.yaml:3") + + "\n\n" + + chalk.grey("1:") + " property1: value1\n" + + chalk.grey("2:") + " property2: value2\n" + + chalk.bgRed(chalk.grey("3:") + " property3: value3\n") + + " ".repeat(14) + chalk.red("^"); + + const yamlExtract = ValidationError.getYamlExtract({error, yaml}); + + t.is(yamlExtract, expectedYamlExtract); +}); + +test.serial("ValidationError.getSourceExtract", (t) => { + const yamlSource = +`property1: value1 +property2: value2 +`; + const line = 2; + const column = 1; + + const expected = + chalk.grey("1:") + " property1: value1\n" + + chalk.bgRed(chalk.grey("2:") + " property2: value2\n") + + " ".repeat(3) + chalk.red("^"); + + const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column); + + t.is(sourceExtract, expected, "getSourceExtract should return expected string"); +}); + +test.serial("ValidationError.getSourceExtract (Windows Line-Endings)", (t) => { + const yamlSource = +"property1: value1\r\n" + +"property2: value2\r\n"; + const line = 2; + const column = 1; + + const expected = + chalk.grey("1:") + " property1: value1\n" + + chalk.bgRed(chalk.grey("2:") + " property2: value2\n") + + " ".repeat(3) + chalk.red("^"); + + const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column); + + t.is(sourceExtract, expected, "getSourceExtract should return expected string"); +}); + +test.serial("ValidationError.analyzeYamlError: Property", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: value2 +property3: value3 +property4: value4 +property5: value5 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested property", (t) => { + const error = {dataPath: "/property2/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: + property3: value3 +property3: value3 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 3}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Array", (t) => { + const error = {dataPath: "/property/list/2/name"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property: + list: + - name: ' - - - - -' + - name: other - name- with- hyphens + - name: name3 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested array", (t) => { + const error = {dataPath: "/items/2/subItems/1"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: + - subItems: + - foo + - bar + - subItems: + - foo + - bar + - subItems: + - foo + - bar +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 10, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested array (Windows Line-Endings)", (t) => { + const error = {dataPath: "/items/2/subItems/1"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +"items:\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n", + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 10, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Array with square brackets (not supported)", (t) => { + const error = {dataPath: "/items/2"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: [1, 2, 3] +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Multiline array with square brackets (not supported)", (t) => { + const error = {dataPath: "/items/2"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: [ + 1, + 2, + 3 +] +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested property with comments", (t) => { + const error = {dataPath: "/property1/property2/property3/property4"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: + property2: + property3: + # property4: value4444 + property4: value4 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested properties with same name", (t) => { + const error = {dataPath: "/property/property/property/property"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property: + property: + property: + # property: foo + property: bar +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=required, no dataPath", (t) => { + const error = {dataPath: "", keyword: "required"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: ``, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=required", (t) => { + const error = {dataPath: "/property2", keyword: "required"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: true +property2: + property3: true +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 2, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=additionalProperties", (t) => { + const error = { + dataPath: "/property2", + keyword: "additionalProperties", + params: { + additionalProperty: "property3" + } + }; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: true +property2: + property3: true +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 3}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 4, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator and empty lines)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +` + + + +--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 8, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 15, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 16, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator and empty lines)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +` + + + + +--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 21, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +`, + documentIndex: 1 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +`, + documentIndex: 1 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.formatMessage: keyword=type dataPath=", (t) => { + const error = { + dataPath: "", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "#/type", + }; + + const expectedErrorMessage = "Configuration must be of type 'object'"; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=type", (t) => { + const error = { + dataPath: "/foo", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "#/type", + }; + + const expectedErrorMessage = `Configuration ${chalk.underline(chalk.red("foo"))} must be of type 'object'`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=required w/o dataPath", (t) => { + const error = { + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + }, + schemaPath: "#/required", + }; + + const expectedErrorMessage = "Configuration must have required property 'specVersion'"; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=required", (t) => { + const error = { + keyword: "required", + dataPath: "/metadata", + schemaPath: "#/definitions/metadata/required", + params: {missingProperty: "name"}, + message: "should have required property 'name'" + }; + + const expectedErrorMessage = + `Configuration ${chalk.underline(chalk.red("metadata"))} must have required property 'name'`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=errorMessage", (t) => { + const error = { + dataPath: "/specVersion", + keyword: "errorMessage", + message: +`Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "2.0", "1.1", "1.0", "0.1" +For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "2.0", + "1.1", + "1.0", + "0.1", + ], + }, + schemaPath: "#/properties/specVersion/enum", + }, + ], + }, + schemaPath: "#/properties/specVersion/errorMessage", + }; + + const expectedErrorMessage = +`Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "2.0", "1.1", "1.0", "0.1" +For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions`; + + const errorMessage = ValidationError.formatMessage(error, {}); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=additionalProperties", (t) => { + const error = { + keyword: "additionalProperties", + dataPath: "/resources/configuration", + schemaPath: "#/properties/configuration/additionalProperties", + params: {additionalProperty: "propertiesFileEncoding"}, + message: "should NOT have additional properties" + }; + + const expectedErrorMessage = + `Configuration ${chalk.underline(chalk.red("resources/configuration"))} ` + + `property propertiesFileEncoding must not be provided here`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=enum", (t) => { + const error = { + keyword: "enum", + dataPath: "/type", + schemaPath: "#/properties/type/enum", + params: { + allowedValues: ["application", "library", "theme-library", "module"] + }, + message: "should be equal to one of the allowed values" + }; + + const expectedErrorMessage = +`Configuration ${chalk.underline(chalk.red("type"))} must be equal to one of the allowed values +Allowed values: application, library, theme-library, module`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +// test.serial.skip("ValidationError.formatMessage: keyword=pattern", (t) => { +// const error = {}; + +// const expectedErrorMessage = +// ``; + +// const errorMessage = ValidationError.formatMessage(error); +// t.is(errorMessage, expectedErrorMessage); +// }); diff --git a/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js new file mode 100644 index 00000000000..144590c1740 --- /dev/null +++ b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js @@ -0,0 +1,269 @@ +import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js"; + +/** + * Common test functionality for builder/bundles/bundleOptions section in config + */ +export default { + /** + * Executes the tests for different kind of projects, e.g. "application", "library" + * + * @param {Function} test ava test + * @param {Function} assertValidation assertion function + * @param {string} type one of "application", "library" + */ + defineTests: function(test, assertValidation, type) { + // Version specific tests + SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) { + test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "optimize": false, + "decorateBootstrapModule": false, + "addTryCatchRestartWrapper": true, + "numberOfParts": 8, + "sourceMap": false + } + }] + } + }); + }); + + test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions properties removal`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "usePredefineCalls": true + } + }] + } + }, [ + { + keyword: "additionalProperties", + dataPath: "/builder/bundles/0/bundleOptions", + params: { + additionalProperty: "usePredefineCalls", + }, + message: "should NOT have additional properties", + }, + ]); + }); + + test(`${type} invalid (specVersion ${specVersion}): builder/bundles/bundleOptions config`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "optimize": "invalid value", + "decorateBootstrapModule": {"invalid": "value"}, + "addTryCatchRestartWrapper": ["invalid value"], + "numberOfParts": true, + "sourceMap": 55 + } + }] + } + }, [ + { + keyword: "type", + dataPath: "/builder/bundles/0/bundleOptions/optimize", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/decorateBootstrapModule", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/addTryCatchRestartWrapper", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/numberOfParts", + params: { + type: "number", + }, + message: "should be number" + }, + { + keyword: "type", + dataPath: "/builder/bundles/0/bundleOptions/sourceMap", + params: { + type: "boolean", + }, + message: "should be boolean" + } + ]); + }); + }); + + SpecificationVersion.getVersionsForRange("3.0 - 3.2").forEach(function(specVersion) { + test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "optimize": false, + "decorateBootstrapModule": false, + "addTryCatchRestartWrapper": true, + "usePredefineCalls": true, + "numberOfParts": 8, + "sourceMap": false + } + }] + } + }); + }); + + test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions properties removal`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "debugMode": true + } + }] + } + }, [ + { + keyword: "additionalProperties", + dataPath: "/builder/bundles/0/bundleOptions", + params: { + additionalProperty: "debugMode", + }, + message: "should NOT have additional properties", + }, + ]); + }); + + test(`${type} invalid (specVersion ${specVersion}): builder/bundles/bundleOptions config`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": type, + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleOptions": { + "optimize": "invalid value", + "decorateBootstrapModule": {"invalid": "value"}, + "addTryCatchRestartWrapper": ["invalid value"], + "usePredefineCalls": 12, + "numberOfParts": true, + "sourceMap": 55 + } + }] + } + }, [ + { + keyword: "type", + dataPath: "/builder/bundles/0/bundleOptions/optimize", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/decorateBootstrapModule", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/addTryCatchRestartWrapper", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/usePredefineCalls", + params: { + type: "boolean", + }, + message: "should be boolean" + }, + { + keyword: "type", + dataPath: + "/builder/bundles/0/bundleOptions/numberOfParts", + params: { + type: "number", + }, + message: "should be number" + }, + { + keyword: "type", + dataPath: "/builder/bundles/0/bundleOptions/sourceMap", + params: { + type: "boolean", + }, + message: "should be boolean" + } + ]); + }); + }); + } +}; diff --git a/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js new file mode 100644 index 00000000000..34db358c983 --- /dev/null +++ b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js @@ -0,0 +1,55 @@ +import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js"; + +/** + * Common test functionality for customConfiguration section in config + */ +export default { + /** + * Executes the tests for different kind of projects, + * e.g. "application", "library", "theme-library" and "module" + * + * @param {Function} test ava test + * @param {Function} assertValidation assertion function + * @param {string} type one of "project-shim", "server-middleware" "task", + * "application", "library", "theme-library" and "module" + * @param {object} additionalConfiguration additional configuration content + */ + defineTests: function(test, assertValidation, type, additionalConfiguration) { + additionalConfiguration = additionalConfiguration || {}; + // version specific tests for customConfiguration + test(`${type}: Invalid customConfiguration (specVersion 2.0)`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": "2.0", + "type": type, + "metadata": { + "name": "my-" + type + }, + "customConfiguration": {} + }, additionalConfiguration), [ + { + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "customConfiguration", + } + } + ]); + }); + + SpecificationVersion.getVersionsForRange(">=2.1").forEach((specVersion) => { + test(`${type}: Valid customConfiguration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, Object.assign( { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "customConfiguration": { + "foo": "bar" + } + }, additionalConfiguration)); + }); + }); + } +}; diff --git a/packages/project/test/lib/validation/schema/__helper__/extension.js b/packages/project/test/lib/validation/schema/__helper__/extension.js new file mode 100644 index 00000000000..c36624a0f8a --- /dev/null +++ b/packages/project/test/lib/validation/schema/__helper__/extension.js @@ -0,0 +1,123 @@ +import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js"; +import customConfiguration from "./customConfiguration.js"; + +/** + * Common test functionality to be able to run the same tests for different types of kind "extension" + */ +export default { + /** + * Executes the tests for different types of kind extension, e.g. "project-shim", "server-middleware" and "task" + * + * @param {Function} test ava test + * @param {Function} assertValidation assertion function + * @param {string} type one of "project-shim", "server-middleware" and "task" + * @param {object} additionalConfiguration additional configuration content + */ + defineTests: function(test, assertValidation, type, additionalConfiguration) { + additionalConfiguration = additionalConfiguration || {}; + additionalConfiguration = Object.assign({"kind": "extension"}, additionalConfiguration); + + customConfiguration.defineTests(test, assertValidation, type, additionalConfiguration); + + SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + test(`kind: extension / type: ${type} basic (${specVersion})`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + } + }, additionalConfiguration)); + }); + + test(`kind: extension / type: ${type} additionalProperties (${specVersion})`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "resources": {} + }, additionalConfiguration), [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "resources" + } + }]); + }); + + test(`kind: extension / type: ${type} Invalid configuration: Additional property (${specVersion})`, + async (t) => { + await assertValidation(t, Object.assign( { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "notAllowed": true + }, additionalConfiguration), [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }]); + }); + }); + + SpecificationVersion.getVersionsForRange("2.0 - 2.6").forEach((specVersion) => { + test(`kind: extension / type: ${type}: Invalid metadata.name (${specVersion})`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": specVersion, + "type": type, + "metadata": { + "name": {} + } + }, additionalConfiguration), [{ + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + }]); + }); + }); + + SpecificationVersion.getVersionsForRange(">=3.0").forEach((specVersion) => { + test(`kind: extension / type: ${type}: Invalid metadata.name (${specVersion})`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": specVersion, + "type": type, + "metadata": { + "name": {} + } + }, additionalConfiguration), [{ + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }] + }, + }]); + }); + }); + } +}; diff --git a/packages/project/test/lib/validation/schema/__helper__/framework.js b/packages/project/test/lib/validation/schema/__helper__/framework.js new file mode 100644 index 00000000000..841ce8fc790 --- /dev/null +++ b/packages/project/test/lib/validation/schema/__helper__/framework.js @@ -0,0 +1,254 @@ +import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js"; + +/** + * Common test functionality for framework section in config + */ +export default { + /** + * Executes the tests for different types of kind project, + * e.g. "application", library" and "theme-library" + * + * @param {Function} test ava test + * @param {Function} assertValidation assertion function + * @param {string} type one of "application", library" and "theme-library" + */ + defineTests: function(test, assertValidation, type) { + SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + test(`${type} (specVersion ${specVersion}): framework configuration: OpenUI5`, async (t) => { + const config = { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "framework": { + "name": "OpenUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }; + await assertValidation(t, config); + }); + + test(`${type} (specVersion ${specVersion}): framework configuration: SAPUI5`, async (t) => { + const config = { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "framework": { + "name": "SAPUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true}, + {"name": "sap.ui.comp", "development": true, "optional": false}, + {"name": "sap.fe", "development": false, "optional": true}, + { + "name": "sap.ui.export", + "development": false, + "optional": false + } + ] + } + }; + await assertValidation(t, config); + }); + + test(`${type} (specVersion ${specVersion}): framework configuration: Invalid`, async (t) => { + const config = { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "framework": { + "name": "FooUI5", + "version": "1.75", + "libraries": [ + "sap.ui.core", + {"library": "sap.m"}, + {"name": "sap.f", "optional": "x"}, + {"name": "sap.f", "development": "no"} + ] + } + }; + + await assertValidation(t, config, [ + { + dataPath: "/framework/name", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "OpenUI5", + "SAPUI5", + ], + } + }, + { + dataPath: "/framework/version", + keyword: "errorMessage", + message: "Not a valid version according to the Semantic Versioning specification (https://semver.org/)", + params: { + errors: [ + { + dataPath: "/framework/version", + keyword: "pattern", + message: + "should match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*" + + "[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"", + params: { + pattern: + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*" + + "[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-]" + + "[0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + } + } + ] + } + }, + { + dataPath: "/framework/libraries/0", + keyword: "type", + message: "should be object", + params: { + type: "object", + } + }, + { + dataPath: "/framework/libraries/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "library", + } + }, + { + dataPath: "/framework/libraries/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/framework/libraries/2/optional", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + } + }, + { + dataPath: "/framework/libraries/3/development", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}): framework configuration: Missing 'name'`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "framework": {} + }, [ + { + dataPath: "/framework", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name" + } + } + ]); + }); + + test( + `${type} (specVersion ${specVersion}): framework configuration: library with optional and development`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "framework": { + "name": "OpenUI5", + "libraries": [ + { + name: "sap.ui.lib1", + development: true, + optional: true + }, + { + // This should only complain about wrong types, not that both are true + name: "sap.ui.lib2", + development: "true", + optional: "true" + } + ] + } + }, [ + { + dataPath: "/framework/libraries/0", + keyword: "errorMessage", + message: "Either \"development\" or \"optional\" can be true, but not both", + params: { + errors: [ + { + dataPath: "/framework/libraries/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "development", + } + }, + { + dataPath: "/framework/libraries/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "optional", + } + }, + ], + } + }, + { + dataPath: "/framework/libraries/1/optional", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/framework/libraries/1/development", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + ]); + }); + }); + } +}; diff --git a/packages/project/test/lib/validation/schema/__helper__/project.js b/packages/project/test/lib/validation/schema/__helper__/project.js new file mode 100644 index 00000000000..cbac64534f7 --- /dev/null +++ b/packages/project/test/lib/validation/schema/__helper__/project.js @@ -0,0 +1,313 @@ +import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js"; +import framework from "./framework.js"; +import customConfiguration from "./customConfiguration.js"; +import bundleOptions from "./builder-bundleOptions.js"; + +/** + * Common test functionality to be able to run the same tests for different types of kind "project" + */ +export default { + /** + * Executes the tests for different types of kind project, + * e.g. "application", "library", "theme-library" and "module" + * + * @param {Function} test ava test + * @param {Function} assertValidation assertion function + * @param {string} type one of "application", "library", "theme-library" and "module" + */ + defineTests: function(test, assertValidation, type) { + // framework tests + if (["application", "library", "theme-library"].includes(type)) { + framework.defineTests(test, assertValidation, type); + } + + // customConfiguration tests + customConfiguration.defineTests(test, assertValidation, type); + + // builder.bundleOptions tests + if (["application", "library"].includes(type)) { + bundleOptions.defineTests(test, assertValidation, type); + } + + // version specific tests + SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + // tests for all kinds and version 2.0 and above + test(`${type} (specVersion ${specVersion}): No metadata`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + } + }]); + }); + + test(`${type} (specVersion ${specVersion}): Metadata not type object`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": "foo" + }, [{ + dataPath: "/metadata", + keyword: "type", + message: "should be object", + params: { + type: "object", + } + }]); + }); + + test(`${type} (specVersion ${specVersion}): No metadata.name`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": {} + }, [{ + dataPath: "/metadata", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }]); + }); + + test(`${type} (specVersion ${specVersion}): Invalid metadata.copyright`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "foo", + "copyright": 123 + } + }, [ + { + dataPath: "/metadata/copyright", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}): Additional metadata property`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "foo", + "copyrihgt": "typo" + } + }, [ + { + dataPath: "/metadata", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "copyrihgt" + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}): metadata.deprecated: true`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "deprecated": true + } + }); + }); + + test(`${type} (specVersion ${specVersion}): metadata.deprecated: false`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "deprecated": false + } + }); + }); + + test(`${type} (specVersion ${specVersion}): Invalid metadata.deprecated`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "deprecated": "Yes" + } + }, [ + { + dataPath: "/metadata/deprecated", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}): metadata.sapInternal: true`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "sapInternal": true + } + }); + }); + + test(`${type} (specVersion ${specVersion}): metadata.sapInternal: false`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "sapInternal": false + } + }); + }); + + test(`${type} (specVersion ${specVersion}): Invalid metadata.sapInternal`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "sapInternal": "Yes" + } + }, [ + { + dataPath: "/metadata/sapInternal", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}): metadata.allowSapInternal: true`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "allowSapInternal": true + } + }); + }); + + test(`${type} (specVersion ${specVersion}): metadata.allowSapInternal: false`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "allowSapInternal": false + } + }); + }); + + test(`${type} (specVersion ${specVersion}): Invalid metadata.allowSapInternal`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type, + "allowSapInternal": "Yes" + } + }, [ + { + dataPath: "/metadata/allowSapInternal", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + } + ]); + }); + + test(`${type} (specVersion ${specVersion}) Invalid configuration: Additional property`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": "my-" + type + }, + "notAllowed": true + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }]); + }); + }); + + ["2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"].forEach((specVersion) => { + test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": {} + } + }, [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + } + ]); + }); + }); + + SpecificationVersion.getVersionsForRange(">=3.0").forEach((specVersion) => { + test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": {} + } + }, [ + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }] + }, + } + ]); + }); + }); + } +}; diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension.js new file mode 100644 index 00000000000..54fbb1fdc78 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension.js @@ -0,0 +1,196 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../lib/validation/ValidationError.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/extension.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension"}); + const thresholds = { + statements: 80, + branches: 70, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + test(`Type project-shim (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-project-shim" + }, + "shims": {} + }); + }); + + test(`Type server-middleware (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "my-server-middleware" + }, + "middleware": { + "path": "middleware.js" + } + }); + }); + + test(`Type task (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "task", + "metadata": { + "name": "my-task" + }, + "task": { + "path": "task.js" + } + }); + }); + + test(`No type (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + } + }]); + }); + + test(`Invalid type (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "foo", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "task", + "server-middleware", + "project-shim" + ], + } + }]); + }); + + test(`No specVersion (${specVersion})`, async (t) => { + await assertValidation(t, { + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-library" + }, + "shims": {} + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + } + }]); + }); + + test(`No metadata (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "shims": {} + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + } + }]); + }); +}); + +test("Legacy: Special characters in name (task)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "task", + "metadata": { + "name": "ä".repeat(81) + }, + "task": { + "path": "task.js" + } + }); +}); + +test("Legacy: Special characters in name (server-middleware)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "@my(middleware)" + }, + "middleware": { + "path": "middleware.js" + } + }); +}); + +test("Legacy: Special characters in name (project-shim)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my/(project)-shim" + }, + "shims": {} + }); +}); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js new file mode 100644 index 00000000000..286f3b0bddd --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js @@ -0,0 +1,243 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import extension from "../../../__helper__/extension.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/extension/project-shim.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-project-shim"}); + const thresholds = { + statements: 75, + branches: 60, + functions: 100, + lines: 70 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + test(`kind: extension / type: project-shim (${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-project-shim" + }, + "shims": { + "configurations": { + "invalid": { + "specVersion": "4.0", + "type": "does-not-exist", + "metadata": { + "name": "my-application" + } + } + }, + "dependencies": { + "my-dependency": { + "foo": "bar" + } + }, + "collections": { + "foo": { + "modules": { + "lib-1": { + "path": "src/lib1" + } + }, + "notAllowed": true + } + }, + "notAllowed": true + }, + "middleware": {} + }, [ + { + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "middleware" + } + }, + { + dataPath: "/shims", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/shims/dependencies/my-dependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/shims/collections/foo", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/shims/collections/foo/modules/lib-1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + } + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid extension name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "illegal/name" + }, + "shims": {} + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "a" + }, + "shims": {} + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "a".repeat(81) + }, + "shims": {} + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + } + }] + }, + }]); + }); +}); + +const additionalConfiguration = { + "shims": { + "configurations": { + "my-dependency": { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + } + }, + "my-other-dependency": { + "specVersion": "4.0", + "type": "does-not-exist", + "metadata": { + "name": "my-application" + } + } + }, + "dependencies": { + "my-dependency": [ + "my-other-dependency" + ], + "my-other-dependency": [ + "some-lib", + "some-other-lib" + ] + }, + "collections": { + "my-dependency": { + "modules": { + "lib-1": "src/lib1", + "lib-2": "src/lib2" + } + } + } + } +}; + +extension.defineTests(test, assertValidation, "project-shim", additionalConfiguration); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js new file mode 100644 index 00000000000..ceb1e6dc3c5 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js @@ -0,0 +1,135 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import extension from "../../../__helper__/extension.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/extension/server-middleware.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-server-middleware"}); + const thresholds = { + statements: 70, + branches: 55, + functions: 100, + lines: 70 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid extension name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "illegal-🦜" + }, + "middleware": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "a" + }, + "middleware": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "a".repeat(81) + }, + "middleware": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + } + }] + }, + }]); + }); +}); + +const additionalConfiguration = { + "middleware": { + "path": "/foo" + } +}; + +extension.defineTests(test, assertValidation, "server-middleware", additionalConfiguration); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js new file mode 100644 index 00000000000..e8ad322537e --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js @@ -0,0 +1,135 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import extension from "../../../__helper__/extension.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/extension/task.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-task"}); + const thresholds = { + statements: 70, + branches: 55, + functions: 100, + lines: 70 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid extension name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "task", + "metadata": { + "name": "illegal-🦜" + }, + "task": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "task", + "metadata": { + "name": "a" + }, + "task": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "kind": "extension", + "type": "task", + "metadata": { + "name": "a".repeat(81) + }, + "task": { + "path": "/bar" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid extension name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + } + }] + }, + }]); + }); +}); + +const additionalConfiguration = { + "task": { + "path": "/foo" + } +}; + +extension.defineTests(test, assertValidation, "task", additionalConfiguration); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project.js b/packages/project/test/lib/validation/schema/specVersion/kind/project.js new file mode 100644 index 00000000000..ba9d09ca579 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project.js @@ -0,0 +1,237 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import AjvCoverage from "../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../lib/validation/ValidationError.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project"}); + const thresholds = { + statements: 85, + branches: 75, + functions: 100, + lines: 90 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Type application", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "application", + "metadata": { + "name": "my-application" + } + }); +}); + +test("Type application (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + } + }); +}); + +test("Type library", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "library", + "metadata": { + "name": "my-library" + } + }); +}); + +test("Type library (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my-library" + } + }); +}); + +test("Type theme-library", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + } + }); +}); + +test("Type theme-library (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + } + }); +}); + +test("Type module", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + } + }); +}); + +test("Type module (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my-module" + } + }); +}); + +test("No type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + } + }]); +}); + +test("No type, no kind", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + } + }]); +}); + +test("Invalid type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "foo", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "application", + "library", + "theme-library", + "module", + ], + } + }]); +}); + +test("No specVersion", async (t) => { + await assertValidation(t, { + "kind": "project", + "type": "library", + "metadata": { + "name": "my-library" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + } + }]); +}); + +test("Legacy: Special characters in name (application)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "/".repeat(81) + } + }); +}); + +test("Legacy: Special characters in name (library)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my/(library)" + } + }); +}); + +test("Legacy: Special characters in name (theme-library)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my/(theme)-library" + } + }); +}); + +test("Legacy: Special characters in name (module)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my/(module)" + } + }); +}); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js new file mode 100644 index 00000000000..db7a75b6826 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js @@ -0,0 +1,1523 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import project from "../../../__helper__/project.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project/application.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-application"}); + const thresholds = { + statements: 80, + branches: 75, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) { + test(`Valid configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "okay" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "webapp": "/my/path" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "my-raw-section", + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false, + "declareRawModules": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "async": false + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "cachebuster": { + "signatureType": "hash" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + { + "name": "custom-task-2", + "beforeTask": "not-valid", + "configuration": false + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + }, + { + "name": "myCustomMiddleware-2", + "beforeMiddleware": "myCustomMiddleware", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`Invalid resources configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation( + t, + { + specVersion: specVersion, + type: "application", + metadata: { + name: "com.sap.ui5.test", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "FOO", + paths: { + app: "webapp", + webapp: { + path: "invalid", + }, + }, + notAllowed: true, + }, + notAllowed: true, + }, + builder: { + // cachebuster is only supported for type application + cachebuster: { + signatureType: "time", + }, + bundles: [ + { + bundleDefinition: { + name: "app.js", + defaultFileTypes: [".js"], + sections: [ + { + name: "some-app-preload", + mode: "preload", + filters: ["some/app/Component.js"], + resolve: true, + sort: true, + declareRawModules: false, + async: false, + }, + { + mode: "require", + filters: ["ui5loader-autoconfig.js"], + resolve: true, + async: false, + }, + ], + }, + bundleOptions: { + optimize: true, + numberOfParts: 3, + }, + }, + ], + }, + }, + [ + { + dataPath: "/resources", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: + "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: ["UTF-8", "ISO-8859-1"], + }, + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "app", + }, + }, + { + dataPath: "/resources/configuration/paths/webapp", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "async", + }, + }, + ] + ); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "paths": "webapp" + } + } + }, [ + { + dataPath: "/resources/configuration/paths", + keyword: "type", + message: "should be object", + params: { + type: "object" + } + } + ]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 3.2").forEach(function(specVersion) { + test(`Valid configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "okay" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "webapp": "/my/path" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "my-raw-section", + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false, + "declareRawModules": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true, + "usePredefineCalls": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "cachebuster": { + "signatureType": "hash" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + { + "name": "custom-task-2", + "beforeTask": "not-valid", + "configuration": false + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + }, + { + "name": "myCustomMiddleware-2", + "beforeMiddleware": "myCustomMiddleware", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`Invalid resources configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "FOO", + "paths": { + "app": "webapp", + "webapp": { + "path": "invalid" + } + }, + "notAllowed": true + }, + "notAllowed": true + } + }, [ + { + dataPath: "/resources", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1" + ], + } + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "app", + } + }, + { + dataPath: "/resources/configuration/paths/webapp", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + } + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "paths": "webapp" + } + } + }, [ + { + dataPath: "/resources/configuration/paths", + keyword: "type", + message: "should be object", + params: { + type: "object" + } + } + ]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 2.2").forEach(function(specVersion) { + test(`Unsupported builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }, [ + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "excludes", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.3").forEach(function(specVersion) { + test(`application (specVersion ${specVersion}): builder/componentPreload/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/componentPreload/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.4").forEach(function(specVersion) { + // Unsupported cases for older spec-versions already tested via "allowedValues" comparison above + test(`application (specVersion ${specVersion}): builder/bundles/bundleDefinition/sections/mode: bundleInfo`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleDefinition": { + "name": "my-bundle.js", + "sections": [{ + "name": "my-bundle-info", + "mode": "bundleInfo", + "filters": [] + }] + } + }] + } + }); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) { + test(`application (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + "sap.a", + "sap.b" + ], + "includeDependencyRegExp": [ + ".ui.[a-z]+", + "^sap.[mf]$" + ], + "includeDependencyTree": [ + "sap.c", + "sap.d" + ] + } + } + }); + }); + test(`Invalid builder/settings/includeDependency* configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": "a", + "includeDependencyRegExp": "b", + "includeDependencyTree": "c" + } + } + }, [ + { + dataPath: "/builder/settings/includeDependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + true, + 1, + {} + ], + "includeDependencyRegExp": [ + true, + 1, + {} + ], + "includeDependencyTree": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/settings", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/settings/includeDependency/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.6").forEach(function(specVersion) { + test(`application (specVersion ${specVersion}): builder/minification/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/minification/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/minification/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/minification", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/minification/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid project name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "illegal/name" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "a" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "a".repeat(81) + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + }] + }, + }]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 3.1").forEach(function(specVersion) { + test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // jsdoc is not supported for type application + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": true, + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true, + "declareModules": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": [] + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "libraryPreload": {} // Only supported for type library + } + }, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "libraryPreload" + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "declareModules", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0/declareRawModules", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: ["3.1", "3.0", "2.6", "2.5", "2.4"].includes(specVersion) ? [ + "raw", + "preload", + "require", + "provided", + "bundleInfo" + ] : [ + "raw", + "preload", + "require", + "provided" + ] + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + } + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + } + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.2").forEach(function(specVersion) { + test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // jsdoc is not supported for type application + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": true, + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true, + "declareModules": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": [] + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "libraryPreload": {} // Only supported for type library + } + }, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "libraryPreload" + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "declareModules", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0/declareRawModules", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "raw", + "preload", + "require", + "provided", + "bundleInfo", + "depCache" + ] + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + } + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + } + ]); + }); +}); + +project.defineTests(test, assertValidation, "application"); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js new file mode 100644 index 00000000000..b51e43bcb96 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js @@ -0,0 +1,1751 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import project from "../../../__helper__/project.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project/library.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-library"}); + const thresholds = { + statements: 80, + branches: 75, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): Valid configuration`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "src/main/uilib", + "test": "src/test/uilib" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "my-raw-section", + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false, + "declareRawModules": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false, + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "async": false + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`library (specVersion ${specVersion}): Invalid builder configuration`, async (t) => { + const config = { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // cachebuster is only supported for type application + "cachebuster": { + "signatureType": "time" + }, + "bundles": [ + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false, + "async": false + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "async": false + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + } + }; + await assertValidation(t, config, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "cachebuster", + }, + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "async", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 3.2").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): Valid configuration`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "src/main/uilib", + "test": "src/test/uilib" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "my-raw-section", + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false, + "declareRawModules": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true, + "usePredefineCalls": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`library (specVersion ${specVersion}): Invalid builder configuration`, async (t) => { + const config = { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // cachebuster is only supported for type application + "cachebuster": { + "signatureType": "time" + } + } + }; + await assertValidation(t, config, [{ + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "cachebuster" + } + }]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 2.2").forEach(function(specVersion) { + test(`Unsupported builder/libraryPreload configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "libraryPreload": {} + } + }, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "libraryPreload", + }, + }, + ]); + }); + test(`Unsupported builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }, [ + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "excludes", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.3").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): builder/libraryPreload/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "libraryPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/libraryPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "libraryPreload": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/libraryPreload/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "libraryPreload": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/libraryPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/libraryPreload/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/libraryPreload/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/libraryPreload/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); + + + test(`library (specVersion ${specVersion}): builder/componentPreload/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/componentPreload/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.4").forEach(function(specVersion) { + // Unsupported cases for older spec-versions already tested via "allowedValues" comparison above + test(`library (specVersion ${specVersion}): builder/bundles/bundleDefinition/sections/mode: bundleInfo`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleDefinition": { + "name": "my-bundle.js", + "sections": [{ + "name": "my-bundle-info", + "mode": "bundleInfo", + "filters": [] + }] + } + }] + } + }); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + "sap.a", + "sap.b" + ], + "includeDependencyRegExp": [ + ".ui.[a-z]+", + "^sap.[mf]$" + ], + "includeDependencyTree": [ + "sap.c", + "sap.d" + ] + } + } + }); + }); + test(`Invalid builder/settings/includeDependency* configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": "a", + "includeDependencyRegExp": "b", + "includeDependencyTree": "c" + } + } + }, [ + { + dataPath: "/builder/settings/includeDependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + true, + 1, + {} + ], + "includeDependencyRegExp": [ + true, + 1, + {} + ], + "includeDependencyTree": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/settings", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/settings/includeDependency/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.6").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): builder/minification/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/minification/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/minification/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/minification", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/minification/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid project name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "illegal-🦜" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "a" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "a".repeat(81) + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + }] + }, + }]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 3.1").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): Invalid configuration`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF8", + "paths": { + "src": {"path": "src"}, + "test": {"path": "test"}, + "webapp": "app" + } + } + }, + "builder": { + "resources": { + "excludes": "/resources/some/project/name/test_results/**" + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": true, + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true, + "declareModules": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": [] + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "jsdoc": { + "excludes": "some/project/name/thirdparty/**" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "afterTask": "replaceCopyright", + }, + { + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + "my-task" + ] + }, + "server": { + "settings": { + "httpPort": "1337", + "httpsPort": "1443" + } + } + }, [ + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1", + ], + } + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "webapp", + } + }, + { + dataPath: "/resources/configuration/paths/src", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/resources/configuration/paths/test", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/resources/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/jsdoc/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "declareModules", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0/declareRawModules", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: ["3.1", "3.0", "2.6", "2.5", "2.4"].includes(specVersion) ? [ + "raw", + "preload", + "require", + "provided", + "bundleInfo" + ] : [ + "raw", + "preload", + "require", + "provided" + ] + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + } + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + } + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "beforeTask", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'beforeTask'", + params: { + missingProperty: "beforeTask", + } + }, + { + dataPath: "/builder/customTasks/2", + keyword: "type", + message: "should be object", + params: { + type: "object", + } + }, + { + dataPath: "/server/settings/httpPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/server/settings/httpsPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + } + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.2").forEach(function(specVersion) { + test(`library (specVersion ${specVersion}): Invalid configuration`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF8", + "paths": { + "src": {"path": "src"}, + "test": {"path": "test"}, + "webapp": "app" + } + } + }, + "builder": { + "resources": { + "excludes": "/resources/some/project/name/test_results/**" + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": true, + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true, + "declareModules": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": [] + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "jsdoc": { + "excludes": "some/project/name/thirdparty/**" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "afterTask": "replaceCopyright", + }, + { + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + "my-task" + ] + }, + "server": { + "settings": { + "httpPort": "1337", + "httpsPort": "1443" + } + } + }, [ + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1", + ], + } + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "webapp", + } + }, + { + dataPath: "/resources/configuration/paths/src", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/resources/configuration/paths/test", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/resources/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/jsdoc/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "declareModules", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0/declareRawModules", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "raw", + "preload", + "require", + "provided", + "bundleInfo", + "depCache" + ] + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + } + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + } + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "beforeTask", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'beforeTask'", + params: { + missingProperty: "beforeTask", + } + }, + { + dataPath: "/builder/customTasks/2", + keyword: "type", + message: "should be object", + params: { + type: "object", + } + }, + { + dataPath: "/server/settings/httpPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/server/settings/httpsPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + } + ]); + }); +}); + +project.defineTests(test, assertValidation, "library"); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js new file mode 100644 index 00000000000..2899c22a040 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js @@ -0,0 +1,442 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import project from "../../../__helper__/project.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project/module.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-module"}); + const thresholds = { + statements: 75, + branches: 65, + functions: 100, + lines: 75 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + test(`Valid configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + }, + "resources": { + "configuration": { + "paths": { + "/resources/my/library/module-xy/": "lib", + "/resources/my/library/module-xy-min/": "dist" + } + } + } + }); + }); + + test(`No framework configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "framework": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "framework" + } + }]); + }); + + test(`No propertiesFileSourceEncoding configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8" + } + } + }, [{ + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "propertiesFileSourceEncoding" + } + }]); + }); +}); + +SpecificationVersion.getVersionsForRange("2.0 - 2.4").forEach((specVersion) => { + test(`No server configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "server": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "server" + } + }]); + }); + + test(`No builder configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "builder" + } + }]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) { + test(`Server configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`module (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": { + "settings": { + "includeDependency": [ + "sap.a", + "sap.b" + ], + "includeDependencyRegExp": [ + ".ui.[a-z]+", + "^sap.[mf]$" + ], + "includeDependencyTree": [ + "sap.c", + "sap.d" + ] + } + } + }); + }); + + test(`Invalid builder/settings/includeDependency* configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": { + "settings": { + "includeDependency": "a", + "includeDependencyRegExp": "b", + "includeDependencyTree": "c" + } + } + }, [ + { + dataPath: "/builder/settings/includeDependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": { + "settings": { + "includeDependency": [ + true, + 1, + {} + ], + "includeDependencyRegExp": [ + true, + 1, + {} + ], + "includeDependencyTree": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/settings", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/settings/includeDependency/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid project name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "illegal-🦜" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + } + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "a" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "module", + "metadata": { + "name": "a".repeat(81) + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + }] + }, + }]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.1").forEach(function(specVersion) { + test(`Builder resource excludes (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "!/test-resources/some/project/name/demo-app/**" + ] + } + } + }); + }); +}); + +project.defineTests(test, assertValidation, "module"); diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js new file mode 100644 index 00000000000..19bc8a09467 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js @@ -0,0 +1,418 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import project from "../../../__helper__/project.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project/theme-library.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-theme-library"}); + const thresholds = { + statements: 80, + branches: 70, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=2.0").forEach(function(specVersion) { + test(`Valid configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "my-theme-library", + "copyright": "Copyright goes here" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "src/main/uilib", + "test": "src/test/uilib" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // cachebuster is only supported for type application + "cachebuster": { + "signatureType": "time" + }, + // jsdoc is only supported for type library + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + // componentPreload is only supported for types application/library + "componentPreload": {}, + // libraryPreload is only supported for type library + "libraryPreload": {}, + } + }, [{ + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "cachebuster" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "componentPreload" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "libraryPreload" + } + }]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) { + test(`theme-library (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "theme-library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + "sap.a", + "sap.b" + ], + "includeDependencyRegExp": [ + ".ui.[a-z]+", + "^sap.[mf]$" + ], + "includeDependencyTree": [ + "sap.c", + "sap.d" + ] + } + } + }); + }); + test(`Invalid builder/settings/includeDependency* configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": "a", + "includeDependencyRegExp": "b", + "includeDependencyTree": "c" + } + } + }, [ + { + dataPath: "/builder/settings/includeDependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + true, + 1, + {} + ], + "includeDependencyRegExp": [ + true, + 1, + {} + ], + "includeDependencyTree": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/settings", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/settings/includeDependency/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); +}); + +SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) { + test(`Invalid project name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "illegal-🦜" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "a" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "theme-library", + "metadata": { + "name": "a".repeat(81) + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + }] + }, + }]); + }); +}); +project.defineTests(test, assertValidation, "theme-library"); diff --git a/packages/project/test/lib/validation/schema/ui5-workspace.js b/packages/project/test/lib/validation/schema/ui5-workspace.js new file mode 100644 index 00000000000..3bc5877542e --- /dev/null +++ b/packages/project/test/lib/validation/schema/ui5-workspace.js @@ -0,0 +1,464 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import AjvCoverage from "../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../lib/validation/validator.js"; +import ValidationError from "../../../../lib/validation/ValidationError.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({ + config, + project: {id: "my-project"} + }); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError", + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5-workspace"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/ui5-workspace.json"], + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", { + dir: "coverage/ajv-ui5-workspace", + }); + const thresholds = { + statements: 85, + branches: 75, + functions: 100, + lines: 85, + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Empty config", async (t) => { + await assertValidation( + t, + { + specVersion: "0.1", + }, + [ + { + dataPath: "/specVersion", + keyword: "errorMessage", + message: `Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "workspace/1.0" +For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: + "should be equal to one of the allowed values", + params: { + allowedValues: ["workspace/1.0"], + }, + }, + ], + }, + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + }, + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'dependencyManagement'", + params: { + missingProperty: "dependencyManagement", + }, + }, + ] + ); +}); + +test("Valid spec", async (t) => { + await assertValidation(t, { + specVersion: "workspace/1.0", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: [ + { + path: "path/to/resource/1", + }, + { + path: "path/to/resource/2", + }, + ], + }, + }); +}); + +test("Missing metadata.name", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: {}, + dependencyManagement: { + resolutions: [ + { + path: "path/to/resource/1", + }, + ], + }, + }, + [ + { + dataPath: "/metadata", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + }, + ] + ); +}); + +test("Invalid metadata.name: Illegal characters", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "🦭🦭🦭" + }, + dependencyManagement: { + resolutions: [ + { + path: "path/to/resource/1", + }, + ], + }, + }, + [ + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name", + params: { + errors: [ + { + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + }, + }, + ], + }, + }, + ] + ); +}); + +test("Invalid metadata.name: Too short", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "a" + }, + dependencyManagement: { + resolutions: [ + { + path: "path/to/resource/1", + }, + ], + }, + }, + [ + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name", + params: { + errors: [ + { + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }, + ], + }, + }, + ] + ); +}); + + +test("Invalid metadata.name: Too long", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "b".repeat(81) + }, + dependencyManagement: { + resolutions: [ + { + path: "path/to/resource/1", + }, + ], + }, + }, + [ + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name", + params: { + errors: [ + { + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + } + ], + }, + }, + ] + ); +}); + +test("Invalid fields", async (t) => { + await assertValidation( + t, + { + specVersion: 12, + metadata: { + name: {}, + }, + dependencyManagement: { + resolutions: { + path: "path/to/resource/1", + }, + }, + }, + [ + { + dataPath: "/specVersion", + keyword: "errorMessage", + message: `Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "workspace/1.0" +For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: + "should be equal to one of the allowed values", + params: { + allowedValues: ["workspace/1.0"], + }, + }, + ], + }, + }, + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name", + params: { + errors: [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ], + }, + }, + { + dataPath: "/dependencyManagement/resolutions", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/dependencyManagement/resolutions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + }, + }, + ] + ); +}); + +test("Invalid types", async (t) => { + await assertValidation( + t, + { + specVersion: 42, + metadata: { + name: 15, + }, + dependencyManagement: "simple string", + }, + [ + { + dataPath: "/specVersion", + keyword: "errorMessage", + message: `Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "workspace/1.0" +For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: + "should be equal to one of the allowed values", + params: { + allowedValues: ["workspace/1.0"], + }, + }, + ], + }, + }, + { + dataPath: "/metadata/name", + keyword: "errorMessage", + message: "Not a valid workspace name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#name", + params: { + errors: [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ], + }, + }, + { + dataPath: "/dependencyManagement", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + }, + ] + ); +}); + +test("Invalid dependencyManagement", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: "Invalid type", + }, + }, + [ + { + dataPath: "/dependencyManagement/resolutions", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ] + ); + + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: ["invalid type"], + }, + }, + [ + { + dataPath: "/dependencyManagement/resolutions/0", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + }, + ] + ); + + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: [{path: 12}], + }, + }, + [ + { + dataPath: "/dependencyManagement/resolutions/0/path", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ] + ); +}); diff --git a/packages/project/test/lib/validation/schema/ui5.js b/packages/project/test/lib/validation/schema/ui5.js new file mode 100644 index 00000000000..7cdf1df7423 --- /dev/null +++ b/packages/project/test/lib/validation/schema/ui5.js @@ -0,0 +1,196 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import AjvCoverage from "../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../lib/validation/validator.js"; +import ValidationError from "../../../../lib/validation/ValidationError.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/ui5.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-ui5"}); + const thresholds = { + statements: 95, + branches: 80, + functions: 100, + lines: 95 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Undefined", async (t) => { + await assertValidation(t, undefined, [{ + dataPath: "", + keyword: "type", + message: "should be object", + params: { + type: "object", + } + }]); +}); + +test("Missing specVersion, type", async (t) => { + await assertValidation(t, {}, [ + { + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + } + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + } + } + + ]); +}); + +test("Missing type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0" + }, [ + { + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + } + } + ]); +}); + +test("Invalid specVersion", async (t) => { + await assertValidation(t, { + "specVersion": "0.0" + }, [ + { + dataPath: "/specVersion", + keyword: "errorMessage", + message: +"Unsupported \"specVersion\"\n" + +"Your UI5 CLI installation might be outdated.\n" + +"Supported specification versions: \"4.0\", \"3.2\", \"3.1\", \"3.0\", \"2.6\", " + +"\"2.5\", \"2.4\", \"2.3\", \"2.2\", \"2.1\", \"2.0\", \"1.1\", \"1.0\", \"0.1\"\n" + +"For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions", + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "4.0", + "3.2", + "3.1", + "3.0", + "2.6", + "2.5", + "2.4", + "2.3", + "2.2", + "2.1", + "2.0", + "1.1", + "1.0", + "0.1", + ], + } + }, + ], + } + } + ]); +}); + +test("Invalid type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "foo" + }, [ + { + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "application", + "library", + "theme-library", + "module" + ] + } + } + ]); +}); + +test("Invalid kind", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "foo" + }, [ + { + dataPath: "/kind", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "project", + "extension", + null + ], + } + } + ]); +}); + +test("specVersion 0.1", async (t) => { + await assertValidation(t, { + "specVersion": "0.1" + }); +}); + +test("specVersion 1.0", async (t) => { + await assertValidation(t, { + "specVersion": "1.0" + }); +}); + +test("specVersion 1.1", async (t) => { + await assertValidation(t, { + "specVersion": "1.1" + }); +}); diff --git a/packages/project/test/lib/validation/validator.js b/packages/project/test/lib/validation/validator.js new file mode 100644 index 00000000000..858ce8b3ed6 --- /dev/null +++ b/packages/project/test/lib/validation/validator.js @@ -0,0 +1,138 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + const Ajv = t.context.Ajv = sinon.stub(); + const ajvErrors = t.context.ajvErrors = sinon.stub(); + + t.context.validatorModule = await esmock.p("../../../lib/validation/validator.js", { + "ajv": Ajv, + "ajv-errors": ajvErrors + }); + const {validate, validateWorkspace, _Validator: Validator} = t.context.validatorModule; + + t.context.validate = validate; + t.context.validateWorkspace = validateWorkspace; + t.context.Validator = Validator; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.validatorModule); +}); + +test("validate function calls Validator#validate method", async (t) => { + const {sinon, Validator, validate} = t.context; + const config = {config: true}; + const project = {project: true}; + const yaml = {yaml: true}; + + const validateStub = sinon.stub(Validator.prototype, "validate"); + validateStub.resolves(); + + const result = await validate({config, project, yaml}); + + t.is(result, undefined, "validate should return undefined"); + t.is(validateStub.callCount, 1, "validate should be called once"); + t.deepEqual(validateStub.getCall(0).args, [{config, project, yaml}]); +}); + +test("validateWorkspace function calls Validator#validate method without project", async (t) => { + const {sinon, Validator, validateWorkspace} = t.context; + const config = {config: true}; + const yaml = {yaml: true}; + + const validateStub = sinon.stub(Validator.prototype, "validate"); + validateStub.resolves(); + + const result = await validateWorkspace({config, yaml}); + + t.is(result, undefined, "validate should return undefined"); + t.is(validateStub.callCount, 1, "validate should be called once"); + t.deepEqual(validateStub.getCall(0).args, [{config, yaml}]); +}); + +test("validateWorkspace throw an Error", async (t) => { + const {validateWorkspace} = await esmock("../../../lib/validation/validator.js"); + const config = {config: true}; + const yaml = {yaml: true}; + + const err = await t.throwsAsync(async () => { + return await validateWorkspace({config, yaml}); + }); + + t.is(err.message.includes("Invalid workspace configuration."), true); +}); + +test("Validator requires schemaName", (t) => { + const {sinon, Validator} = t.context; + + const Ajv = sinon.stub(); + const ajvErrors = sinon.stub(); + const invalidContructor = () => { + new Validator({Ajv, ajvErrors}); + }; + + t.throws(invalidContructor, { + message: + "\"schemaName\" is missing or incorrect. The available schemaName variants are ui5, ui5-workspace", + }); +}); + +test("Validator requires a valid schemaName", (t) => { + const {sinon, Validator} = t.context; + + const Ajv = sinon.stub(); + const ajvErrors = sinon.stub(); + const invalidContructor = () => { + new Validator({Ajv, ajvErrors, schemaName: "invalid schema name"}); + }; + + t.throws(invalidContructor, { + message: + "\"schemaName\" is missing or incorrect. The available schemaName variants are ui5, ui5-workspace", + }); +}); + +test("Validator#_compileSchema cache test", async (t) => { + const {sinon, Validator} = t.context; + + const schema1 = {schema1: true}; + + const loadSchemaStub = sinon.stub(Validator, "loadSchema"); + loadSchemaStub.onCall(0).resolves(schema1); + loadSchemaStub.resolves({schema2: true}); + + const schema1Fn = sinon.stub().named("schema1Fn"); + + const compileAsyncStub = sinon.stub().resolves(); + compileAsyncStub.onCall(0).resolves(schema1Fn); + compileAsyncStub.resolves(sinon.stub().named("schema2Fn")); + + const Ajv = sinon.stub().returns({ + compileAsync: compileAsyncStub + }); + const ajvErrors = sinon.stub(); + + const validator = new Validator({Ajv, ajvErrors, schemaName: "ui5-workspace"}); + + const compile1 = validator._compileSchema(); + const compile2 = validator._compileSchema(); + const compile3 = validator._compileSchema(); + + const compile1Result = await compile1; + const compile2Result = await compile2; + const compile3Result = await compile3; + + t.is(compile1Result, compile2Result); + t.is(compile2Result, compile3Result); + + t.is(loadSchemaStub.callCount, 1); + t.deepEqual(loadSchemaStub.getCall(0).args, ["ui5-workspace.json"]); + + t.is(compileAsyncStub.callCount, 1); + t.deepEqual(compileAsyncStub.getCall(0).args, [schema1]); +}); diff --git a/packages/project/test/utils/AjvCoverage.js b/packages/project/test/utils/AjvCoverage.js new file mode 100644 index 00000000000..90ff01fc234 --- /dev/null +++ b/packages/project/test/utils/AjvCoverage.js @@ -0,0 +1,145 @@ +// Inspired by https://github.com/epoberezkin/ajv-istanbul + +import crypto from "node:crypto"; +import beautify from "js-beautify"; +import libReport from "istanbul-lib-report"; +import reports from "istanbul-reports"; +import libCoverage from "istanbul-lib-coverage"; +import {createInstrumenter} from "istanbul-lib-instrument"; + +const rSchemaName = new RegExp(/sourceURL=([^\s]*)/); +const rRootDataUndefined = /\n(?:\s)*if \(rootData === undefined\) rootData = data;/g; +const rEnsureErrorArray = /\n(?:\s)*if \(vErrors === null\) vErrors = \[err\];(?:\s)*else vErrors\.push\(err\);/g; +const rDataPathOrEmptyString = /dataPath: \(dataPath \|\| ''\)/g; + +function hash(content) { + return crypto.createHash("sha1").update(content).digest("hex").substr(0, 16); +} + +function randomCoverageVar() { + return "__ajv-coverage__" + hash((String(Date.now()) + Math.random())); +} + +class AjvCoverage { + constructor(ajv, options = {}) { + this.ajv = ajv; + this.ajv._opts.processCode = this._processCode.bind(this); + if (options.meta === true) { + this.ajv._metaOpts.processCode = this._processCode.bind(this); + } + this._processFileName = options.processFileName; + this._includes = options.includes; + this._sources = {}; + this._globalCoverageVar = options.globalCoverage === true ? "__coverage__" : randomCoverageVar(); + this._instrumenter = createInstrumenter({ + coverageVariable: this._globalCoverageVar + }); + } + getSummary() { + const coverageMap = this._createCoverageMap(); + const summary = libCoverage.createCoverageSummary(); + + const files = coverageMap.files(); + files.forEach(function(file) { + const fileCoverageSummary = coverageMap.fileCoverageFor(file).toSummary(); + summary.merge(fileCoverageSummary); + return; + }); + + if (files.length === 0 || summary.lines.covered === 0) { + throw new Error("AjvCoverage#getSummary: No coverage data found!"); + } + + return { + branches: summary.branches.pct, + lines: summary.lines.pct, + statements: summary.statements.pct, + functions: summary.functions.pct + }; + } + verify(thresholds) { + const thresholdEntries = Object.entries(thresholds); + if (thresholdEntries.length === 0) { + throw new Error("AjvCoverage#verify: No thresholds defined!"); + } + + const summary = this.getSummary(); + const errors = []; + + thresholdEntries.forEach(function([threshold, expectedPct]) { + const pct = summary[threshold]; + if (pct === undefined) { + errors.push(`Invalid coverage threshold '${threshold}'`); + } else if (pct < expectedPct) { + errors.push( + `Coverage for '${threshold}' (${pct}%) ` + + `does not meet global threshold (${expectedPct}%)`); + } + }); + + if (errors.length > 0) { + const errorMessage = "ERROR:\n" + errors.join("\n"); + throw new Error(errorMessage); + } + } + createReport(name, contextOptions = {}, reportOptions = {}) { + const coverageMap = this._createCoverageMap(); + const context = libReport.createContext(Object.assign({}, contextOptions, { + coverageMap, + sourceFinder: (filePath) => { + if (this._sources[filePath]) { + return this._sources[filePath]; + } + const sourceFinder = contextOptions.sourceFinder; + if (typeof sourceFinder === "function") { + return sourceFinder(filePath); + } + } + })); + const report = reports.create(name, reportOptions); + report.execute(context); + } + _createCoverageMap() { + return libCoverage.createCoverageMap(global[this._globalCoverageVar]); + } + _processCode(originalCode) { + let fileName; + const schemaNameMatch = rSchemaName.exec(originalCode); + if (schemaNameMatch) { + fileName = schemaNameMatch[1]; + } else { + // Probably a definition of a schema that is compiled separately + // Try to find the schema that is currently compiling + const schemas = Object.entries(this.ajv._schemas); + const compilingSchemas = schemas.filter(([, schema]) => schema.compiling); + if (compilingSchemas.length > 0) { + // Last schema is the current one + const lastSchemaEntry = compilingSchemas[compilingSchemas.length - 1]; + fileName = lastSchemaEntry[0] + "-" + hash(originalCode); + } else { + fileName = hash(originalCode); + } + } + + if (typeof this._processFileName === "function") { + fileName = this._processFileName.call(null, fileName); + } + + if (this._includes && this._includes.every((pattern) => !fileName.includes(pattern))) { + return originalCode; + } + + const code = AjvCoverage.insertIgnoreComments(beautify(originalCode, {indent_size: 2})); + const instrumentedCode = this._instrumenter.instrumentSync(code, fileName); + this._sources[fileName] = code; + return instrumentedCode; + } + static insertIgnoreComments(code) { + code = code.replace(rRootDataUndefined, "\n/* istanbul ignore next */$&"); + code = code.replace(rEnsureErrorArray, "\n/* istanbul ignore next */$&"); + code = code.replace(rDataPathOrEmptyString, "dataPath: (dataPath || /* istanbul ignore next */ '')"); + return code; + } +} + +export default AjvCoverage;