├── .chglog ├── CHANGELOG.tpl.md ├── RELEASE.tpl.md ├── config.yml └── release-config.yml ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── in-solidarity.yml └── workflows │ ├── dependabot-auto-merge.yml │ ├── github-ci.yml │ └── reuse-compliance.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── LICENSES └── Apache-2.0.txt ├── README.md ├── REUSE.toml ├── azure-pipelines.yml ├── eslint.common.config.js ├── eslint.config.js ├── jsdoc-plugin.cjs ├── jsdoc.json ├── lib ├── AbstractReader.js ├── AbstractReaderWriter.js ├── DuplexCollection.js ├── ReaderCollection.js ├── ReaderCollectionPrioritized.js ├── Resource.js ├── ResourceFacade.js ├── ResourceTagCollection.js ├── WriterCollection.js ├── adapters │ ├── AbstractAdapter.js │ ├── FileSystem.js │ └── Memory.js ├── fsInterface.js ├── readers │ ├── Filter.js │ └── Link.js ├── resourceFactory.js └── tracing │ ├── Trace.js │ └── traceSummary.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── application.a │ ├── package.json │ ├── ui5.yaml │ └── webapp │ │ ├── index.html │ │ └── test.js ├── application.b │ ├── package.json │ ├── ui5.yaml │ └── webapp │ │ ├── embedded │ │ ├── i18n │ │ │ ├── i18n.properties │ │ │ └── i18n_de.properties │ │ ├── i18n_fr.properties │ │ └── manifest.json │ │ ├── i18n.properties │ │ ├── i18n │ │ ├── i18n.properties │ │ ├── i18n_de.properties │ │ └── l10n.properties │ │ └── manifest.json ├── fsInterfáce │ ├── bâr.txt │ └── foo.txt ├── glob │ ├── application.a │ │ ├── package.json │ │ ├── ui5.yaml │ │ └── webapp │ │ │ ├── index.html │ │ │ └── test.js │ ├── application.b │ │ ├── package.json │ │ ├── ui5.yaml │ │ └── webapp │ │ │ ├── embedded │ │ │ ├── i18n │ │ │ │ ├── i18n.properties │ │ │ │ └── i18n_de.properties │ │ │ ├── i18n_fr.properties │ │ │ └── manifest.json │ │ │ ├── i18n.properties │ │ │ ├── i18n │ │ │ ├── i18n.properties │ │ │ ├── i18n_de.properties │ │ │ └── l10n.properties │ │ │ └── manifest.json │ └── package.json └── library.l │ ├── .gitignore │ ├── package.json │ ├── src │ └── library │ │ └── l │ │ ├── .library │ │ └── some.js │ ├── test │ └── library │ │ └── l │ │ ├── Test.html │ │ └── Test2.html │ └── ui5.yaml └── lib ├── AbstractReader.js ├── AbstractReaderWriter.js ├── DuplexCollection.js ├── ReaderCollection.js ├── ReaderCollectionPrioritized.js ├── Resource.js ├── ResourceFacade.js ├── ResourceTagCollection.js ├── WriterCollection.js ├── adapters ├── AbstractAdapter.js ├── FileSystem.js ├── FileSystem_read.js ├── FileSystem_write.js ├── FileSystem_write_large_file.js ├── Memory_read.js └── Memory_write.js ├── fsInterface.js ├── glob.js ├── package-exports.js ├── readers ├── Filter.js └── Link.js ├── resourceFactory.js ├── resources.js └── tracing └── traceSummary.js /.chglog/RELEASE.tpl.md: -------------------------------------------------------------------------------- 1 | {{ range .Versions }} 2 | {{ range .CommitGroups -}} 3 | ### {{ .Title }} 4 | {{ range .Commits -}} 5 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [`{{ .Hash.Short }}`]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }}) 6 | {{ end }} 7 | {{ end -}} 8 | 9 | {{- if .RevertCommits -}} 10 | ### Reverts 11 | {{ range .RevertCommits -}} 12 | - {{ .Revert.Header }} 13 | {{ end }} 14 | {{ end -}} 15 | 16 | {{- if .NoteGroups -}} 17 | {{ range .NoteGroups -}} 18 | ### {{ .Title }} 19 | {{ range .Notes }} 20 | {{ .Body }} 21 | {{ end }} 22 | {{ end -}} 23 | {{ end -}} 24 | 25 | {{ if .Tag.Previous }} 26 | ### All changes 27 | [`{{ .Tag.Previous.Name }}...{{ .Tag.Name }}`] 28 | {{ end }} 29 | 30 | {{ if .Tag.Previous -}} 31 | [`{{ .Tag.Previous.Name }}...{{ .Tag.Name }}`]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 32 | {{ end -}} 33 | {{ end -}} 34 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/SAP/ui5-fs 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - FEATURE 11 | - FIX 12 | - PERF 13 | - DEPENDENCY 14 | - BREAKING 15 | commit_groups: 16 | title_maps: 17 | FEATURE: Features 18 | FIX: Bug Fixes 19 | PERF: Performance Improvements 20 | DEPENDENCY: Dependency Updates 21 | BREAKING: Breaking Changes 22 | header: 23 | pattern: "^\\[(\\w*)\\]\\s(?:([^\\:]*)\\:\\s)?(.*)$" 24 | pattern_maps: 25 | - Type 26 | - Scope 27 | - Subject 28 | issues: 29 | prefix: 30 | - "#" 31 | notes: 32 | keywords: 33 | - BREAKING CHANGE 34 | -------------------------------------------------------------------------------- /.chglog/release-config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: RELEASE.tpl.md 3 | info: 4 | repository_url: https://github.com/SAP/ui5-fs 5 | options: 6 | 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 7 | commits: 8 | filters: 9 | Type: 10 | - FEATURE 11 | - FIX 12 | - PERF 13 | - DEPENDENCY 14 | - BREAKING 15 | commit_groups: 16 | title_maps: 17 | FEATURE: Features 18 | FIX: Bug Fixes 19 | PERF: Performance Improvements 20 | DEPENDENCY: Dependency Updates 21 | BREAKING: Breaking Changes 22 | header: 23 | pattern: "^\\[(\\w*)\\]\\s(?:([^\\:]*)\\:\\s)?(.*)$" 24 | pattern_maps: 25 | - Type 26 | - Scope 27 | - Subject 28 | issues: 29 | prefix: 30 | - "#" 31 | notes: 32 | keywords: 33 | - BREAKING CHANGE 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | 9 | [*.{css,html,js,cjs,mjs,jsx,ts,tsx,less,txt,json,yml,md}] 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | indent_size = 4 13 | insert_final_newline = true 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 🚨 Issues Have Been Transferred to UI5 Tooling Repository 2 | 3 | Please create new issues in the UI5 Tooling repository: https://github.com/SAP/ui5-tooling/issues/new/choose 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report UI5 Tooling Issues or Request a Feature 4 | url: https://github.com/SAP/ui5-tooling/issues/new/choose 5 | about: Please create new issues in the UI5 Tooling repository 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Thank you for your contribution!** 🙌 2 | 3 | To get it merged faster, kindly review the checklist below: 4 | 5 | ## Pull Request Checklist 6 | - [ ] Reviewed the [Contributing Guidelines](https://github.com/SAP/ui5-tooling/blob/main/CONTRIBUTING.md#-contributing-code) 7 | + Especially the [How to Contribute](https://github.com/SAP/ui5-tooling/blob/main/CONTRIBUTING.md#how-to-contribute) section 8 | - [ ] [No merge commits](https://github.com/SAP/ui5-tooling/blob/main/docs/Guidelines.md#no-merge-commits) 9 | - [ ] [Correct commit message style](https://github.com/SAP/ui5-tooling/blob/main/docs/Guidelines.md#commit-message-style) 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: npm 8 | directory: "/" 9 | schedule: 10 | interval: weekly 11 | day: sunday 12 | time: "10:00" 13 | timezone: Etc/UCT 14 | reviewers: 15 | - "SAP/ui5-foundation" 16 | versioning-strategy: increase 17 | commit-message: 18 | prefix: "[DEPENDENCY] " 19 | prefix-development: "[INTERNAL] " 20 | -------------------------------------------------------------------------------- /.github/in-solidarity.yml: -------------------------------------------------------------------------------- 1 | _extends: ietf/terminology 2 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor == 'dependabot[bot]' && github.event.pull_request.auto_merge == null }} 15 | steps: 16 | - name: Dependabot metadata 17 | id: metadata 18 | uses: dependabot/fetch-metadata@v2 19 | with: 20 | github-token: "${{ secrets.GITHUB_TOKEN }}" 21 | - name: Approve and auto-merge PRs for minor/patch updates of github-actions 22 | if: | 23 | steps.metadata.outputs.package-ecosystem == 'github_actions' && 24 | contains(fromJSON('["version-update:semver-minor", "version-update:semver-patch"]'), steps.metadata.outputs.update-type) 25 | run: gh pr review --approve "$PR_URL" && gh pr merge --auto --rebase "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/github-ci.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # No permissions are required for this workflow 12 | permissions: {} 13 | 14 | jobs: 15 | test: 16 | name: General checks, tests and coverage reporting 17 | runs-on: ubuntu-24.04 18 | steps: 19 | 20 | - uses: actions/checkout@v4 21 | 22 | - name: Use Node.js LTS 20.11.0 23 | uses: actions/setup-node@v4.4.0 24 | with: 25 | node-version: 20.11.0 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Perform checks and tests 31 | run: npm test 32 | 33 | - name: Send report to Coveralls 34 | uses: coverallsapp/github-action@v2.3.6 35 | -------------------------------------------------------------------------------- /.github/workflows/reuse-compliance.yml: -------------------------------------------------------------------------------- 1 | name: REUSE 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # No permissions are required for this workflow 12 | permissions: {} 13 | 14 | jobs: 15 | compliance-check: 16 | name: Compliance Check 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Execute REUSE Compliance Check 21 | uses: fsfe/reuse-action@v5 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # CI (Azure Pipelines) xUnit test results 21 | test-results.xml 22 | 23 | # IDEs 24 | .vscode/ 25 | *.~vsdx 26 | .idea/ 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | # Misc 54 | yarn.lock 55 | .DS_Store 56 | 57 | # Don't include private SSH key for deployment via Travis CI 58 | deploy_key 59 | 60 | # Custom directories 61 | test/tmp/ 62 | jsdocs/ 63 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Enforce public npm registry 2 | registry=https://registry.npmjs.org/ 3 | lockfile-version=3 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the UI5 Tooling 2 | 3 | See CONTRIBUTING.md in the [SAP/ui5-tooling](https://github.com/SAP/ui5-tooling/blob/main/CONTRIBUTING.md) repository. 4 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, 6 | AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, and distribution 13 | as defined by Sections 1 through 9 of this document. 14 | 15 | 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 18 | owner that is granting the License. 19 | 20 | 21 | 22 | "Legal Entity" shall mean the union of the acting entity and all other entities 23 | that control, are controlled by, or are under common control with that entity. 24 | For the purposes of this definition, "control" means (i) the power, direct 25 | or indirect, to cause the direction or management of such entity, whether 26 | by contract or otherwise, or (ii) ownership of fifty percent (50%) or more 27 | of the outstanding shares, or (iii) beneficial ownership of such entity. 28 | 29 | 30 | 31 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions 32 | granted by this License. 33 | 34 | 35 | 36 | "Source" form shall mean the preferred form for making modifications, including 37 | but not limited to software source code, documentation source, and configuration 38 | files. 39 | 40 | 41 | 42 | "Object" form shall mean any form resulting from mechanical transformation 43 | or translation of a Source form, including but not limited to compiled object 44 | code, generated documentation, and conversions to other media types. 45 | 46 | 47 | 48 | "Work" shall mean the work of authorship, whether in Source or Object form, 49 | made available under the License, as indicated by a copyright notice that 50 | is included in or attached to the work (an example is provided in the Appendix 51 | below). 52 | 53 | 54 | 55 | "Derivative Works" shall mean any work, whether in Source or Object form, 56 | that is based on (or derived from) the Work and for which the editorial revisions, 57 | annotations, elaborations, or other modifications represent, as a whole, an 58 | original work of authorship. For the purposes of this License, Derivative 59 | Works shall not include works that remain separable from, or merely link (or 60 | bind by name) to the interfaces of, the Work and Derivative Works thereof. 61 | 62 | 63 | 64 | "Contribution" shall mean any work of authorship, including the original version 65 | of the Work and any modifications or additions to that Work or Derivative 66 | Works thereof, that is intentionally submitted to Licensor for inclusion in 67 | the Work by the copyright owner or by an individual or Legal Entity authorized 68 | to submit on behalf of the copyright owner. For the purposes of this definition, 69 | "submitted" means any form of electronic, verbal, or written communication 70 | sent to the Licensor or its representatives, including but not limited to 71 | communication on electronic mailing lists, source code control systems, and 72 | issue tracking systems that are managed by, or on behalf of, the Licensor 73 | for the purpose of discussing and improving the Work, but excluding communication 74 | that is conspicuously marked or otherwise designated in writing by the copyright 75 | owner as "Not a Contribution." 76 | 77 | 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 80 | of whom a Contribution has been received by Licensor and subsequently incorporated 81 | within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of this 84 | License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, 85 | no-charge, royalty-free, irrevocable copyright license to reproduce, prepare 86 | Derivative Works of, publicly display, publicly perform, sublicense, and distribute 87 | the Work and such Derivative Works in Source or Object form. 88 | 89 | 3. Grant of Patent License. Subject to the terms and conditions of this License, 90 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, 91 | no-charge, royalty-free, irrevocable (except as stated in this section) patent 92 | license to make, have made, use, offer to sell, sell, import, and otherwise 93 | transfer the Work, where such license applies only to those patent claims 94 | licensable by such Contributor that are necessarily infringed by their Contribution(s) 95 | alone or by combination of their Contribution(s) with the Work to which such 96 | Contribution(s) was submitted. If You institute patent litigation against 97 | any entity (including a cross-claim or counterclaim in a lawsuit) alleging 98 | that the Work or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses granted to You 100 | under this License for that Work shall terminate as of the date such litigation 101 | is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the Work or 104 | Derivative Works thereof in any medium, with or without modifications, and 105 | in Source or Object form, provided that You meet the following conditions: 106 | 107 | (a) You must give any other recipients of the Work or Derivative Works a copy 108 | of this License; and 109 | 110 | (b) You must cause any modified files to carry prominent notices stating that 111 | You changed the files; and 112 | 113 | (c) You must retain, in the Source form of any Derivative Works that You distribute, 114 | all copyright, patent, trademark, and attribution notices from the Source 115 | form of the Work, excluding those notices that do not pertain to any part 116 | of the Derivative Works; and 117 | 118 | (d) If the Work includes a "NOTICE" text file as part of its distribution, 119 | then any Derivative Works that You distribute must include a readable copy 120 | of the attribution notices contained within such NOTICE file, excluding those 121 | notices that do not pertain to any part of the Derivative Works, in at least 122 | one of the following places: within a NOTICE text file distributed as part 123 | of the Derivative Works; within the Source form or documentation, if provided 124 | along with the Derivative Works; or, within a display generated by the Derivative 125 | Works, if and wherever such third-party notices normally appear. The contents 126 | of the NOTICE file are for informational purposes only and do not modify the 127 | License. You may add Your own attribution notices within Derivative Works 128 | that You distribute, alongside or as an addendum to the NOTICE text from the 129 | Work, provided that such additional attribution notices cannot be construed 130 | as modifying the License. 131 | 132 | You may add Your own copyright statement to Your modifications and may provide 133 | additional or different license terms and conditions for use, reproduction, 134 | or distribution of Your modifications, or for any such Derivative Works as 135 | a whole, provided Your use, reproduction, and distribution of the Work otherwise 136 | complies with the conditions stated in this License. 137 | 138 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 139 | Contribution intentionally submitted for inclusion in the Work by You to the 140 | Licensor shall be under the terms and conditions of this License, without 141 | any additional terms or conditions. Notwithstanding the above, nothing herein 142 | shall supersede or modify the terms of any separate license agreement you 143 | may have executed with Licensor regarding such Contributions. 144 | 145 | 6. Trademarks. This License does not grant permission to use the trade names, 146 | trademarks, service marks, or product names of the Licensor, except as required 147 | for reasonable and customary use in describing the origin of the Work and 148 | reproducing the content of the NOTICE file. 149 | 150 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to 151 | in writing, Licensor provides the Work (and each Contributor provides its 152 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 153 | KIND, either express or implied, including, without limitation, any warranties 154 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR 155 | A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness 156 | of using or redistributing the Work and assume any risks associated with Your 157 | exercise of permissions under this License. 158 | 159 | 8. Limitation of Liability. In no event and under no legal theory, whether 160 | in tort (including negligence), contract, or otherwise, unless required by 161 | applicable law (such as deliberate and grossly negligent acts) or agreed to 162 | in writing, shall any Contributor be liable to You for damages, including 163 | any direct, indirect, special, incidental, or consequential damages of any 164 | character arising as a result of this License or out of the use or inability 165 | to use the Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all other commercial 167 | damages or losses), even if such Contributor has been advised of the possibility 168 | of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 171 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 172 | acceptance of support, warranty, indemnity, or other liability obligations 173 | and/or rights consistent with this License. However, in accepting such obligations, 174 | You may act only on Your own behalf and on Your sole responsibility, not on 175 | behalf of any other Contributor, and only if You agree to indemnify, defend, 176 | and hold each Contributor harmless for any liability incurred by, or claims 177 | asserted against, such Contributor by reason of your accepting any such warranty 178 | or additional liability. END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following boilerplate 183 | notice, with the fields enclosed by brackets "[]" replaced with your own identifying 184 | information. (Don't include the brackets!) The text should be enclosed in 185 | the appropriate comment syntax for the file format. We also recommend that 186 | a file or class name and description of purpose be included on the same "printed 187 | page" as the copyright notice for easier identification within third-party 188 | archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | 194 | you may not use this file except in compliance with the License. 195 | 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | 204 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 205 | 206 | See the License for the specific language governing permissions and 207 | 208 | limitations under the License. 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![UI5 icon](https://raw.githubusercontent.com/SAP/ui5-tooling/main/docs/images/UI5_logo_wide.png) 2 | 3 | # ui5-fs 4 | > UI5-specific file system abstraction 5 | > Part of the [UI5 Tooling](https://github.com/SAP/ui5-tooling) 6 | 7 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP/ui5-fs)](https://api.reuse.software/info/github.com/SAP/ui5-fs) 8 | [![Build Status](https://dev.azure.com/sap/opensource/_apis/build/status/SAP.ui5-fs?branchName=main)](https://dev.azure.com/sap/opensource/_build/latest?definitionId=36&branchName=main) 9 | [![npm Package Version](https://badge.fury.io/js/%40ui5%2Ffs.svg)](https://www.npmjs.com/package/@ui5/fs) 10 | [![Coverage Status](https://coveralls.io/repos/github/SAP/ui5-fs/badge.svg)](https://coveralls.io/github/SAP/ui5-fs) 11 | 12 | ## Documentation 13 | UI5 FS documentation can be found here: [sap.github.io/ui5-tooling](https://sap.github.io/ui5-tooling/v4/pages/FileSystem/) 14 | 15 | The UI5 FS API Reference can be found here: [`@ui5/fs`](https://sap.github.io/ui5-tooling/v4/api/) 16 | 17 | ## Contributing 18 | Please check our [Contribution Guidelines](https://github.com/SAP/ui5-tooling/blob/main/CONTRIBUTING.md). 19 | 20 | ## Support 21 | Please follow our [Contribution Guidelines](https://github.com/SAP/ui5-tooling/blob/main/CONTRIBUTING.md#report-an-issue) on how to report an issue. 22 | 23 | Please report issues in the main [UI5 Tooling](https://github.com/SAP/ui5-tooling) repository. 24 | 25 | ## Release History 26 | See [CHANGELOG.md](CHANGELOG.md). 27 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "ui5-fs" 3 | SPDX-PackageSupplier = "SAP OpenUI5 " 4 | SPDX-PackageDownloadLocation = "https://github.com/SAP/ui5-fs" 5 | 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." 6 | 7 | [[annotations]] 8 | path = "**" 9 | precedence = "aggregate" 10 | SPDX-FileCopyrightText = "2025 SAP SE or an SAP affiliate company and UI5 Tooling contributors" 11 | SPDX-License-Identifier = "Apache-2.0" 12 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - main 8 | 9 | variables: 10 | CI: true 11 | 12 | strategy: 13 | matrix: 14 | linux_node_lts_20_min_version: 15 | imageName: 'ubuntu-24.04' 16 | node_version: 20.11.0 17 | linux_node_22_min_version: 18 | imageName: 'ubuntu-24.04' 19 | node_version: 22.1.0 20 | linux_node_lts_20: 21 | imageName: 'ubuntu-24.04' 22 | node_version: 20.x 23 | mac_node_lts_20: 24 | imageName: 'macos-13' 25 | node_version: 20.x 26 | windows_node_lts_20: 27 | imageName: 'windows-2022' 28 | node_version: 20.x 29 | linux_node_22: 30 | imageName: 'ubuntu-24.04' 31 | node_version: 22.x 32 | mac_node_22: 33 | imageName: 'macos-13' 34 | node_version: 22.x 35 | windows_node_22: 36 | imageName: 'windows-2022' 37 | node_version: 22.x 38 | linux_node_24: 39 | imageName: 'ubuntu-24.04' 40 | node_version: 24.x 41 | mac_node_24: 42 | imageName: 'macos-13' 43 | node_version: 24.x 44 | windows_node_24: 45 | imageName: 'windows-2022' 46 | node_version: 24.x 47 | 48 | pool: 49 | vmImage: $(imageName) 50 | 51 | steps: 52 | - task: NodeTool@0 53 | inputs: 54 | versionSpec: $(node_version) 55 | displayName: Install Node.js 56 | 57 | - script: npm ci 58 | displayName: Install Dependencies 59 | 60 | - script: npm ls --prod 61 | displayName: Check for missing / extraneous Dependencies 62 | 63 | - script: npm run test-azure 64 | displayName: Run Tests 65 | 66 | - task: PublishTestResults@2 67 | displayName: Publish Test Results 68 | condition: succeededOrFailed() 69 | inputs: 70 | testResultsFormat: 'JUnit' 71 | testResultsFiles: '$(System.DefaultWorkingDirectory)/test-results.xml' 72 | 73 | - task: PublishCodeCoverageResults@2 74 | displayName: Publish Test Coverage Results 75 | condition: succeededOrFailed() 76 | inputs: 77 | summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml' 78 | 79 | - script: npm run coverage 80 | displayName: Run Test Natively in Case of Failures 81 | condition: failed() 82 | -------------------------------------------------------------------------------- /eslint.common.config.js: -------------------------------------------------------------------------------- 1 | import jsdoc from "eslint-plugin-jsdoc"; 2 | import ava from "eslint-plugin-ava"; 3 | import globals from "globals"; 4 | import js from "@eslint/js"; 5 | import google from "eslint-config-google"; 6 | 7 | export default [{ 8 | ignores: [ // Common ignore patterns across all tooling repos 9 | "**/coverage/", 10 | "test/tmp/", 11 | "test/expected/", 12 | "test/fixtures/", 13 | "**/docs/", 14 | "**/jsdocs/", 15 | ], 16 | }, js.configs.recommended, google, ava.configs["flat/recommended"], { 17 | name: "Common ESLint config used for all tooling repos", 18 | 19 | plugins: { 20 | jsdoc, 21 | }, 22 | 23 | languageOptions: { 24 | globals: { 25 | ...globals.node, 26 | }, 27 | 28 | ecmaVersion: 2023, 29 | sourceType: "module", 30 | }, 31 | 32 | settings: { 33 | jsdoc: { 34 | mode: "jsdoc", 35 | 36 | tagNamePreference: { 37 | return: "returns", 38 | augments: "extends", 39 | }, 40 | }, 41 | }, 42 | 43 | rules: { 44 | "indent": ["error", "tab"], 45 | "linebreak-style": ["error", "unix"], 46 | 47 | "quotes": ["error", "double", { 48 | allowTemplateLiterals: true, 49 | }], 50 | 51 | "semi": ["error", "always"], 52 | "no-negated-condition": "off", 53 | "require-jsdoc": "off", 54 | "no-mixed-requires": "off", 55 | 56 | "max-len": ["error", { 57 | code: 120, 58 | ignoreUrls: true, 59 | ignoreRegExpLiterals: true, 60 | }], 61 | 62 | "no-implicit-coercion": [2, { 63 | allow: ["!!"], 64 | }], 65 | 66 | "comma-dangle": "off", 67 | "no-tabs": "off", 68 | "no-console": 2, // Disallow console.log() 69 | "no-eval": 2, 70 | // The following rule must be disabled as of ESLint 9. 71 | // It's removed and causes issues when present 72 | // https://eslint.org/docs/latest/rules/valid-jsdoc 73 | "valid-jsdoc": 0, 74 | "jsdoc/check-examples": 0, 75 | "jsdoc/check-param-names": 2, 76 | "jsdoc/check-tag-names": 2, 77 | "jsdoc/check-types": 2, 78 | "jsdoc/no-undefined-types": 0, 79 | "jsdoc/require-description": 0, 80 | "jsdoc/require-description-complete-sentence": 0, 81 | "jsdoc/require-example": 0, 82 | "jsdoc/require-hyphen-before-param-description": 0, 83 | "jsdoc/require-param": 2, 84 | "jsdoc/require-param-description": 0, 85 | "jsdoc/require-param-name": 2, 86 | "jsdoc/require-param-type": 2, 87 | "jsdoc/require-returns": 0, 88 | "jsdoc/require-returns-description": 0, 89 | "jsdoc/require-returns-type": 2, 90 | 91 | "jsdoc/tag-lines": [2, "any", { 92 | startLines: 1, 93 | }], 94 | 95 | "jsdoc/valid-types": 0, 96 | "ava/assertion-arguments": 0, 97 | }, 98 | } 99 | ]; 100 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintCommonConfig from "./eslint.common.config.js"; 2 | 3 | export default [ 4 | ...eslintCommonConfig, // Load common ESLint config 5 | ]; 6 | -------------------------------------------------------------------------------- /jsdoc-plugin.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * This plugin fixes unexpected JSDoc behavior that prevents us from using types that start with an at-sign (@). 3 | * JSDoc doesn't see "{@" as a valid type expression, probably as there's also {@link ...}. 4 | */ 5 | exports.handlers = { 6 | jsdocCommentFound: function(e) { 7 | e.comment = e.comment.replace(/{@ui5\//g, "{ @ui5/"); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": ["README.md"], 7 | "includePattern": ".+\\.js$", 8 | "excludePattern": "(node_modules(\\\\|/))" 9 | }, 10 | "plugins": [ 11 | "./jsdoc-plugin.cjs" 12 | ], 13 | "opts": { 14 | "encoding": "utf8", 15 | "destination": "jsdocs/", 16 | "recurse": true, 17 | "verbose": true, 18 | "access": "public" 19 | }, 20 | "templates": { 21 | "cleverLinks": false, 22 | "monospaceLinks": false, 23 | "default": { 24 | "useLongnameInNav": true 25 | } 26 | }, 27 | "openGraph": { 28 | "title": "UI5 Tooling - API Reference", 29 | "type": "website", 30 | "image": "https://sap.github.io/ui5-tooling/v4/images/UI5_logo_wide.png", 31 | "site_name": "UI5 Tooling - API Reference", 32 | "url": "https://sap.github.io/ui5-tooling/" 33 | }, 34 | "docdash": { 35 | "sectionOrder": [ 36 | "Modules", 37 | "Namespaces", 38 | "Classes", 39 | "Externals", 40 | "Events", 41 | "Mixins", 42 | "Tutorials", 43 | "Interfaces" 44 | ], 45 | "meta": { 46 | "title": "UI5 Tooling - API Reference - UI5 FS", 47 | "description": "UI5 Tooling - API Reference - UI5 FS", 48 | "keyword": "openui5 sapui5 ui5 build development tool api reference" 49 | }, 50 | "search": true, 51 | "wrap": true, 52 | "menu": { 53 | "GitHub": { 54 | "href": "https://github.com/SAP/ui5-fs", 55 | "target": "_blank", 56 | "class": "menu-item", 57 | "id": "github_link" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/AbstractReader.js: -------------------------------------------------------------------------------- 1 | import randomInt from "random-int"; 2 | import Trace from "./tracing/Trace.js"; 3 | 4 | /** 5 | * Abstract resource locator implementing the general API for reading resources 6 | * 7 | * @abstract 8 | * @public 9 | * @class 10 | * @alias @ui5/fs/AbstractReader 11 | */ 12 | class AbstractReader { 13 | /** 14 | * The constructor. 15 | * 16 | * @public 17 | * @param {string} name Name of the reader. Typically used for tracing purposes 18 | */ 19 | constructor(name) { 20 | if (new.target === AbstractReader) { 21 | throw new TypeError("Class 'AbstractReader' is abstract"); 22 | } 23 | this._name = name; 24 | } 25 | 26 | /* 27 | * Returns the name of the reader instance. This can be used for logging/tracing purposes. 28 | * 29 | * @returns {string} Name of the reader 30 | */ 31 | getName() { 32 | return this._name || ``; 33 | } 34 | 35 | /** 36 | * Locates resources by matching glob patterns. 37 | * 38 | * @example 39 | * byGlob("**‏/*.{html,htm}"); 40 | * byGlob("**‏/.library"); 41 | * byGlob("/pony/*"); 42 | * 43 | * @public 44 | * @param {string|string[]} virPattern glob pattern as string or array of glob patterns for 45 | * virtual directory structure 46 | * @param {object} [options] glob options 47 | * @param {boolean} [options.nodir=true] Do not match directories 48 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 49 | */ 50 | byGlob(virPattern, options = {nodir: true}) { 51 | const trace = new Trace(virPattern); 52 | return this._byGlob(virPattern, options, trace).then(function(result) { 53 | trace.printReport(); 54 | return result; 55 | }).then((resources) => { 56 | if (resources.length > 1) { 57 | // Pseudo randomize result order to prevent consumers from relying on it: 58 | // Swap the first object with a randomly chosen one 59 | const x = 0; 60 | const y = randomInt(0, resources.length - 1); 61 | // Swap object at index "x" with "y" 62 | resources[x] = [resources[y], resources[y]=resources[x]][0]; 63 | } 64 | return resources; 65 | }); 66 | } 67 | 68 | /** 69 | * Locates resources by matching a given path. 70 | * 71 | * @public 72 | * @param {string} virPath Virtual path 73 | * @param {object} [options] Options 74 | * @param {boolean} [options.nodir=true] Do not match directories 75 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 76 | */ 77 | byPath(virPath, options = {nodir: true}) { 78 | const trace = new Trace(virPath); 79 | return this._byPath(virPath, options, trace).then(function(resource) { 80 | trace.printReport(); 81 | return resource; 82 | }); 83 | } 84 | 85 | /** 86 | * Locates resources by one or more glob patterns. 87 | * 88 | * @abstract 89 | * @protected 90 | * @param {string|string[]} virPattern glob pattern as string or an array of 91 | * glob patterns for virtual directory structure 92 | * @param {object} options glob options 93 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 94 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 95 | */ 96 | _byGlob(virPattern, options, trace) { 97 | throw new Error("Function '_byGlob' is not implemented"); 98 | } 99 | 100 | /** 101 | * Locate resources by matching a single glob pattern. 102 | * 103 | * @abstract 104 | * @protected 105 | * @param {string} pattern glob pattern 106 | * @param {object} options glob options 107 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 108 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 109 | */ 110 | _runGlob(pattern, options, trace) { 111 | throw new Error("Function '_runGlob' is not implemented"); 112 | } 113 | 114 | /** 115 | * Locates resources by path. 116 | * 117 | * @abstract 118 | * @protected 119 | * @param {string} virPath Virtual path 120 | * @param {object} options Options 121 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 122 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 123 | */ 124 | _byPath(virPath, options, trace) { 125 | throw new Error("Function '_byPath' is not implemented"); 126 | } 127 | } 128 | 129 | export default AbstractReader; 130 | -------------------------------------------------------------------------------- /lib/AbstractReaderWriter.js: -------------------------------------------------------------------------------- 1 | import AbstractReader from "./AbstractReader.js"; 2 | 3 | /** 4 | * Abstract resource locator implementing the general API for reading and writing resources 5 | * 6 | * @abstract 7 | * @public 8 | * @class 9 | * @alias @ui5/fs/AbstractReaderWriter 10 | * @extends @ui5/fs/AbstractReader 11 | */ 12 | class AbstractReaderWriter extends AbstractReader { 13 | /** 14 | * The constructor. 15 | * 16 | * @public 17 | * @param {string} name Name of the reader/writer. Typically used for tracing purposes 18 | */ 19 | constructor(name) { 20 | if (new.target === AbstractReaderWriter) { 21 | throw new TypeError("Class 'AbstractReaderWriter' is abstract"); 22 | } 23 | super(name); 24 | } 25 | 26 | /* 27 | * Returns the name of the reader/writer instance. This can be used for logging/tracing purposes. 28 | * 29 | * @returns {string} Name of the reader/writer 30 | */ 31 | getName() { 32 | return this._name || ``; 33 | } 34 | 35 | /** 36 | * Writes the content of a resource to a path. 37 | * 38 | * @public 39 | * @param {@ui5/fs/Resource} resource Resource to write 40 | * @param {object} [options] 41 | * @param {boolean} [options.readOnly=false] Whether the resource content shall be written read-only 42 | * Do not use in conjunction with the drain option. 43 | * The written file will be used as the new source of this resources content. 44 | * Therefore the written file should not be altered by any means. 45 | * Activating this option might improve overall memory consumption. 46 | * @param {boolean} [options.drain=false] Whether the resource content shall be emptied during the write process. 47 | * Do not use in conjunction with the readOnly option. 48 | * Activating this option might improve overall memory consumption. 49 | * This should be used in cases where this is the last access to the resource. 50 | * E.g. the final write of a resource after all processing is finished. 51 | * @returns {Promise} Promise resolving once data has been written 52 | */ 53 | write(resource, options = {drain: false, readOnly: false}) { 54 | return this._write(resource, options); 55 | } 56 | 57 | /** 58 | * Writes the content of a resource to a path. 59 | * 60 | * @abstract 61 | * @protected 62 | * @param {@ui5/fs/Resource} resource Resource to write 63 | * @param {object} [options] Write options, see above 64 | * @returns {Promise} Promise resolving once data has been written 65 | */ 66 | _write(resource, options) { 67 | throw new Error("Not implemented"); 68 | } 69 | } 70 | 71 | export default AbstractReaderWriter; 72 | -------------------------------------------------------------------------------- /lib/DuplexCollection.js: -------------------------------------------------------------------------------- 1 | import AbstractReaderWriter from "./AbstractReaderWriter.js"; 2 | import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js"; 3 | 4 | /** 5 | * Wrapper to keep readers and writers together 6 | * 7 | * @public 8 | * @class 9 | * @alias @ui5/fs/DuplexCollection 10 | * @extends @ui5/fs/AbstractReaderWriter 11 | */ 12 | class DuplexCollection extends AbstractReaderWriter { 13 | /** 14 | * The Constructor. 15 | * 16 | * @param {object} parameters 17 | * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers 18 | * @param {@ui5/fs/AbstractReaderWriter} parameters.writer 19 | * A ReaderWriter instance which is only used for writing files 20 | * @param {string} [parameters.name=""] The collection name 21 | */ 22 | constructor({reader, writer, name = ""}) { 23 | super(name); 24 | 25 | if (!reader) { 26 | throw new Error(`Cannot create DuplexCollection ${this._name}: No reader provided`); 27 | } 28 | if (!writer) { 29 | throw new Error(`Cannot create DuplexCollection ${this._name}: No writer provided`); 30 | } 31 | 32 | this._reader = reader; 33 | this._writer = writer; 34 | 35 | this._combo = new ReaderCollectionPrioritized({ 36 | name: `${name} - ReaderCollectionPrioritized`, 37 | readers: [ 38 | writer, 39 | reader 40 | ] 41 | }); 42 | } 43 | 44 | /** 45 | * Locates resources by glob. 46 | * 47 | * @private 48 | * @param {string|string[]} virPattern glob pattern as string or an array of 49 | * glob patterns for virtual directory structure 50 | * @param {object} options glob options 51 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 52 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with a list of resources 53 | */ 54 | _byGlob(virPattern, options, trace) { 55 | return this._combo._byGlob(virPattern, options, trace); 56 | } 57 | 58 | /** 59 | * Locates resources by path. 60 | * 61 | * @private 62 | * @param {string} virPath Virtual path 63 | * @param {object} options Options 64 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 65 | * @returns {Promise<@ui5/fs/Resource|null>} 66 | * Promise resolving to a single resource or null if no resource is found 67 | */ 68 | _byPath(virPath, options, trace) { 69 | return this._combo._byPath(virPath, options, trace); 70 | } 71 | 72 | /** 73 | * Writes the content of a resource to a path. 74 | * 75 | * @private 76 | * @param {@ui5/fs/Resource} resource The Resource to write 77 | * @returns {Promise} Promise resolving once data has been written 78 | */ 79 | _write(resource) { 80 | return this._writer.write(resource); 81 | } 82 | } 83 | 84 | export default DuplexCollection; 85 | -------------------------------------------------------------------------------- /lib/ReaderCollection.js: -------------------------------------------------------------------------------- 1 | import AbstractReader from "./AbstractReader.js"; 2 | 3 | /** 4 | * Resource Locator ReaderCollection 5 | * 6 | * @public 7 | * @class 8 | * @alias @ui5/fs/ReaderCollection 9 | * @extends @ui5/fs/AbstractReader 10 | */ 11 | class ReaderCollection extends AbstractReader { 12 | /** 13 | * The constructor. 14 | * 15 | * @param {object} parameters Parameters 16 | * @param {string} parameters.name The collection name 17 | * @param {@ui5/fs/AbstractReader[]} [parameters.readers] 18 | * List of resource readers (all tried in parallel). 19 | * If none are provided, the collection will never return any results. 20 | */ 21 | constructor({name, readers}) { 22 | super(name); 23 | 24 | // Remove any undefined (empty) readers from array 25 | this._readers = readers.filter(($) => $); 26 | } 27 | 28 | /** 29 | * Locates resources by glob. 30 | * 31 | * @private 32 | * @param {string|string[]} pattern glob pattern as string or an array of 33 | * glob patterns for virtual directory structure 34 | * @param {object} options glob options 35 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 36 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 37 | */ 38 | _byGlob(pattern, options, trace) { 39 | return Promise.all(this._readers.map(function(resourceLocator) { 40 | return resourceLocator._byGlob(pattern, options, trace); 41 | })).then((result) => { 42 | trace.collection(this._name); 43 | return Array.prototype.concat.apply([], result); 44 | }); 45 | } 46 | 47 | /** 48 | * Locates resources by path. 49 | * 50 | * @private 51 | * @param {string} virPath Virtual path 52 | * @param {object} options Options 53 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 54 | * @returns {Promise<@ui5/fs/Resource|null>} 55 | * Promise resolving to a single resource or null if no resource is found 56 | */ 57 | _byPath(virPath, options, trace) { 58 | const that = this; 59 | const resourceLocatorCount = this._readers.length; 60 | let resolveCount = 0; 61 | 62 | if (resourceLocatorCount === 0) { 63 | // Short-circuit if there are no readers (Promise.race does not settle for empty arrays) 64 | trace.collection(that._name); 65 | return Promise.resolve(null); 66 | } 67 | 68 | // Using Promise.race to deliver files that can be found as fast as possible 69 | return Promise.race(this._readers.map(function(resourceLocator) { 70 | return resourceLocator._byPath(virPath, options, trace).then(function(resource) { 71 | return new Promise(function(resolve, reject) { 72 | trace.collection(that._name); 73 | resolveCount++; 74 | if (resource) { 75 | resource.pushCollection(that._name); 76 | resolve(resource); 77 | } else if (resolveCount === resourceLocatorCount) { 78 | resolve(null); 79 | } 80 | }); 81 | }); 82 | })); 83 | } 84 | } 85 | 86 | export default ReaderCollection; 87 | -------------------------------------------------------------------------------- /lib/ReaderCollectionPrioritized.js: -------------------------------------------------------------------------------- 1 | import AbstractReader from "./AbstractReader.js"; 2 | 3 | /** 4 | * Prioritized Resource Locator Collection 5 | * 6 | * @public 7 | * @class 8 | * @alias @ui5/fs/ReaderCollectionPrioritized 9 | * @extends @ui5/fs/AbstractReader 10 | */ 11 | class ReaderCollectionPrioritized extends AbstractReader { 12 | /** 13 | * The constructor. 14 | * 15 | * @param {object} parameters 16 | * @param {string} parameters.name The collection name 17 | * @param {@ui5/fs/AbstractReader[]} [parameters.readers] 18 | * Prioritized list of resource readers (tried in the order provided). 19 | * If none are provided, the collection will never return any results. 20 | */ 21 | constructor({readers, name}) { 22 | super(name); 23 | 24 | // Remove any undefined (empty) readers from array 25 | this._readers = readers.filter(($) => $); 26 | } 27 | 28 | /** 29 | * Locates resources by glob. 30 | * 31 | * @private 32 | * @param {string|string[]} pattern glob pattern as string or an array of 33 | * glob patterns for virtual directory structure 34 | * @param {object} options glob options 35 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 36 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 37 | */ 38 | _byGlob(pattern, options, trace) { 39 | return Promise.all(this._readers.map(function(resourceLocator) { 40 | return resourceLocator._byGlob(pattern, options, trace); 41 | })).then((result) => { 42 | const files = Object.create(null); 43 | const resources = []; 44 | // Prefer files found in preceding resource locators 45 | for (let i = 0; i < result.length; i++) { 46 | for (let j = 0; j < result[i].length; j++) { 47 | const resource = result[i][j]; 48 | const path = resource.getPath(); 49 | if (!files[path]) { 50 | files[path] = true; 51 | resources.push(resource); 52 | } 53 | } 54 | } 55 | 56 | trace.collection(this._name); 57 | return resources; 58 | }); 59 | } 60 | 61 | /** 62 | * Locates resources by path. 63 | * 64 | * @private 65 | * @param {string} virPath Virtual path 66 | * @param {object} options Options 67 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 68 | * @returns {Promise<@ui5/fs/Resource|null>} 69 | * Promise resolving to a single resource or null if no resource is found 70 | */ 71 | _byPath(virPath, options, trace) { 72 | const that = this; 73 | const byPath = (i) => { 74 | if (i > this._readers.length - 1) { 75 | return null; 76 | } 77 | return this._readers[i]._byPath(virPath, options, trace).then((resource) => { 78 | if (resource) { 79 | resource.pushCollection(that._name); 80 | return resource; 81 | } else { 82 | return byPath(++i); 83 | } 84 | }); 85 | }; 86 | return byPath(0); 87 | } 88 | } 89 | 90 | export default ReaderCollectionPrioritized; 91 | -------------------------------------------------------------------------------- /lib/ResourceFacade.js: -------------------------------------------------------------------------------- 1 | import posixPath from "node:path/posix"; 2 | 3 | /** 4 | * A {@link @ui5/fs/Resource Resource} with a different path than it's original 5 | * 6 | * @public 7 | * @class 8 | * @alias @ui5/fs/ResourceFacade 9 | */ 10 | class ResourceFacade { 11 | #path; 12 | #name; 13 | #resource; 14 | 15 | /** 16 | * 17 | * @public 18 | * @param {object} parameters Parameters 19 | * @param {string} parameters.path Virtual path of the facade resource 20 | * @param {@ui5/fs/Resource} parameters.resource Resource to conceal 21 | */ 22 | constructor({path, resource}) { 23 | if (!path) { 24 | throw new Error("Unable to create ResourceFacade: Missing parameter 'path'"); 25 | } 26 | if (!resource) { 27 | throw new Error("Unable to create ResourceFacade: Missing parameter 'resource'"); 28 | } 29 | path = posixPath.normalize(path); 30 | if (!posixPath.isAbsolute(path)) { 31 | throw new Error(`Unable to create ResourceFacade: Parameter 'path' must be absolute: ${path}`); 32 | } 33 | this.#path = path; 34 | this.#name = posixPath.basename(path); 35 | this.#resource = resource; 36 | } 37 | 38 | /** 39 | * Gets the resources path 40 | * 41 | * @public 42 | * @returns {string} (Virtual) path of the resource 43 | */ 44 | getPath() { 45 | return this.#path; 46 | } 47 | 48 | /** 49 | * Gets the resource name 50 | * 51 | * @public 52 | * @returns {string} Name of the resource 53 | */ 54 | getName() { 55 | return this.#name; 56 | } 57 | 58 | /** 59 | * Sets the resources path 60 | * 61 | * @public 62 | * @param {string} path (Virtual) path of the resource 63 | */ 64 | setPath(path) { 65 | throw new Error(`The path of a ResourceFacade can't be changed`); 66 | } 67 | 68 | /** 69 | * Returns a clone of the resource. The clones content is independent from that of the original resource. 70 | * A ResourceFacade becomes a Resource 71 | * 72 | * @public 73 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving with the clone 74 | */ 75 | async clone() { 76 | // Cloning resolves the facade 77 | const resourceClone = await this.#resource.clone(); 78 | resourceClone.setPath(this.getPath()); 79 | return resourceClone; 80 | } 81 | 82 | /** 83 | * ====================================================================== 84 | * Call through functions to original resource 85 | * ====================================================================== 86 | */ 87 | /** 88 | * Gets a buffer with the resource content. 89 | * 90 | * @public 91 | * @returns {Promise} Promise resolving with a buffer of the resource content. 92 | */ 93 | async getBuffer() { 94 | return this.#resource.getBuffer(); 95 | } 96 | 97 | /** 98 | * Sets a Buffer as content. 99 | * 100 | * @public 101 | * @param {Buffer} buffer Buffer instance 102 | */ 103 | setBuffer(buffer) { 104 | return this.#resource.setBuffer(buffer); 105 | } 106 | 107 | /** 108 | * Gets a string with the resource content. 109 | * 110 | * @public 111 | * @returns {Promise} Promise resolving with the resource content. 112 | */ 113 | getString() { 114 | return this.#resource.getString(); 115 | } 116 | 117 | /** 118 | * Sets a String as content 119 | * 120 | * @public 121 | * @param {string} string Resource content 122 | */ 123 | setString(string) { 124 | return this.#resource.setString(string); 125 | } 126 | 127 | /** 128 | * Gets a readable stream for the resource content. 129 | * 130 | * Repetitive calls of this function are only possible if new content has been set in the meantime (through 131 | * [setStream]{@link @ui5/fs/Resource#setStream}, [setBuffer]{@link @ui5/fs/Resource#setBuffer} 132 | * or [setString]{@link @ui5/fs/Resource#setString}). This 133 | * is to prevent consumers from accessing drained streams. 134 | * 135 | * @public 136 | * @returns {stream.Readable} Readable stream for the resource content. 137 | */ 138 | getStream() { 139 | return this.#resource.getStream(); 140 | } 141 | 142 | /** 143 | * Sets a readable stream as content. 144 | * 145 | * @public 146 | * @param {stream.Readable|@ui5/fs/Resource~createStream} stream Readable stream of the resource content or 147 | callback for dynamic creation of a readable stream 148 | */ 149 | setStream(stream) { 150 | return this.#resource.setStream(stream); 151 | } 152 | 153 | /** 154 | * Gets the resources stat info. 155 | * Note that a resources stat information is not updated when the resource is being modified. 156 | * Also, depending on the used adapter, some fields might be missing which would be present for a 157 | * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance. 158 | * 159 | * @public 160 | * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} 161 | * or similar object 162 | */ 163 | getStatInfo() { 164 | return this.#resource.getStatInfo(); 165 | } 166 | 167 | /** 168 | * Size in bytes allocated by the underlying buffer. 169 | * 170 | * @see {TypedArray#byteLength} 171 | * @returns {Promise} size in bytes, 0 if there is no content yet 172 | */ 173 | async getSize() { 174 | return this.#resource.getSize(); 175 | } 176 | 177 | /** 178 | * Adds a resource collection name that was involved in locating this resource. 179 | * 180 | * @param {string} name Resource collection name 181 | */ 182 | pushCollection(name) { 183 | return this.#resource.pushCollection(name); 184 | } 185 | 186 | /** 187 | * Tracing: Get tree for printing out trace 188 | * 189 | * @returns {object} Trace tree 190 | */ 191 | getPathTree() { 192 | return this.#resource.getPathTree(); 193 | } 194 | 195 | /** 196 | * Retrieve the project assigned to the resource 197 | *
198 | * Note for UI5 Tooling extensions (i.e. custom tasks, custom middleware): 199 | * In order to ensure compatibility across UI5 Tooling versions, consider using the 200 | * getProject(resource) method provided by 201 | * [TaskUtil]{@link module:@ui5/project/build/helpers/TaskUtil} and 202 | * [MiddlewareUtil]{@link module:@ui5/server.middleware.MiddlewareUtil}, which will 203 | * return a Specification Version-compatible Project interface. 204 | * 205 | * @public 206 | * @returns {@ui5/project/specifications/Project} Project this resource is associated with 207 | */ 208 | getProject() { 209 | return this.#resource.getProject(); 210 | } 211 | 212 | /** 213 | * Assign a project to the resource 214 | * 215 | * @public 216 | * @param {@ui5/project/specifications/Project} project Project this resource is associated with 217 | */ 218 | setProject(project) { 219 | return this.#resource.setProject(project); 220 | } 221 | 222 | /** 223 | * Check whether a project has been assigned to the resource 224 | * 225 | * @public 226 | * @returns {boolean} True if the resource is associated with a project 227 | */ 228 | hasProject() { 229 | return this.#resource.hasProject(); 230 | } 231 | /** 232 | * Check whether the content of this resource has been changed during its life cycle 233 | * 234 | * @public 235 | * @returns {boolean} True if the resource's content has been changed 236 | */ 237 | isModified() { 238 | return this.#resource.isModified(); 239 | } 240 | 241 | /** 242 | * Returns source metadata if any where provided during the creation of this resource. 243 | * Typically set by an adapter to store information for later retrieval. 244 | * 245 | * @returns {object|null} 246 | */ 247 | getSourceMetadata() { 248 | return this.#resource.getSourceMetadata(); 249 | } 250 | 251 | 252 | /** 253 | * Returns the resource concealed by this facade 254 | * 255 | * @returns {@ui5/fs/Resource} 256 | */ 257 | getConcealedResource() { 258 | return this.#resource; 259 | } 260 | } 261 | 262 | export default ResourceFacade; 263 | -------------------------------------------------------------------------------- /lib/ResourceTagCollection.js: -------------------------------------------------------------------------------- 1 | const tagNamespaceRegExp = /^[a-z][a-z0-9]+$/; // part before the colon 2 | const tagNameRegExp = /^[A-Z][A-Za-z0-9]+$/; // part after the colon 3 | import ResourceFacade from "./ResourceFacade.js"; 4 | 5 | /** 6 | * A ResourceTagCollection 7 | * 8 | * @private 9 | * @class 10 | * @alias @ui5/fs/internal/ResourceTagCollection 11 | */ 12 | class ResourceTagCollection { 13 | constructor({allowedTags = [], allowedNamespaces = [], tags}) { 14 | this._allowedTags = allowedTags; // Allowed tags are validated during use 15 | this._allowedNamespaces = allowedNamespaces; 16 | 17 | if (allowedNamespaces.length) { 18 | let allowedNamespacesRegExp = allowedNamespaces.reduce((regex, tagNamespace, idx) => { 19 | // Ensure alphanum namespace to ensure working regex 20 | if (!tagNamespaceRegExp.test(tagNamespace)) { 21 | throw new Error(`Invalid namespace ${tagNamespace}: ` + 22 | `Namespace must be alphanumeric, lowercase and start with a letter`); 23 | } 24 | return `${regex}${idx === 0 ? "" : "|"}${tagNamespace}`; 25 | }, "^(?:"); 26 | allowedNamespacesRegExp += "):.+$"; 27 | this._allowedNamespacesRegExp = new RegExp(allowedNamespacesRegExp); 28 | } else { 29 | this._allowedNamespacesRegExp = null; 30 | } 31 | 32 | this._pathTags = tags || Object.create(null); 33 | } 34 | 35 | setTag(resourcePath, tag, value = true) { 36 | resourcePath = this._getPath(resourcePath); 37 | this._validateTag(tag); 38 | this._validateValue(value); 39 | 40 | if (!this._pathTags[resourcePath]) { 41 | this._pathTags[resourcePath] = Object.create(null); 42 | } 43 | this._pathTags[resourcePath][tag] = value; 44 | } 45 | 46 | clearTag(resourcePath, tag) { 47 | resourcePath = this._getPath(resourcePath); 48 | this._validateTag(tag); 49 | 50 | if (this._pathTags[resourcePath]) { 51 | this._pathTags[resourcePath][tag] = undefined; 52 | } 53 | } 54 | 55 | getTag(resourcePath, tag) { 56 | resourcePath = this._getPath(resourcePath); 57 | this._validateTag(tag); 58 | 59 | if (this._pathTags[resourcePath]) { 60 | return this._pathTags[resourcePath][tag]; 61 | } 62 | } 63 | 64 | getAllTags() { 65 | return this._pathTags; 66 | } 67 | 68 | acceptsTag(tag) { 69 | if (this._allowedTags.includes(tag) || this._allowedNamespacesRegExp?.test(tag)) { 70 | return true; 71 | } 72 | return false; 73 | } 74 | 75 | _getPath(resourcePath) { 76 | if (typeof resourcePath !== "string") { 77 | if (resourcePath instanceof ResourceFacade) { 78 | resourcePath = resourcePath.getConcealedResource().getPath(); 79 | } else { 80 | resourcePath = resourcePath.getPath(); 81 | } 82 | } 83 | if (!resourcePath) { 84 | throw new Error(`Invalid Resource: Resource path must not be empty`); 85 | } 86 | return resourcePath; 87 | } 88 | 89 | _validateTag(tag) { 90 | if (!tag.includes(":")) { 91 | throw new Error(`Invalid Tag "${tag}": Colon required after namespace`); 92 | } 93 | const parts = tag.split(":"); 94 | if (parts.length > 2) { 95 | throw new Error(`Invalid Tag "${tag}": Expected exactly one colon but found ${parts.length - 1}`); 96 | } 97 | 98 | const [tagNamespace, tagName] = parts; 99 | if (!tagNamespaceRegExp.test(tagNamespace)) { 100 | throw new Error( 101 | `Invalid Tag "${tag}": Namespace part must be alphanumeric, lowercase and start with a letter`); 102 | } 103 | if (!tagNameRegExp.test(tagName)) { 104 | throw new Error(`Invalid Tag "${tag}": Name part must be alphanumeric and start with a capital letter`); 105 | } 106 | 107 | if (!this.acceptsTag(tag)) { 108 | throw new Error( 109 | `Tag "${tag}" not accepted by this collection. Accepted tags are: ` + 110 | `${this._allowedTags.join(", ") || "*none*"}. Accepted namespaces are: ` + 111 | `${this._allowedNamespaces.join(", ") || "*none*"}`); 112 | } 113 | } 114 | 115 | _validateValue(value) { 116 | const type = typeof value; 117 | if (!["string", "number", "boolean"].includes(type)) { 118 | throw new Error( 119 | `Invalid Tag Value: Must be of type string, number or boolean but is ${type}`); 120 | } 121 | } 122 | } 123 | 124 | export default ResourceTagCollection; 125 | -------------------------------------------------------------------------------- /lib/WriterCollection.js: -------------------------------------------------------------------------------- 1 | import AbstractReaderWriter from "./AbstractReaderWriter.js"; 2 | import ReaderCollection from "./ReaderCollection.js"; 3 | import escapeStringRegExp from "escape-string-regexp"; 4 | 5 | /** 6 | * Resource Locator WriterCollection 7 | * 8 | * @public 9 | * @class 10 | * @alias @ui5/fs/WriterCollection 11 | * @extends @ui5/fs/AbstractReaderWriter 12 | */ 13 | class WriterCollection extends AbstractReaderWriter { 14 | /** 15 | * The constructor. 16 | * 17 | * @param {object} parameters Parameters 18 | * @param {string} parameters.name The collection name 19 | * @param {object.} parameters.writerMapping 20 | * Mapping of virtual base paths to writers. Path are matched greedy 21 | * 22 | * @example 23 | * new WriterCollection({ 24 | * name: "Writer Collection", 25 | * writerMapping: { 26 | * "/": writerA, 27 | * "/my/path/": writerB, 28 | * } 29 | * }); 30 | */ 31 | constructor({name, writerMapping}) { 32 | super(name); 33 | 34 | if (!writerMapping) { 35 | throw new Error(`Cannot create WriterCollection ${this._name}: Missing parameter 'writerMapping'`); 36 | } 37 | const basePaths = Object.keys(writerMapping); 38 | if (!basePaths.length) { 39 | throw new Error(`Cannot create WriterCollection ${this._name}: Empty parameter 'writerMapping'`); 40 | } 41 | 42 | // Create a regular expression (which is greedy by nature) from all paths to easily 43 | // find the correct writer for any given resource path 44 | this._basePathRegex = basePaths.sort().reduce((regex, basePath, idx) => { 45 | // Validate base path 46 | if (!basePath) { 47 | throw new Error(`Empty path in path mapping of WriterCollection ${this._name}`); 48 | } 49 | if (!basePath.startsWith("/")) { 50 | throw new Error( 51 | `Missing leading slash in path mapping '${basePath}' of WriterCollection ${this._name}`); 52 | } 53 | if (!basePath.endsWith("/")) { 54 | throw new Error( 55 | `Missing trailing slash in path mapping '${basePath}' of WriterCollection ${this._name}`); 56 | } 57 | 58 | return `${regex}(?:${escapeStringRegExp(basePath)})??`; 59 | }, "^(") + ")+.*?$"; 60 | 61 | this._writerMapping = writerMapping; 62 | this._readerCollection = new ReaderCollection({ 63 | name: `Reader collection of writer collection '${this._name}'`, 64 | readers: Object.values(writerMapping) 65 | }); 66 | } 67 | 68 | /** 69 | * Locates resources by glob. 70 | * 71 | * @private 72 | * @param {string|string[]} pattern glob pattern as string or an array of 73 | * glob patterns for virtual directory structure 74 | * @param {object} options glob options 75 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 76 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 77 | */ 78 | _byGlob(pattern, options, trace) { 79 | return this._readerCollection._byGlob(pattern, options, trace); 80 | } 81 | 82 | /** 83 | * Locates resources by path. 84 | * 85 | * @private 86 | * @param {string} virPath Virtual path 87 | * @param {object} options Options 88 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 89 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 90 | */ 91 | _byPath(virPath, options, trace) { 92 | return this._readerCollection._byPath(virPath, options, trace); 93 | } 94 | 95 | /** 96 | * Writes the content of a resource to a path. 97 | * 98 | * @private 99 | * @param {@ui5/fs/Resource} resource The Resource to write 100 | * @param {object} [options] Write options, see above 101 | * @returns {Promise} Promise resolving once data has been written 102 | */ 103 | _write(resource, options) { 104 | const resourcePath = resource.getPath(); 105 | 106 | const basePathMatch = resourcePath.match(this._basePathRegex); 107 | if (!basePathMatch || basePathMatch.length < 2) { 108 | throw new Error( 109 | `Failed to find a writer for resource with path ${resourcePath} in WriterCollection ${this._name}. ` + 110 | `Base paths handled by this collection are: ${Object.keys(this._writerMapping).join(", ")}`); 111 | } 112 | const writer = this._writerMapping[basePathMatch[1]]; 113 | return writer._write(resource, options); 114 | } 115 | } 116 | 117 | export default WriterCollection; 118 | -------------------------------------------------------------------------------- /lib/adapters/AbstractAdapter.js: -------------------------------------------------------------------------------- 1 | import path from "node:path/posix"; 2 | import {getLogger} from "@ui5/logger"; 3 | const log = getLogger("resources:adapters:AbstractAdapter"); 4 | import {minimatch} from "minimatch"; 5 | import micromatch from "micromatch"; 6 | import AbstractReaderWriter from "../AbstractReaderWriter.js"; 7 | import Resource from "../Resource.js"; 8 | 9 | /** 10 | * Abstract Resource Adapter 11 | * 12 | * @abstract 13 | * @public 14 | * @class 15 | * @alias @ui5/fs/adapters/AbstractAdapter 16 | * @extends @ui5/fs/AbstractReaderWriter 17 | */ 18 | class AbstractAdapter extends AbstractReaderWriter { 19 | /** 20 | * The constructor 21 | * 22 | * @public 23 | * @param {object} parameters Parameters 24 | * @param {string} parameters.virBasePath 25 | * Virtual base path. Must be absolute, POSIX-style, and must end with a slash 26 | * @param {string[]} [parameters.excludes] List of glob patterns to exclude 27 | * @param {object} [parameters.project] Experimental, internal parameter. Do not use 28 | */ 29 | constructor({virBasePath, excludes = [], project}) { 30 | if (new.target === AbstractAdapter) { 31 | throw new TypeError("Class 'AbstractAdapter' is abstract"); 32 | } 33 | super(); 34 | 35 | if (!virBasePath) { 36 | throw new Error(`Unable to create adapter: Missing parameter 'virBasePath'`); 37 | } 38 | if (!path.isAbsolute(virBasePath)) { 39 | throw new Error(`Unable to create adapter: Virtual base path must be absolute but is '${virBasePath}'`); 40 | } 41 | if (!virBasePath.endsWith("/")) { 42 | throw new Error( 43 | `Unable to create adapter: Virtual base path must end with a slash but is '${virBasePath}'`); 44 | } 45 | this._virBasePath = virBasePath; 46 | this._virBaseDir = virBasePath.slice(0, -1); 47 | this._excludes = excludes; 48 | this._excludesNegated = excludes.map((pattern) => `!${pattern}`); 49 | this._project = project; 50 | } 51 | /** 52 | * Locates resources by glob. 53 | * 54 | * @abstract 55 | * @private 56 | * @param {string|string[]} virPattern glob pattern as string or an array of 57 | * glob patterns for virtual directory structure 58 | * @param {object} [options={}] glob options 59 | * @param {boolean} [options.nodir=true] Do not match directories 60 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 61 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 62 | */ 63 | async _byGlob(virPattern, options = {nodir: true}, trace) { 64 | const excludes = this._excludesNegated; 65 | 66 | if (!(virPattern instanceof Array)) { 67 | virPattern = [virPattern]; 68 | } 69 | 70 | // Append static exclude patterns 71 | virPattern = Array.prototype.concat.apply(virPattern, excludes); 72 | let patterns = virPattern.map(this._normalizePattern, this); 73 | patterns = Array.prototype.concat.apply([], patterns); 74 | if (patterns.length === 0) { 75 | return []; 76 | } 77 | 78 | if (!options.nodir) { 79 | for (let i = patterns.length - 1; i >= 0; i--) { 80 | const idx = this._virBaseDir.indexOf(patterns[i]); 81 | if (patterns[i] && idx !== -1 && idx < this._virBaseDir.length) { 82 | const subPath = patterns[i]; 83 | return [ 84 | this._createResource({ 85 | statInfo: { // TODO: make closer to fs stat info 86 | isDirectory: function() { 87 | return true; 88 | } 89 | }, 90 | source: { 91 | adapter: "Abstract" 92 | }, 93 | path: subPath 94 | }) 95 | ]; 96 | } 97 | } 98 | } 99 | return await this._runGlob(patterns, options, trace); 100 | } 101 | 102 | /** 103 | * Validate if virtual path should be excluded 104 | * 105 | * @param {string} virPath Virtual Path 106 | * @returns {boolean} True if path is excluded, otherwise false 107 | */ 108 | _isPathExcluded(virPath) { 109 | return micromatch(virPath, this._excludes).length > 0; 110 | } 111 | 112 | /** 113 | * Validate if virtual path should be handled by the adapter. 114 | * This means that it either starts with the virtual base path of the adapter 115 | * or equals the base directory (base path without a trailing slash) 116 | * 117 | * @param {string} virPath Virtual Path 118 | * @returns {boolean} True if path should be handled 119 | */ 120 | _isPathHandled(virPath) { 121 | // Check whether path starts with base path, or equals base directory 122 | return virPath.startsWith(this._virBasePath) || virPath === this._virBaseDir; 123 | } 124 | 125 | /** 126 | * Normalizes virtual glob patterns. 127 | * 128 | * @private 129 | * @param {string} virPattern glob pattern for virtual directory structure 130 | * @returns {string[]} A list of normalized glob patterns 131 | */ 132 | _normalizePattern(virPattern) { 133 | const that = this; 134 | const mm = new minimatch.Minimatch(virPattern); 135 | 136 | const basePathParts = this._virBaseDir.split("/"); 137 | 138 | function matchSubset(subset) { 139 | let i; 140 | for (i = 0; i < basePathParts.length; i++) { 141 | const globPart = subset[i]; 142 | if (globPart === undefined) { 143 | log.verbose("Ran out of glob parts to match (this should not happen):"); 144 | if (that._project) { // project is optional 145 | log.verbose(`Project: ${that._project.getName()}`); 146 | } 147 | log.verbose(`Virtual base path: ${that._virBaseDir}`); 148 | log.verbose(`Pattern to match: ${virPattern}`); 149 | log.verbose(`Current subset (tried index ${i}):`); 150 | log.verbose(subset); 151 | return {idx: i, virtualMatch: true}; 152 | } 153 | const basePathPart = basePathParts[i]; 154 | if (typeof globPart === "string") { 155 | if (globPart !== basePathPart) { 156 | return null; 157 | } else { 158 | continue; 159 | } 160 | } else if (globPart === minimatch.GLOBSTAR) { 161 | return {idx: i}; 162 | } else { // Regex 163 | if (!globPart.test(basePathPart)) { 164 | return null; 165 | } else { 166 | continue; 167 | } 168 | } 169 | } 170 | if (subset.length === basePathParts.length) { 171 | return {rootMatch: true}; 172 | } 173 | return {idx: i}; 174 | } 175 | 176 | const resultGlobs = []; 177 | for (let i = 0; i < mm.set.length; i++) { 178 | const match = matchSubset(mm.set[i]); 179 | if (match) { 180 | let resultPattern; 181 | if (match.virtualMatch) { 182 | resultPattern = basePathParts.slice(0, match.idx).join("/"); 183 | } else if (match.rootMatch) { // matched one up 184 | resultPattern = ""; // root "/" 185 | } else { // matched at some part of the glob 186 | resultPattern = mm.globParts[i].slice(match.idx).join("/"); 187 | if (resultPattern.startsWith("/")) { 188 | resultPattern = resultPattern.substr(1); 189 | } 190 | } 191 | if (mm.negate) { 192 | resultPattern = "!" + resultPattern; 193 | } 194 | resultGlobs.push(resultPattern); 195 | } 196 | } 197 | return resultGlobs; 198 | } 199 | 200 | _createResource(parameters) { 201 | if (this._project) { 202 | parameters.project = this._project; 203 | } 204 | return new Resource(parameters); 205 | } 206 | 207 | _migrateResource(resource) { 208 | // This function only returns a promise if a migration is necessary. 209 | // Since this is rarely the case, we therefore reduce the amount of 210 | // created Promises by making this differentiation 211 | 212 | // Check if its a fs/Resource v3, function 'hasProject' was 213 | // introduced with v3 therefore take it as the indicator 214 | if (resource.hasProject) { 215 | return resource; 216 | } 217 | return this._createFromLegacyResource(resource); 218 | } 219 | 220 | async _createFromLegacyResource(resource) { 221 | const options = { 222 | path: resource._path, 223 | statInfo: resource._statInfo, 224 | source: resource._source 225 | }; 226 | 227 | if (resource._stream) { 228 | options.buffer = await resource._getBufferFromStream(); 229 | } else if (resource._createStream) { 230 | options.createStream = resource._createStream; 231 | } else if (resource._buffer) { 232 | options.buffer = resource._buffer; 233 | } 234 | return new Resource(options); 235 | } 236 | 237 | _assignProjectToResource(resource) { 238 | if (this._project) { 239 | // Assign project to resource if necessary 240 | if (resource.hasProject()) { 241 | if (resource.getProject() !== this._project) { 242 | throw new Error( 243 | `Unable to write resource associated with project ` + 244 | `${resource.getProject().getName()} into adapter of project ${this._project.getName()}: ` + 245 | resource.getPath()); 246 | } 247 | return; 248 | } 249 | log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`); 250 | resource.setProject(this._project); 251 | } 252 | } 253 | 254 | _resolveVirtualPathToBase(inputVirPath, writeMode = false) { 255 | if (!path.isAbsolute(inputVirPath)) { 256 | throw new Error(`Failed to resolve virtual path '${inputVirPath}': Path must be absolute`); 257 | } 258 | // Resolve any ".." segments to make sure we compare the effective start of the path 259 | // with the virBasePath 260 | const virPath = path.normalize(inputVirPath); 261 | 262 | if (!writeMode) { 263 | // When reading resources, validate against path excludes and return null if the given path 264 | // does not match this adapters base path 265 | if (!this._isPathHandled(virPath)) { 266 | if (log.isLevelEnabled("silly")) { 267 | log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + 268 | `Resolved path does not start with adapter base path '${this._virBasePath}' or equals ` + 269 | `base dir: ${this._virBaseDir}`); 270 | } 271 | return null; 272 | } 273 | if (this._isPathExcluded(virPath)) { 274 | if (log.isLevelEnabled("silly")) { 275 | log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + 276 | `Resolved path is excluded by configuration of adapter with base path '${this._virBasePath}'`); 277 | } 278 | return null; 279 | } 280 | } else if (!this._isPathHandled(virPath)) { 281 | // Resolved path is not within the configured base path and does 282 | // not equal the virtual base directory. 283 | // Since we don't want to write resources to foreign locations, we throw an error 284 | throw new Error( 285 | `Failed to write resource with virtual path '${inputVirPath}': Path must start with ` + 286 | `the configured virtual base path of the adapter. Base path: '${this._virBasePath}'`); 287 | } 288 | 289 | const relPath = virPath.substr(this._virBasePath.length); 290 | return relPath; 291 | } 292 | } 293 | 294 | export default AbstractAdapter; 295 | -------------------------------------------------------------------------------- /lib/adapters/Memory.js: -------------------------------------------------------------------------------- 1 | import {getLogger} from "@ui5/logger"; 2 | const log = getLogger("resources:adapters:Memory"); 3 | import micromatch from "micromatch"; 4 | import AbstractAdapter from "./AbstractAdapter.js"; 5 | 6 | const ADAPTER_NAME = "Memory"; 7 | 8 | /** 9 | * Virtual resource Adapter 10 | * 11 | * @public 12 | * @class 13 | * @alias @ui5/fs/adapters/Memory 14 | * @extends @ui5/fs/adapters/AbstractAdapter 15 | */ 16 | class Memory extends AbstractAdapter { 17 | /** 18 | * The constructor. 19 | * 20 | * @public 21 | * @param {object} parameters Parameters 22 | * @param {string} parameters.virBasePath 23 | * Virtual base path. Must be absolute, POSIX-style, and must end with a slash 24 | * @param {string[]} [parameters.excludes] List of glob patterns to exclude 25 | * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) 26 | */ 27 | constructor({virBasePath, project, excludes}) { 28 | super({virBasePath, project, excludes}); 29 | this._virFiles = Object.create(null); // map full of files 30 | this._virDirs = Object.create(null); // map full of directories 31 | } 32 | 33 | /** 34 | * Matches and returns resources from a given map (either _virFiles or _virDirs). 35 | * 36 | * @private 37 | * @param {string[]} patterns 38 | * @param {object} resourceMap 39 | * @returns {Promise} 40 | */ 41 | async _matchPatterns(patterns, resourceMap) { 42 | const resourcePaths = Object.keys(resourceMap); 43 | const matchedPaths = micromatch(resourcePaths, patterns, { 44 | dot: true 45 | }); 46 | return await Promise.all(matchedPaths.map((virPath) => { 47 | const resource = resourceMap[virPath]; 48 | if (resource) { 49 | return this._cloneResource(resource); 50 | } 51 | })); 52 | } 53 | 54 | async _cloneResource(resource) { 55 | const clonedResource = await resource.clone(); 56 | if (this._project) { 57 | clonedResource.setProject(this._project); 58 | } 59 | return clonedResource; 60 | } 61 | 62 | /** 63 | * Locate resources by glob. 64 | * 65 | * @private 66 | * @param {Array} patterns array of glob patterns 67 | * @param {object} [options={}] glob options 68 | * @param {boolean} [options.nodir=true] Do not match directories 69 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 70 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 71 | */ 72 | async _runGlob(patterns, options = {nodir: true}, trace) { 73 | if (patterns[0] === "" && !options.nodir) { // Match virtual root directory 74 | return [ 75 | this._createResource({ 76 | project: this._project, 77 | statInfo: { // TODO: make closer to fs stat info 78 | isDirectory: function() { 79 | return true; 80 | } 81 | }, 82 | sourceMetadata: { 83 | adapter: ADAPTER_NAME 84 | }, 85 | path: this._virBasePath.slice(0, -1) 86 | }) 87 | ]; 88 | } 89 | 90 | let matchedResources = await this._matchPatterns(patterns, this._virFiles); 91 | 92 | if (!options.nodir) { 93 | const matchedDirs = await this._matchPatterns(patterns, this._virDirs); 94 | matchedResources = matchedResources.concat(matchedDirs); 95 | } 96 | 97 | return matchedResources; 98 | } 99 | 100 | /** 101 | * Locates resources by path. 102 | * 103 | * @private 104 | * @param {string} virPath Virtual path 105 | * @param {object} options Options 106 | * @param {@ui5/fs/tracing.Trace} trace Trace instance 107 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 108 | */ 109 | async _byPath(virPath, options, trace) { 110 | const relPath = this._resolveVirtualPathToBase(virPath); 111 | if (relPath === null) { 112 | return null; 113 | } 114 | 115 | trace.pathCall(); 116 | 117 | const resource = this._virFiles[relPath]; 118 | 119 | if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) { 120 | return null; 121 | } else { 122 | return await this._cloneResource(resource); 123 | } 124 | } 125 | 126 | /** 127 | * Writes the content of a resource to a path. 128 | * 129 | * @private 130 | * @param {@ui5/fs/Resource} resource The Resource to write 131 | * @returns {Promise} Promise resolving once data has been written 132 | */ 133 | async _write(resource) { 134 | resource = this._migrateResource(resource); 135 | if (resource instanceof Promise) { 136 | // Only await if the migrate function returned a promise 137 | // Otherwise await would automatically create a Promise, causing unwanted overhead 138 | resource = await resource; 139 | } 140 | this._assignProjectToResource(resource); 141 | const relPath = this._resolveVirtualPathToBase(resource.getPath(), true); 142 | log.silly(`Writing to virtual path ${resource.getPath()}`); 143 | this._virFiles[relPath] = await resource.clone(); 144 | 145 | // Add virtual directories for all path segments of the written resource 146 | // TODO: Add tests for all this 147 | const pathSegments = relPath.split("/"); 148 | pathSegments.pop(); // Remove last segment representing the resource itself 149 | 150 | pathSegments.forEach((segment, i) => { 151 | if (i >= 1) { 152 | segment = pathSegments[i - 1] + "/" + segment; 153 | } 154 | pathSegments[i] = segment; 155 | }); 156 | 157 | for (let i = pathSegments.length - 1; i >= 0; i--) { 158 | const segment = pathSegments[i]; 159 | if (!this._virDirs[segment]) { 160 | this._virDirs[segment] = this._createResource({ 161 | project: this._project, 162 | sourceMetadata: { 163 | adapter: ADAPTER_NAME 164 | }, 165 | statInfo: { // TODO: make closer to fs stat info 166 | isDirectory: function() { 167 | return true; 168 | } 169 | }, 170 | path: this._virBasePath + segment 171 | }); 172 | } 173 | } 174 | } 175 | } 176 | 177 | export default Memory; 178 | -------------------------------------------------------------------------------- /lib/fsInterface.js: -------------------------------------------------------------------------------- 1 | function toPosix(inputPath) { 2 | return inputPath.replace(/\\/g, "/"); 3 | } 4 | 5 | /** 6 | * @public 7 | * @module @ui5/fs/fsInterface 8 | */ 9 | 10 | /** 11 | * Wraps readers to access them through a [Node.js fs]{@link https://nodejs.org/api/fs.html} styled interface. 12 | * 13 | * @public 14 | * @function default 15 | * @static 16 | * @param {@ui5/fs/AbstractReader} reader Resource Reader or Collection 17 | * 18 | * @returns {object} Object with [Node.js fs]{@link https://nodejs.org/api/fs.html} styled functions 19 | * [readFile]{@link https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback}, 20 | * [stat]{@link https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback}, 21 | * [readdir]{@link https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback} and 22 | * [mkdir]{@link https://nodejs.org/api/fs.html#fs_fs_mkdir_path_options_callback} 23 | */ 24 | function fsInterface(reader) { 25 | return { 26 | readFile(fsPath, options, callback) { 27 | if (typeof options === "function") { 28 | callback = options; 29 | options = undefined; 30 | } 31 | if (typeof options === "string") { 32 | options = {encoding: options}; 33 | } 34 | const posixPath = toPosix(fsPath); 35 | reader.byPath(posixPath, { 36 | nodir: false 37 | }).then(function(resource) { 38 | if (!resource) { 39 | const error = new Error(`ENOENT: no such file or directory, open '${fsPath}'`); 40 | error.code = "ENOENT"; // "File or directory does not exist" 41 | callback(error); 42 | return; 43 | } 44 | 45 | return resource.getBuffer().then(function(buffer) { 46 | let res; 47 | 48 | if (options && options.encoding) { 49 | res = buffer.toString(options.encoding); 50 | } else { 51 | res = buffer; 52 | } 53 | 54 | callback(null, res); 55 | }); 56 | }).catch(callback); 57 | }, 58 | stat(fsPath, callback) { 59 | const posixPath = toPosix(fsPath); 60 | reader.byPath(posixPath, { 61 | nodir: false 62 | }).then(function(resource) { 63 | if (!resource) { 64 | const error = new Error(`ENOENT: no such file or directory, stat '${fsPath}'`); 65 | error.code = "ENOENT"; // "File or directory does not exist" 66 | callback(error); 67 | } else { 68 | callback(null, resource.getStatInfo()); 69 | } 70 | }).catch(callback); 71 | }, 72 | readdir(fsPath, callback) { 73 | let posixPath = toPosix(fsPath); 74 | if (!posixPath.match(/\/$/)) { 75 | // Add trailing slash if not present 76 | posixPath += "/"; 77 | } 78 | reader.byGlob(posixPath + "*", { 79 | nodir: false 80 | }).then((resources) => { 81 | const files = resources.map((resource) => { 82 | return resource.getName(); 83 | }); 84 | callback(null, files); 85 | }).catch(callback); 86 | }, 87 | mkdir(fsPath, callback) { 88 | setTimeout(callback, 0); 89 | } 90 | }; 91 | } 92 | export default fsInterface; 93 | -------------------------------------------------------------------------------- /lib/readers/Filter.js: -------------------------------------------------------------------------------- 1 | import AbstractReader from "../AbstractReader.js"; 2 | 3 | /** 4 | * A reader that allows dynamic filtering of resources passed through it 5 | * 6 | * @public 7 | * @class 8 | * @alias @ui5/fs/readers/Filter 9 | * @extends @ui5/fs/AbstractReader 10 | */ 11 | class Filter extends AbstractReader { 12 | /** 13 | * Filter callback 14 | * 15 | * @public 16 | * @callback @ui5/fs/readers/Filter~callback 17 | * @param {@ui5/fs/Resource} resource Resource to test 18 | * @returns {boolean} Whether to keep the resource 19 | */ 20 | 21 | /** 22 | * Constructor 23 | * 24 | * @public 25 | * @param {object} parameters Parameters 26 | * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap 27 | * @param {@ui5/fs/readers/Filter~callback} parameters.callback 28 | * Filter function. Will be called for every resource read through this reader. 29 | */ 30 | constructor({reader, callback}) { 31 | super(); 32 | if (!reader) { 33 | throw new Error(`Missing parameter "reader"`); 34 | } 35 | if (!callback) { 36 | throw new Error(`Missing parameter "callback"`); 37 | } 38 | this._reader = reader; 39 | this._callback = callback; 40 | } 41 | 42 | /** 43 | * Locates resources by glob. 44 | * 45 | * @private 46 | * @param {string|string[]} pattern glob pattern as string or an array of 47 | * glob patterns for virtual directory structure 48 | * @param {object} options glob options 49 | * @param {@ui5/fs/tracing/Trace} trace Trace instance 50 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 51 | */ 52 | async _byGlob(pattern, options, trace) { 53 | const result = await this._reader._byGlob(pattern, options, trace); 54 | return result.filter(this._callback); 55 | } 56 | 57 | /** 58 | * Locates resources by path. 59 | * 60 | * @private 61 | * @param {string} virPath Virtual path 62 | * @param {object} options Options 63 | * @param {@ui5/fs/tracing/Trace} trace Trace instance 64 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 65 | */ 66 | async _byPath(virPath, options, trace) { 67 | const result = await this._reader._byPath(virPath, options, trace); 68 | if (result && !this._callback(result)) { 69 | return null; 70 | } 71 | return result; 72 | } 73 | } 74 | 75 | export default Filter; 76 | -------------------------------------------------------------------------------- /lib/readers/Link.js: -------------------------------------------------------------------------------- 1 | import AbstractReader from "../AbstractReader.js"; 2 | import ResourceFacade from "../ResourceFacade.js"; 3 | import {prefixGlobPattern} from "../resourceFactory.js"; 4 | import {getLogger} from "@ui5/logger"; 5 | const log = getLogger("resources:readers:Link"); 6 | 7 | /** 8 | * A reader that allows for rewriting paths segments of all resources passed through it. 9 | * 10 | * @example 11 | * import Link from "@ui5/fs/readers/Link"; 12 | * const linkedReader = new Link({ 13 | * reader: sourceReader, 14 | * pathMapping: { 15 | * linkPath: `/app`, 16 | * targetPath: `/resources/my-app-name/` 17 | * } 18 | * }); 19 | * 20 | * // The following resolves with a @ui5/fs/ResourceFacade of the resource 21 | * // located at "/resources/my-app-name/Component.js" in the sourceReader 22 | * const resource = await linkedReader.byPath("/app/Component.js"); 23 | * 24 | * @public 25 | * @class 26 | * @alias @ui5/fs/readers/Link 27 | * @extends @ui5/fs/AbstractReader 28 | */ 29 | class Link extends AbstractReader { 30 | /** 31 | * Path mapping for a [Link]{@link @ui5/fs/readers/Link} 32 | * 33 | * @public 34 | * @typedef {object} @ui5/fs/readers/Link/PathMapping 35 | * @property {string} linkPath Path to match and replace in the requested path or pattern 36 | * @property {string} targetPath Path to use as a replacement in the request for the source reader 37 | */ 38 | 39 | /** 40 | * Constructor 41 | * 42 | * @public 43 | * @param {object} parameters Parameters 44 | * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap 45 | * @param {@ui5/fs/readers/Link/PathMapping} parameters.pathMapping 46 | */ 47 | constructor({reader, pathMapping}) { 48 | super(); 49 | if (!reader) { 50 | throw new Error(`Missing parameter "reader"`); 51 | } 52 | if (!pathMapping) { 53 | throw new Error(`Missing parameter "pathMapping"`); 54 | } 55 | this._reader = reader; 56 | this._pathMapping = pathMapping; 57 | Link._validatePathMapping(pathMapping); 58 | } 59 | 60 | /** 61 | * Locates resources by glob. 62 | * 63 | * @private 64 | * @param {string|string[]} patterns glob pattern as string or an array of 65 | * glob patterns for virtual directory structure 66 | * @param {object} options glob options 67 | * @param {@ui5/fs/tracing/Trace} trace Trace instance 68 | * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources 69 | */ 70 | async _byGlob(patterns, options, trace) { 71 | if (!(patterns instanceof Array)) { 72 | patterns = [patterns]; 73 | } 74 | patterns = patterns.map((pattern) => { 75 | if (pattern.startsWith(this._pathMapping.linkPath)) { 76 | pattern = pattern.substr(this._pathMapping.linkPath.length); 77 | } 78 | return prefixGlobPattern(pattern, this._pathMapping.targetPath); 79 | }); 80 | 81 | // Flatten prefixed patterns 82 | patterns = Array.prototype.concat.apply([], patterns); 83 | 84 | // Keep resource's internal path unchanged for now 85 | const resources = await this._reader._byGlob(patterns, options, trace); 86 | return resources.map((resource) => { 87 | const resourcePath = resource.getPath(); 88 | if (resourcePath.startsWith(this._pathMapping.targetPath)) { 89 | return new ResourceFacade({ 90 | resource, 91 | path: this._pathMapping.linkPath + resourcePath.substr(this._pathMapping.targetPath.length) 92 | }); 93 | } 94 | }); 95 | } 96 | 97 | /** 98 | * Locates resources by path. 99 | * 100 | * @private 101 | * @param {string} virPath Virtual path 102 | * @param {object} options Options 103 | * @param {@ui5/fs/tracing/Trace} trace Trace instance 104 | * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource 105 | */ 106 | async _byPath(virPath, options, trace) { 107 | if (!virPath.startsWith(this._pathMapping.linkPath)) { 108 | return null; 109 | } 110 | const targetPath = this._pathMapping.targetPath + virPath.substr(this._pathMapping.linkPath.length); 111 | log.silly(`byPath: Rewriting virtual path ${virPath} to ${targetPath}`); 112 | 113 | const resource = await this._reader._byPath(targetPath, options, trace); 114 | if (resource) { 115 | return new ResourceFacade({ 116 | resource, 117 | path: this._pathMapping.linkPath + resource.getPath().substr(this._pathMapping.targetPath.length) 118 | }); 119 | } 120 | return null; 121 | } 122 | 123 | static _validatePathMapping({linkPath, targetPath}) { 124 | if (!linkPath) { 125 | throw new Error(`Path mapping is missing attribute "linkPath"`); 126 | } 127 | if (!targetPath) { 128 | throw new Error(`Path mapping is missing attribute "targetPath"`); 129 | } 130 | if (!linkPath.endsWith("/")) { 131 | throw new Error(`Link path must end with a slash: ${linkPath}`); 132 | } 133 | if (!targetPath.endsWith("/")) { 134 | throw new Error(`Target path must end with a slash: ${targetPath}`); 135 | } 136 | } 137 | } 138 | 139 | export default Link; 140 | -------------------------------------------------------------------------------- /lib/resourceFactory.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import {minimatch} from "minimatch"; 3 | import DuplexCollection from "./DuplexCollection.js"; 4 | import FsAdapter from "./adapters/FileSystem.js"; 5 | import MemAdapter from "./adapters/Memory.js"; 6 | import ReaderCollection from "./ReaderCollection.js"; 7 | import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js"; 8 | import Resource from "./Resource.js"; 9 | import WriterCollection from "./WriterCollection.js"; 10 | import Filter from "./readers/Filter.js"; 11 | import Link from "./readers/Link.js"; 12 | import {getLogger} from "@ui5/logger"; 13 | const log = getLogger("resources:resourceFactory"); 14 | 15 | /** 16 | * @module @ui5/fs/resourceFactory 17 | * @description A collection of resource related APIs 18 | * @public 19 | */ 20 | 21 | /** 22 | * Creates a resource ReaderWriter. 23 | * 24 | * If a file system base path is given, file system resource ReaderWriter is returned. 25 | * In any other case a virtual one. 26 | * 27 | * @public 28 | * @param {object} parameters Parameters 29 | * @param {string} parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash 30 | * @param {string} [parameters.fsBasePath] 31 | * File System base path. 32 | * If this parameter is supplied, a File System adapter will be created instead of a Memory adapter. 33 | * The provided path must be absolute and must use platform-specific path segment separators. 34 | * @param {string[]} [parameters.excludes] List of glob patterns to exclude 35 | * @param {object} [parameters.useGitignore=false] 36 | * Whether to apply any excludes defined in an optional .gitignore in the base directory. 37 | * This parameter only takes effect in conjunction with the fsBasePath parameter. 38 | * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) 39 | * @returns {@ui5/fs/adapters/FileSystem|@ui5/fs/adapters/Memory} File System- or Virtual Adapter 40 | */ 41 | export function createAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}) { 42 | if (fsBasePath) { 43 | return new FsAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}); 44 | } else { 45 | return new MemAdapter({virBasePath, project, excludes}); 46 | } 47 | } 48 | 49 | /** 50 | * Creates a File System adapter and wraps it in a ReaderCollection 51 | * 52 | * @public 53 | * @param {object} parameters Parameters 54 | * @param {string} parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash 55 | * @param {string} parameters.fsBasePath 56 | * File System base path. Must be absolute and must use platform-specific path segment separators 57 | * @param {object} [parameters.project] Experimental, internal parameter. Do not use 58 | * @param {string[]} [parameters.excludes] List of glob patterns to exclude 59 | * @param {string} [parameters.name] Name for the reader collection 60 | * @returns {@ui5/fs/ReaderCollection} Reader collection wrapping an adapter 61 | */ 62 | export function createReader({fsBasePath, virBasePath, project, excludes = [], name}) { 63 | if (!fsBasePath) { 64 | // Creating a reader with a memory adapter seems pointless right now 65 | // since there would be no way to fill the adapter with resources 66 | throw new Error(`Unable to create reader: Missing parameter "fsBasePath"`); 67 | } 68 | let normalizedExcludes = excludes; 69 | // If a project is supplied, and that project is of type application, 70 | // Prefix all exclude patterns with the virtual base path (unless it already starts with that) 71 | // TODO 4.0: // TODO specVersion 4.0: Disallow excludes without namespaced prefix in configuration 72 | // Specifying an exclude for "/test" is disambigous as it neither reflects the source path nor the 73 | // ui5 runtime path of the excluded resources. Therefore, only allow paths like /resources//test 74 | // starting with specVersion 4.0 75 | if (excludes.length && project && project.getType() === "application") { 76 | normalizedExcludes = excludes.map((pattern) => { 77 | if (pattern.startsWith(virBasePath) || pattern.startsWith("!" + virBasePath)) { 78 | return pattern; 79 | } 80 | log.verbose( 81 | `Prefixing exclude pattern defined in application project ${project.getName()}: ${pattern}`); 82 | return prefixGlobPattern(pattern, virBasePath); 83 | }); 84 | // Flatten list of patterns 85 | normalizedExcludes = Array.prototype.concat.apply([], normalizedExcludes); 86 | log.verbose(`Effective exclude patterns for application project ${project.getName()}:\n` + 87 | normalizedExcludes.join(", ")); 88 | } 89 | return new ReaderCollection({ 90 | name, 91 | readers: [createAdapter({ 92 | fsBasePath, 93 | virBasePath, 94 | project, 95 | excludes: normalizedExcludes 96 | })] 97 | }); 98 | } 99 | 100 | /** 101 | * Creates a ReaderCollection 102 | * 103 | * @public 104 | * @param {object} parameters Parameters 105 | * @param {string} parameters.name The collection name 106 | * @param {@ui5/fs/AbstractReader[]} parameters.readers List of resource readers (all tried in parallel) 107 | * @returns {@ui5/fs/ReaderCollection} Reader collection wrapping provided readers 108 | */ 109 | export function createReaderCollection({name, readers}) { 110 | return new ReaderCollection({ 111 | name, 112 | readers 113 | }); 114 | } 115 | 116 | /** 117 | * Creates a ReaderCollectionPrioritized 118 | * 119 | * @public 120 | * @param {object} parameters 121 | * @param {string} parameters.name The collection name 122 | * @param {@ui5/fs/AbstractReader[]} parameters.readers Prioritized list of resource readers 123 | * (first is tried first) 124 | * @returns {@ui5/fs/ReaderCollectionPrioritized} Reader collection wrapping provided readers 125 | */ 126 | export function createReaderCollectionPrioritized({name, readers}) { 127 | return new ReaderCollectionPrioritized({ 128 | name, 129 | readers 130 | }); 131 | } 132 | 133 | /** 134 | * Creates a WriterCollection 135 | * 136 | * @public 137 | * @param {object} parameters 138 | * @param {string} parameters.name The collection name 139 | * @param {object.} parameters.writerMapping Mapping of virtual base 140 | * paths to writers. Path are matched greedy 141 | * @returns {@ui5/fs/WriterCollection} Writer collection wrapping provided writers 142 | */ 143 | export function createWriterCollection({name, writerMapping}) { 144 | return new WriterCollection({ 145 | name, 146 | writerMapping 147 | }); 148 | } 149 | 150 | /** 151 | * Creates a [Resource]{@link @ui5/fs/Resource}. 152 | * Accepts the same parameters as the [Resource]{@link @ui5/fs/Resource} constructor. 153 | * 154 | * @public 155 | * @param {object} parameters Parameters to be passed to the resource constructor 156 | * @returns {@ui5/fs/Resource} Resource 157 | */ 158 | export function createResource(parameters) { 159 | return new Resource(parameters); 160 | } 161 | 162 | /** 163 | * Creates a Workspace 164 | * 165 | * A workspace is a DuplexCollection which reads from the project sources. It is used during the build process 166 | * to write modified files into a separate writer, this is usually a Memory adapter. If a file already exists it is 167 | * fetched from the memory to work on it in further build steps. 168 | * 169 | * @public 170 | * @param {object} parameters 171 | * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers 172 | * @param {@ui5/fs/AbstractReaderWriter} [parameters.writer] A ReaderWriter instance which is 173 | * only used for writing files. If not supplied, a Memory adapter will be created. 174 | * @param {string} [parameters.name="workspace"] Name of the collection 175 | * @param {string} [parameters.virBasePath="/"] Virtual base path 176 | * @returns {@ui5/fs/DuplexCollection} DuplexCollection which wraps the provided resource locators 177 | */ 178 | export function createWorkspace({reader, writer, virBasePath = "/", name = "workspace"}) { 179 | if (!writer) { 180 | writer = new MemAdapter({ 181 | virBasePath 182 | }); 183 | } 184 | 185 | return new DuplexCollection({ 186 | reader, 187 | writer, 188 | name 189 | }); 190 | } 191 | 192 | /** 193 | * Create a [Filter-Reader]{@link @ui5/fs/readers/Filter} with the given reader. 194 | * The provided callback is called for every resource that is retrieved through the 195 | * reader and decides whether the resource shall be passed on or dropped. 196 | * 197 | * @public 198 | * @param {object} parameters 199 | * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers 200 | * @param {@ui5/fs/readers/Filter~callback} parameters.callback 201 | * Filter function. Will be called for every resource passed through this reader. 202 | * @returns {@ui5/fs/readers/Filter} Reader instance 203 | */ 204 | export function createFilterReader(parameters) { 205 | return new Filter(parameters); 206 | } 207 | 208 | /** 209 | * Create a [Link-Reader]{@link @ui5/fs/readers/Filter} with the given reader. 210 | * The provided path mapping allows for rewriting paths segments of all resources passed through it. 211 | * 212 | * @example 213 | * import {createLinkReader} from "@ui5/fs/resourceFactory"; 214 | * const linkedReader = createLinkReader({ 215 | * reader: sourceReader, 216 | * pathMapping: { 217 | * linkPath: `/app`, 218 | * targetPath: `/resources/my-app-name/` 219 | * } 220 | * }); 221 | * 222 | * // The following resolves with a @ui5/fs/ResourceFacade of the resource 223 | * // located at "/resources/my-app-name/Component.js" in the sourceReader 224 | * const resource = await linkedReader.byPath("/app/Component.js"); 225 | * 226 | * @public 227 | * @param {object} parameters 228 | * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers 229 | * @param {@ui5/fs/readers/Link/PathMapping} parameters.pathMapping 230 | * @returns {@ui5/fs/readers/Link} Reader instance 231 | */ 232 | export function createLinkReader(parameters) { 233 | return new Link(parameters); 234 | } 235 | 236 | /** 237 | * Create a [Link-Reader]{@link @ui5/fs/readers/Link} where all requests are prefixed with 238 | * /resources/. 239 | * 240 | * This simulates "flat" resource access, which is for example common for projects of type 241 | * "application". 242 | * 243 | * @public 244 | * @param {object} parameters 245 | * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers 246 | * @param {string} parameters.namespace Project namespace 247 | * @returns {@ui5/fs/readers/Link} Reader instance 248 | */ 249 | export function createFlatReader({reader, namespace}) { 250 | return new Link({ 251 | reader: reader, 252 | pathMapping: { 253 | linkPath: `/`, 254 | targetPath: `/resources/${namespace}/` 255 | } 256 | }); 257 | } 258 | 259 | /** 260 | * Normalizes virtual glob patterns by prefixing them with 261 | * a given virtual base directory path 262 | * 263 | * @param {string} virPattern glob pattern for virtual directory structure 264 | * @param {string} virBaseDir virtual base directory path to prefix the given patterns with 265 | * @returns {string[]} A list of normalized glob patterns 266 | */ 267 | export function prefixGlobPattern(virPattern, virBaseDir) { 268 | const mm = new minimatch.Minimatch(virPattern); 269 | 270 | const resultGlobs = []; 271 | for (let i = 0; i < mm.globSet.length; i++) { 272 | let resultPattern = path.posix.join(virBaseDir, mm.globSet[i]); 273 | 274 | if (mm.negate) { 275 | resultPattern = "!" + resultPattern; 276 | } 277 | resultGlobs.push(resultPattern); 278 | } 279 | return resultGlobs; 280 | } 281 | -------------------------------------------------------------------------------- /lib/tracing/Trace.js: -------------------------------------------------------------------------------- 1 | import {getLogger} from "@ui5/logger"; 2 | const log = getLogger("resources:tracing:Trace"); 3 | const logGlobs = getLogger("resources:tracing:Trace:globs"); 4 | const logPaths = getLogger("resources:tracing:Trace:paths"); 5 | import prettyHrtime from "pretty-hrtime"; 6 | import summaryTrace from "./traceSummary.js"; 7 | const hasOwnProperty = Object.prototype.hasOwnProperty; 8 | 9 | /** 10 | * Trace 11 | * 12 | * @private 13 | * @class 14 | */ 15 | class Trace { 16 | constructor(name) { 17 | if (!log.isLevelEnabled("silly")) { 18 | return; 19 | } 20 | this._name = name; 21 | this._startTime = process.hrtime(); 22 | this._globCalls = 0; 23 | this._pathCalls = 0; 24 | this._collections = Object.create(null); 25 | summaryTrace.traceStarted(); 26 | } 27 | 28 | globCall() { 29 | if (!log.isLevelEnabled("silly")) { 30 | return; 31 | } 32 | this._globCalls++; 33 | summaryTrace.globCall(); 34 | } 35 | 36 | pathCall() { 37 | if (!log.isLevelEnabled("silly")) { 38 | return; 39 | } 40 | this._pathCalls++; 41 | summaryTrace.pathCall(); 42 | } 43 | 44 | collection(name) { 45 | if (!log.isLevelEnabled("silly")) { 46 | return; 47 | } 48 | const collection = this._collections[name]; 49 | if (collection) { 50 | this._collections[name].calls++; 51 | } else { 52 | this._collections[name] = { 53 | calls: 1 54 | }; 55 | } 56 | summaryTrace.collection(name); 57 | } 58 | 59 | printReport() { 60 | if (!log.isLevelEnabled("silly")) { 61 | return; 62 | } 63 | let report = ""; 64 | const timeDiff = process.hrtime(this._startTime); 65 | const time = prettyHrtime(timeDiff); 66 | const colCount = Object.keys(this._collections).length; 67 | 68 | report += `[Trace: ${this._name}\n`; 69 | report += ` ${time} elapsed time \n`; 70 | if (this._globCalls) { 71 | report += ` ${this._globCalls} glob executions\n`; 72 | } 73 | if (this._pathCalls) { 74 | report += ` ${this._pathCalls} path stats\n`; 75 | } 76 | report += ` ${colCount} reader-collections involed:\n`; 77 | 78 | for (const coll in this._collections) { 79 | if (hasOwnProperty.call(this._collections, coll)) { 80 | report += ` ${this._collections[coll].calls}x ${coll}\n`; 81 | } 82 | } 83 | report += "======================]"; 84 | 85 | if (this._globCalls && this._pathCalls) { 86 | log.silly(report); 87 | } else if (this._globCalls) { 88 | logGlobs.silly(report); 89 | } else { 90 | logPaths.silly(report); 91 | } 92 | 93 | summaryTrace.traceEnded(); 94 | } 95 | } 96 | 97 | export default Trace; 98 | -------------------------------------------------------------------------------- /lib/tracing/traceSummary.js: -------------------------------------------------------------------------------- 1 | import {getLogger} from "@ui5/logger"; 2 | const log = getLogger("resources:tracing:total"); 3 | 4 | import prettyHrtime from "pretty-hrtime"; 5 | const hasOwnProperty = Object.prototype.hasOwnProperty; 6 | let timeoutId; 7 | let active = false; 8 | let tracesRunning = 0; 9 | let traceData; 10 | 11 | function init() { 12 | traceData = { 13 | startTime: process.hrtime(), 14 | pathCalls: 0, 15 | globCalls: 0, 16 | collections: {}, 17 | traceCalls: 0 18 | }; 19 | active = true; 20 | } 21 | 22 | function reset() { 23 | traceData = null; 24 | active = false; 25 | } 26 | 27 | function report() { 28 | let report = ""; 29 | const time = prettyHrtime(traceData.timeDiff); 30 | const colCount = Object.keys(traceData.collections).length; 31 | 32 | report += "==========================\n[=> TRACE SUMMARY:\n"; 33 | report += ` ${time} elapsed time \n`; 34 | report += ` ${traceData.traceCalls} trace calls \n`; 35 | if (traceData.globCalls) { 36 | report += ` ${traceData.globCalls} glob executions\n`; 37 | } 38 | if (traceData.pathCalls) { 39 | report += ` ${traceData.pathCalls} path stats\n`; 40 | } 41 | report += ` ${colCount} rl-collections involed:\n`; 42 | 43 | for (const coll in traceData.collections) { 44 | if (hasOwnProperty.call(traceData.collections, coll)) { 45 | report += ` ${traceData.collections[coll].calls}x ${coll}\n`; 46 | } 47 | } 48 | report += "======================]"; 49 | log.silly(report); 50 | } 51 | 52 | function someTraceStarted() { 53 | if (!log.isLevelEnabled("silly")) { 54 | return; 55 | } 56 | if (!traceData) { 57 | init(); 58 | } 59 | tracesRunning++; 60 | traceData.traceCalls++; 61 | 62 | if (timeoutId) { 63 | clearTimeout(timeoutId); 64 | } 65 | } 66 | 67 | function someTraceEnded() { 68 | return new Promise(function(resolve, reject) { 69 | if (!active) { 70 | resolve(); 71 | return; 72 | } 73 | tracesRunning--; 74 | if (tracesRunning > 0) { 75 | resolve(); 76 | return; 77 | } 78 | 79 | if (timeoutId) { 80 | clearTimeout(timeoutId); 81 | } 82 | traceData.timeDiff = process.hrtime(traceData.startTime); 83 | timeoutId = setTimeout(function() { 84 | report(); 85 | reset(); 86 | resolve(); 87 | }, 2000); 88 | }); 89 | } 90 | 91 | function pathCall() { 92 | if (!active) { 93 | return; 94 | } 95 | traceData.pathCalls++; 96 | } 97 | 98 | function globCall() { 99 | if (!active) { 100 | return; 101 | } 102 | traceData.globCalls++; 103 | } 104 | 105 | function collection(name) { 106 | if (!active) { 107 | return; 108 | } 109 | const collection = traceData.collections[name]; 110 | if (collection) { 111 | traceData.collections[name].calls++; 112 | } else { 113 | traceData.collections[name] = { 114 | calls: 1 115 | }; 116 | } 117 | } 118 | 119 | export default { 120 | pathCall: pathCall, 121 | globCall: globCall, 122 | collection: collection, 123 | traceStarted: someTraceStarted, 124 | traceEnded: someTraceEnded 125 | }; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5/fs", 3 | "version": "4.0.1", 4 | "description": "UI5 Tooling - File System Abstraction", 5 | "author": { 6 | "name": "SAP SE", 7 | "email": "openui5@sap.com", 8 | "url": "https://www.sap.com" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "openui5", 13 | "sapui5", 14 | "ui5", 15 | "build", 16 | "development", 17 | "tool" 18 | ], 19 | "type": "module", 20 | "exports": { 21 | "./adapters/*": "./lib/adapters/*.js", 22 | "./AbstractReader": "./lib/AbstractReader.js", 23 | "./AbstractReaderWriter": "./lib/AbstractReaderWriter.js", 24 | "./DuplexCollection": "./lib/DuplexCollection.js", 25 | "./fsInterface": "./lib/fsInterface.js", 26 | "./readers/*": "./lib/readers/*.js", 27 | "./ReaderCollection": "./lib/ReaderCollection.js", 28 | "./ReaderCollectionPrioritized": "./lib/ReaderCollectionPrioritized.js", 29 | "./Resource": "./lib/Resource.js", 30 | "./resourceFactory": "./lib/resourceFactory.js", 31 | "./package.json": "./package.json", 32 | "./internal/ResourceTagCollection": "./lib/ResourceTagCollection.js" 33 | }, 34 | "engines": { 35 | "node": "^20.11.0 || >=22.0.0", 36 | "npm": ">= 8" 37 | }, 38 | "scripts": { 39 | "test": "npm run lint && npm run jsdoc-generate && npm run coverage && npm run depcheck", 40 | "test-azure": "npm run coverage-xunit", 41 | "lint": "eslint ./", 42 | "unit": "rimraf test/tmp && ava", 43 | "unit-verbose": "rimraf test/tmp && cross-env UI5_LOG_LVL=verbose ava --verbose --serial", 44 | "unit-watch": "npm run unit -- --watch", 45 | "unit-xunit": "rimraf test/tmp && ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml", 46 | "unit-inspect": "cross-env UI5_LOG_LVL=verbose ava debug --break", 47 | "coverage": "rimraf test/tmp && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", 48 | "coverage-xunit": "nyc --reporter=text --reporter=text-summary --reporter=cobertura npm run unit-xunit", 49 | "jsdoc": "npm run jsdoc-generate && open-cli jsdocs/index.html", 50 | "jsdoc-generate": "jsdoc -c ./jsdoc.json -t $(node -p 'path.dirname(require.resolve(\"docdash\"))') ./lib/ || (echo 'Error during JSDoc generation! Check log.' && exit 1)", 51 | "jsdoc-watch": "npm run jsdoc && chokidar \"./lib/**/*.js\" -c \"npm run jsdoc-generate\"", 52 | "preversion": "npm test", 53 | "version": "git-chglog --sort semver --next-tag v$npm_package_version -o CHANGELOG.md v4.0.0.. && git add CHANGELOG.md", 54 | "prepublishOnly": "git push --follow-tags", 55 | "release-note": "git-chglog --sort semver -c .chglog/release-config.yml v$npm_package_version", 56 | "depcheck": "depcheck --ignores @ui5/fs,docdash,@istanbuljs/esm-loader-hook" 57 | }, 58 | "files": [ 59 | "CHANGELOG.md", 60 | "CONTRIBUTING.md", 61 | "jsdoc.json", 62 | "lib/**", 63 | "LICENSES/**", 64 | ".reuse/**" 65 | ], 66 | "ava": { 67 | "files": [ 68 | "test/lib/**/*.js" 69 | ], 70 | "watchMode": { 71 | "ignoreChanges": [ 72 | "test/tmp/**" 73 | ] 74 | }, 75 | "nodeArguments": [ 76 | "--loader=esmock", 77 | "--no-warnings" 78 | ], 79 | "workerThreads": false 80 | }, 81 | "nyc": { 82 | "reporter": [ 83 | "lcov", 84 | "text", 85 | "text-summary" 86 | ], 87 | "exclude": [ 88 | "docs/**", 89 | "jsdocs/**", 90 | "coverage/**", 91 | "test/**", 92 | ".eslintrc.cjs", 93 | "jsdoc-plugin.cjs" 94 | ], 95 | "check-coverage": true, 96 | "statements": 85, 97 | "branches": 80, 98 | "functions": 90, 99 | "lines": 85, 100 | "watermarks": { 101 | "statements": [ 102 | 70, 103 | 90 104 | ], 105 | "branches": [ 106 | 70, 107 | 90 108 | ], 109 | "functions": [ 110 | 70, 111 | 90 112 | ], 113 | "lines": [ 114 | 70, 115 | 90 116 | ] 117 | }, 118 | "cache": true, 119 | "all": true 120 | }, 121 | "repository": { 122 | "type": "git", 123 | "url": "git@github.com:SAP/ui5-fs.git" 124 | }, 125 | "dependencies": { 126 | "@ui5/logger": "^4.0.1", 127 | "clone": "^2.1.2", 128 | "escape-string-regexp": "^5.0.0", 129 | "globby": "^14.1.0", 130 | "graceful-fs": "^4.2.11", 131 | "micromatch": "^4.0.8", 132 | "minimatch": "^10.0.1", 133 | "pretty-hrtime": "^1.0.3", 134 | "random-int": "^3.0.0" 135 | }, 136 | "devDependencies": { 137 | "@eslint/js": "^9.8.0", 138 | "@istanbuljs/esm-loader-hook": "^0.3.0", 139 | "ava": "^6.4.0", 140 | "chokidar-cli": "^3.0.0", 141 | "cross-env": "^7.0.3", 142 | "depcheck": "^1.4.7", 143 | "docdash": "^2.0.2", 144 | "eslint": "^9.28.0", 145 | "eslint-config-google": "^0.14.0", 146 | "eslint-plugin-ava": "^15.0.1", 147 | "eslint-plugin-jsdoc": "^50.7.1", 148 | "esmock": "^2.7.0", 149 | "globals": "^16.2.0", 150 | "jsdoc": "^4.0.4", 151 | "nyc": "^17.1.0", 152 | "open-cli": "^8.0.0", 153 | "rimraf": "^6.0.1", 154 | "sinon": "^20.0.0", 155 | "tap-xunit": "^2.4.1" 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/fixtures/application.a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application.a", 3 | "version": "1.0.0", 4 | "description": "Simple SAPUI5 based application", 5 | "main": "index.html", 6 | "dependencies": { 7 | "library.d": "file:../library.d", 8 | "collection": "file:../collection" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/application.a/ui5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | specVersion: "0.1" 3 | type: application 4 | metadata: 5 | name: application.a 6 | -------------------------------------------------------------------------------- /test/fixtures/application.a/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application A 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/application.a/webapp/test.js: -------------------------------------------------------------------------------- 1 | function test(paramA) { 2 | var variableA = paramA; 3 | console.log(variableA); 4 | } 5 | test(); 6 | -------------------------------------------------------------------------------- /test/fixtures/application.b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application.b", 3 | "version": "1.0.0", 4 | "description": "Simple SAPUI5 based application", 5 | "main": "index.html", 6 | "dependencies": { 7 | "library.d": "file:../library.d", 8 | "collection": "file:../collection" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/application.b/ui5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | specVersion: "0.1" 3 | type: application 4 | metadata: 5 | name: application.b 6 | -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/embedded/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | title=embedded-i18n -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties: -------------------------------------------------------------------------------- 1 | title=embedded-i18n_de -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/embedded/i18n_fr.properties: -------------------------------------------------------------------------------- 1 | title=embedded-i18n_fr-wrong -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/embedded/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.1.0", 3 | "sap.app": { 4 | "_version": "1.1.0", 5 | "id": "id1.embedded", 6 | "type": "component", 7 | "applicationVersion": { 8 | "version": "1.2.2" 9 | }, 10 | "embeddedBy": "../", 11 | "title": "{{title}}" 12 | } 13 | } -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/i18n.properties: -------------------------------------------------------------------------------- 1 | title=app-i18n-wrong -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | title=app-i18n -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/i18n/i18n_de.properties: -------------------------------------------------------------------------------- 1 | title=app-i18n_de -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/i18n/l10n.properties: -------------------------------------------------------------------------------- 1 | title=app-i18n-wrong -------------------------------------------------------------------------------- /test/fixtures/application.b/webapp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.1.0", 3 | "sap.app": { 4 | "_version": "1.1.0", 5 | "id": "id1", 6 | "type": "application", 7 | "applicationVersion": { 8 | "version": "1.2.2" 9 | }, 10 | "embeds": ["embedded"], 11 | "title": "{{title}}" 12 | } 13 | } -------------------------------------------------------------------------------- /test/fixtures/fsInterfáce/bâr.txt: -------------------------------------------------------------------------------- 1 | content -------------------------------------------------------------------------------- /test/fixtures/fsInterfáce/foo.txt: -------------------------------------------------------------------------------- 1 | content -------------------------------------------------------------------------------- /test/fixtures/glob/application.a/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.a/package.json -------------------------------------------------------------------------------- /test/fixtures/glob/application.a/ui5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | specVersion: "0.1" 3 | type: application 4 | metadata: 5 | name: application.a 6 | -------------------------------------------------------------------------------- /test/fixtures/glob/application.a/webapp/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.a/webapp/index.html -------------------------------------------------------------------------------- /test/fixtures/glob/application.a/webapp/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.a/webapp/test.js -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/package.json -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/ui5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | specVersion: "0.1" 3 | type: application 4 | metadata: 5 | name: application.b 6 | -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/embedded/manifest.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/embedded/manifest.json -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/i18n.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/i18n.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/i18n/i18n.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/i18n/l10n.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/i18n/l10n.properties -------------------------------------------------------------------------------- /test/fixtures/glob/application.b/webapp/manifest.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/application.b/webapp/manifest.json -------------------------------------------------------------------------------- /test/fixtures/glob/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/glob/package.json -------------------------------------------------------------------------------- /test/fixtures/library.l/.gitignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /test/fixtures/library.l/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library.l", 3 | "version": "1.0.0", 4 | "description": "Simple SAPUI5 based library - test for glob excludes", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/library.l/src/library/l/.library: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | library.l 5 | SAP SE 6 | ${copyright} 7 | ${version} 8 | 9 | Library L 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/library.l/src/library/l/some.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | console.log('HelloWorld'); -------------------------------------------------------------------------------- /test/fixtures/library.l/test/library/l/Test.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/library.l/test/library/l/Test.html -------------------------------------------------------------------------------- /test/fixtures/library.l/test/library/l/Test2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-fs/8395c40bb9a8805093afa75fe8115507ac35b4f4/test/fixtures/library.l/test/library/l/Test2.html -------------------------------------------------------------------------------- /test/fixtures/library.l/ui5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | specVersion: "0.1" 3 | type: library 4 | metadata: 5 | name: library.l 6 | copyright: |- 7 | UI development toolkit for HTML5 (OpenUI5) 8 | * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. 9 | * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. 10 | builder: 11 | resources: 12 | excludes: 13 | - /resources/**/some.js 14 | - /test-resources/** -------------------------------------------------------------------------------- /test/lib/AbstractReader.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import AbstractReader from "../../lib/AbstractReader.js"; 3 | 4 | test("AbstractReader: constructor throws an error", (t) => { 5 | t.throws(() => { 6 | new AbstractReader(); 7 | }, { 8 | instanceOf: TypeError, 9 | message: "Class 'AbstractReader' is abstract" 10 | }); 11 | }); 12 | 13 | test("Incomplete AbstractReader subclass: Abstract functions throw error", (t) => { 14 | class Dummy extends AbstractReader {} 15 | 16 | const instance = new Dummy(); 17 | t.throws(() => { 18 | instance._byGlob(); 19 | }, { 20 | instanceOf: Error, 21 | message: "Function '_byGlob' is not implemented" 22 | }); 23 | 24 | t.throws(() => { 25 | instance._runGlob(); 26 | }, { 27 | instanceOf: Error, 28 | message: "Function '_runGlob' is not implemented" 29 | }); 30 | 31 | t.throws(() => { 32 | instance._byPath(); 33 | }, { 34 | instanceOf: Error, 35 | message: "Function '_byPath' is not implemented" 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/lib/AbstractReaderWriter.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import AbstractReaderWriter from "../../lib/AbstractReaderWriter.js"; 3 | 4 | test("AbstractReaderWriter: constructor throws an error", (t) => { 5 | t.throws(() => { 6 | new AbstractReaderWriter(); 7 | }, { 8 | instanceOf: TypeError, 9 | message: "Class 'AbstractReaderWriter' is abstract" 10 | }); 11 | }); 12 | 13 | test("Incomplete AbstractReaderWriter subclass: Abstract functions throw error", (t) => { 14 | class Dummy extends AbstractReaderWriter {} 15 | 16 | const instance = new Dummy(); 17 | 18 | t.throws(() => { 19 | instance._write(); 20 | }, { 21 | instanceOf: Error, 22 | message: "Not implemented" 23 | }); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /test/lib/DuplexCollection.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import DuplexCollection from "../../lib/DuplexCollection.js"; 4 | import ReaderCollectionPrioritized from "../../lib/ReaderCollectionPrioritized.js"; 5 | import Resource from "../../lib/Resource.js"; 6 | 7 | test("DuplexCollection: constructor", (t) => { 8 | const duplexCollection = new DuplexCollection({ 9 | name: "myCollection", 10 | reader: {}, 11 | writer: {} 12 | }); 13 | 14 | t.deepEqual(duplexCollection._reader, {}, "reader assigned"); 15 | t.deepEqual(duplexCollection._writer, {}, "writer assigned"); 16 | t.true(duplexCollection._combo instanceof ReaderCollectionPrioritized, "prioritized reader collection created"); 17 | t.is(duplexCollection._combo.getName(), "myCollection - ReaderCollectionPrioritized", "name assigned"); 18 | t.deepEqual(duplexCollection._combo._readers, [{}, {}], "reader and writer assigned to readers"); 19 | }); 20 | 21 | test("DuplexCollection: constructor with setting default name of an empty string", (t) => { 22 | const duplexCollection = new DuplexCollection({ 23 | reader: {}, 24 | writer: {} 25 | }); 26 | 27 | t.deepEqual(duplexCollection._reader, {}, "reader assigned"); 28 | t.deepEqual(duplexCollection._writer, {}, "writer assigned"); 29 | t.true(duplexCollection._combo instanceof ReaderCollectionPrioritized, "prioritized reader collection created"); 30 | t.is(duplexCollection._combo.getName(), " - ReaderCollectionPrioritized", "name assigned"); 31 | t.deepEqual(duplexCollection._combo._readers, [{}, {}], "reader and writer assigned to readers"); 32 | }); 33 | 34 | test("DuplexCollection: _byGlob w/o finding a resource", async (t) => { 35 | t.plan(3); 36 | 37 | const abstractReader = { 38 | _byGlob: sinon.stub().returns(Promise.resolve([])) 39 | }; 40 | const duplexCollection = new DuplexCollection({ 41 | name: "myCollection", 42 | reader: abstractReader, 43 | writer: abstractReader 44 | }); 45 | const trace = { 46 | collection: sinon.spy() 47 | }; 48 | const comboSpy = sinon.spy(duplexCollection._combo, "_byGlob"); 49 | 50 | const resources = await duplexCollection._byGlob("anyPattern", {someOption: true}, trace); 51 | 52 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 53 | t.true(resources.length === 0, "No resources found"); 54 | t.true(comboSpy.calledWithExactly("anyPattern", {someOption: true}, trace), 55 | "Delegated globbing task correctly to readers"); 56 | }); 57 | 58 | test("DuplexCollection: _byGlob", async (t) => { 59 | t.plan(5); 60 | 61 | const resource = new Resource({ 62 | path: "/my/path", 63 | buffer: Buffer.from("content") 64 | }); 65 | const abstractReader = { 66 | _byGlob: sinon.stub().returns(Promise.resolve([resource])) 67 | }; 68 | const duplexCollection = new DuplexCollection({ 69 | name: "myCollection", 70 | reader: abstractReader, 71 | writer: abstractReader 72 | }); 73 | const trace = { 74 | collection: sinon.spy() 75 | }; 76 | const comboSpy = sinon.spy(duplexCollection._combo, "_byGlob"); 77 | const resources = await duplexCollection._byGlob("anyPattern", {someOption: true}, trace); 78 | const resourceContent = await resource.getString(); 79 | 80 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 81 | t.true(resources.length === 1, "Resource found"); 82 | t.is(resource.getPath(), "/my/path", "Resource has expected path"); 83 | t.true(comboSpy.calledWithExactly("anyPattern", {someOption: true}, trace), 84 | "Delegated globbing task correctly to readers"); 85 | t.is(resourceContent, "content", "Resource has expected content"); 86 | }); 87 | 88 | test("DuplexCollection: _byPath with reader finding a resource", async (t) => { 89 | t.plan(4); 90 | 91 | const resource = new Resource({ 92 | path: "/path", 93 | buffer: Buffer.from("content") 94 | }); 95 | const pushCollectionSpy = sinon.spy(resource, "pushCollection"); 96 | const abstractReader = { 97 | _byPath: sinon.stub().returns(Promise.resolve(resource)) 98 | }; 99 | const trace = { 100 | collection: sinon.spy() 101 | }; 102 | const duplexCollection = new DuplexCollection({ 103 | name: "myCollection", 104 | reader: abstractReader, 105 | writer: abstractReader 106 | }); 107 | const comboSpy = sinon.spy(duplexCollection._combo, "_byPath"); 108 | const readResource = await duplexCollection._byPath("anyVirtualPath", {someOption: true}, trace); 109 | const readResourceContent = await readResource.getString(); 110 | 111 | t.true(comboSpy.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 112 | "Delegated globbing task correctly to readers"); 113 | t.true(pushCollectionSpy.called, "pushCollection called on resource"); 114 | t.is(readResource.getPath(), "/path", "Resource has expected path"); 115 | t.is(readResourceContent, "content", "Resource has expected content"); 116 | }); 117 | 118 | test("DuplexCollection: _byPath with two readers both finding no resource", async (t) => { 119 | t.plan(3); 120 | 121 | const abstractReaderOne = { 122 | _byPath: sinon.stub().returns(Promise.resolve()) 123 | }; 124 | const abstractReaderTwo = { 125 | _byPath: sinon.stub().returns(Promise.resolve()) 126 | }; 127 | const trace = { 128 | collection: sinon.stub() 129 | }; 130 | const duplexCollection = new DuplexCollection({ 131 | name: "myCollection", 132 | reader: abstractReaderOne, 133 | writer: abstractReaderTwo 134 | }); 135 | const readResource = await duplexCollection._byPath("anyVirtualPath", {someOption: true}, trace); 136 | 137 | t.true(abstractReaderOne._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 138 | "Delegated globbing task correctly to reader one"); 139 | t.true(abstractReaderTwo._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 140 | "Delegated globbing task correctly to reader two"); 141 | t.falsy(readResource, "No resource found"); 142 | }); 143 | 144 | test("DuplexCollection: _write successful", async (t) => { 145 | t.plan(1); 146 | 147 | const resource = new Resource({ 148 | path: "/my/path", 149 | buffer: Buffer.from("content") 150 | }); 151 | const duplexCollection = new DuplexCollection({ 152 | name: "myCollection", 153 | reader: {}, 154 | writer: { 155 | write: sinon.stub().returns(Promise.resolve()) 156 | } 157 | }); 158 | await duplexCollection._write(resource); 159 | 160 | t.pass("write on writer called"); 161 | }); 162 | 163 | test("DuplexCollection: Throws for empty reader", (t) => { 164 | t.throws(() => { 165 | new DuplexCollection({ 166 | name: "myReader", 167 | writer: {} 168 | }); 169 | }, { 170 | message: "Cannot create DuplexCollection myReader: No reader provided" 171 | }); 172 | }); 173 | 174 | test("DuplexCollection: Throws for empty writer", (t) => { 175 | t.throws(() => { 176 | new DuplexCollection({ 177 | name: "myReader", 178 | reader: {} 179 | }); 180 | }, { 181 | message: "Cannot create DuplexCollection myReader: No writer provided" 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /test/lib/ReaderCollection.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import ReaderCollection from "../../lib/ReaderCollection.js"; 4 | import Resource from "../../lib/Resource.js"; 5 | 6 | test("ReaderCollection: constructor", (t) => { 7 | const readerCollection = new ReaderCollection({ 8 | name: "myReader", 9 | readers: [{}, {}, {}] 10 | }); 11 | 12 | t.is(readerCollection.getName(), "myReader", "correct name assigned"); 13 | t.deepEqual(readerCollection._readers, [{}, {}, {}], "correct readers assigned"); 14 | }); 15 | 16 | test("ReaderCollection: _byGlob w/o finding a resource", async (t) => { 17 | t.plan(4); 18 | 19 | const abstractReader = { 20 | _byGlob: sinon.stub().returns(Promise.resolve([])) 21 | }; 22 | const trace = { 23 | collection: sinon.spy() 24 | }; 25 | const readerCollection = new ReaderCollection({ 26 | name: "myReader", 27 | readers: [abstractReader] 28 | }); 29 | const resources = await readerCollection._byGlob("anyPattern", {someOption: true}, trace); 30 | 31 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 32 | t.true(resources.length === 0, "No resources found"); 33 | t.true(abstractReader._byGlob.calledWithExactly("anyPattern", {someOption: true}, trace), 34 | "Delegated globbing task correctly to readers"); 35 | t.true(trace.collection.called, "Trace.collection called"); 36 | }); 37 | 38 | test("ReaderCollection: _byGlob with finding a resource", async (t) => { 39 | t.plan(6); 40 | 41 | const resource = new Resource({ 42 | path: "/my/path", 43 | buffer: Buffer.from("content") 44 | }); 45 | const abstractReader = { 46 | _byGlob: sinon.stub().returns(Promise.resolve([resource])) 47 | }; 48 | const trace = { 49 | collection: sinon.spy() 50 | }; 51 | const readerCollection = new ReaderCollection({ 52 | name: "myReader", 53 | readers: [abstractReader] 54 | }); 55 | 56 | const resources = await readerCollection._byGlob("anyPattern", {someOption: true}, trace); 57 | const resourceContent = await resources[0].getString(); 58 | 59 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 60 | t.true(resources.length === 1, "Resource found"); 61 | t.true(abstractReader._byGlob.calledWithExactly("anyPattern", {someOption: true}, trace), 62 | "Delegated globbing task correctly to readers"); 63 | t.true(trace.collection.called, "Trace.collection called"); 64 | t.is(resources[0].getPath(), "/my/path", "Resource has expected path"); 65 | t.is(resourceContent, "content", "Resource has expected content"); 66 | }); 67 | 68 | test("ReaderCollection: _byPath with reader finding a resource", async (t) => { 69 | t.plan(5); 70 | 71 | const resource = new Resource({ 72 | path: "/my/path", 73 | buffer: Buffer.from("content") 74 | }); 75 | const pushCollectionSpy = sinon.spy(resource, "pushCollection"); 76 | const abstractReader = { 77 | _byPath: sinon.stub().returns(Promise.resolve(resource)) 78 | }; 79 | const trace = { 80 | collection: sinon.spy() 81 | }; 82 | const readerCollection = new ReaderCollection({ 83 | name: "myReader", 84 | readers: [abstractReader] 85 | }); 86 | 87 | const readResource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); 88 | const readResourceContent = await resource.getString(); 89 | 90 | t.true(abstractReader._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 91 | "Delegated globbing task correctly to readers"); 92 | t.true(trace.collection.called, "Trace.collection called"); 93 | t.true(pushCollectionSpy.called, "pushCollection called on resource"); 94 | t.is(readResource.getPath(), "/my/path", "Resource has expected path"); 95 | t.is(readResourceContent, "content", "Resource has expected content"); 96 | }); 97 | 98 | test("ReaderCollection: _byPath with two readers both finding no resource", async (t) => { 99 | t.plan(4); 100 | 101 | const abstractReaderOne = { 102 | _byPath: sinon.stub().returns(Promise.resolve()) 103 | }; 104 | const abstractReaderTwo = { 105 | _byPath: sinon.stub().returns(Promise.resolve()) 106 | }; 107 | const trace = { 108 | collection: sinon.spy() 109 | }; 110 | const readerCollection = new ReaderCollection({ 111 | name: "myReader", 112 | readers: [abstractReaderOne, abstractReaderTwo] 113 | }); 114 | 115 | const resource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); 116 | 117 | t.falsy(resource, "No resource found"); 118 | t.true(abstractReaderOne._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 119 | "Delegated globbing task correctly to reader one"); 120 | t.true(abstractReaderTwo._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 121 | "Delegated globbing task correctly to reader two"); 122 | t.true(trace.collection.calledTwice, "Trace.collection called"); 123 | }); 124 | 125 | test("ReaderCollection: _byPath with empty readers array", async (t) => { 126 | const trace = { 127 | collection: sinon.spy() 128 | }; 129 | const readerCollection = new ReaderCollection({ 130 | name: "myReader", 131 | readers: [] 132 | }); 133 | 134 | const resource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); 135 | t.is(resource, null, "Promise resolves to null, as no readers got configured"); 136 | }); 137 | 138 | test("ReaderCollection: _byPath with some empty readers", async (t) => { 139 | const resource = new Resource({ 140 | path: "/my/path", 141 | buffer: Buffer.from("content") 142 | }); 143 | const abstractReaderOne = { 144 | _byPath: sinon.stub().resolves(resource) 145 | }; 146 | const abstractReaderTwo = { 147 | _byPath: sinon.stub().resolves() 148 | }; 149 | 150 | const trace = { 151 | collection: sinon.spy() 152 | }; 153 | const readerCollection = new ReaderCollection({ 154 | name: "myReader", 155 | readers: [abstractReaderOne, undefined, abstractReaderTwo] 156 | }); 157 | 158 | const res = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); 159 | t.is(res, resource, "Found expected resource"); 160 | }); 161 | 162 | test("ReaderCollection: _byGlob with empty readers array", async (t) => { 163 | const trace = { 164 | collection: sinon.spy() 165 | }; 166 | const readerCollection = new ReaderCollection({ 167 | name: "myReader", 168 | readers: [] 169 | }); 170 | 171 | const resource = await readerCollection.byGlob("anyPattern", {someOption: true}, trace); 172 | t.deepEqual(resource, [], "Promise resolves to null, as no readers got configured"); 173 | }); 174 | 175 | test("ReaderCollection: _byGlob with some empty readers", async (t) => { 176 | const resource = new Resource({ 177 | path: "/my/path", 178 | buffer: Buffer.from("content") 179 | }); 180 | const abstractReaderOne = { 181 | _byGlob: sinon.stub().resolves([resource]) 182 | }; 183 | const abstractReaderTwo = { 184 | _byGlob: sinon.stub().resolves([]) 185 | }; 186 | 187 | const trace = { 188 | collection: sinon.spy() 189 | }; 190 | const readerCollection = new ReaderCollection({ 191 | name: "myReader", 192 | readers: [abstractReaderOne, undefined, abstractReaderTwo] 193 | }); 194 | 195 | const res = await readerCollection._byGlob("anyVirtualPath", {someOption: true}, trace); 196 | t.is(res.length, 1, "Found one resource"); 197 | t.is(res[0], resource, "Found expected resource"); 198 | }); 199 | -------------------------------------------------------------------------------- /test/lib/ReaderCollectionPrioritized.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import ReaderCollectionPrioritized from "../../lib/ReaderCollectionPrioritized.js"; 4 | import Resource from "../../lib/Resource.js"; 5 | 6 | test("ReaderCollectionPrioritized: constructor", (t) => { 7 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 8 | name: "myReader", 9 | readers: [{}, {}, {}] 10 | }); 11 | 12 | t.is(readerCollectionPrioritized.getName(), "myReader", "correct name assigned"); 13 | t.deepEqual(readerCollectionPrioritized._readers, [{}, {}, {}], "correct readers assigned"); 14 | }); 15 | 16 | test("ReaderCollectionPrioritized: _byGlob w/o finding a resource", async (t) => { 17 | const abstractReader = { 18 | _byGlob: sinon.stub().returns(Promise.resolve([])) 19 | }; 20 | const trace = { 21 | collection: sinon.spy() 22 | }; 23 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 24 | name: "myReader", 25 | readers: [abstractReader] 26 | }); 27 | const resources = await readerCollectionPrioritized._byGlob("anyPattern", {someOption: true}, trace); 28 | 29 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 30 | t.true(resources.length === 0, "No resources found"); 31 | t.true(abstractReader._byGlob.calledWithExactly("anyPattern", {someOption: true}, trace), 32 | "Delegated globbing task correctly to readers"); 33 | t.true(trace.collection.called, "Trace.collection called"); 34 | }); 35 | 36 | test("ReaderCollectionPrioritized: _byGlob with finding a resource", async (t) => { 37 | const resource = new Resource({ 38 | path: "/my/path", 39 | buffer: Buffer.from("content") 40 | }); 41 | const abstractReader = { 42 | _byGlob: sinon.stub().returns(Promise.resolve([resource])) 43 | }; 44 | const trace = { 45 | collection: sinon.spy() 46 | }; 47 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 48 | name: "myReader", 49 | readers: [abstractReader] 50 | }); 51 | 52 | const resources = await readerCollectionPrioritized._byGlob("anyPattern", {someOption: true}, trace); 53 | const resourceContent = await resources[0].getString(); 54 | 55 | t.true(Array.isArray(resources), "Found resources are returned as an array"); 56 | t.true(resources.length === 1, "Resource found"); 57 | t.true(abstractReader._byGlob.calledWithExactly("anyPattern", {someOption: true}, trace), 58 | "Delegated globbing task correctly to readers"); 59 | t.true(trace.collection.called, "Trace.collection called"); 60 | t.is(resources[0].getPath(), "/my/path", "Resource has expected path"); 61 | t.is(resourceContent, "content", "Resource has expected content"); 62 | }); 63 | 64 | test("ReaderCollectionPrioritized: _byPath with reader finding a resource", async (t) => { 65 | const resource = new Resource({ 66 | path: "/my/path", 67 | buffer: Buffer.from("content") 68 | }); 69 | const pushCollectionSpy = sinon.spy(resource, "pushCollection"); 70 | const abstractReader = { 71 | _byPath: sinon.stub().returns(Promise.resolve(resource)) 72 | }; 73 | const trace = { 74 | collection: sinon.spy() 75 | }; 76 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 77 | name: "myReader", 78 | readers: [abstractReader] 79 | }); 80 | 81 | const readResource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); 82 | const readResourceContent = await resource.getString(); 83 | 84 | t.true(abstractReader._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 85 | "Delegated globbing task correctly to readers"); 86 | t.true(pushCollectionSpy.called, "pushCollection called on resource"); 87 | t.is(readResource.getPath(), "/my/path", "Resource has expected path"); 88 | t.is(readResourceContent, "content", "Resource has expected content"); 89 | }); 90 | 91 | test("ReaderCollectionPrioritized: _byPath with two readers both finding no resource", async (t) => { 92 | const abstractReaderOne = { 93 | _byPath: sinon.stub().returns(Promise.resolve()) 94 | }; 95 | const abstractReaderTwo = { 96 | _byPath: sinon.stub().returns(Promise.resolve()) 97 | }; 98 | const trace = { 99 | collection: sinon.spy() 100 | }; 101 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 102 | name: "myReader", 103 | readers: [abstractReaderOne, abstractReaderTwo] 104 | }); 105 | 106 | const resource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); 107 | 108 | t.falsy(resource, "No resource found"); 109 | t.true(abstractReaderOne._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 110 | "Delegated globbing task correctly to reader one"); 111 | t.true(abstractReaderTwo._byPath.calledWithExactly("anyVirtualPath", {someOption: true}, trace), 112 | "Delegated globbing task correctly to reader two"); 113 | }); 114 | 115 | test("ReaderCollectionPrioritized: _byPath with empty readers array", async (t) => { 116 | const trace = { 117 | collection: sinon.spy() 118 | }; 119 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 120 | name: "myReader", 121 | readers: [] 122 | }); 123 | 124 | const resource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); 125 | t.is(resource, null, "Promise resolves to null, as no readers got configured"); 126 | }); 127 | 128 | test("ReaderCollectionPrioritized: _byPath with some empty readers", async (t) => { 129 | const resource = new Resource({ 130 | path: "/my/path", 131 | buffer: Buffer.from("content") 132 | }); 133 | const abstractReaderOne = { 134 | _byPath: sinon.stub().resolves(resource) 135 | }; 136 | const abstractReaderTwo = { 137 | _byPath: sinon.stub().resolves() 138 | }; 139 | 140 | const trace = { 141 | collection: sinon.spy() 142 | }; 143 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 144 | name: "myReader", 145 | readers: [abstractReaderOne, undefined, abstractReaderTwo] 146 | }); 147 | 148 | const res = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); 149 | t.is(res, resource, "Found expected resource"); 150 | }); 151 | 152 | test("ReaderCollectionPrioritized: _byGlob with empty readers array", async (t) => { 153 | const trace = { 154 | collection: sinon.spy() 155 | }; 156 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 157 | name: "myReader", 158 | readers: [] 159 | }); 160 | 161 | const resource = await readerCollectionPrioritized.byGlob("anyPattern", {someOption: true}, trace); 162 | t.deepEqual(resource, [], "Promise resolves to null, as no readers got configured"); 163 | }); 164 | 165 | test("ReaderCollectionPrioritized: _byGlob with some empty readers", async (t) => { 166 | const resource = new Resource({ 167 | path: "/my/path", 168 | buffer: Buffer.from("content") 169 | }); 170 | const abstractReaderOne = { 171 | _byGlob: sinon.stub().resolves([resource]) 172 | }; 173 | const abstractReaderTwo = { 174 | _byGlob: sinon.stub().resolves([]) 175 | }; 176 | 177 | const trace = { 178 | collection: sinon.spy() 179 | }; 180 | const readerCollectionPrioritized = new ReaderCollectionPrioritized({ 181 | name: "myReader", 182 | readers: [abstractReaderOne, undefined, abstractReaderTwo] 183 | }); 184 | 185 | const res = await readerCollectionPrioritized._byGlob("anyVirtualPath", {someOption: true}, trace); 186 | t.is(res.length, 1, "Found one resource"); 187 | t.is(res[0], resource, "Found expected resource"); 188 | }); 189 | -------------------------------------------------------------------------------- /test/lib/ResourceFacade.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import Resource from "../../lib/Resource.js"; 4 | import ResourceFacade from "../../lib/ResourceFacade.js"; 5 | 6 | test.afterEach.always( (t) => { 7 | sinon.restore(); 8 | }); 9 | 10 | test("Create instance", (t) => { 11 | const resource = new Resource({ 12 | path: "/my/path/to/resource", 13 | string: "my content" 14 | }); 15 | const resourceFacade = new ResourceFacade({ 16 | path: "/my/path", 17 | resource 18 | }); 19 | t.is(resourceFacade.getPath(), "/my/path", "Returns correct path"); 20 | t.is(resourceFacade.getName(), "path", "Returns correct name"); 21 | t.is(resourceFacade.getConcealedResource(), resource, "Returns correct concealed resource"); 22 | }); 23 | 24 | test("Create instance: Missing parameters", (t) => { 25 | t.throws(() => { 26 | new ResourceFacade({ 27 | path: "/my/path", 28 | }); 29 | }, { 30 | instanceOf: Error, 31 | message: "Unable to create ResourceFacade: Missing parameter 'resource'" 32 | }); 33 | t.throws(() => { 34 | new ResourceFacade({ 35 | resource: {}, 36 | }); 37 | }, { 38 | instanceOf: Error, 39 | message: "Unable to create ResourceFacade: Missing parameter 'path'" 40 | }); 41 | }); 42 | 43 | test("ResourceFacade #clone", async (t) => { 44 | const resource = new Resource({ 45 | path: "/my/path/to/resource", 46 | string: "my content" 47 | }); 48 | const resourceFacade = new ResourceFacade({ 49 | path: "/my/path", 50 | resource 51 | }); 52 | 53 | const clone = await resourceFacade.clone(); 54 | t.true(clone instanceof Resource, "Cloned resource facade is an instance of Resource"); 55 | t.is(clone.getPath(), "/my/path", "Cloned resource has path of resource facade"); 56 | }); 57 | 58 | test("ResourceFacade #setPath", (t) => { 59 | const resource = new Resource({ 60 | path: "/my/path/to/resource", 61 | string: "my content" 62 | }); 63 | const resourceFacade = new ResourceFacade({ 64 | path: "/my/path", 65 | resource 66 | }); 67 | 68 | const err = t.throws(() => { 69 | resourceFacade.setPath("my/other/path"); 70 | }); 71 | t.is(err.message, "The path of a ResourceFacade can't be changed", "Threw with expected error message"); 72 | }); 73 | 74 | test("ResourceFacade provides same public functions as Resource", (t) => { 75 | const resource = new Resource({ 76 | path: "/my/path/to/resource", 77 | string: "my content" 78 | }); 79 | const resourceFacade = new ResourceFacade({ 80 | path: "/my/path", 81 | resource 82 | }); 83 | 84 | const methods = Object.getOwnPropertyNames(Resource.prototype) 85 | .filter((p) => (!p.startsWith("_") && typeof resource[p] === "function")); 86 | 87 | methods.forEach((method) => { 88 | t.truthy(resourceFacade[method], `resourceFacade provides function #${method}`); 89 | if (["constructor", "getPath", "getName", "setPath", "clone"].includes(method)) { 90 | // special functions with separate tests 91 | return; 92 | } 93 | const stub = sinon.stub(resource, method); 94 | resourceFacade[method]("argument"); 95 | t.is(stub.callCount, 1, `Resource#${method} stub got called once by resourceFacade#${method}`); 96 | stub.restore(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/lib/ResourceTagCollection.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import Resource from "../../lib/Resource.js"; 4 | import ResourceTagCollection from "../../lib/ResourceTagCollection.js"; 5 | 6 | test.afterEach.always((t) => { 7 | sinon.restore(); 8 | }); 9 | 10 | test("setTag", (t) => { 11 | const resource = new Resource({ 12 | path: "/some/path" 13 | }); 14 | const tagCollection = new ResourceTagCollection({ 15 | allowedTags: ["abc:MyTag"] 16 | }); 17 | 18 | const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); 19 | const validateTagSpy = sinon.spy(tagCollection, "_validateTag"); 20 | const validateValueSpy = sinon.spy(tagCollection, "_validateValue"); 21 | 22 | tagCollection.setTag(resource, "abc:MyTag", "my value"); 23 | 24 | t.deepEqual(tagCollection._pathTags, { 25 | "/some/path": { 26 | "abc:MyTag": "my value" 27 | } 28 | }, "Tag correctly stored"); 29 | 30 | t.is(validateResourceSpy.callCount, 1, "_getPath called once"); 31 | t.is(validateResourceSpy.getCall(0).args[0], resource, 32 | "_getPath called with correct arguments"); 33 | 34 | t.is(validateTagSpy.callCount, 1, "_validateTag called once"); 35 | t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag", 36 | "_validateTag called with correct arguments"); 37 | 38 | t.is(validateValueSpy.callCount, 1, "_validateValue called once"); 39 | t.is(validateValueSpy.getCall(0).args[0], "my value", 40 | "_validateValue called with correct arguments"); 41 | }); 42 | 43 | test("setTag: Value defaults to true", (t) => { 44 | const resource = new Resource({ 45 | path: "/some/path" 46 | }); 47 | const tagCollection = new ResourceTagCollection({ 48 | allowedTags: ["abc:MyTag"] 49 | }); 50 | tagCollection.setTag(resource, "abc:MyTag"); 51 | 52 | t.deepEqual(tagCollection._pathTags, { 53 | "/some/path": { 54 | "abc:MyTag": true 55 | } 56 | }, "Tag correctly stored"); 57 | }); 58 | 59 | test("getTag", (t) => { 60 | const resource = new Resource({ 61 | path: "/some/path" 62 | }); 63 | const tagCollection = new ResourceTagCollection({ 64 | allowedTags: ["abc:MyTag"] 65 | }); 66 | tagCollection.setTag(resource, "abc:MyTag", 123); 67 | 68 | const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); 69 | const validateTagSpy = sinon.spy(tagCollection, "_validateTag"); 70 | 71 | const value = tagCollection.getTag(resource, "abc:MyTag"); 72 | 73 | t.is(value, 123, "Got correct tag value"); 74 | 75 | t.is(validateResourceSpy.callCount, 1, "_getPath called once"); 76 | t.is(validateResourceSpy.getCall(0).args[0], resource, 77 | "_getPath called with correct arguments"); 78 | 79 | t.is(validateTagSpy.callCount, 1, "_validateTag called once"); 80 | t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag", 81 | "_validateTag called with correct arguments"); 82 | }); 83 | 84 | test("getTag with prefilled tags", (t) => { 85 | const resource = new Resource({ 86 | path: "/some/path" 87 | }); 88 | const tagCollection = new ResourceTagCollection({ 89 | allowedTags: ["abc:MyTag"], 90 | tags: { 91 | "/some/path": { 92 | "abc:MyTag": 123 93 | } 94 | } 95 | }); 96 | 97 | const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); 98 | const validateTagSpy = sinon.spy(tagCollection, "_validateTag"); 99 | 100 | const value = tagCollection.getTag(resource, "abc:MyTag"); 101 | 102 | t.is(value, 123, "Got correct tag value"); 103 | 104 | t.is(validateResourceSpy.callCount, 1, "_getPath called once"); 105 | t.is(validateResourceSpy.getCall(0).args[0], resource, 106 | "_getPath called with correct arguments"); 107 | 108 | t.is(validateTagSpy.callCount, 1, "_validateTag called once"); 109 | t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag", 110 | "_validateTag called with correct arguments"); 111 | }); 112 | 113 | test("clearTag", (t) => { 114 | const resource = new Resource({ 115 | path: "/some/path" 116 | }); 117 | const tagCollection = new ResourceTagCollection({ 118 | allowedTags: ["abc:MyTag"] 119 | }); 120 | 121 | tagCollection.setTag(resource, "abc:MyTag", 123); 122 | 123 | const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); 124 | const validateTagSpy = sinon.spy(tagCollection, "_validateTag"); 125 | 126 | tagCollection.clearTag(resource, "abc:MyTag"); 127 | 128 | t.deepEqual(tagCollection._pathTags, { 129 | "/some/path": { 130 | "abc:MyTag": undefined 131 | } 132 | }, "Tag value set to undefined"); 133 | 134 | t.is(validateResourceSpy.callCount, 1, "_getPath called once"); 135 | t.is(validateResourceSpy.getCall(0).args[0], resource, 136 | "_getPath called with correct arguments"); 137 | 138 | t.is(validateTagSpy.callCount, 1, "_validateTag called once"); 139 | t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag", 140 | "_validateTag called with correct arguments"); 141 | }); 142 | 143 | test("_validateTag: Not in list of allowed tags", (t) => { 144 | const tagCollection = new ResourceTagCollection({ 145 | allowedTags: ["abc:MyTag"] 146 | }); 147 | t.throws(() => { 148 | tagCollection._validateTag("abc:MyOtherTag"); 149 | }, { 150 | instanceOf: Error, 151 | message: `Tag "abc:MyOtherTag" not accepted by this collection. ` + 152 | `Accepted tags are: abc:MyTag. Accepted namespaces are: *none*` 153 | }); 154 | }); 155 | 156 | test("_validateTag: Empty list of tags and namespaces", (t) => { 157 | const tagCollection = new ResourceTagCollection({ 158 | allowedTags: [], 159 | allowedNamespaces: [] 160 | }); 161 | t.throws(() => { 162 | tagCollection._validateTag("abc:MyOtherTag"); 163 | }, { 164 | instanceOf: Error, 165 | message: `Tag "abc:MyOtherTag" not accepted by this collection. ` + 166 | `Accepted tags are: *none*. Accepted namespaces are: *none*` 167 | }); 168 | }); 169 | 170 | test("_validateTag: Missing colon", (t) => { 171 | const tagCollection = new ResourceTagCollection({ 172 | allowedTags: ["aBcMyTag"] 173 | }); 174 | t.throws(() => { 175 | tagCollection._validateTag("aBcMyTag"); 176 | }, { 177 | instanceOf: Error, 178 | message: `Invalid Tag "aBcMyTag": Colon required after namespace` 179 | }); 180 | }); 181 | 182 | test("_validateTag: Too many colons", (t) => { 183 | const tagCollection = new ResourceTagCollection({ 184 | allowedTags: ["aBc:My:Tag"] 185 | }); 186 | t.throws(() => { 187 | tagCollection._validateTag("aBc:My:Tag"); 188 | }, { 189 | instanceOf: Error, 190 | message: `Invalid Tag "aBc:My:Tag": Expected exactly one colon but found 2` 191 | }); 192 | }); 193 | 194 | test("_validateTag: Invalid namespace with uppercase letter", (t) => { 195 | const tagCollection = new ResourceTagCollection({ 196 | allowedTags: ["aBc:MyTag"] 197 | }); 198 | t.throws(() => { 199 | tagCollection._validateTag("aBc:MyTag"); 200 | }, { 201 | instanceOf: Error, 202 | message: `Invalid Tag "aBc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` 203 | }); 204 | }); 205 | 206 | test("_validateTag: Invalid namespace starting with number", (t) => { 207 | const tagCollection = new ResourceTagCollection({ 208 | allowedTags: ["0abc:MyTag"] 209 | }); 210 | t.throws(() => { 211 | tagCollection._validateTag("0abc:MyTag"); 212 | }, { 213 | instanceOf: Error, 214 | message: `Invalid Tag "0abc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` 215 | }); 216 | }); 217 | 218 | test("_validateTag: Invalid namespace containing an illegal character", (t) => { 219 | const tagCollection = new ResourceTagCollection({ 220 | allowedTags: ["a🦦c:MyTag"] 221 | }); 222 | t.throws(() => { 223 | tagCollection._validateTag("a🦦c:MyTag"); 224 | }, { 225 | instanceOf: Error, 226 | message: `Invalid Tag "a🦦c:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` 227 | }); 228 | }); 229 | 230 | test("_validateTag: Invalid tag name starting with number", (t) => { 231 | const tagCollection = new ResourceTagCollection({ 232 | allowedTags: ["abc:0MyTag"] 233 | }); 234 | t.throws(() => { 235 | tagCollection._validateTag("abc:0MyTag"); 236 | }, { 237 | instanceOf: Error, 238 | message: `Invalid Tag "abc:0MyTag": Name part must be alphanumeric and start with a capital letter` 239 | }); 240 | }); 241 | 242 | test("_validateTag: Invalid tag name starting with lowercase letter", (t) => { 243 | const tagCollection = new ResourceTagCollection({ 244 | allowedTags: ["abc:myTag"] 245 | }); 246 | t.throws(() => { 247 | tagCollection._validateTag("abc:myTag"); 248 | }, { 249 | instanceOf: Error, 250 | message: `Invalid Tag "abc:myTag": Name part must be alphanumeric and start with a capital letter` 251 | }); 252 | }); 253 | 254 | test("_validateTag: Invalid tag name containing an illegal character", (t) => { 255 | const tagCollection = new ResourceTagCollection({ 256 | allowedTags: ["abc:My/Tag"] 257 | }); 258 | t.throws(() => { 259 | tagCollection._validateTag("abc:My/Tag"); 260 | }, { 261 | instanceOf: Error, 262 | message: `Invalid Tag "abc:My/Tag": Name part must be alphanumeric and start with a capital letter` 263 | }); 264 | }); 265 | 266 | test("_validateValue: Valid values", (t) => { 267 | const tagCollection = new ResourceTagCollection({ 268 | allowedTags: ["abc:MyTag"] 269 | }); 270 | tagCollection._validateValue("bla"); 271 | tagCollection._validateValue(""); 272 | tagCollection._validateValue(true); 273 | tagCollection._validateValue(false); 274 | tagCollection._validateValue(123); 275 | tagCollection._validateValue(0); 276 | tagCollection._validateValue(NaN); // Is a number 🤷 277 | t.pass("No exception thrown"); 278 | }); 279 | 280 | test("_validateValue: Invalid value of type object", (t) => { 281 | const tagCollection = new ResourceTagCollection({ 282 | allowedTags: ["abc:MyTag"] 283 | }); 284 | t.throws(() => { 285 | tagCollection._validateValue({foo: "bar"}); 286 | }, { 287 | instanceOf: Error, 288 | message: "Invalid Tag Value: Must be of type string, number or boolean but is object" 289 | }); 290 | }); 291 | 292 | test("_validateValue: Invalid value undefined", (t) => { 293 | const tagCollection = new ResourceTagCollection({ 294 | allowedTags: ["abc:MyTag"] 295 | }); 296 | t.throws(() => { 297 | tagCollection._validateValue(undefined); 298 | }, { 299 | instanceOf: Error, 300 | message: "Invalid Tag Value: Must be of type string, number or boolean but is undefined" 301 | }); 302 | }); 303 | 304 | test("_validateValue: Invalid value null", (t) => { 305 | const tagCollection = new ResourceTagCollection({ 306 | allowedTags: ["abc:MyTag"] 307 | }); 308 | t.throws(() => { 309 | tagCollection._validateValue(null); 310 | }, { 311 | instanceOf: Error, 312 | message: "Invalid Tag Value: Must be of type string, number or boolean but is object" 313 | }); 314 | }); 315 | 316 | test("_getPath: Empty path", (t) => { 317 | const tagCollection = new ResourceTagCollection({ 318 | allowedTags: ["abc:MyTag"] 319 | }); 320 | t.throws(() => { 321 | tagCollection._getPath({ 322 | getPath: () => "" 323 | }); 324 | }, { 325 | instanceOf: Error, 326 | message: "Invalid Resource: Resource path must not be empty" 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /test/lib/WriterCollection.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import WriterCollection from "../../lib/WriterCollection.js"; 4 | import Resource from "../../lib/Resource.js"; 5 | 6 | test("Constructor: Path mapping regex", (t) => { 7 | const myWriter = {}; 8 | const writer = new WriterCollection({ 9 | name: "myCollection", 10 | writerMapping: { 11 | "/": myWriter, 12 | "/my/path/": myWriter, 13 | "/my/": myWriter, 14 | } 15 | }); 16 | t.is(writer._basePathRegex.toString(), "^((?:/)??(?:/my/)??(?:/my/path/)??)+.*?$", 17 | "Created correct path mapping regular expression"); 18 | }); 19 | 20 | test("Constructor: Path mapping regex has correct escaping", (t) => { 21 | const myWriter = {}; 22 | const writer = new WriterCollection({ 23 | name: "myCollection", 24 | writerMapping: { 25 | "/My\\Weird.Path/": myWriter, 26 | "/my/pa$h/": myWriter, 27 | "/my/": myWriter, 28 | } 29 | }); 30 | t.is(writer._basePathRegex.toString(), "^((?:/My\\\\Weird\\.Path/)??(?:/my/)??(?:/my/pa\\$h/)??)+.*?$", 31 | "Created correct path mapping regular expression"); 32 | }); 33 | 34 | test("Constructor: Throws for missing path mapping", (t) => { 35 | const err = t.throws(() => { 36 | new WriterCollection({ 37 | name: "myCollection" 38 | }); 39 | }); 40 | t.is(err.message, "Cannot create WriterCollection myCollection: Missing parameter 'writerMapping'", 41 | "Threw with expected error message"); 42 | }); 43 | 44 | test("Constructor: Throws for empty path mapping", (t) => { 45 | const err = t.throws(() => { 46 | new WriterCollection({ 47 | name: "myCollection", 48 | writerMapping: {} 49 | }); 50 | }); 51 | t.is(err.message, "Cannot create WriterCollection myCollection: Empty parameter 'writerMapping'", 52 | "Threw with expected error message"); 53 | }); 54 | 55 | test("Constructor: Throws for empty path", (t) => { 56 | const myWriter = { 57 | _write: sinon.stub() 58 | }; 59 | const err = t.throws(() => { 60 | new WriterCollection({ 61 | name: "myCollection", 62 | writerMapping: { 63 | "": myWriter 64 | } 65 | }); 66 | }); 67 | t.is(err.message, "Empty path in path mapping of WriterCollection myCollection", 68 | "Threw with expected error message"); 69 | }); 70 | 71 | test("Constructor: Throws for missing leading slash", (t) => { 72 | const myWriter = { 73 | _write: sinon.stub() 74 | }; 75 | const err = t.throws(() => { 76 | new WriterCollection({ 77 | name: "myCollection", 78 | writerMapping: { 79 | "my/path/": myWriter 80 | } 81 | }); 82 | }); 83 | t.is(err.message, "Missing leading slash in path mapping 'my/path/' of WriterCollection myCollection", 84 | "Threw with expected error message"); 85 | }); 86 | 87 | test("Constructor: Throws for missing trailing slash", (t) => { 88 | const myWriter = { 89 | _write: sinon.stub() 90 | }; 91 | const err = t.throws(() => { 92 | new WriterCollection({ 93 | name: "myCollection", 94 | writerMapping: { 95 | "/my/path": myWriter 96 | } 97 | }); 98 | }); 99 | t.is(err.message, "Missing trailing slash in path mapping '/my/path' of WriterCollection myCollection", 100 | "Threw with expected error message"); 101 | }); 102 | 103 | test("Write", async (t) => { 104 | const myPathWriter = { 105 | _write: sinon.stub() 106 | }; 107 | const myWriter = { 108 | _write: sinon.stub() 109 | }; 110 | const generalWriter = { 111 | _write: sinon.stub() 112 | }; 113 | const writerCollection = new WriterCollection({ 114 | name: "myCollection", 115 | writerMapping: { 116 | "/my/path/": myPathWriter, 117 | "/my/": myWriter, 118 | "/": generalWriter 119 | } 120 | }); 121 | 122 | const myPathResource = new Resource({ 123 | path: "/my/path/resource.res", 124 | string: "content" 125 | }); 126 | const myResource = new Resource({ 127 | path: "/my/resource.res", 128 | string: "content" 129 | }); 130 | const resource = new Resource({ 131 | path: "/resource.res", 132 | string: "content" 133 | }); 134 | 135 | await writerCollection.write(myPathResource, "options 1"); 136 | await writerCollection.write(myResource, "options 2"); 137 | await writerCollection.write(resource, "options 3"); 138 | 139 | t.is(myPathWriter._write.callCount, 1, "One write to /my/path/ writer"); 140 | t.is(myWriter._write.callCount, 1, "One write to /my/ writer"); 141 | t.is(generalWriter._write.callCount, 1, "One write to / writer"); 142 | 143 | t.is(myPathWriter._write.getCall(0).args[0], myPathResource, "Correct resource for /my/path/ writer"); 144 | t.is(myPathWriter._write.getCall(0).args[1], "options 1", "Correct write options for /my/path/ writer"); 145 | t.is(myWriter._write.getCall(0).args[0], myResource, "Correct resource for /my/ writer"); 146 | t.is(myWriter._write.getCall(0).args[1], "options 2", "Correct write options for /my/ writer"); 147 | t.is(generalWriter._write.getCall(0).args[0], resource, "Correct resource for / writer"); 148 | t.is(generalWriter._write.getCall(0).args[1], "options 3", "Correct write options for / writer"); 149 | }); 150 | 151 | test("byGlob", async (t) => { 152 | const myPathWriter = { 153 | _byGlob: sinon.stub().resolves([]) 154 | }; 155 | const myWriter = { 156 | _byGlob: sinon.stub().resolves([]) 157 | }; 158 | const generalWriter = { 159 | _byGlob: sinon.stub().resolves([]) 160 | }; 161 | const writerCollection = new WriterCollection({ 162 | name: "myCollection", 163 | writerMapping: { 164 | "/my/path/": myPathWriter, 165 | "/my/": myWriter, 166 | "/": generalWriter 167 | } 168 | }); 169 | 170 | await writerCollection.byGlob("/**"); 171 | 172 | t.is(myPathWriter._byGlob.callCount, 1, "One _byGlob call to /my/path/ writer"); 173 | t.is(myWriter._byGlob.callCount, 1, "One _byGlob call to /my/ writer"); 174 | t.is(generalWriter._byGlob.callCount, 1, "One _byGlob call to / writer"); 175 | 176 | t.is(myPathWriter._byGlob.getCall(0).args[0], "/**", "Correct glob pattern passed to /my/path/ writer"); 177 | t.is(myWriter._byGlob.getCall(0).args[0], "/**", "Correct glob pattern passed to /my/ writer"); 178 | t.is(generalWriter._byGlob.getCall(0).args[0], "/**", "Correct glob pattern passed to / writer"); 179 | }); 180 | 181 | test("byPath", async (t) => { 182 | const myPathWriter = { 183 | _byPath: sinon.stub().resolves(null) 184 | }; 185 | const myWriter = { 186 | _byPath: sinon.stub().resolves(null) 187 | }; 188 | const generalWriter = { 189 | _byPath: sinon.stub().resolves(null) 190 | }; 191 | const writerCollection = new WriterCollection({ 192 | name: "myCollection", 193 | writerMapping: { 194 | "/my/path/": myPathWriter, 195 | "/my/": myWriter, 196 | "/": generalWriter 197 | } 198 | }); 199 | 200 | await writerCollection.byPath("/my/resource.res"); 201 | 202 | t.is(myPathWriter._byPath.callCount, 1, "One _byPath to /my/path/ writer"); 203 | t.is(myWriter._byPath.callCount, 1, "One _byPath to /my/ writer"); 204 | t.is(generalWriter._byPath.callCount, 1, "One _byPath to / writer"); 205 | 206 | t.is(myPathWriter._byPath.getCall(0).args[0], "/my/resource.res", 207 | "Correct _byPath argument passed to /my/path/ writer"); 208 | t.is(myWriter._byPath.getCall(0).args[0], "/my/resource.res", 209 | "Correct _byPath argument passed to /my/ writer"); 210 | t.is(generalWriter._byPath.getCall(0).args[0], "/my/resource.res", 211 | "Correct _byPath argument passed to / writer"); 212 | }); 213 | -------------------------------------------------------------------------------- /test/lib/adapters/AbstractAdapter.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import AbstractAdapter from "../../../lib/adapters/AbstractAdapter.js"; 3 | import {createResource} from "../../../lib/resourceFactory.js"; 4 | 5 | class MyAbstractAdapter extends AbstractAdapter { } 6 | 7 | test("Missing paramter: virBasePath", (t) => { 8 | t.throws(() => { 9 | new MyAbstractAdapter({}); 10 | }, { 11 | message: "Unable to create adapter: Missing parameter 'virBasePath'" 12 | }, "Threw with expected error message"); 13 | }); 14 | 15 | test("virBasePath must be absolute", (t) => { 16 | t.throws(() => { 17 | new MyAbstractAdapter({ 18 | virBasePath: "foo" 19 | }); 20 | }, { 21 | message: "Unable to create adapter: Virtual base path must be absolute but is 'foo'" 22 | }, "Threw with expected error message"); 23 | }); 24 | 25 | test("virBasePath must end with a slash", (t) => { 26 | t.throws(() => { 27 | new MyAbstractAdapter({ 28 | virBasePath: "/foo" 29 | }); 30 | }, { 31 | message: "Unable to create adapter: Virtual base path must end with a slash but is '/foo'" 32 | }, "Threw with expected error message"); 33 | }); 34 | 35 | test("_migrateResource", async (t) => { 36 | // Any JS object which might be a kind of resource 37 | const resource = { 38 | _path: "/test.js" 39 | }; 40 | 41 | const writer = new MyAbstractAdapter({ 42 | virBasePath: "/" 43 | }); 44 | 45 | const migratedResource = await writer._migrateResource(resource); 46 | 47 | t.is(migratedResource.getPath(), "/test.js"); 48 | }); 49 | 50 | test("_assignProjectToResource: Resource is already assigned to another project than provided in the adapter", (t) => { 51 | const resource = createResource({ 52 | path: "/test.js", 53 | project: { 54 | getName: () => "test.lib", 55 | getVersion: () => "2.0.0" 56 | } 57 | }); 58 | 59 | const writer = new MyAbstractAdapter({ 60 | virBasePath: "/", 61 | project: { 62 | getName: () => "test.lib1", 63 | getVersion: () => "2.0.0" 64 | } 65 | }); 66 | 67 | const error = t.throws(() => writer._assignProjectToResource(resource)); 68 | t.is(error.message, 69 | "Unable to write resource associated with project test.lib into adapter of project test.lib1: /test.js"); 70 | }); 71 | 72 | test("_isPathHandled", (t) => { 73 | const writer = new MyAbstractAdapter({ 74 | virBasePath: "/dest2/writer/", 75 | project: { 76 | getName: () => "test.lib1", 77 | getVersion: () => "2.0.0" 78 | } 79 | }); 80 | 81 | t.true(writer._isPathHandled("/dest2/writer/test.js"), "Returned expected result"); 82 | t.true(writer._isPathHandled("/dest2/writer/"), "Returned expected result"); 83 | t.true(writer._isPathHandled("/dest2/writer"), "Returned expected result"); 84 | t.false(writer._isPathHandled("/dest2/write"), "Returned expected result"); 85 | t.false(writer._isPathHandled("/dest2/writerisimo"), "Returned expected result"); 86 | t.false(writer._isPathHandled(""), "Returned expected result"); 87 | }); 88 | test("_resolveVirtualPathToBase (read mode)", (t) => { 89 | const writer = new MyAbstractAdapter({ 90 | virBasePath: "/dest2/writer/", 91 | project: { 92 | getName: () => "test.lib1", 93 | getVersion: () => "2.0.0" 94 | } 95 | }); 96 | 97 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/test.js"), "test.js", "Returned expected path"); 98 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/../writer/test.js"), "test.js", "Returned expected path"); 99 | t.is(writer._resolveVirtualPathToBase("/dest2/writer"), "", "Returned expected path"); 100 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/"), "", "Returned expected path"); 101 | t.is(writer._resolveVirtualPathToBase("/../../dest2/writer/test.js"), "test.js", "Returned expected path"); 102 | }); 103 | 104 | test("_resolveVirtualPathToBase (read mode): Path does not starting with path configured in the adapter", (t) => { 105 | const writer = new MyAbstractAdapter({ 106 | virBasePath: "/dest2/writer/", 107 | project: { 108 | getName: () => "test.lib1", 109 | getVersion: () => "2.0.0" 110 | } 111 | }); 112 | 113 | t.is(writer._resolveVirtualPathToBase("/dest2/tmp/test.js"), null, "Returned null"); 114 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/../reader/"), null, "Returned null"); 115 | t.is(writer._resolveVirtualPathToBase("/dest2/write"), null, "Returned null"); 116 | t.is(writer._resolveVirtualPathToBase("/..//write"), null, "Returned null"); 117 | }); 118 | 119 | test("_resolveVirtualPathToBase (read mode): Path Must be absolute", (t) => { 120 | const writer = new MyAbstractAdapter({ 121 | virBasePath: "/dest2/writer/", 122 | project: { 123 | getName: () => "test.lib1", 124 | getVersion: () => "2.0.0" 125 | } 126 | }); 127 | 128 | t.throws(() => writer._resolveVirtualPathToBase("./dest2/write"), { 129 | message: 130 | `Failed to resolve virtual path './dest2/write': Path must be absolute` 131 | }, "Threw with expected error message"); 132 | }); 133 | 134 | test("_resolveVirtualPathToBase (write mode)", (t) => { 135 | const writer = new MyAbstractAdapter({ 136 | virBasePath: "/dest2/writer/", 137 | project: { 138 | getName: () => "test.lib1", 139 | getVersion: () => "2.0.0" 140 | } 141 | }); 142 | 143 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/test.js", true), "test.js", "Returned expected path"); 144 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/../writer/test.js", true), "test.js", 145 | "Returned expected path"); 146 | t.is(writer._resolveVirtualPathToBase("/dest2/writer", true), "", "Returned expected path"); 147 | t.is(writer._resolveVirtualPathToBase("/dest2/writer/", true), "", "Returned expected path"); 148 | t.is(writer._resolveVirtualPathToBase("/../../dest2/writer/test.js", true), "test.js", "Returned expected path"); 149 | }); 150 | 151 | test("_resolveVirtualPathToBase (write mode): Path does not starting with path configured in the adapter", (t) => { 152 | const writer = new MyAbstractAdapter({ 153 | virBasePath: "/dest2/writer/", 154 | project: { 155 | getName: () => "test.lib1", 156 | getVersion: () => "2.0.0" 157 | } 158 | }); 159 | 160 | t.throws(() => writer._resolveVirtualPathToBase("/dest2/tmp/test.js", true), { 161 | message: 162 | `Failed to write resource with virtual path '/dest2/tmp/test.js': ` + 163 | `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` 164 | }, "Threw with expected error message"); 165 | 166 | t.throws(() => writer._resolveVirtualPathToBase("/dest2/writer/../reader", true), { 167 | message: 168 | `Failed to write resource with virtual path '/dest2/writer/../reader': ` + 169 | `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` 170 | }, "Threw with expected error message"); 171 | 172 | t.throws(() => writer._resolveVirtualPathToBase("/dest2/write", true), { 173 | message: 174 | `Failed to write resource with virtual path '/dest2/write': ` + 175 | `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` 176 | }, "Threw with expected error message"); 177 | 178 | t.throws(() => writer._resolveVirtualPathToBase("/..//write", true), { 179 | message: 180 | `Failed to write resource with virtual path '/..//write': ` + 181 | `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` 182 | }, "Threw with expected error message"); 183 | }); 184 | 185 | test("_resolveVirtualPathToBase (write mode): Path Must be absolute", (t) => { 186 | const writer = new MyAbstractAdapter({ 187 | virBasePath: "/dest2/writer/", 188 | project: { 189 | getName: () => "test.lib1", 190 | getVersion: () => "2.0.0" 191 | } 192 | }); 193 | 194 | t.throws(() => writer._resolveVirtualPathToBase("./dest2/write", true), { 195 | message: 196 | `Failed to resolve virtual path './dest2/write': Path must be absolute` 197 | }, "Threw with expected error message"); 198 | }); 199 | 200 | test("_normalizePattern", async (t) => { 201 | const writer = new MyAbstractAdapter({ 202 | virBasePath: "/path/", 203 | project: { 204 | getName: () => "test.lib1", 205 | getVersion: () => "2.0.0" 206 | } 207 | }); 208 | 209 | t.deepEqual(await writer._normalizePattern("/*/{src,test}/**"), [ 210 | "src/**", 211 | "test/**" 212 | ], "Returned expected patterns"); 213 | }); 214 | 215 | test("_normalizePattern: Match base directory", async (t) => { 216 | const writer = new MyAbstractAdapter({ 217 | virBasePath: "/path/", 218 | project: { 219 | getName: () => "test.lib1", 220 | getVersion: () => "2.0.0" 221 | } 222 | }); 223 | 224 | t.deepEqual(await writer._normalizePattern("/*"), [""], 225 | "Returned an empty pattern since the input pattern matches the base directory only"); 226 | }); 227 | 228 | test("_normalizePattern: Match sub-directory", async (t) => { 229 | const writer = new MyAbstractAdapter({ 230 | virBasePath: "/path/", 231 | project: { 232 | getName: () => "test.lib1", 233 | getVersion: () => "2.0.0" 234 | } 235 | }); 236 | 237 | t.deepEqual(await writer._normalizePattern("/path/*"), ["*"], 238 | "Returned expected patterns"); 239 | }); 240 | 241 | test("_normalizePattern: Match all", (t) => { 242 | const writer = new MyAbstractAdapter({ 243 | virBasePath: "/path/", 244 | project: { 245 | getName: () => "test.lib1", 246 | getVersion: () => "2.0.0" 247 | } 248 | }); 249 | 250 | t.deepEqual(writer._normalizePattern("/**/*"), ["**/*"], 251 | "Returned expected patterns"); 252 | }); 253 | 254 | test("_normalizePattern: Relative path segment above virtual root directory", (t) => { 255 | const writer = new MyAbstractAdapter({ 256 | virBasePath: "/path/", 257 | project: { 258 | getName: () => "test.lib1", 259 | getVersion: () => "2.0.0" 260 | } 261 | }); 262 | 263 | t.deepEqual(writer._normalizePattern("/path/../../*"), [], 264 | "Returned no pattern"); 265 | }); 266 | 267 | test("_normalizePattern: Relative path segment resolving to base directory", (t) => { 268 | const writer = new MyAbstractAdapter({ 269 | virBasePath: "/path/", 270 | project: { 271 | getName: () => "test.lib1", 272 | getVersion: () => "2.0.0" 273 | } 274 | }); 275 | 276 | t.deepEqual(writer._normalizePattern("/*/../*"), [""], 277 | "Returned an empty pattern since the input pattern matches the base directory only"); 278 | }); 279 | 280 | test("_normalizePattern: Relative path segment", (t) => { 281 | const writer = new MyAbstractAdapter({ 282 | virBasePath: "/path/", 283 | project: { 284 | getName: () => "test.lib1", 285 | getVersion: () => "2.0.0" 286 | } 287 | }); 288 | 289 | t.deepEqual(writer._normalizePattern("/path/../*"), [""], 290 | "Returned an empty pattern since the input pattern matches the base directory only"); 291 | }); 292 | 293 | test("_normalizePattern: Relative path segment within base directory, matching all", (t) => { 294 | const writer = new MyAbstractAdapter({ 295 | virBasePath: "/path/", 296 | project: { 297 | getName: () => "test.lib1", 298 | getVersion: () => "2.0.0" 299 | } 300 | }); 301 | 302 | t.deepEqual(writer._normalizePattern("/path/path2/../**/*"), ["**/*"], 303 | "Returned expected patterns"); 304 | }); 305 | -------------------------------------------------------------------------------- /test/lib/adapters/FileSystem.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | import FileSystem from "../../../lib/adapters/FileSystem.js"; 4 | 5 | test.serial("Missing parameter: fsBasePath", (t) => { 6 | t.throws(() => { 7 | new FileSystem({ 8 | virBasePath: "/" 9 | }); 10 | }, { 11 | message: "Unable to create adapter: Missing parameter 'fsBasePath'" 12 | }, "Threw with expected error message"); 13 | }); 14 | -------------------------------------------------------------------------------- /test/lib/adapters/FileSystem_write_large_file.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {fileURLToPath} from "node:url"; 3 | import {Buffer} from "node:buffer"; 4 | import {readFile} from "node:fs/promises"; 5 | 6 | import FileSystem from "../../../lib/adapters/FileSystem.js"; 7 | import Resource from "../../../lib/Resource.js"; 8 | import path from "node:path"; 9 | 10 | test.serial("Stream a large file from source to target", async (t) => { 11 | // This test used to fail. The FileSystem adapter would directly pipe the read-stream into a write stream with the 12 | // same path. Leading to only the first chunk of the source file or nothing at all being written into the target 13 | // This has been fixed with https://github.com/SAP/ui5-fs/pull/472 14 | 15 | const fsBasePath = fileURLToPath(new URL("../../tmp/adapters/FileSystemWriteLargeFile/", import.meta.url)); 16 | 17 | const fileSystem = new FileSystem({ 18 | fsBasePath, 19 | virBasePath: "/" 20 | }); 21 | 22 | const largeBuffer = Buffer.alloc(1048576); // 1MB 23 | 24 | await fileSystem.write(new Resource({ 25 | path: "/large-file.txt", 26 | buffer: largeBuffer 27 | })); 28 | 29 | t.deepEqual(await readFile(path.join(fsBasePath, "large-file.txt")), largeBuffer, 30 | "Large file should be written as expected"); 31 | 32 | const largeResource = await fileSystem.byPath("/large-file.txt"); 33 | 34 | largeResource.setStream(largeResource.getStream()); 35 | 36 | await fileSystem.write(largeResource); 37 | 38 | t.deepEqual((await readFile(path.join(fsBasePath, "large-file.txt"))).length, largeBuffer.length, 39 | "Large file should be overwritten with exact same contents"); 40 | }); 41 | -------------------------------------------------------------------------------- /test/lib/adapters/Memory_write.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {createAdapter, createResource} from "../../../lib/resourceFactory.js"; 3 | import sinon from "sinon"; 4 | 5 | test("glob resources from application.a w/ virtual base path prefix", async (t) => { 6 | const dest = createAdapter({ 7 | virBasePath: "/app/" 8 | }); 9 | 10 | const res = createResource({ 11 | path: "/app/index.html" 12 | }); 13 | await dest.write(res); 14 | const resources = await dest.byGlob("/app/*.html"); 15 | t.is(resources.length, 1, "Found exactly one resource"); 16 | t.not(resources[0], res, "Not the same resource instance"); 17 | }); 18 | 19 | test("glob resources from application.a w/o virtual base path prefix", async (t) => { 20 | const dest = createAdapter({ 21 | virBasePath: "/app/" 22 | }); 23 | 24 | const res = createResource({ 25 | path: "/app/index.html" 26 | }); 27 | await dest.write(res); 28 | const resources = await dest.byGlob("/**/*.html"); 29 | t.is(resources.length, 1, "Found exactly one resource"); 30 | }); 31 | 32 | test("Write resource w/ virtual base path", async (t) => { 33 | const readerWriter = createAdapter({ 34 | virBasePath: "/app/" 35 | }); 36 | 37 | const res = createResource({ 38 | path: "/app/test.html" 39 | }); 40 | await readerWriter.write(res); 41 | 42 | t.deepEqual(readerWriter._virFiles, { 43 | "test.html": res 44 | }, "Adapter added resource with correct path"); 45 | 46 | t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories"); 47 | t.not(readerWriter._virFiles["test.html"], res, "Not the same resource instance"); 48 | }); 49 | 50 | test("Write resource w/o virtual base path", async (t) => { 51 | const readerWriter = createAdapter({ 52 | virBasePath: "/" 53 | }); 54 | 55 | const res = createResource({ 56 | path: "/one/two/three/test.html" 57 | }); 58 | await readerWriter.write(res); 59 | 60 | t.deepEqual(readerWriter._virFiles, { 61 | "one/two/three/test.html": res 62 | }, "Adapter added resource with correct path"); 63 | 64 | t.deepEqual(Object.keys(readerWriter._virDirs), [ 65 | "one/two/three", 66 | "one/two", 67 | "one" 68 | ], "Adapter added correct virtual directories"); 69 | 70 | const dirRes = readerWriter._virDirs["one/two/three"]; 71 | t.is(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); 72 | t.is(dirRes.getPath(), "/one/two/three", "Directory resource has correct path"); 73 | }); 74 | 75 | test("Write resource w/ deep virtual base path", async (t) => { 76 | const readerWriter = createAdapter({ 77 | virBasePath: "/app/a/" 78 | }); 79 | 80 | const res = createResource({ 81 | path: "/app/a/one/two/three/test.html" 82 | }); 83 | await readerWriter.write(res); 84 | 85 | t.deepEqual(readerWriter._virFiles, { 86 | "one/two/three/test.html": res 87 | }, "Adapter added resource with correct path"); 88 | 89 | t.deepEqual(Object.keys(readerWriter._virDirs), [ 90 | "one/two/three", 91 | "one/two", 92 | "one" 93 | ], "Adapter added correct virtual directories"); 94 | 95 | const dirRes = readerWriter._virDirs["one/two/three"]; 96 | t.is(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); 97 | t.is(dirRes.getPath(), "/app/a/one/two/three", "Directory resource has correct path"); 98 | }); 99 | 100 | test("Write resource w/ crazy virtual base path", async (t) => { 101 | const readerWriter = createAdapter({ 102 | virBasePath: "/app/🐛/" 103 | }); 104 | 105 | const res = createResource({ 106 | path: "/app/🐛/one\\/2/3️⃣/test" 107 | }); 108 | await readerWriter.write(res); 109 | 110 | t.deepEqual(readerWriter._virFiles, { 111 | "one\\/2/3️⃣/test": res 112 | }, "Adapter added resource with correct path"); 113 | 114 | t.deepEqual(Object.keys(readerWriter._virDirs), [ 115 | "one\\/2/3️⃣", 116 | "one\\/2", 117 | "one\\" 118 | ], "Adapter added correct virtual directories"); 119 | }); 120 | 121 | test("Migration of resource is executed", async (t) => { 122 | const writer = createAdapter({ 123 | virBasePath: "/" 124 | }); 125 | 126 | const resource = createResource({ 127 | path: "/test.js" 128 | }); 129 | 130 | const migrateResourceWriterSpy = sinon.spy(writer, "_migrateResource"); 131 | await writer.write(resource); 132 | t.is(migrateResourceWriterSpy.callCount, 1); 133 | }); 134 | 135 | test("Resource: Change instance after write", async (t) => { 136 | const writer = createAdapter({ 137 | virBasePath: "/" 138 | }); 139 | 140 | const resource = createResource({ 141 | path: "/test.js", 142 | string: "MyInitialContent" 143 | }); 144 | 145 | await writer.write(resource); 146 | 147 | resource.setString("MyNewContent"); 148 | 149 | const resource1 = await writer.byPath("/test.js"); 150 | 151 | t.is(await resource.getString(), "MyNewContent"); 152 | t.is(await resource1.getString(), "MyInitialContent"); 153 | 154 | await writer.write(resource); 155 | 156 | const resource2 = await writer.byPath("/test.js"); 157 | t.is(await resource.getString(), "MyNewContent"); 158 | t.is(await resource1.getString(), "MyInitialContent"); 159 | t.is(await resource2.getString(), "MyNewContent"); 160 | }); 161 | -------------------------------------------------------------------------------- /test/lib/fsInterface.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {promisify} from "node:util"; 3 | import {Buffer} from "node:buffer"; 4 | import path from "node:path"; 5 | import {fileURLToPath} from "node:url"; 6 | import fsSync from "node:fs"; 7 | const stat = promisify(fsSync.stat); 8 | import {readFile} from "node:fs/promises"; 9 | import fsInterface from "../../lib/fsInterface.js"; 10 | import MemAdapter from "../../lib/adapters/Memory.js"; 11 | import FsAdapter from "../../lib/adapters/FileSystem.js"; 12 | import Resource from "../../lib/Resource.js"; 13 | 14 | const assertReadFile = async (t, readFile, basepath, filepath, content) => { 15 | content = content || "content of " + filepath; 16 | const fullpath = path.join(basepath, filepath); 17 | let buffer = await readFile(fullpath); 18 | t.true(Buffer.isBuffer(buffer)); 19 | t.deepEqual(buffer.toString(), content); 20 | 21 | buffer = await readFile(fullpath, {}); 22 | t.true(Buffer.isBuffer(buffer)); 23 | t.deepEqual(buffer.toString(), content); 24 | buffer = await readFile(fullpath, {encoding: null}); 25 | t.true(Buffer.isBuffer(buffer)); 26 | t.deepEqual(buffer.toString(), content); 27 | buffer = await readFile(fullpath, "utf8"); 28 | t.is(typeof buffer, "string"); 29 | t.deepEqual(buffer, content); 30 | 31 | buffer = await readFile(fullpath, {encoding: "utf8"}); 32 | t.is(typeof buffer, "string"); 33 | t.deepEqual(content, content); 34 | }; 35 | 36 | function getPath() { 37 | return fileURLToPath(new URL("../fixtures/fsInterfáce", import.meta.url)); 38 | } 39 | 40 | test("MemAdapter: readFile", async (t) => { 41 | const memAdapter = new MemAdapter({ 42 | virBasePath: "/" 43 | }); 44 | const fs = fsInterface(memAdapter); 45 | const readFile = promisify(fs.readFile); 46 | 47 | const fsPath = path.join("/", "foo.txt"); 48 | await memAdapter.write(new Resource({ 49 | path: "/foo.txt", 50 | string: `content of ${fsPath}` 51 | })); 52 | `content of ${fsPath}`; 53 | await assertReadFile(t, readFile, "", fsPath); 54 | }); 55 | 56 | test("FsAdapter: readFile with non-ASCII characters in path", async (t) => { 57 | const fsAdapter = new FsAdapter({ 58 | virBasePath: "/", 59 | fsBasePath: getPath() 60 | }); 61 | const fs = fsInterface(fsAdapter); 62 | const readFile = promisify(fs.readFile); 63 | 64 | await assertReadFile(t, readFile, "", path.join("/", "bâr.txt"), "content"); 65 | }); 66 | 67 | test("fs: readFile", async (t) => { 68 | await assertReadFile(t, readFile, 69 | getPath(), path.join("/", "foo.txt"), "content"); 70 | }); 71 | 72 | const assertStat = async (t, stat, basepath, filepath) => { 73 | const fullpath = path.join(basepath, filepath); 74 | const stats = await stat(fullpath); 75 | 76 | t.is(stats.isFile(), true); 77 | t.is(stats.isDirectory(), false); 78 | t.is(stats.isBlockDevice(), false); 79 | t.is(stats.isCharacterDevice(), false); 80 | t.is(stats.isSymbolicLink(), false); 81 | t.is(stats.isFIFO(), false); 82 | t.is(stats.isSocket(), false); 83 | }; 84 | 85 | test("MemAdapter: stat", async (t) => { 86 | const memAdapter = new MemAdapter({ 87 | virBasePath: "/" 88 | }); 89 | const fs = fsInterface(memAdapter); 90 | const stat = promisify(fs.stat); 91 | 92 | const fsPath = path.join("/", "foo.txt"); 93 | await memAdapter.write(new Resource({ 94 | path: "/foo.txt", 95 | string: `content of ${fsPath}` 96 | })); 97 | await assertStat(t, stat, "", fsPath); 98 | }); 99 | 100 | test("FsAdapter: stat", async (t) => { 101 | const fsAdapter = new FsAdapter({ 102 | virBasePath: "/", 103 | fsBasePath: getPath() 104 | }); 105 | const fs = fsInterface(fsAdapter); 106 | const stat = promisify(fs.stat); 107 | 108 | await assertStat(t, stat, "", path.join("/", "foo.txt")); 109 | }); 110 | 111 | test("fs: stat", async (t) => { 112 | await assertStat(t, stat, getPath(), path.join("/", "foo.txt")); 113 | }); 114 | 115 | test("MemAdapter: mkdir", async (t) => { 116 | const memAdapter = new MemAdapter({ 117 | virBasePath: "/" 118 | }); 119 | const fs = fsInterface(memAdapter); 120 | const mkdir = promisify(fs.mkdir); 121 | 122 | await t.notThrowsAsync(mkdir("pony"), "mkdir executes successfully"); 123 | }); 124 | -------------------------------------------------------------------------------- /test/lib/package-exports.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {createRequire} from "node:module"; 3 | 4 | // Using CommonsJS require since JSON module imports are still experimental 5 | const require = createRequire(import.meta.url); 6 | 7 | // package.json should be exported to allow reading version (e.g. from @ui5/cli) 8 | test("export of package.json", (t) => { 9 | t.truthy(require("@ui5/fs/package.json").version); 10 | }); 11 | 12 | // Check number of definied exports 13 | test("check number of exports", (t) => { 14 | const packageJson = require("@ui5/fs/package.json"); 15 | t.is(Object.keys(packageJson.exports).length, 12); 16 | }); 17 | 18 | // Public API contract (exported modules) 19 | [ 20 | { 21 | exportedSpecifier: "@ui5/fs/adapters/AbstractAdapter", 22 | mappedModule: "../../lib/adapters/AbstractAdapter.js" 23 | }, 24 | { 25 | exportedSpecifier: "@ui5/fs/adapters/FileSystem", 26 | mappedModule: "../../lib/adapters/FileSystem.js" 27 | }, 28 | { 29 | exportedSpecifier: "@ui5/fs/adapters/Memory", 30 | mappedModule: "../../lib/adapters/Memory.js" 31 | }, 32 | { 33 | exportedSpecifier: "@ui5/fs/AbstractReader", 34 | mappedModule: "../../lib/AbstractReader.js" 35 | }, 36 | { 37 | exportedSpecifier: "@ui5/fs/AbstractReaderWriter", 38 | mappedModule: "../../lib/AbstractReaderWriter.js" 39 | }, 40 | { 41 | exportedSpecifier: "@ui5/fs/DuplexCollection", 42 | mappedModule: "../../lib/DuplexCollection.js" 43 | }, 44 | { 45 | exportedSpecifier: "@ui5/fs/fsInterface", 46 | mappedModule: "../../lib/fsInterface.js" 47 | }, 48 | { 49 | exportedSpecifier: "@ui5/fs/ReaderCollection", 50 | mappedModule: "../../lib/ReaderCollection.js" 51 | }, 52 | { 53 | exportedSpecifier: "@ui5/fs/ReaderCollectionPrioritized", 54 | mappedModule: "../../lib/ReaderCollectionPrioritized.js" 55 | }, 56 | { 57 | exportedSpecifier: "@ui5/fs/readers/Filter", 58 | mappedModule: "../../lib/readers/Filter.js" 59 | }, 60 | { 61 | exportedSpecifier: "@ui5/fs/readers/Link", 62 | mappedModule: "../../lib/readers/Link.js" 63 | }, 64 | { 65 | exportedSpecifier: "@ui5/fs/Resource", 66 | mappedModule: "../../lib/Resource.js" 67 | }, 68 | { 69 | exportedSpecifier: "@ui5/fs/resourceFactory", 70 | mappedModule: "../../lib/resourceFactory.js" 71 | }, 72 | // Internal modules (only to be used by @ui5/* packages) 73 | { 74 | exportedSpecifier: "@ui5/fs/internal/ResourceTagCollection", 75 | mappedModule: "../../lib/ResourceTagCollection.js" 76 | }, 77 | ].forEach(({exportedSpecifier, mappedModule}) => { 78 | test(`${exportedSpecifier}`, async (t) => { 79 | const actual = await import(exportedSpecifier); 80 | const expected = await import(mappedModule); 81 | t.is(actual, expected, "Correct module exported"); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/lib/readers/Filter.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import Filter from "../../../lib/readers/Filter.js"; 4 | 5 | test("_byGlob: Basic filter", async (t) => { 6 | const abstractReader = { 7 | _byGlob: sinon.stub().returns(Promise.resolve(["resource a", "resource b"])) 8 | }; 9 | const trace = { 10 | collection: sinon.spy() 11 | }; 12 | const readerCollection = new Filter({ 13 | reader: abstractReader, 14 | callback: function(resource) { 15 | if (resource === "resource a") { 16 | return false; 17 | } 18 | return true; 19 | } 20 | }); 21 | 22 | const resources = await readerCollection._byGlob("anyPattern", {}, trace); 23 | t.deepEqual(resources, ["resource b"], "Correct resource in result"); 24 | }); 25 | 26 | test("_byPath: Negative filter", async (t) => { 27 | const abstractReader = { 28 | _byPath: sinon.stub().returns(Promise.resolve("resource a")) 29 | }; 30 | const trace = { 31 | collection: sinon.spy() 32 | }; 33 | const readerCollection = new Filter({ 34 | reader: abstractReader, 35 | callback: function(resource) { 36 | if (resource === "resource a") { 37 | return false; 38 | } 39 | return true; 40 | } 41 | }); 42 | 43 | const resource = await readerCollection._byPath("anyPattern", {}, trace); 44 | t.is(resource, null, "Correct empty result"); 45 | }); 46 | 47 | test("_byPath: Positive filter", async (t) => { 48 | const abstractReader = { 49 | _byPath: sinon.stub().returns(Promise.resolve("resource b")) 50 | }; 51 | const trace = { 52 | collection: sinon.spy() 53 | }; 54 | const readerCollection = new Filter({ 55 | reader: abstractReader, 56 | callback: function(resource) { 57 | if (resource === "resource a") { 58 | return false; 59 | } 60 | return true; 61 | } 62 | }); 63 | 64 | const resource = await readerCollection._byPath("anyPattern", {}, trace); 65 | t.is(resource, "resource b", "Correct resource in result"); 66 | }); 67 | -------------------------------------------------------------------------------- /test/lib/readers/Link.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import Link from "../../../lib/readers/Link.js"; 4 | import ResourceFacade from "../../../lib/ResourceFacade.js"; 5 | 6 | test("_byGlob: Basic Link", async (t) => { 7 | const dummyResourceA = { 8 | getPath: () => "/resources/some/lib/FileA.js" 9 | }; 10 | const dummyResourceB = { 11 | getPath: () => "/resources/some/lib/dir/FileB.js" 12 | }; 13 | const abstractReader = { 14 | _byGlob: sinon.stub().resolves([dummyResourceA, dummyResourceB]) 15 | }; 16 | const trace = { 17 | collection: sinon.spy() 18 | }; 19 | const readerCollection = new Link({ 20 | reader: abstractReader, 21 | pathMapping: { 22 | linkPath: `/`, 23 | targetPath: `/resources/some/lib/` 24 | } 25 | }); 26 | const options = "options"; 27 | const resources = await readerCollection._byGlob("anyPattern", options, trace); 28 | t.is(resources.length, 2, "Glob returned two resources"); 29 | 30 | t.true(resources[0] instanceof ResourceFacade, "First resource is an instance of ResourceFacade"); 31 | t.is(resources[0].getConcealedResource(), dummyResourceA, "First resource contains dummy resource A"); 32 | t.is(resources[0].getPath(), "/FileA.js", "First resource has correct rewritten path"); 33 | 34 | t.true(resources[1] instanceof ResourceFacade, "Second resource is an instance of ResourceFacade"); 35 | t.is(resources[1].getConcealedResource(), dummyResourceB, "Second resource contains dummy resource B"); 36 | t.is(resources[1].getPath(), "/dir/FileB.js", "Second resource has correct rewritten path"); 37 | 38 | t.is(abstractReader._byGlob.callCount, 1, "Mocked _byGlob got called once"); 39 | t.deepEqual(abstractReader._byGlob.getCall(0).args[0], ["/resources/some/lib/anyPattern"], 40 | "Mocked _byGlob got called with expected patterns"); 41 | t.is(abstractReader._byGlob.getCall(0).args[1], options, 42 | "Mocked _byGlob got called with expected options"); 43 | t.is(abstractReader._byGlob.getCall(0).args[2], trace, 44 | "Mocked _byGlob got called with expected trace object"); 45 | }); 46 | 47 | test("_byGlob: Complex pattern", async (t) => { 48 | const abstractReader = { 49 | _byGlob: sinon.stub().resolves([]) 50 | }; 51 | const trace = { 52 | collection: sinon.spy() 53 | }; 54 | const readerCollection = new Link({ 55 | reader: abstractReader, 56 | pathMapping: { 57 | linkPath: `/`, 58 | targetPath: `/resources/some/lib/` 59 | } 60 | }); 61 | 62 | const options = "options"; 63 | const resources = await readerCollection._byGlob("{anyPattern,otherPattern}/**", options, trace); 64 | t.is(resources.length, 0, "Glob returned no resources"); 65 | 66 | t.is(abstractReader._byGlob.callCount, 1, "Mocked _byGlob got called once"); 67 | t.deepEqual(abstractReader._byGlob.getCall(0).args[0], [ 68 | "/resources/some/lib/anyPattern/**", 69 | "/resources/some/lib/otherPattern/**", 70 | ], "Mocked _byGlob got called with expected patterns"); 71 | t.is(abstractReader._byGlob.getCall(0).args[1], options, 72 | "Mocked _byGlob got called with expected options"); 73 | t.is(abstractReader._byGlob.getCall(0).args[2], trace, 74 | "Mocked _byGlob got called with expected trace object"); 75 | }); 76 | 77 | test("_byGlob: Request prefixed with target path", async (t) => { 78 | const abstractReader = { 79 | _byGlob: sinon.stub().resolves([]) 80 | }; 81 | const trace = { 82 | collection: sinon.spy() 83 | }; 84 | const readerCollection = new Link({ 85 | reader: abstractReader, 86 | pathMapping: { 87 | linkPath: `/my/lib/`, 88 | targetPath: `/some/lib/` 89 | } 90 | }); 91 | const options = "options"; 92 | const resources = await readerCollection._byGlob("/some/lib/dir/**", options, trace); 93 | t.is(resources.length, 0, "Glob returned no resources"); 94 | 95 | t.is(abstractReader._byGlob.callCount, 1, "Mocked _byGlob got called once"); 96 | t.deepEqual(abstractReader._byGlob.getCall(0).args[0], [ 97 | "/some/lib/some/lib/dir/**", // Weird, but expected. We prefix everything with the targetPath 98 | ], "Mocked _byGlob got called with expected patterns"); 99 | t.is(abstractReader._byGlob.getCall(0).args[1], options, 100 | "Mocked _byGlob got called with expected options"); 101 | t.is(abstractReader._byGlob.getCall(0).args[2], trace, 102 | "Mocked _byGlob got called with expected trace object"); 103 | }); 104 | 105 | test("_byGlob: Request prefixed with link path", async (t) => { 106 | const abstractReader = { 107 | _byGlob: sinon.stub().resolves([]) 108 | }; 109 | const trace = { 110 | collection: sinon.spy() 111 | }; 112 | const readerCollection = new Link({ 113 | reader: abstractReader, 114 | pathMapping: { 115 | linkPath: `/my/lib/`, 116 | targetPath: `/some/lib/` 117 | } 118 | }); 119 | const options = "options"; 120 | const resources = await readerCollection._byGlob("/my/lib/dir/**", options, trace); 121 | t.is(resources.length, 0, "Glob returned no resources"); 122 | 123 | t.is(abstractReader._byGlob.callCount, 1, "Mocked _byGlob got called once"); 124 | t.deepEqual(abstractReader._byGlob.getCall(0).args[0], [ 125 | "/some/lib/dir/**", 126 | ], "Mocked _byGlob got called with expected patterns"); 127 | t.is(abstractReader._byGlob.getCall(0).args[1], options, 128 | "Mocked _byGlob got called with expected options"); 129 | t.is(abstractReader._byGlob.getCall(0).args[2], trace, 130 | "Mocked _byGlob got called with expected trace object"); 131 | }); 132 | 133 | test("_byPath: Basic Link", async (t) => { 134 | const dummyResource = { 135 | getPath: () => "/resources/some/lib/dir/File.js" 136 | }; 137 | const abstractReader = { 138 | _byPath: sinon.stub().resolves(dummyResource) 139 | }; 140 | const trace = { 141 | collection: sinon.spy() 142 | }; 143 | const readerCollection = new Link({ 144 | reader: abstractReader, 145 | pathMapping: { 146 | linkPath: `/`, 147 | targetPath: `/resources/some/lib/` 148 | } 149 | }); 150 | const options = "options"; 151 | const resource = await readerCollection._byPath("/dir/File.js", options, trace); 152 | 153 | t.true(resource instanceof ResourceFacade, "First resource is an instance of ResourceFacade"); 154 | t.is(resource.getConcealedResource(), dummyResource, "First resource contains dummy resource A"); 155 | t.is(resource.getPath(), "/dir/File.js", "First resource has correct rewritten path"); 156 | 157 | t.is(abstractReader._byPath.callCount, 1, "Mocked _byPath got called once"); 158 | t.is(abstractReader._byPath.getCall(0).args[0], "/resources/some/lib/dir/File.js", 159 | "Mocked _byPath got called with expected patterns"); 160 | t.is(abstractReader._byPath.getCall(0).args[1], options, 161 | "Mocked _byPath got called with expected options"); 162 | t.is(abstractReader._byPath.getCall(0).args[2], trace, 163 | "Mocked _byPath got called with expected trace object"); 164 | }); 165 | 166 | test("_byPath: Rewrite on same level", async (t) => { 167 | const dummyResource = { 168 | getPath: () => "/some/lib/dir/File.js" 169 | }; 170 | const abstractReader = { 171 | _byPath: sinon.stub().resolves(dummyResource) 172 | }; 173 | const trace = { 174 | collection: sinon.spy() 175 | }; 176 | const readerCollection = new Link({ 177 | reader: abstractReader, 178 | pathMapping: { 179 | linkPath: `/my/lib/`, 180 | targetPath: `/some/lib/` 181 | } 182 | }); 183 | const options = "options"; 184 | const resource = await readerCollection._byPath("/my/lib/dir/File.js", options, trace); 185 | 186 | t.true(resource instanceof ResourceFacade, "First resource is an instance of ResourceFacade"); 187 | t.is(resource.getConcealedResource(), dummyResource, "First resource contains dummy resource A"); 188 | t.is(resource.getPath(), "/my/lib/dir/File.js", "First resource has correct rewritten path"); 189 | 190 | t.is(abstractReader._byPath.callCount, 1, "Mocked _byPath got called once"); 191 | t.is(abstractReader._byPath.getCall(0).args[0], "/some/lib/dir/File.js", 192 | "Mocked _byPath got called with expected patterns"); 193 | t.is(abstractReader._byPath.getCall(0).args[1], options, 194 | "Mocked _byPath got called with expected options"); 195 | t.is(abstractReader._byPath.getCall(0).args[2], trace, 196 | "Mocked _byPath got called with expected trace object"); 197 | }); 198 | 199 | test("_byPath: No resource found", async (t) => { 200 | const abstractReader = { 201 | _byPath: sinon.stub().resolves(null) 202 | }; 203 | const trace = { 204 | collection: sinon.spy() 205 | }; 206 | const readerCollection = new Link({ 207 | reader: abstractReader, 208 | pathMapping: { 209 | linkPath: `/`, 210 | targetPath: `/some/lib/` 211 | } 212 | }); 213 | const options = "options"; 214 | const resource = await readerCollection._byPath("/dir/File.js", options, trace); 215 | 216 | t.is(resource, null, "No resource returned"); 217 | 218 | t.is(abstractReader._byPath.callCount, 1, "Mocked _byPath got called once"); 219 | t.is(abstractReader._byPath.getCall(0).args[0], "/some/lib/dir/File.js", 220 | "Mocked _byPath got called with expected patterns"); 221 | t.is(abstractReader._byPath.getCall(0).args[1], options, 222 | "Mocked _byPath got called with expected options"); 223 | t.is(abstractReader._byPath.getCall(0).args[2], trace, 224 | "Mocked _byPath got called with expected trace object"); 225 | }); 226 | 227 | test("_byPath: Request different prefix", async (t) => { 228 | const abstractReader = { 229 | _byPath: sinon.stub() 230 | }; 231 | const trace = { 232 | collection: sinon.spy() 233 | }; 234 | const readerCollection = new Link({ 235 | reader: abstractReader, 236 | pathMapping: { 237 | linkPath: `/my/lib/`, 238 | targetPath: `/some/lib/` 239 | } 240 | }); 241 | const options = "options"; 242 | const resource = await readerCollection._byPath("/some/lib/dir/File.js", options, trace); 243 | 244 | t.is(resource, null, "No resource returned"); 245 | t.is(abstractReader._byPath.callCount, 0, "Mocked _byPath never got called"); 246 | }); 247 | 248 | test("Missing attributes", (t) => { 249 | const abstractReader = {}; 250 | let err = t.throws(() => { 251 | new Link({ 252 | pathMapping: { 253 | linkPath: `/`, 254 | targetPath: `/`, 255 | } 256 | }); 257 | }); 258 | t.is(err.message, `Missing parameter "reader"`, 259 | "Threw with expected error message"); 260 | 261 | err = t.throws(() => { 262 | new Link({ 263 | reader: abstractReader 264 | }); 265 | }); 266 | t.is(err.message, `Missing parameter "pathMapping"`, 267 | "Threw with expected error message"); 268 | 269 | err = t.throws(() => { 270 | new Link({ 271 | reader: abstractReader, 272 | pathMapping: { 273 | targetPath: `/`, 274 | } 275 | }); 276 | }); 277 | t.is(err.message, `Path mapping is missing attribute "linkPath"`, 278 | "Threw with expected error message"); 279 | 280 | err = t.throws(() => { 281 | new Link({ 282 | reader: abstractReader, 283 | pathMapping: { 284 | linkPath: `/`, 285 | } 286 | }); 287 | }); 288 | t.is(err.message, `Path mapping is missing attribute "targetPath"`, 289 | "Threw with expected error message"); 290 | 291 | err = t.throws(() => { 292 | new Link({ 293 | reader: abstractReader, 294 | pathMapping: { 295 | linkPath: `/path`, 296 | targetPath: `/`, 297 | } 298 | }); 299 | }); 300 | t.is(err.message, `Link path must end with a slash: /path`, 301 | "Threw with expected error message"); 302 | 303 | err = t.throws(() => { 304 | new Link({ 305 | reader: abstractReader, 306 | pathMapping: { 307 | linkPath: `/`, 308 | targetPath: `/path`, 309 | } 310 | }); 311 | }); 312 | t.is(err.message, `Target path must end with a slash: /path`, 313 | "Threw with expected error message"); 314 | }); 315 | -------------------------------------------------------------------------------- /test/lib/resources.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import {readFile} from "node:fs/promises"; 4 | 5 | import {createAdapter, createFilterReader, 6 | createFlatReader, createLinkReader, createResource} from "../../lib/resourceFactory.js"; 7 | 8 | test.afterEach.always((t) => { 9 | sinon.restore(); 10 | }); 11 | 12 | function getFileContent(path) { 13 | return readFile(path, "utf8"); 14 | } 15 | 16 | async function fileEqual(t, actual, expected) { 17 | const actualContent = await getFileContent(actual); 18 | const expectedContent = await getFileContent(expected); 19 | t.is(actualContent, expectedContent); 20 | } 21 | 22 | ["FileSystem", "Memory"].forEach((adapter) => { 23 | async function getAdapter(config) { 24 | if (adapter === "Memory") { 25 | const fsAdapter = createAdapter(config); 26 | const fsResources = await fsAdapter.byGlob("**/*"); 27 | // By removing the fsBasePath a MemAdapter will be created 28 | delete config.fsBasePath; 29 | const memAdapter = createAdapter(config); 30 | for (const resource of fsResources) { 31 | await memAdapter.write(resource); 32 | } 33 | return memAdapter; 34 | } else if (adapter === "FileSystem") { 35 | return createAdapter(config); 36 | } 37 | } 38 | 39 | /* BEWARE: 40 | Always make sure that every test writes to a separate file! By default, tests are running concurrent. 41 | */ 42 | test(adapter + 43 | ": Get resource from application.a (/index.html) and write it to /dest/ using a ReadableStream", async (t) => { 44 | const source = await getAdapter({ 45 | fsBasePath: "./test/fixtures/application.a/webapp", 46 | virBasePath: "/app/" 47 | }); 48 | const dest = await getAdapter({ 49 | fsBasePath: "./test/tmp/readerWriters/application.a/simple-read-write", 50 | virBasePath: "/dest/" 51 | }); 52 | 53 | // Get resource from one readerWriter 54 | const resource = await source.byPath("/app/index.html"); 55 | 56 | // Write resource content to another readerWriter 57 | resource.setPath("/dest/index_readableStreamTest.html"); 58 | await dest.write(resource); 59 | 60 | t.notThrows(async () => { 61 | if (adapter === "FileSystem") { 62 | await fileEqual( 63 | t, 64 | "./test/tmp/readerWriters/application.a/simple-read-write/index_readableStreamTest.html", 65 | "./test/fixtures/application.a/webapp/index.html"); 66 | } else { 67 | const destResource = await dest.byPath("/dest/index_readableStreamTest.html"); 68 | t.deepEqual(await destResource.getString(), await resource.getString()); 69 | } 70 | }); 71 | }); 72 | 73 | test(adapter + ": Create resource, write and change content", async (t) => { 74 | const dest = await getAdapter({ 75 | fsBasePath: "./test/tmp/writer/", 76 | virBasePath: "/dest/writer/" 77 | }); 78 | 79 | const resource = createResource({ 80 | path: "/dest/writer/content/test.js", 81 | string: "MyInitialContent" 82 | }); 83 | 84 | await dest.write(resource); 85 | 86 | resource.setString("MyNewContent"); 87 | 88 | const resource1 = await dest.byPath("/dest/writer/content/test.js"); 89 | 90 | t.is(await resource.getString(), "MyNewContent"); 91 | t.is(await resource1.getString(), "MyInitialContent"); 92 | 93 | t.is(await resource.getString(), "MyNewContent"); 94 | t.is(await resource1.getString(), "MyInitialContent"); 95 | 96 | await dest.write(resource); 97 | 98 | const resource2 = await dest.byPath("/dest/writer/content/test.js"); 99 | t.is(await resource.getString(), "MyNewContent"); 100 | t.is(await resource2.getString(), "MyNewContent"); 101 | }); 102 | 103 | test(adapter + ": Create resource, write and change path", async (t) => { 104 | const dest = await getAdapter({ 105 | fsBasePath: "./test/tmp/writer/", 106 | virBasePath: "/dest/writer/" 107 | }); 108 | 109 | const resource = createResource({ 110 | path: "/dest/writer/path/test.js", 111 | string: "MyInitialContent" 112 | }); 113 | 114 | await dest.write(resource); 115 | 116 | resource.setPath("/dest/writer/path/test2.js"); 117 | 118 | const resourceOldPath = await dest.byPath("/dest/writer/path/test.js"); 119 | const resourceNewPath = await dest.byPath("/dest/writer/path/test2.js"); 120 | 121 | t.is(await resource.getPath(), "/dest/writer/path/test2.js"); 122 | t.truthy(resourceOldPath); 123 | t.is(await resourceOldPath.getString(), await resource.getString()); 124 | t.is(await resourceOldPath.getPath(), "/dest/writer/path/test.js"); 125 | t.not(resourceNewPath); 126 | 127 | await dest.write(resource); 128 | 129 | const resourceOldPath1 = await dest.byPath("/dest/writer/path/test.js"); 130 | const resourceNewPath1 = await dest.byPath("/dest/writer/path/test2.js"); 131 | 132 | t.is(await resource.getPath(), "/dest/writer/path/test2.js"); 133 | t.truthy(resourceNewPath1); 134 | t.is(await resourceNewPath1.getString(), await resource.getString()); 135 | t.is(await resourceNewPath1.getPath(), "/dest/writer/path/test2.js"); 136 | t.not(resourceOldPath1); 137 | }); 138 | 139 | test(adapter + 140 | ": Create a resource with a path different from the path configured in the adapter", async (t) => { 141 | t.pass(2); 142 | const dest = await getAdapter({ 143 | fsBasePath: "./test/tmp/writer/", 144 | virBasePath: "/dest2/writer/" 145 | }); 146 | 147 | const resource = createResource({ 148 | path: "/dest2/tmp/test.js", 149 | string: "MyContent" 150 | }); 151 | 152 | const error = await t.throwsAsync(dest.write(resource)); 153 | t.is(error.message, 154 | "Failed to write resource with virtual path '/dest2/tmp/test.js': Path must start with the " + 155 | "configured virtual base path of the adapter. Base path: '/dest2/writer/'", 156 | "Threw with expected error message"); 157 | }); 158 | 159 | test(adapter + 160 | ": Create a resource with a path above the path configured in the adapter", async (t) => { 161 | t.pass(2); 162 | const dest = await getAdapter({ 163 | fsBasePath: "./test/tmp/writer/", 164 | virBasePath: "/dest2/writer/" 165 | }); 166 | 167 | const resource = createResource({ 168 | path: "/dest2/test.js", 169 | string: "MyContent" 170 | }); 171 | 172 | const error = await t.throwsAsync(dest.write(resource)); 173 | t.is(error.message, 174 | "Failed to write resource with virtual path '/dest2/test.js': Path must start with the " + 175 | "configured virtual base path of the adapter. Base path: '/dest2/writer/'", 176 | "Threw with expected error message"); 177 | }); 178 | 179 | test(adapter + 180 | ": Create a resource with a path resolving outside the path configured in the adapter", async (t) => { 181 | t.pass(2); 182 | const dest = await getAdapter({ 183 | fsBasePath: "./test/tmp/writer/", 184 | virBasePath: "/dest/writer/" 185 | }); 186 | 187 | const resource = createResource({ 188 | path: "/dest/writer/../relative.js", 189 | string: "MyContent" 190 | }); 191 | // Resource will already resolve relative path segments 192 | t.is(resource.getPath(), "/dest/relative.js", "Resource path resolved"); 193 | 194 | // This would cause the base path to not match 195 | // So we cheat, simulating a misbehaving or old Resource instance, using a relative path segment 196 | sinon.stub(resource, "getPath").returns("/dest/writer/../relative.js"); 197 | 198 | await t.throwsAsync(dest.write(resource), { 199 | message: 200 | "Failed to write resource with virtual path '/dest/writer/../relative.js': " + 201 | "Path must start with the configured virtual base path of the adapter. Base path: '/dest/writer/'" 202 | }, "Threw with expected error message"); 203 | }); 204 | 205 | test(adapter + ": Filter resources", async (t) => { 206 | const source = createAdapter({ 207 | fsBasePath: "./test/fixtures/application.a/webapp", 208 | virBasePath: "/app/" 209 | }); 210 | const filteredSource = createFilterReader({ 211 | reader: source, 212 | callback: (resource) => { 213 | return resource.getPath().endsWith(".js"); 214 | } 215 | }); 216 | const sourceResources = await source.byGlob("**"); 217 | t.is(sourceResources.length, 2, "Found two resources in source"); 218 | 219 | const resources = await filteredSource.byGlob("**"); 220 | 221 | t.is(resources.length, 1, "Found exactly one resource via filter"); 222 | t.is(resources[0].getPath(), "/app/test.js", "Found correct resource"); 223 | }); 224 | 225 | test(adapter + ": Flatten resources", async (t) => { 226 | const source = await getAdapter({ 227 | fsBasePath: "./test/fixtures/application.a/webapp", 228 | virBasePath: "/resources/app/" 229 | }); 230 | const transformedSource = createFlatReader({ 231 | reader: source, 232 | namespace: "app" 233 | }); 234 | 235 | const resources = await transformedSource.byGlob("**/*.js"); 236 | t.is(resources.length, 1, "Found one resource via transformer"); 237 | t.is(resources[0].getPath(), "/test.js", "Found correct resource"); 238 | }); 239 | 240 | test(adapter + ": Link resources", async (t) => { 241 | const source = await getAdapter({ 242 | fsBasePath: "./test/fixtures/application.a/webapp", 243 | virBasePath: "/resources/app/" 244 | }); 245 | const transformedSource = createLinkReader({ 246 | reader: source, 247 | pathMapping: { 248 | linkPath: "/wow/this/is/a/beautiful/path/just/wow/", 249 | targetPath: "/resources/" 250 | } 251 | }); 252 | 253 | const resources = await transformedSource.byGlob("**/*.js"); 254 | t.is(resources.length, 1, "Found one resource via transformer"); 255 | t.is(resources[0].getPath(), "/wow/this/is/a/beautiful/path/just/wow/app/test.js", "Found correct resource"); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /test/lib/tracing/traceSummary.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import sinon from "sinon"; 3 | import esmock from "esmock"; 4 | 5 | async function createMock(t, isLevelEnabled=true) { 6 | t.context.loggerStub = { 7 | silly: sinon.stub(), 8 | isLevelEnabled: () => { 9 | return isLevelEnabled; 10 | } 11 | }; 12 | t.context.traceSummary = await esmock("../../../lib/tracing/traceSummary.js", { 13 | "@ui5/logger": { 14 | getLogger: sinon.stub().returns(t.context.loggerStub) 15 | } 16 | }); 17 | return t.context; 18 | } 19 | 20 | test.afterEach.always((t) => { 21 | sinon.restore(); 22 | }); 23 | 24 | test.serial("traceSummary", async (t) => { 25 | t.plan(2); 26 | 27 | const {traceSummary, loggerStub} = await createMock(t); 28 | 29 | // Measure always constant time 30 | sinon.stub(process, "hrtime").returns([3, 426604599]); 31 | 32 | const expectedReport = "==========================\n[=> TRACE SUMMARY:\n" + 33 | " 3.43 s elapsed time \n" + 34 | " 1 trace calls \n" + 35 | " 1 glob executions\n" + 36 | " 1 path stats\n" + 37 | " 1 rl-collections involed:\n" + 38 | " 2x collection_two\n" + 39 | "======================]"; 40 | 41 | // Add collection, byPath call and byGlob call w/o having a an active tracing started yet. 42 | // Those calls will not be traced. 43 | traceSummary.collection("collection_one"); 44 | traceSummary.pathCall(); 45 | traceSummary.globCall(); 46 | 47 | // Start tracing 48 | traceSummary.traceStarted(); 49 | 50 | traceSummary.collection("collection_two"); 51 | 52 | // Add an already existing collection 53 | traceSummary.collection("collection_two"); 54 | 55 | traceSummary.pathCall(); 56 | traceSummary.globCall(); 57 | 58 | // Print reporting and reset tracing 59 | await traceSummary.traceEnded(); 60 | 61 | t.is(loggerStub.silly.callCount, 1, "Logger has been called exactly once"); 62 | t.deepEqual(loggerStub.silly.getCall(0).args[0], expectedReport, "Correct report logged to the console"); 63 | }); 64 | 65 | test.serial("traceSummary no silly logging", async (t) => { 66 | t.plan(1); 67 | 68 | const {traceSummary, loggerStub} = await createMock(t, false); 69 | 70 | // Add collection, byPath call and byGlob call w/o having a an active tracing started yet. 71 | // Those calls will not be traced. 72 | traceSummary.collection("collection_one"); 73 | traceSummary.pathCall(); 74 | traceSummary.globCall(); 75 | 76 | // Start tracing 77 | traceSummary.traceStarted(); 78 | 79 | traceSummary.collection("collection_two"); 80 | 81 | // Add an already existing collection 82 | traceSummary.collection("collection_two"); 83 | 84 | traceSummary.pathCall(); 85 | traceSummary.globCall(); 86 | 87 | // Print reporting and reset tracing 88 | await traceSummary.traceEnded(); 89 | 90 | t.is(loggerStub.silly.callCount, 0, "Logger has not been called (due to disabled silly logging)"); 91 | }); 92 | --------------------------------------------------------------------------------