├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── commitlint.yml │ ├── release-manual.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .reuse └── dep5 ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── README.md ├── docs ├── README.md └── why.md ├── lerna.json ├── package.json ├── packages ├── dts-generator │ ├── .gitignore │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── README.md │ ├── api-extractor.json │ ├── api-report │ │ └── dts-generator.api.md │ ├── docs │ │ ├── TECHNICAL.md │ │ ├── end-to-end-sample.md │ │ └── ts-in-js-sample.png │ ├── hints-for-control-developers.md │ ├── package.json │ ├── src │ │ ├── checkCompile │ │ │ ├── check-compile.ts │ │ │ ├── diagnostic-analysis.ts │ │ │ ├── ignore-check.ts │ │ │ └── ignore-list.json │ │ ├── checkDtslint │ │ │ ├── check-dtslint.ts │ │ │ ├── dtslint-hint-list.json │ │ │ ├── dtslint-ignore-list.json │ │ │ └── dtslintConfig │ │ │ │ ├── .eslintrc.json │ │ │ │ ├── .npm___ignore │ │ │ │ ├── forDefinitelyTypedDir │ │ │ │ ├── .eslintrc.cjs │ │ │ │ └── notNeededPackages.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── openui5-tests.ts │ │ │ │ ├── package.json │ │ │ │ └── tsconfig.json │ │ ├── download-apijson.ts │ │ ├── generate-from-objects.ts │ │ ├── generate-from-paths.ts │ │ ├── generate.ts │ │ ├── index.ts │ │ ├── js-utils │ │ │ ├── ui5-metadata.js │ │ │ └── write-url-to-file.js │ │ ├── phases │ │ │ ├── ast-fixer.ts │ │ │ ├── ast-transform.ts │ │ │ ├── dts-code-gen.ts │ │ │ ├── json-fixer.ts │ │ │ ├── json-to-ast.ts │ │ │ ├── post-process.ts │ │ │ └── symbols.ts │ │ ├── resources │ │ │ └── core-preamble.d.ts │ │ ├── runCheck.ts │ │ ├── types │ │ │ ├── api-json.d.ts │ │ │ ├── ast.d.ts │ │ │ └── ui5-logger-types.d.ts │ │ └── utils │ │ │ ├── arguments-download-apijson.ts │ │ │ ├── arguments-index.ts │ │ │ ├── arguments-runCheck.ts │ │ │ ├── ast-utils.ts │ │ │ ├── base-utils.ts │ │ │ ├── file-utils.ts │ │ │ ├── json-constructor-settings-interfaces.ts │ │ │ ├── json-event-parameter-interfaces.ts │ │ │ ├── runtime-checks.ts │ │ │ ├── ts-ast-type-builder.ts │ │ │ └── type-parser.ts │ └── tsconfig.json └── ts-interface-generator │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── DEVELOPMENT.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── addSourceExports.ts │ ├── astGenerationHelper.ts │ ├── astToString.ts │ ├── collectClassInfo.ts │ ├── generateTSInterfaces.ts │ ├── generateTSInterfacesAPI.ts │ ├── interfaceGenerationHelper.ts │ ├── jsdocGenerator.ts │ ├── preferences.ts │ ├── test │ │ ├── generateMethods.test.ts │ │ ├── interfaceGenerationHelper.test.ts │ │ ├── samples │ │ │ ├── sampleControl │ │ │ │ ├── SampleControl.gen.d.ts │ │ │ │ └── SampleControl.ts │ │ │ ├── sampleManagedObject │ │ │ │ ├── SampleAnotherManagedObject.gen.d.ts │ │ │ │ ├── SampleAnotherManagedObject.ts │ │ │ │ ├── SampleManagedObject.gen.d.ts │ │ │ │ └── SampleManagedObject.ts │ │ │ ├── sampleWebComponent │ │ │ │ ├── App.codetest.ts │ │ │ │ ├── SampleWebComponent.gen.d.ts │ │ │ │ └── SampleWebComponent.ts │ │ │ └── tsfiles │ │ │ │ ├── someFile.js │ │ │ │ └── someFile.ts │ │ └── testcases │ │ │ ├── instance-exported │ │ │ ├── MyControl.gen.d.ts │ │ │ └── MyControl.ts │ │ │ ├── separate-export │ │ │ ├── MyControl.gen.d.ts │ │ │ └── MyControl.ts │ │ │ ├── simple-control │ │ │ ├── MyControl.gen.d.ts │ │ │ └── MyControl.ts │ │ │ ├── testcaseRunner.test.ts │ │ │ ├── tsconfig-path-relative │ │ │ ├── MyControl.gen.d.ts │ │ │ ├── MyControl.ts │ │ │ ├── MyDependency.gen.d.ts │ │ │ ├── MyDependency.ts │ │ │ ├── library.ts │ │ │ └── tsconfig.json │ │ │ ├── type-used-in-api │ │ │ ├── MyControl.gen.d.ts │ │ │ └── MyControl.ts │ │ │ ├── web-component │ │ │ ├── SampleWebComponent.gen.d.ts │ │ │ └── SampleWebComponent.ts │ │ │ └── xl-control-with-all-features │ │ │ ├── SampleControl.gen.d.ts │ │ │ └── SampleControl.ts │ ├── typeScriptEnvironment.ts │ └── types.d.ts │ ├── tsconfig-testcontrol.json │ ├── tsconfig-testmanagedobject.json │ ├── tsconfig-tests.json │ ├── tsconfig-testwebcomponent.json │ └── tsconfig.json ├── test-packages └── openui5-snapshot-test │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── input-sdk │ ├── openui5-meta.json │ ├── sap.f.api.json │ ├── sap.f.dtsgenrc │ ├── sap.m.api.json │ ├── sap.m.dtsgenrc │ ├── sap.tnt.api.json │ ├── sap.ui.core.api.json │ ├── sap.ui.core.dtsgenrc │ ├── sap.ui.layout.api.json │ └── sap.ui.unified.api.json │ ├── lib │ ├── download-sdk.js │ ├── generate-dts.js │ └── utils │ │ ├── dts-gen-wrapper.js │ │ └── logger.js │ ├── output-dts │ ├── index.d.ts │ ├── sap.f.d.ts │ ├── sap.m.d.ts │ ├── sap.tnt.d.ts │ ├── sap.ui.core.d.ts │ ├── sap.ui.layout.d.ts │ └── sap.ui.unified.d.ts │ ├── package.json │ ├── playground │ └── playground.js │ ├── test │ └── snapshot-spec.js │ └── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ** IMPORTANT ** 10 | This tracker is for issues caused by the tools maintained here (type definition generator and ts-interface generator for control development support). 11 | But if the bug is within the original OpenUI5/SAPUI5 code/JSDoc, then please report the issue there! 12 | 13 | Example: if a UI5 API is defined wrongly in the TypeScript type definitions but the same wrong definition can also be found in the [UI5 API documentation](https://ui5.sap.com), then the issue originates probably from the JSDoc inside the UI5 code, from which both are generated, and needs to be reported in the [OpenUI5 issue tracker](https://github.com/SAP/openui5/issues/new) or the one for SAPUI5, respectively. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Additional context** 22 | Add any other context about the problem here, e.g. whether the issue is only/not present in the `-esm` flavor of the UI5 type definitions and in which version of the type definitions it is. 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the main branch 5 | # See: https://github.community/t/duplicate-checks-on-push-and-pull-request-simultaneous-event/18012 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | name: Full Build (node ${{ matrix.node_version }}) 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node_version: 20 | - 18.x 21 | - 20.x 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node_version }} 27 | 28 | - name: Install dependencies 29 | run: yarn 30 | 31 | - name: Build 32 | run: yarn run ci 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: "42 9 * * 5" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the main branch 5 | # See: https://github.community/t/duplicate-checks-on-push-and-pull-request-simultaneous-event/18012 6 | push: 7 | branches: 8 | - main 9 | - gh-pages 10 | pull_request: 11 | branches: 12 | - main 13 | - gh-pages 14 | 15 | jobs: 16 | commitlint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: wagoid/commitlint-github-action@v6 23 | -------------------------------------------------------------------------------- /.github/workflows/release-manual.yml: -------------------------------------------------------------------------------- 1 | name: Release (manual) 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | release: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: lts/* 14 | 15 | - name: Install dependencies 16 | run: yarn 17 | 18 | - name: Build 19 | run: yarn build 20 | 21 | - name: Publish to NPM 22 | run: | 23 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 24 | echo ":::: Publishing as $(npm whoami)" 25 | npm run release:publish-manual 26 | env: 27 | NPM_TOKEN: ${{ secrets.NPM_RELEASE_AUTH_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | if: "contains(github.event.head_commit.message, 'chore(release): publish')" 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | 17 | - name: Install dependencies 18 | run: yarn 19 | 20 | - name: Build 21 | run: yarn build 22 | 23 | - name: Publish to NPM 24 | run: | 25 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 26 | echo ":::: Publishing as $(npm whoami)" 27 | npm run release:publish 28 | env: 29 | NPM_TOKEN: ${{ secrets.NPM_RELEASE_AUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sub-packages files for https://reuse.software (avoid duplication) 2 | .reuse 3 | LICENSES 4 | 5 | # root files for https://reuse.software 6 | !/.reuse 7 | !/LICENSES 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Some Mac stuff 17 | .DS_Store 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | 74 | # npm lock file 75 | package-lock.json 76 | .idea 77 | 78 | # build results 79 | dist/ 80 | 81 | # vscode metadata 82 | .vscode/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npm run hooks:commit-msg 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run hooks:pre-commit 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | demos/ 2 | packages/dts-generator/api-report/ 3 | packages/dts-generator/dist/ 4 | packages/dts-generator/src/checkDtslint/dtslintConfig/openui5-tests.ts 5 | packages/dts-generator/src/resources/core-preamble.d.ts 6 | packages/dts-generator/temp/ 7 | packages/ts-interface-generator/dist/ 8 | packages/ts-interface-generator/src/test/samples/sampleControl/SampleControl.gen.d.ts 9 | packages/ts-interface-generator/src/test/samples/sampleControl/SampleControl.ts 10 | packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleManagedObject.gen.d.ts 11 | packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleManagedObject.ts 12 | packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnotherManagedObject.gen.d.ts 13 | packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnotherManagedObject.ts 14 | packages/ts-interface-generator/src/test/samples/sampleWebComponent/SampleWebComponent.gen.d.ts 15 | packages/ts-interface-generator/src/test/testcases/ 16 | test-packages 17 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: ui5-typescript 3 | Upstream-Contact: Shachar Soel 4 | Source: https://github.com/SAP/ui5-typescript 5 | 6 | Files: * 7 | Copyright: 2025 SAP SE or an SAP affiliate company and UI5-TypeScript contributors 8 | License: Apache-2.0 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | What you are looking for is the changelog of one of the sub-packages, like [of the dts-generator](./packages/dts-generator/CHANGELOG.md) or [of the ts-interface-generator](./packages/ts-interface-generator/CHANGELOG.md). 4 | 5 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This is the common top-level contribution guide for this monorepo. 4 | A sub-package **may** have an additional CONTRIBUTING.md file if needed. 5 | 6 | ## Legal 7 | 8 | All contributors must sign the DCO 9 | 10 | - https://cla-assistant.io/SAP-samples/ecmascript-monorepo-template 11 | 12 | This is managed automatically via https://cla-assistant.io/ pull request voter. 13 | 14 | ### Contributing with AI-generated code 15 | 16 | As artificial intelligence evolves, AI-generated code is becoming valuable for many software projects, including open-source initiatives. While we recognize the potential benefits of incorporating AI-generated content into our open-source projects there are certain requirements that need to be reflected and adhered to when making contributions. 17 | 18 | Please see our [guideline for AI-generated code contributions to SAP Open Source Software Projects](https://github.com/SAP/.github/blob/main/CONTRIBUTING_USING_GENAI.md) for these requirements. 19 | 20 | ## Development Environment 21 | 22 | ### pre-requisites 23 | 24 | - [Yarn](https://yarnpkg.com/lang/en/docs/install/) >= 1.4.2 25 | - A current [Long-Term Support version](https://nodejs.org/en/about/releases/) of node.js 26 | - (optional) [commitizen](https://github.com/commitizen/cz-cli#installing-the-command-line-tool) for managing commit messages. 27 | 28 | ### Initial Setup 29 | 30 | The initial setup is trivial: 31 | 32 | - clone this repo 33 | - `yarn` 34 | 35 | ### Commit Messages format 36 | 37 | This project enforces the [conventional-commits][conventional_commits] commit message formats. 38 | The possible commits types prefixes are limited to those defined by [conventional-commit-types][commit_types]. 39 | This promotes a clean project history and enabled automatically generating a changelog. 40 | 41 | The commit message format will be inspected both on a git pre-commit hook 42 | and during the central CI build and will **fail the build** if issues are found. 43 | 44 | It is recommended to use `git cz` to construct valid conventional commit messages. 45 | 46 | - requires [commitizen](https://github.com/commitizen/cz-cli#installing-the-command-line-tool) to be installed. 47 | 48 | [commit_types]: https://github.com/commitizen/conventional-commit-types/blob/master/index.json 49 | [conventional_commits]: https://www.conventionalcommits.org/en/v1.0.0/ 50 | 51 | ### Formatting 52 | 53 | [Prettier](https://prettier.io/) is used to ensure consistent code formatting in this repository. 54 | This is normally transparent as it automatically activated in a pre-commit hook using [lint-staged](https://github.com/okonet/lint-staged). 55 | However, this does mean that dev flows that do not use a full dev env (e.g editing directly on github) may result in voter failures due to formatting errors. 56 | 57 | ### Compiling 58 | 59 | See the respective sub-packages for instructions (if needed at all). 60 | 61 | ### Testing 62 | 63 | [Mocha][mocha] and [Chai][chai] are used for unit-testing. 64 | 65 | [mocha]: https://mochajs.org/ 66 | [chai]: https://www.chaijs.com 67 | 68 | - To run the tests, execute `yarn test` in a specific sub-package. 69 | - Note that not all sub-packages contain tests. 70 | 71 | ### Full Build 72 | 73 | To run the full **C**ontinuous **I**ntegration build run `yarn ci` in either the top-level package or a specific subpackage. 74 | 75 | ### Release Life-Cycle 76 | 77 | This monorepo uses Lerna's [Independent][lerna-mode] mode which allows subpackages to have different versions. 78 | 79 | [lerna-mode]: https://github.com/lerna/lerna#independent-mode 80 | 81 | ### Release Process 82 | 83 | Performing a release requires push permissions to the repository. 84 | 85 | - Ensure you are on the default branch and synced with origin. 86 | - `yarn run release:version` 87 | - Follow the lerna CLI instructions. 88 | - Track the newly pushed commit with the message `chore(release): publish` in the build system for a successful completion (the `Release` GitHub action must be triggered!). 89 | - Inspect the newly published artifacts on npmjs.com. 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Continuous Integration](https://github.com/SAP/ui5-typescript/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/SAP/ui5-typescript/actions/workflows/ci.yml) 2 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 3 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP/ui5-typescript)](https://api.reuse.software/info/github.com/SAP/ui5-typescript) 4 | 5 | # UI5-TypeScript 6 | 7 | UI5-TypeScript is an npm [mono-repo][mono-repo] that contains tooling to support [TypeScript][typescript] in SAPUI5 and OpenUI5 Projects. 8 | This tooling can enable: 9 | 10 | - Better IDE integration for UI5 projects (e.g content assist). 11 | - Using TypeScript compiler to perform type checks on UI5 application code. 12 | - More easily implementing UI5 applications and controls in TypeScript thus enjoying the general benefits of TypeScript. 13 | 14 | It currently contains two public packages: 15 | 16 | - [@ui5/dts-generator](./packages/dts-generator) [![npm-ui5-dts-generator][npm-ui5-dts-generator-image]][npm-ui5-dts-generator-url] A generator which transforms the UI5 `api.json` format to TypeScript type definition (`*.d.ts`) format. This is useful to enable the type-safe usage of a UI5 control library written in JavaScript in code that uses TypeScript. 17 | 18 | - [@ui5/ts-interface-generator](./packages/ts-interface-generator) [![npm-ui5-ts-interface-generator][npm-ui5-ts-interface-generator-image]][npm-ui5-ts-interface-generator-url] A tool supporting control development in TypeScript. It is used at development time and generates type definitions for the control API methods which are only created at runtime by the UI5 framework. 19 | 20 | [npm-ui5-dts-generator-image]: https://img.shields.io/npm/v/@ui5/dts-generator.svg 21 | [npm-ui5-dts-generator-url]: https://www.npmjs.com/package/@ui5/dts-generator 22 | [npm-ui5-ts-interface-generator-image]: https://img.shields.io/npm/v/@ui5/ts-interface-generator.svg 23 | [npm-ui5-ts-interface-generator-url]: https://www.npmjs.com/package/@ui5/ts-interface-generator 24 | 25 | ## How to obtain the UI5 TypeScript signatures? 26 | 27 | The UI5 type signatures are created and published as part of the UI5 build process. They are available for SAPUI5 as well as OpenUI5. The SAPUI5 type definitions can be obtained like this: 28 | 29 | With npm 30 | 31 | `npm install @sapui5/types --save-dev` 32 | 33 | With Yarn 34 | 35 | `yarn add @sapui5/types --dev` 36 | 37 | > **NOTE:** the type definitions define ES6-style module names for the entities. They require the usage of modern JavaScript syntax with ES modules and classes, which requires an additional transformation step that can be run together with the anyway required TypeScript transpilation. 38 | > 39 | > Before the type definitions were generated in ES module style, they did declare all APIs with their global names, which are discouraged to be used and will no longer be available in UI5 2.x. The dts-generator still has the capability to generate this legacy "globals" version of the type definitions, for compatibility reasons. These legacy definitions are released as "ts-types" instead of "types", but will no longer be produced for UI5 2.x. 40 | 41 | Find all information about using UI5 with TypeScript at https://sap.github.io/ui5-typescript! 42 | 43 | ## Usage 44 | 45 | To see the suggested project setup for TypeScript development with the `types` packages, please check out the [TypeScript Hello World app](https://github.com/SAP-samples/ui5-typescript-helloworld). It not only can serve as copy template, but also includes a [detailed step-by-step guide](https://github.com/SAP-samples/ui5-typescript-helloworld/blob/main/step-by-step.md) for creating this setup from scratch. 46 | 47 | The [TypeScript branch of the "UI5 CAP Event App"](https://github.com/SAP-samples/ui5-cap-event-app/tree/typescript) sample demonstrates a slightly more complex application, using the same setup. It comes with an [explanation](https://github.com/SAP-samples/ui5-cap-event-app/blob/typescript/docs/typescript.md) of what UI5 TypeScript code usually looks like and what to consider. 48 | 49 | As mentioned, the best resource for using UI5 with TypeScript is https://sap.github.io/ui5-typescript. 50 | 51 | See the [demos](./demos) directory for consumption examples of the legacy signatures. 52 | 53 | ## Support 54 | 55 | For issues caused by the generators please open [issues](https://github.com/SAP/ui5-typescript/issues) on GitHub.
56 | However, issues in the UI5 type definitions which are also present in the [API documentation](https://ui5.sap.com/#/api) originate from the JSDoc comments in the original OpenUI5/SAPUI5 code, so please directly open an [OpenUI5](https://github.com/SAP/openui5/issues)/SAPUI5 ticket in this case. 57 | 58 | ## Contributing 59 | 60 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 61 | 62 | [typescript]: https://www.typescriptlang.org/ 63 | [mono-repo]: https://github.com/babel/babel/blob/master/doc/design/monorepo.md 64 | [openui5]: https://openui5.org/ 65 | [ui5-tooling]: https://github.com/SAP/ui5-tooling 66 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Most documentation can be found in the respective sub-package. This folder only contains the general motivation for [why this project was created](why.md). 4 | -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | # Why UI5-TypeScript? 2 | 3 | This document lists the main potential benefits for the UI5-TypeScript project 4 | and how it benefits the UI5 eco-system in general. 5 | 6 | ## Short Version 7 | 8 | - Better Integrating SAPUI5 and the **modern** JavaScript eco-system. 9 | 10 | - **Editor Tooling** & Functionality. 11 | 12 | - Working with many Editors / IDEs. 13 | - Content Assist. 14 | - Type Checks. 15 | - Go To Definition. 16 | - ... 17 | 18 | - UI5 applications written in **TypeScript** 19 | 20 | - CLI Tooling integration (tsc). 21 | 22 | - Improving UI5 Docs. 23 | 24 | ## In Depth 25 | 26 | ### Better Integrating SAPUI5 and the **modern** JavaScript eco-system: 27 | 28 | By providing TypeScript type signatures for UI5 libraries it becomes possible to 29 | integrate UI5 application into the modern JavaScript ecosystem. 30 | 31 | #### Editor Tooling 32 | 33 | UI5 uses proprietary syntax for modules (import / exports) and class inheritance. 34 | This makes it difficult for popular IDEs to provide editor related capabilities (e.g content assist) 35 | as those tools simply do not understand the UI5 syntax. 36 | 37 | By creating TypeScript signatures for UI5 libraries we are effectively **_bridging the gap_** 38 | thus enabling the IDEs and Editors to provide their advanced capabilities even when implementing 39 | UI5 applications in JavaScript. 40 | 41 | - Note: these advance editor services are available in [many IDEs][editors-lsp] and Editors via the [LSP](LSP) protocol. 42 | 43 | It is even possible to extend the functionality of some of these editor services via Language Server Plugins. 44 | 45 | - https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin 46 | 47 | For example see the Angular Language Service: 48 | 49 | - https://angular.io/guide/language-service 50 | 51 | #### CLI Tooling 52 | 53 | The integration into the JS ecosystem is not limited to editor functionality. 54 | The type checking capabilities of the TypeScript compiler can be used directly from the CLI 55 | (e.g in a Continuous Integration flow) to increase the quality of a UI5 application. 56 | 57 | Note that this capability of the TypeScript Compiler (tsc) is even available for 58 | JavaScript sources: 59 | 60 | - https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html 61 | 62 | #### UI5 applications written in **TypeScript** 63 | 64 | TypeScript is a popular superset of JavaScript which (among other things) adds support for an optional type system. 65 | This type system capability which is sorely lacking in JavaScript is very suitable for the development 66 | of large and complex applications. 67 | 68 | By providing type signatures for UI5 libraries we enable easier integration of UI5 apps 69 | and the TypeScript "flavor" of JavaScript. Without such signatures no type checking can be performed 70 | on API usage of UI5 libraries. 71 | 72 | References on TypeScript popularity: 73 | 74 | - https://www.wired.com/story/typescript-microsoft-javascript-alternative-most-popular/ 75 | - https://www.npmtrends.com/typescript (50M weekly downloads on npm as of early 2024, up from less than 20M three years before and 2M six years before). 76 | - https://2020.stateofjs.com/en-US/technologies/javascript-flavors/, https://2022.stateofjs.com/en-US/usage/#js_ts_balance 77 | 78 | #### Improving UI5 Docs 79 | 80 | There are two elements to this topic 81 | 82 | 1. If the UI5 docs are automatically transformed to TypeScript definitions 83 | Then the **UI5 type system slowly conforms more strongly to the TypeScript type system**. 84 | The latter being much more robust than the combination of UI5/JSDocs type systems. 85 | 86 | 2. **Promoting end users to raise issues with errors and inaccuracies in the API reference**: 87 | 88 | SAPUI5 has a large in depth API Reference: 89 | 90 | - https://ui5.sap.com/#/api 91 | 92 | However there exists no validation between an end user's source code and the content of the API Reference. 93 | In essence the API Reference is meant **only** for human consumption. Once the API Reference is available 94 | as a "compiler readable" format (`*.d.ts`), inaccuracies and mistakes in the API reference begin surfacing 95 | simply because once type checking is enabled these inaccuracies and mistakes cause 96 | distractions to **end users** (app developers). 97 | 98 | For example: If a method argument is marked as mandatory but is in fact optional 99 | It would not be possible to use it as optional without receiving a type error. 100 | The end user would have to add a `// @ts-ignore annotation` to silence the error 101 | and it is thus much more likely for an issue to be raised by the end user asking to improve the API Reference. 102 | 103 | [lsp]: https://langserver.org/ 104 | [editors-lsp]: https://microsoft.github.io/language-server-protocol/implementors/tools/ 105 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "test-packages/*", "demos/*"], 3 | "npmClient": "yarn", 4 | "command": { 5 | "publish": { 6 | "conventionalCommits": true 7 | }, 8 | "version": { 9 | "message": "chore(release): publish" 10 | } 11 | }, 12 | "version": "independent" 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*", 7 | "test-packages/*", 8 | "demos/*" 9 | ] 10 | }, 11 | "scripts": { 12 | "release:version": "lerna version", 13 | "release:version-simulate": "lerna version --no-push --no-git-tag-version", 14 | "release:publish": "lerna publish from-git --yes --no-verify-access", 15 | "release:publish-manual": "lerna publish from-package --yes --no-verify-access", 16 | "build": "lerna run build", 17 | "preci": "lerna run build", 18 | "ci": "npm-run-all format:validate ci:subpackages legal:*", 19 | "format:fix": "prettier --write \"**/*.@(ts|js|json|md|yml)\"", 20 | "format:validate": "prettier --check \"**/*.@(ts|js|json|md|yml)\"", 21 | "ci:subpackages": "lerna run ci", 22 | "legal:delete": "lerna exec \"shx rm -rf .reuse LICENSES\" || true", 23 | "legal:copy": "lerna exec \"shx cp -r ../../.reuse .reuse && shx cp -r ../../LICENSES LICENSES\"", 24 | "prepare": "husky", 25 | "hooks:pre-commit": "lint-staged", 26 | "hooks:commit-msg": "commitlint -e", 27 | "ncu": "ncu -ws --root", 28 | "ncu-u": "ncu -ws --root -u" 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "19.3.0", 32 | "@commitlint/config-conventional": "19.2.2", 33 | "chai": "4.4.1", 34 | "cz-conventional-changelog": "3.3.0", 35 | "husky": "9.0.11", 36 | "lerna": "8.1.5", 37 | "lint-staged": "15.2.7", 38 | "mocha": "10.5.1", 39 | "npm-run-all": "4.1.5", 40 | "prettier": "3.3.2", 41 | "shx": "0.3.4", 42 | "typescript": "5.5.2" 43 | }, 44 | "lint-staged": { 45 | "*.{ts,js,json,md,yml}": [ 46 | "prettier --write" 47 | ] 48 | }, 49 | "config": { 50 | "commitizen": { 51 | "path": "cz-conventional-changelog" 52 | } 53 | }, 54 | "commitlint": { 55 | "extends": [ 56 | "@commitlint/config-conventional" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/dts-generator/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | temp 3 | temp-dtslint -------------------------------------------------------------------------------- /packages/dts-generator/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This is the dts-generator package contribution guide. 4 | Please read the shared [mono-repo contribution](../../CONTRIBUTING.md) guide first. 5 | 6 | ## Issue Reporting 7 | 8 | For problems caused by the transformation process implemented in this dts-generator, please open [issues](https://github.com/SAP/ui5-typescript/issues) in this repository on GitHub.
9 | However, issues in the UI5 type definitions which are also present in the [API documentation](https://ui5.sap.com/#/api) originate from the JSDoc comments in the original OpenUI5/SAPUI5 code, so please directly open an [OpenUI5](https://github.com/SAP/openui5/issues)/SAPUI5 ticket in this case. 10 | 11 | ## SAPUI5 Integration 12 | 13 | When proposing changes or contributing code keep in mind that a change also needs to work with SAP's internal build of the various UI5 deliveries. It's not obvious what this means, but this topic might come up when a change is being discussed. 14 | 15 | ## Heuristics 16 | 17 | The transformation process from an api.json to a d.ts file is not straight-forward. 18 | This is because: 19 | 20 | - The api.json structure is not well defined and also subject to change. 21 | - The contents of the api.json files are collected from SAPUI5 source code, This means that we are dealing 22 | with manually entered data which is often not even well validated (e.g JSDoc syntax). 23 | - The semantics of the SAPUI5 type system often does not perfectly match the semantics of the TypeScript type system. 24 | 25 | Due to these reasons there are many heuristics and transformations happening during the compilation process. 26 | Inspect the source code and the log when the dts-generator is executed to learn more. 27 | -------------------------------------------------------------------------------- /packages/dts-generator/api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/dist/index.d.ts", 4 | "bundledPackages": [], 5 | "compiler": {}, 6 | "apiReport": { 7 | "enabled": true, 8 | "reportFolder": "/api-report/" 9 | }, 10 | "docModel": { 11 | "enabled": true 12 | }, 13 | "dtsRollup": { 14 | "enabled": true, 15 | "untrimmedFilePath": "/dist/index.d.ts" 16 | }, 17 | "tsdocMetadata": {}, 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | "extractorMessageReporting": { 25 | "default": { 26 | "logLevel": "warning" 27 | } 28 | }, 29 | "tsdocMessageReporting": { 30 | "default": { 31 | "logLevel": "warning" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/dts-generator/api-report/dts-generator.api.md: -------------------------------------------------------------------------------- 1 | ## API Report File for "@ui5/dts-generator" 2 | 3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). 4 | 5 | ```ts 6 | 7 | import type { FunctionType } from './ast.d.ts'; 8 | import ts from 'typescript'; 9 | import type { TypeReference } from './ast.d.ts'; 10 | 11 | // @public 12 | export type ApiJSON = { 13 | library: string; 14 | version: string; 15 | symbols: ConcreteSymbol[]; 16 | }; 17 | 18 | // @public 19 | export function checkCompile(options: CheckCompileConfig): boolean; 20 | 21 | // @public 22 | export interface CheckCompileConfig { 23 | dependencyFiles: string[]; 24 | errorOutputFile?: string; 25 | mainFile?: string; 26 | tsOptions: ts.BuildOptions; 27 | } 28 | 29 | // @public 30 | export function checkDtslint(dtsFileOrDir: string, options?: CheckDtslintConfig): void; 31 | 32 | // @public 33 | export interface CheckDtslintConfig { 34 | librariesToTest?: string[]; 35 | } 36 | 37 | // @public 38 | export interface Directives { 39 | badInterfaces: string[]; 40 | badMethods: string[]; 41 | badSymbols: string[]; 42 | deprecatedEnumAliases: { 43 | [fqn: string]: string; 44 | }; 45 | forwardDeclarations: { 46 | [libraryName: string]: ConcreteSymbol[]; 47 | }; 48 | fqnToIgnore: { 49 | [fqn: string]: string; 50 | }; 51 | namespacesToInterfaces: { 52 | [orgNamespace: string]: true | [true] | [true, "keep_original_ns"]; 53 | }; 54 | overlays: { 55 | [libraryName: string]: (ConcreteSymbol & { 56 | esmOnly?: boolean; 57 | })[]; 58 | }; 59 | typeTyposMap: { 60 | [orgType: string]: string; 61 | }; 62 | } 63 | 64 | // @public 65 | export function downloadApiJson(libs: string[], version: string, targetDir?: string): Promise; 66 | 67 | // @public 68 | export function generate({ apiFile, dependencyApiFiles, directiveFiles, targetFile, dependencyDTSFilesForCheck, dependenciesTypePackagesForCheck, generateGlobals, runCheckCompile, errorOutputFile, }: GenerateConfig): Promise; 69 | 70 | // @public 71 | export type GenerateConfig = { 72 | apiFile: string; 73 | dependencyApiFiles: string[]; 74 | directiveFiles: string[]; 75 | targetFile: string; 76 | dependencyDTSFilesForCheck: string[]; 77 | dependenciesTypePackagesForCheck?: string[]; 78 | generateGlobals?: boolean; 79 | runCheckCompile?: boolean; 80 | errorOutputFile?: string; 81 | }; 82 | 83 | // @public 84 | export function generateFromObjects(config: GenerateFromObjectsConfig): Promise<{ 85 | library: string; 86 | dtsText: string; 87 | }>; 88 | 89 | // @public 90 | export interface GenerateFromObjectsConfig { 91 | apiObject: ApiJSON; 92 | dependencyApiObjects: ApiJSON[]; 93 | directives: Directives; 94 | generateGlobals?: boolean; 95 | } 96 | 97 | // @public 98 | export function generateFromPaths(config: GenerateFromPathsConfig): Promise; 99 | 100 | // @public 101 | export interface GenerateFromPathsConfig { 102 | apiFile: string; 103 | dependenciesApiPath: string; 104 | dependenciesDTSPathForCheck: string; 105 | dependenciesTypePackagesForCheck?: string; 106 | directivesPath: string; 107 | generateGlobals?: boolean; 108 | runCheckCompile?: boolean; 109 | targetFile: string; 110 | verbose?: boolean; 111 | } 112 | 113 | // @public (undocumented) 114 | export const ModuleKind: typeof ts.ModuleKind; 115 | 116 | // @public (undocumented) 117 | export const ModuleResolutionKind: typeof ts.ModuleResolutionKind; 118 | 119 | // @public (undocumented) 120 | export const ScriptTarget: typeof ts.ScriptTarget; 121 | 122 | // Warnings were encountered during analysis: 123 | // 124 | // dist/types/api-json.d.ts:13:3 - (ae-forgotten-export) The symbol "ConcreteSymbol" needs to be exported by the entry point index.d.ts 125 | 126 | // (No @packageDocumentation comment for this package) 127 | 128 | ``` 129 | -------------------------------------------------------------------------------- /packages/dts-generator/docs/ts-in-js-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-typescript/b6c0fe52c1603042439df035b5ba8f5439b732f8/packages/dts-generator/docs/ts-in-js-sample.png -------------------------------------------------------------------------------- /packages/dts-generator/hints-for-control-developers.md: -------------------------------------------------------------------------------- 1 | # Hints for Control Developers 2 | 3 | This dts-generator creates TypeScript type definitions for the API of UI5 control libraries developed in JavaScript, so application developers can use TypeScript. This comes with some implications for development of UI5 controls and framework, which are described on this page. 4 | 5 | ## How it works 6 | 7 | The UI5 API documentation is generated from the source code and JSDoc comments during the build. This same information is also used to generate \*.d.ts files, which are TypeScript's standard way of providing type information for an otherwise untyped JavaScript library. 8 | 9 | ## Restrictions 10 | 11 | Not everything that can be done in an largely untyped language like JavaScript also makes sense and is allowed in a typed language like TypeScript. Even for UI5 development which remains being done in JavaScript and keeps most of its freedom, needs some restrictions, for example in the area of class inheritance: if a superclass has certain methods, inheriting subclasses cannot freely change the signature of these inherited methods. 12 | 13 | ## Checks 14 | 15 | After the \*.d.ts files have been generated, they are checked with the TypeScript compiler (`tsc`). In the UI5 libraries there some existing errors, whic hcannot be easily fixed due to compatibility reasons and they are being ignored by this check (only given as "warning"), see [packages/dts-generator/src/checkCompile/ignore-list.json](the ignore list for details). But we want to avoid the introduction of new errors, hence any new errors break the check (and SAP-internally break the library build). 16 | 17 | Because it can be a bit hard to understand what tsc errors mean for the UI5 source code, we have enriched the error message with additional information, like the probably UI5 source file that triggered the issue, the `*.d.ts` file content around the error, and in many cases with a "solution hint" which explains the situation and likely causes and suggests possible solutions. 18 | 19 | ## FAQ 20 | 21 | ### Is UI5 now going to use TypeScript for UI5 control development? 22 | 23 | A general switch to TypeScript is currently not planned. This dts-generator is mostly about enabling app developers and required _because_ development continues to a big extent in JavaScript. However some libraries have been converted to TypeScript and others are interested, so the plan is to enable those libraries who want to switch. 24 | 25 | ### There is a TypeScript error in the check. What to do now? 26 | 27 | Look at the entire error message in detail and try to understand it and correct the problem. It should provide plenty of context to make understanding the issue easier. 28 | 29 | ### There is a TypeScript error, but it seems completely unrelated to my change! 30 | 31 | The UI5 module mentioned in the error message is not always the actual source of the issue. Also a change in a base class can lead to an error occurring in an inheriting class. Or the module-finding logic is just wrong. But if the error message is really about something completely different on which others are working, try rebasing your change - maybe they have fixed it. 32 | -------------------------------------------------------------------------------- /packages/dts-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5/dts-generator", 3 | "description": "Generates TypeScript type definitions from UI5 api.json files", 4 | "version": "3.7.2", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:SAP/ui5-typescript.git", 9 | "directory": "packages/dts-generator" 10 | }, 11 | "type": "module", 12 | "main": "dist/index.js", 13 | "bin": { 14 | "@ui5/dts-generator": "dist/index.js", 15 | "ui5-dts-generator": "dist/index.js", 16 | "ui5-download-apijson": "dist/download-apijson.js" 17 | }, 18 | "types": "dist/index.d.ts", 19 | "files": [ 20 | "/dist", 21 | ".reuse", 22 | "LICENSES", 23 | "types" 24 | ], 25 | "dependencies": { 26 | "@definitelytyped/dtslint": "latest", 27 | "@definitelytyped/eslint-plugin": "latest", 28 | "@ui5/logger": "^3.0.0", 29 | "argparse": "^2.0.1", 30 | "es-main": "^1.3.0", 31 | "fs-extra": "11.3.0", 32 | "lodash": "4.17.21", 33 | "lodash.combinations": "18.11.1", 34 | "node-fetch": "^3.3.2", 35 | "prettier": "3.5.3", 36 | "resolve": "^1.22.10", 37 | "sanitize-html": "2.17.0", 38 | "strip-json-comments": "^5.0.1", 39 | "typescript": "^5.8.3" 40 | }, 41 | "devDependencies": { 42 | "@microsoft/api-extractor": "^7.52.8", 43 | "@types/argparse": "^2.0.17", 44 | "@types/lodash": "4.17.16", 45 | "@types/sanitize-html": "2.16.0", 46 | "@types/urijs": "1.19.25", 47 | "copyfiles": "^2.4.1", 48 | "del-cli": "^6.0.0", 49 | "npm-run-all": "^4.1.5" 50 | }, 51 | "scripts": { 52 | "clean": "del-cli -f dist", 53 | "copy-files": "copyfiles -V -u 1 \"src/**/*.json\" \"src/**/core-preamble.d.ts\" \"src/**/dtslintConfig/.npm___ignore\" \"src/**/dtslintConfig/openui5-tests.ts\" \"src/**/dtslintConfig/.eslintrc.json\" \"src/**/dtslintConfig/forDefinitelyTypedDir/.eslintrc.cjs\" \"src/**/api-json.d.ts\" dist/", 54 | "prebuild": "npm-run-all clean copy-files", 55 | "build": "tsc", 56 | "postbuild": "npm-run-all build-api-types clean-implementation-types", 57 | "build-api-types": "api-extractor run --local --verbose", 58 | "clean-implementation-types": "del-cli -f \"dist/**/*.d.ts.map\" \"dist/**/*.d.ts\" \"!dist/**/index.d.ts\" \"!dist/**/core-preamble.d.ts\"", 59 | "ci": "npm-run-all test:*", 60 | "test:apis": "tsc ./src/types/api-json.d.ts ./src/types/ast.d.ts ./src/types/ui5-logger-types.d.ts", 61 | "prewatch": "npm-run-all clean copy-files", 62 | "watch": "tsc -w" 63 | }, 64 | "publishConfig": { 65 | "access": "public" 66 | }, 67 | "engines": { 68 | "node": ">=16.18.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkCompile/check-compile.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from "@ui5/logger"; 2 | import { analyzeError, ErrorObject } from "./diagnostic-analysis.js"; 3 | import ts from "typescript"; 4 | import { writeFileSafe } from "../utils/file-utils.js"; 5 | 6 | const log = getLogger("@ui5/dts-generator/checkCompile"); 7 | 8 | /** 9 | * @see ts.ScriptTarget 10 | * @public 11 | */ 12 | const ScriptTarget = ts.ScriptTarget; 13 | 14 | /** 15 | * @see ts.ModuleKind 16 | * @public 17 | */ 18 | const ModuleKind = ts.ModuleKind; 19 | 20 | /** 21 | * @see ts.ModuleResolutionKind 22 | * @public 23 | */ 24 | const ModuleResolutionKind = ts.ModuleResolutionKind; 25 | 26 | export { ScriptTarget, ModuleKind, ModuleResolutionKind }; 27 | 28 | /** 29 | * The configuration for a checkCompile run. 30 | * 31 | * @public 32 | */ 33 | export interface CheckCompileConfig { 34 | /** 35 | * File path+name, e.g. the index.d.ts file referencing all other d.ts files or one specific d.ts file (which may depend on others). 36 | * Can be omitted/undefined if all the files are given as "dependencyFiles". 37 | * When both, mainFile and dependencyFiles, are given, then any check errors will only be reported for mainFile, as it is assumed 38 | * that the dependencyFiles have been checked separately previously. 39 | */ 40 | mainFile?: string; 41 | 42 | /** 43 | * Further d.ts files - or all files to check, when mainFile is undefined/null. 44 | */ 45 | dependencyFiles: string[]; 46 | 47 | /** 48 | * The options for the TypeScript compiler, a map with properties like "strict", "noEmit", "moduleResolution" etc. (note that enums are given like ts.ModuleResolutionKind.NodeJs) 49 | */ 50 | tsOptions: ts.BuildOptions; 51 | 52 | /** 53 | * Optional path+name of a file into which any test compilation errors should be written 54 | */ 55 | errorOutputFile?: string; 56 | } 57 | 58 | /** 59 | * Runs the TypeScript compiler on the given *.d.ts files, analyzes the errors, 60 | * ignores certain errors contained in the ignore list, and outputs hints for all real errors, so developers working on the 61 | * respective UI5 libraries can understand the origin of issues easier. 62 | * 63 | * @param options - the options for checking 64 | * @returns true if the compilation was successful 65 | * 66 | * @public 67 | */ 68 | export default function checkCompile(options: CheckCompileConfig) { 69 | const files = [options.mainFile, ...options.dependencyFiles].filter(Boolean); 70 | const program = ts.createProgram(files, options.tsOptions); 71 | const emitResult = program.emit(); 72 | 73 | const allDiagnostics = ts 74 | .getPreEmitDiagnostics(program) 75 | .concat(emitResult.diagnostics); 76 | 77 | log.info(`TypeScript compilation results:\n`); 78 | let ignoredMessagesWithoutFile = 0; 79 | let ignoredMessagesOnIgnoreList = 0; 80 | let significantErrorOccurred = false; 81 | const errorObjects: ErrorObject[] = [], 82 | warningObjects: ErrorObject[] = [], 83 | errors: string[] = [], 84 | warnings: string[] = []; 85 | allDiagnostics.forEach((diagnostic, index) => { 86 | if (diagnostic.file) { 87 | if ( 88 | options.mainFile == null || 89 | diagnostic.file.fileName === options.mainFile || 90 | options.dependencyFiles == null || 91 | options.dependencyFiles.length === 0 92 | ) { 93 | const { errorObject, errorMessage, hasHint, ignore } = analyzeError( 94 | diagnostic, 95 | { index, total: allDiagnostics.length }, 96 | ); 97 | // prepare logging grouped by severity 98 | if (ignore) { 99 | ignoredMessagesOnIgnoreList++; 100 | warnings.push(errorMessage); 101 | warningObjects.push(errorObject as ErrorObject); 102 | } else { 103 | significantErrorOccurred = true; 104 | errors.push(errorMessage); 105 | errorObjects.push(errorObject); 106 | } 107 | } else { 108 | ignoredMessagesWithoutFile++; 109 | } 110 | } else { 111 | log.error( 112 | " " + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n "), 113 | ); 114 | } 115 | }); 116 | 117 | // do the logging, first warnings, then errors 118 | warnings.forEach((warning: string, index) => { 119 | warning = `============= Warning ${index + 1} of ${ 120 | warnings.length 121 | } =======================================\n${warning}`; 122 | log.warn(" " + warning.split("\n").join("\n ")); 123 | }); 124 | 125 | errors.forEach((error, index) => { 126 | error = `============= ERROR ${index + 1} of ${ 127 | errors.length 128 | } =======================================\n${error}`; 129 | log.error(" " + error.split("\n").join("\n ")); 130 | }); 131 | 132 | const errorCount = 133 | allDiagnostics.length > 0 134 | ? allDiagnostics.length - 135 | ignoredMessagesWithoutFile - 136 | ignoredMessagesOnIgnoreList 137 | : 0; 138 | const ignoredOnListText = 139 | ignoredMessagesOnIgnoreList > 0 140 | ? `, ignored ${ignoredMessagesOnIgnoreList} warning(s) (warnings are errors which are on the ignore list)` 141 | : ""; 142 | const ignoredInOtherText = 143 | ignoredMessagesWithoutFile > 0 144 | ? `, ignored ${ignoredMessagesWithoutFile} more error(s) in external file(s)` 145 | : ""; 146 | if (errorCount > 0) { 147 | log.error( 148 | ` ${errorCount} error(s)${ignoredOnListText}${ignoredInOtherText}`, 149 | ); 150 | log.info( 151 | " (Further information at https://github.com/SAP/ui5-typescript/blob/main/hints-for-control-developers.md)\n\n", 152 | ); 153 | } else { 154 | log.info( 155 | ` ${errorCount} error(s)${ignoredOnListText}${ignoredInOtherText}\n\n`, 156 | ); 157 | } 158 | 159 | let warningCounter: number; 160 | let errorCounter = (warningCounter = 0); 161 | if (options.errorOutputFile) { 162 | const errorFileContent: string = errorObjects 163 | .concat(warningObjects) 164 | .reduce((content, error, index) => { 165 | return ( 166 | content + 167 | `\n${index + 1}\t${ 168 | error.ignore 169 | ? "Warning " + warningCounter++ 170 | : "Error " + errorCounter++ 171 | }\t${error.dtsFileName}\t${error.dtsFilePosition}\t${ 172 | error.ui5Module 173 | }\t${error.codeChain}\t${error.tsError}\t${error.hint.replace( 174 | /\n/g, 175 | " ### ", 176 | )}\t${error.ignore}` 177 | ); 178 | }, "Number\tID\t*.d.ts File Name\tError position in *.d.ts File\tUI5 Module Name (or best guess for error location)\tError Code(s)\tTypeScript Error Message\tSolution Hint\tignore"); 179 | 180 | writeFileSafe(options.errorOutputFile, errorFileContent).then( 181 | () => { 182 | log.info(" File written: " + options.errorOutputFile); 183 | }, 184 | (err) => { 185 | log.error( 186 | ` Error when writing error log file ${options.errorOutputFile}: ${err}`, 187 | ); 188 | }, 189 | ); 190 | } 191 | 192 | return !significantErrorOccurred; 193 | } 194 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkCompile/ignore-check.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * the following single-line alternative requires typescript ^4.5.0 AND esnext/nodenext setting 3 | import ignoreList from './ignore-list.json' assert { type: 'json' }; 4 | */ 5 | import * as fs from "fs"; 6 | const ignoreList = JSON.parse( 7 | fs.readFileSync(new URL("./ignore-list.json", import.meta.url), { 8 | encoding: "utf8", 9 | }), 10 | ); 11 | 12 | // Matches the end of error messages where a large number of mismatching properties are enumerated. 13 | // These are likely to change for independent reasons and we still want the ignore check to find the match. 14 | const LIST_REGEX = /: ([\w0-9_]+, )+and \d+ more.$/; 15 | 16 | export default function (module: string, message: string, dtsFileName: string) { 17 | let normalizedMessage = message.replace(LIST_REGEX, "_PROPERTYLIST_"); 18 | for (let i = 0; i < ignoreList.length; i++) { 19 | if ( 20 | module === ignoreList[i].module && 21 | (!ignoreList[i].dtsFileName || dtsFileName === ignoreList[i].dtsFileName) 22 | ) { 23 | // module must match. If dtsFileName is given, it must also match 24 | let normalizedIgnorelistMessage = ignoreList[i].message.replace( 25 | LIST_REGEX, 26 | "_PROPERTYLIST_", 27 | ); // could be done in the original list, but it's easier to handle like this 28 | if (normalizedMessage === normalizedIgnorelistMessage) { 29 | return true; 30 | } 31 | } 32 | } 33 | return false; 34 | } 35 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkCompile/ignore-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "############################# this is the beginning of the ignorelist for esm ##################################################", 4 | "module": "", 5 | "dtsFileName": "sap.fe.macros.d.ts", 6 | "message": "',' expected." 7 | }, 8 | 9 | { 10 | "comment": "############################# this is the beginning of the ignorelist for globals ##################################################", 11 | "module": "sap.apf.utils.Filter", 12 | "message": "Cannot find namespace 'constants'." 13 | }, 14 | { 15 | "module": "sap.apf.utils.FilterAnd", 16 | "message": "Cannot find namespace 'constants'." 17 | }, 18 | { 19 | "module": "sap.apf.utils.FilterExpression", 20 | "message": "Cannot find namespace 'constants'." 21 | }, 22 | { 23 | "module": "sap.apf.utils.FilterOr", 24 | "message": "Cannot find namespace 'constants'." 25 | }, 26 | { 27 | "module": "sap.fe.test.ListReport", 28 | "message": "Class static side 'typeof ListReport' incorrectly extends base class static side 'typeof TemplatePage'. ### Types of property 'actions' are incompatible. ### Type 'actions' is missing the following properties from type 'actions': onActionDialog, onConfirmationDialog, onCreateDialog, onDialog, and 3 more." 29 | }, 30 | { 31 | "module": "sap.fe.test.ObjectPage", 32 | "message": "Class static side 'typeof ObjectPage' incorrectly extends base class static side 'typeof TemplatePage'. ### Types of property 'actions' are incompatible. ### Type 'actions' is missing the following properties from type 'actions': onActionDialog, onConfirmationDialog, onCreateDialog, onDialog, and 3 more." 33 | }, 34 | { 35 | "module": "sap.gantt.shape.ext.rls", 36 | "message": "Interface '$RelationshipSettings' incorrectly extends interface '$PathSettings'. ### Types of property 'selectedShape' are incompatible. ### Type 'SelectedRelationship' is missing the following properties from type 'SelectedShape': getHeight, setHeight" 37 | }, 38 | { 39 | "module": "sap.m", 40 | "message": "Duplicate identifier 'MessageBox'." 41 | }, 42 | { 43 | "module": "sap.m.MessageBox", 44 | "message": "Duplicate identifier 'MessageBox'." 45 | }, 46 | { 47 | "module": "sap.ui", 48 | "message": "Duplicate identifier 'Device'." 49 | }, 50 | { 51 | "module": "sap.ui.commons", 52 | "message": "Duplicate identifier 'MessageBox'." 53 | }, 54 | { 55 | "module": "sap.ui.commons.MessageBox", 56 | "message": "Duplicate identifier 'MessageBox'." 57 | }, 58 | { 59 | "module": "sap.ui.comp.transport.TransportDialog", 60 | "message": "Property 'fl' does not exist on type 'typeof ui'." 61 | }, 62 | { 63 | "module": "sap.ui.core", 64 | "message": "Duplicate identifier 'Configuration'." 65 | }, 66 | { 67 | "module": "sap.ui.core.Configuration", 68 | "message": "Duplicate identifier 'Configuration'." 69 | }, 70 | { 71 | "module": "sap.ui.Device", 72 | "message": "Duplicate identifier 'Device'." 73 | }, 74 | { 75 | "module": "sap.ui.Device", 76 | "message": "Duplicate identifier 'browser'." 77 | }, 78 | { 79 | "module": "sap.ui.Device", 80 | "message": "Duplicate identifier 'media'." 81 | }, 82 | { 83 | "module": "sap.ui.Device", 84 | "message": "Duplicate identifier 'os'." 85 | }, 86 | { 87 | "module": "sap.ui.Device.browser", 88 | "message": "Duplicate identifier 'browser'." 89 | }, 90 | { 91 | "module": "sap.ui.Device.media", 92 | "message": "Duplicate identifier 'media'." 93 | }, 94 | { 95 | "module": "sap.ui.Device.os", 96 | "message": "Duplicate identifier 'os'." 97 | }, 98 | { 99 | "module": "sap.ui.model.analytics", 100 | "message": "Duplicate identifier 'odata4analytics'." 101 | }, 102 | { 103 | "module": "sap.ui.model.analytics.odata4analytics", 104 | "message": "Duplicate identifier 'odata4analytics'." 105 | }, 106 | { 107 | "module": "sap.ui.test.gherkin", 108 | "message": "Duplicate identifier 'dataTableUtils'." 109 | }, 110 | { 111 | "module": "sap.ui.test.gherkin.dataTableUtils", 112 | "message": "Duplicate identifier 'dataTableUtils'." 113 | }, 114 | { 115 | "module": "sap.viz.ui5.format", 116 | "message": "Duplicate identifier 'ChartFormatter'." 117 | }, 118 | { 119 | "module": "sap.viz.ui5.format.ChartFormatter", 120 | "message": "Duplicate identifier 'ChartFormatter'." 121 | }, 122 | { 123 | "module": "sap.collaboration.components.feed.Component", 124 | "message": "Cannot find module 'sap/suite/ui/commons/library'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 125 | }, 126 | { 127 | "module": "sap.collaboration.components.socialtimeline.Component", 128 | "message": "Cannot find module 'sap/suite/ui/commons/library'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 129 | }, 130 | { 131 | "module": "sap.ui.vtm.MatrixUtilities", 132 | "message": "Cannot find module 'sap/ui/vk/TransformationMatrix'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 133 | }, 134 | { 135 | "module": "sap.ui.generic.app.navigation.service.NavError", 136 | "message": "Cannot find module 'sap/fe/navigation/NavError'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 137 | }, 138 | { 139 | "module": "sap.ui.generic.app.navigation.service.NavigationHandler", 140 | "message": "Cannot find module 'sap/fe/navigation/NavigationHandler'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 141 | }, 142 | { 143 | "module": "sap.ui.generic.app.navigation.service.PresentationVariant", 144 | "message": "Cannot find module 'sap/fe/navigation/PresentationVariant'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 145 | }, 146 | { 147 | "module": "sap.ui.generic.app.navigation.service.SelectionVariant", 148 | "message": "Cannot find module 'sap/fe/navigation/SelectionVariant'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?" 149 | }, 150 | 151 | { 152 | "comment": "################## this one and the next six are for issues in sap.firefly.fiori.esm-d.ts, which occur only outside our sandbox #####################################", 153 | "module": "", 154 | "message": "Parameter declaration expected." 155 | }, 156 | { 157 | "module": "", 158 | "message": "';' expected." 159 | }, 160 | { 161 | "module": "", 162 | "message": "Unexpected token. A constructor, method, accessor, or property was expected." 163 | }, 164 | { 165 | "module": "", 166 | "message": "Parameter declaration expected." 167 | }, 168 | { 169 | "module": "", 170 | "message": "Parameter 'undefined' implicitly has an 'any' type." 171 | }, 172 | { 173 | "module": "", 174 | "message": "Parameter declaration expected." 175 | }, 176 | { 177 | "module": "", 178 | "message": "Parameter 'undefined' implicitly has an 'any' type." 179 | } 180 | ] 181 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslint-hint-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleName": "unified-signatures", 4 | "hint": "This COULD be caused by a method which has more than one optional parameter followed by non-optional parameters. Because this is not allowed in TypeScript, but happening in the existing UI5 code base (though discouraged!!), the TypeScript dts generator generates multiple method documentations out of this, with all required permutations of the optional parameters before mandatory ones. The TypeScript linting check, however, does not like those multiple declarations where only one parameter can have either one or onother type. To solve this, avoid the use of optional parameters which are followed by non-optional ones!" 5 | }, 6 | { 7 | "ruleName": "redundant-undefined", 8 | "hint": "When a parameter/property is marked as optional in JSDoc, then do not specify 'undefined' as one of the possible values. The fact that it is optional already implies that the value can be undefined. Simply remove 'undefined' from the list of possible values." 9 | }, 10 | { 11 | "ruleName": "no-irregular-whitespace", 12 | "hint": "Do not encode space characters in JSDoc as ' ' - it would be converted to a special whitespace character. If you want a space, use a space, and if you want the HTML entity ' ' to appear literally, write it as '&npsp;'" 13 | }, 14 | { 15 | "ruleName": "ban-types", 16 | "hint": "This error is usually caused by a simple built-in JavaScript type like 'boolean' or 'string' or 'number' written with a leading uppercase letter. The types 'Boolean', 'String' and 'Number' also exist as object wrappers, but are usually not the intended ones. If you mean a regular boolean/string/number, please use lower-case initial letters! And in cae of 'number' even better differentiate between 'float' and 'int' if possible." 17 | }, 18 | { 19 | "ruleName": "no-redundant-jsdoc-2", 20 | "hint": "This error is usually caused by a '@returns' JSDoc tag followed by either a broken type (e.g. duplicate curly braces like '{{this}' or a typing mistake in a type reference) or by TWO subsequent curly-braces blocks. The latter could look like this: '@returns {SomeHelper} {@link SomeHelper} which helps you'. Try to add another word before the '{@link ...}' in this case." 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslint-ignore-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleName": "unified-signatures", 4 | "ui5Module": "sap.fe.navigation.NavigationHandler", 5 | "status": "warning", 6 | "comment": "still current as of 2022-02-03" 7 | }, 8 | { 9 | "ruleName": "unified-signatures", 10 | "ui5Module": "sap.ui.core.tmpl.Template", 11 | "status": "warning", 12 | "comment": "still current as of 2022-02-03" 13 | }, 14 | { 15 | "ruleName": "unified-signatures", 16 | "ui5Module": "sap.ui.core.tmpl.TemplateControl", 17 | "status": "warning", 18 | "comment": "still current as of 2022-02-03" 19 | }, 20 | { 21 | "ruleName": "unified-signatures", 22 | "ui5Module": "sap.ui.core.util.MockServer", 23 | "status": "warning", 24 | "comment": "still current as of 2022-02-03" 25 | }, 26 | { 27 | "ruleName": "unified-signatures", 28 | "ui5Module": "sap.ui.model.odata.v2.ODataListBinding", 29 | "status": "warning", 30 | "comment": "still current as of 2022-02-03" 31 | }, 32 | { 33 | "ruleName": "unified-signatures", 34 | "ui5Module": "sap.ui", 35 | "status": "warning", 36 | "comment": "still current as of 2022-02-03" 37 | }, 38 | { 39 | "ruleName": "unified-signatures", 40 | "ui5Module": "sap.ui.generic.app.navigation.service.NavigationHandler", 41 | "status": "warning", 42 | "comment": "still current as of 2022-02-03" 43 | }, 44 | 45 | { 46 | "ruleName": "no-irregular-whitespace", 47 | "ui5Module": "sap.ui.webc.fiori.ShellBar", 48 | "status": "warning", 49 | "comment": "webc" 50 | }, 51 | { 52 | "ruleName": "ban-types", 53 | "errorMessage": "Don't use 'Boolean' as a type. Avoid using the `Boolean` type. Did you mean `boolean`?", 54 | "ui5Module": "sap.ui.webc.fiori.Wizard", 55 | "status": "warning", 56 | "comment": "webc" 57 | }, 58 | { 59 | "ruleName": "ban-types", 60 | "errorMessage": "Don't use 'String' as a type. Avoid using the `String` type. Did you mean `string`?", 61 | "ui5Module": "sap.ui.webc.main.ColorPalette", 62 | "status": "warning", 63 | "comment": "webc" 64 | }, 65 | 66 | { 67 | "ruleName": "ban-types", 68 | "errorMessage": "Don't use 'Function' as a type. Avoid using the `Function` type. Prefer a specific function type, like `() => void`.", 69 | "status": "ignore", 70 | "comment": "this entry needs to stay for the time being; only few occurrences are being fixed (non-Object/Function ones); could be further improved with quite some work: some cases originate from JSDoc" 71 | }, 72 | { 73 | "ruleName": "ban-types", 74 | "errorMessage": "Don't use 'Object' as a type. Avoid using the `Object` type. Did you mean `object`?", 75 | "status": "ignore", 76 | "comment": "this entry needs to stay for the time being; only few occurrences are being fixed (non-Object/Function ones); could be further improved with quite some work: some cases originate from JSDoc" 77 | }, 78 | { 79 | "fileName": "sap.fe.macros.d.ts", 80 | "status": "ignore", 81 | "comment": "this entry needs to stay for the time being; masses of errors in sap.fe.macros.d.ts" 82 | }, 83 | 84 | { 85 | "ruleName": "no-redundant-jsdoc-2", 86 | "ui5Module": "sap.ushell.services.Personalization", 87 | "status": "warning", 88 | "comment": "" 89 | }, 90 | { 91 | "ruleName": "no-redundant-jsdoc-2", 92 | "ui5Module": "sap.ushell.services.Personalization.VariantSet", 93 | "status": "warning", 94 | "comment": "" 95 | }, 96 | { 97 | "ruleName": "no-redundant-jsdoc-2", 98 | "ui5Module": "sap.ushell.services.Personalization.VariantSetAdapter", 99 | "status": "warning", 100 | "comment": "" 101 | }, 102 | { 103 | "ruleName": "no-redundant-jsdoc-2", 104 | "ui5Module": "sap.ushell.services.PersonalizationContainer", 105 | "status": "warning", 106 | "comment": "" 107 | }, 108 | { 109 | "ruleName": "no-redundant-jsdoc-2", 110 | "ui5Module": "sap.ushell.services.PersonalizationContainerVariantSet", 111 | "status": "warning", 112 | "comment": "" 113 | }, 114 | 115 | { 116 | "fileName": "sap.gantt.d.ts", 117 | "status": "warning", 118 | "comment": "Let's just exclude the more problematic dist-layer libraries. They don't have this check in their voter, so new errors are likely as well as unproblematic, as dtslint only needs to succeed for DefinitelyTyped (=OpenUI5)." 119 | }, 120 | { 121 | "fileName": "sap.sac.df.d.ts", 122 | "status": "warning" 123 | } 124 | ] 125 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "rules": { 5 | "@definitelytyped/no-any-union": "off", 6 | "@definitelytyped/no-unnecessary-generics": "off", 7 | "@definitelytyped/strict-export-declare-modifiers": "off", 8 | "@definitelytyped/no-single-declare-module": "off", 9 | "@definitelytyped/npm-naming": "off", 10 | "@typescript-eslint/no-unsafe-function-type": "off", 11 | "@typescript-eslint/no-wrapper-object-types": "off", 12 | "@typescript-eslint/no-empty-interface": "off", 13 | "@typescript-eslint/naming-convention": "off", 14 | "@typescript-eslint/consistent-type-definitions": "off", 15 | "no-duplicate-imports": "off", 16 | 17 | "jsdoc/check-tag-names": [ 18 | "error", 19 | { "definedTags": ["ui5-protected", "experimental", "route"] } 20 | ], 21 | "@typescript-eslint/no-invalid-void-type": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/.npm___ignore: -------------------------------------------------------------------------------- 1 | * 2 | !**/*.d.ts 3 | !**/*.d.cts 4 | !**/*.d.mts 5 | !**/*.d.*.ts -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/forDefinitelyTypedDir/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["plugin:@definitelytyped/all"], 4 | }; -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/index.d.ts: -------------------------------------------------------------------------------- 1 | // dummy to make the error in tsconfig in the same directory go away. This file is not used for anything. 2 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/openui5-tests.ts: -------------------------------------------------------------------------------- 1 | import UI5Event from "sap/ui/base/Event"; 2 | import Core from "sap/ui/core/Core"; 3 | import UIComponent from "sap/ui/core/UIComponent"; 4 | import XMLView from "sap/ui/core/mvc/XMLView"; 5 | import Controller from "sap/ui/core/mvc/Controller"; 6 | import JSONModel from "sap/ui/model/json/JSONModel"; 7 | import ODataModel from "sap/ui/model/odata/v2/ODataModel"; 8 | import ODataV4Model from "sap/ui/model/odata/v4/ODataModel"; 9 | import Text from "sap/m/Text"; 10 | import Table from "sap/m/Table"; 11 | import Toolbar from "sap/m/Toolbar"; 12 | import Button from "sap/m/Button"; 13 | import DatePicker from "sap/m/DatePicker"; 14 | import Label from "sap/m/Label"; 15 | import Column from "sap/m/Column"; 16 | import Dialog from "sap/m/Dialog"; 17 | import MessageBox from "sap/m/MessageBox"; 18 | import FileUploader, { 19 | FileUploader$UploadCompleteEvent, 20 | } from "sap/ui/unified/FileUploader"; 21 | import FileUploaderParameter from "sap/ui/unified/FileUploaderParameter"; 22 | import ODataV4ListBinding, { 23 | ODataListBinding$CreateCompletedEvent, 24 | } from "sap/ui/model/odata/v4/ODataListBinding"; 25 | import Target from "sap/ui/core/routing/Target"; 26 | import { TitleLevel } from "sap/ui/core/library"; 27 | import DateTimePicker from "sap/m/DateTimePicker"; 28 | import RenderManager from "sap/ui/core/RenderManager"; 29 | import NumberFormat from "sap/ui/core/format/NumberFormat"; 30 | import CalendarUtils from "sap/ui/core/date/CalendarUtils"; 31 | import PlanningCalendar from "sap/m/PlanningCalendar"; 32 | import WebSocket from "sap/ui/core/ws/WebSocket"; 33 | import QUnit from "sap/ui/thirdparty/qunit-2"; 34 | import IllustratedMessage from "sap/m/IllustratedMessage"; 35 | import { SingleControlSelector } from "sap/ui/test/Opa5"; 36 | import Mobile from "sap/ui/util/Mobile"; 37 | import Input from "sap/m/Input"; 38 | import { DynamicDateRangeGroups, ITableItem } from "sap/m/library"; 39 | import ColumnListItem from "sap/m/ColumnListItem"; 40 | import Filter from "sap/ui/model/Filter"; 41 | import Model from "sap/ui/model/Model"; 42 | 43 | /* 44 | * REMARK: the type definition files are automatically generated and this generation is tested, 45 | * so the importance of these tests here is very limited. Hence there is no focus on making them 46 | * as complete or meaningful as possible. 47 | */ 48 | 49 | class Ctrl extends Controller { 50 | onShowHello(): void { 51 | // show a native JavaScript alert 52 | alert("Hello World"); 53 | } 54 | 55 | onInit() { 56 | // set data model on view 57 | const oData = { 58 | recipient: { 59 | name: "World", 60 | }, 61 | }; 62 | const oModel = new JSONModel(oData); 63 | const view = this.getView(); 64 | if (!view) { 65 | return; 66 | } 67 | 68 | view.setModel(oModel); 69 | 70 | const dp = new DatePicker({ dateValue: "{myModel>/myPropertyName}" }); 71 | dp.setShowCurrentDateButton(true); 72 | } 73 | } 74 | 75 | export class BaseController extends Controller { 76 | getRouter() { 77 | return (this.getOwnerComponent()).getRouter(); 78 | } 79 | getJSONModel(name: string) { 80 | const view = this.getView(); 81 | if (!view) { 82 | return; 83 | } 84 | return view.getModel(name); 85 | } 86 | getModel(name: string) { 87 | const view = this.getView(); 88 | if (!view) { 89 | return; 90 | } 91 | return view.getModel(name); 92 | } 93 | suspendDefaultTarget() { 94 | const router = (this.getOwnerComponent()).getRouter(); 95 | const target = router.getTarget("default") as Target; 96 | target.suspend(); 97 | } 98 | } 99 | 100 | const oTable = new Table({ 101 | headerToolbar: new Toolbar({ 102 | content: [ 103 | new Button({ 104 | text: "Create user", 105 | press: () => {}, 106 | }), 107 | ], 108 | }), 109 | beforeOpenContextMenu: (oEvent: UI5Event) => { 110 | const params = oEvent.getParameters(); 111 | }, 112 | }); 113 | 114 | const lbl = new Label(undefined); 115 | lbl.setText("text"); 116 | const col = new Column(); 117 | 118 | type Headers = { 119 | "x-csrf-token": string; 120 | }; 121 | 122 | const oUploadDialog = new Dialog(undefined); 123 | oUploadDialog.setTitle("Upload photo"); 124 | const oDataV2Model = oUploadDialog.getModel() as ODataModel; 125 | oDataV2Model.refreshSecurityToken(); 126 | oDataV2Model.bindList("/", undefined, [], [], { createdEntitiesKey: "test" }); 127 | // prepare the FileUploader control 128 | const oFileUploader = new FileUploader({ 129 | headerParameters: [ 130 | new FileUploaderParameter({ 131 | name: "x-csrf-token", 132 | value: (oDataV2Model.getHeaders() as Headers)["x-csrf-token"], 133 | }), 134 | ], 135 | uploadComplete: (oEvent: FileUploader$UploadCompleteEvent) => { 136 | // 1.115.1: types not only for event parameters, but also for events 137 | const sResponse = oEvent.getParameter("response"); // 1.115: event objects are now specifically typed 138 | if (sResponse) { 139 | oUploadDialog.close(); 140 | MessageBox.show("Return Code: " + sResponse); 141 | } 142 | }, 143 | }); 144 | // create a button to trigger the upload 145 | const oTriggerButton = new Button({ 146 | text: "Upload", 147 | press: () => { 148 | // call the upload method 149 | oFileUploader.insertHeaderParameter( 150 | new FileUploaderParameter({ 151 | name: "slug", 152 | value: oFileUploader.getValue(), 153 | }), 154 | 0 155 | ); 156 | oFileUploader.upload(); 157 | }, 158 | }); 159 | 160 | const dateTimePicker = new DateTimePicker({ showCurrentTimeButton: true }); 161 | dateTimePicker.setShowCurrentTimeButton( 162 | !dateTimePicker.getShowCurrentTimeButton() 163 | ); 164 | oUploadDialog.addContent(oFileUploader); 165 | oUploadDialog.addContent(oTriggerButton); 166 | oUploadDialog.addContent(dateTimePicker); 167 | oUploadDialog.open(); 168 | 169 | const illustratedMessage: IllustratedMessage = new IllustratedMessage(); 170 | illustratedMessage.setAriaTitleLevel(TitleLevel.H1); 171 | const focusable = illustratedMessage.isFocusable(); 172 | 173 | const odataV4ListBinding = illustratedMessage.getBinding( 174 | "additionalContent" 175 | ) as ODataV4ListBinding; 176 | const odataV4ListBindingCount = odataV4ListBinding.getCount(); 177 | const context = odataV4ListBinding.getKeepAliveContext("x"); 178 | const odataV4Model = odataV4ListBinding.getModel() as ODataV4Model; 179 | odataV4Model.delete("something"); 180 | let eTagMap: Record; 181 | eTagMap = odataV4Model.getMetaModel().getETags(); 182 | odataV4Model.getKeyPredicate("some/path", {}); 183 | 184 | const integer = NumberFormat.getIntegerInstance({ 185 | strictGroupingValidation: true, 186 | }); 187 | 188 | const weekConfigurationValues = CalendarUtils.getWeekConfigurationValues(); 189 | 190 | const pc = new PlanningCalendar(); 191 | pc.getSecondaryCalendarType(); 192 | 193 | const ws = new WebSocket("someUrl"); 194 | ws.close("end"); 195 | 196 | // 1.112: QUnit declared as importable module instead of just globally available 197 | QUnit.config.autostart = false; 198 | 199 | // 1.113: OPA improvements 200 | const scs: SingleControlSelector = { 201 | id: "myControlId", 202 | }; 203 | 204 | // 1.114: more details in the APIs 205 | Mobile.setIcons({ precomposed: false }); 206 | 207 | // 1.116: sap.ui.require (for use in plain JS) callback function parameters fixed 208 | sap.ui.require( 209 | ["sap/m/Button", "sap/m/Input"], 210 | (B: typeof Button, I: typeof Input) => { 211 | const b = new B({ text: "Hello" }); 212 | const i = new I(); 213 | } 214 | ); 215 | 216 | // 1.116.1: more event parameters defined 217 | odataV4ListBinding.attachCreateCompleted( 218 | (evt: ODataListBinding$CreateCompletedEvent) => { 219 | const contect = evt.getParameter("context"); 220 | } 221 | ); 222 | 223 | // 1.117.0: it's just an update of the types! 224 | 225 | // 1.118 226 | const ddrg: DynamicDateRangeGroups = DynamicDateRangeGroups.SingleDates; 227 | 228 | // 1.119 229 | const iti: ITableItem = new ColumnListItem(); 230 | 231 | // 1.120 232 | const noneFilter = Filter.NONE; 233 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@types/openui5", 4 | "version": "1.120.9999", 5 | "nonNpm": true, 6 | "nonNpmDescription": "openui5", 7 | "projects": [ 8 | "https://github.com/SAP/openui5" 9 | ], 10 | "minimumTypeScriptVersion": "5.0", 11 | "dependencies": { 12 | "@types/jquery": "~3.5.13", 13 | "@types/qunit": "^2.5.4" 14 | }, 15 | "devDependencies": { 16 | "@types/openui5": "workspace:." 17 | }, 18 | "owners": [ 19 | { 20 | "name": "OpenUI5 Bot", 21 | "githubUsername": "openui5bot" 22 | }, 23 | { 24 | "name": "Peter Muessig", 25 | "githubUsername": "petermuessig" 26 | }, 27 | { 28 | "name": "Frank Weigel", 29 | "githubUsername": "codeworrior" 30 | }, 31 | { 32 | "name": "Andreas Kunz", 33 | "githubUsername": "akudev" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/dts-generator/src/checkDtslint/dtslintConfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6", "dom"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictFunctionTypes": true, 8 | "strictNullChecks": true, 9 | "types": [], 10 | "noEmit": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "files": ["index.d.ts", "openui5-tests.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/dts-generator/src/download-apijson.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { getLogger } from "@ui5/logger"; 4 | const log = getLogger("@ui5/dts-generator/download-apijson"); 5 | import esMain from "es-main"; 6 | import _ from "lodash"; 7 | const { pick } = _; 8 | import { 9 | getSAPUI5LibsMeta, 10 | getOpenUI5PossibleLibNames, 11 | expandTransitiveDeps, 12 | downloadApiJsonFiles, 13 | } from "./js-utils/ui5-metadata.js"; 14 | 15 | /** 16 | * Utility function that downloads the api.json files for the given OpenUI5 libraries in the given version. 17 | * Automatically also downloads the api.json files for transitive dependencies. 18 | * These files are needed for using the dts-generator on an own library. 19 | * 20 | * @param libs - the OpenUI5 libraries which are direct dependencies (usually sap.ui.core, but could be more) 21 | * @param version - the version of UI5 on which this library depends (example: 1.120.2) 22 | * @param targetDir - the directory into which the api.json files of the dependencies should be downloaded. 23 | * The directory must be non-existing or empty. Files that end with *api.json or *.dtsgenrc are ok, but will be deleted. 24 | * If no directory is given, "./temp/dependency-apijson" is used. 25 | * @public 26 | */ 27 | export async function download( 28 | libs: string[], 29 | version: string, 30 | targetDir = "./temp/dependency-apijson", 31 | ) { 32 | const ui5LibsMeta = await getSAPUI5LibsMeta(version); 33 | const possibleOpenUI5LibNames = await getOpenUI5PossibleLibNames(); 34 | const openUI5Meta = pick(ui5LibsMeta, possibleOpenUI5LibNames); 35 | const allDependentOpenUI5Libs = expandTransitiveDeps(libs, openUI5Meta); 36 | log.info("Transitive dependencies: " + allDependentOpenUI5Libs.join(", ")); 37 | 38 | //const inputSdkDir = resolve(__dirname, "..", "input-sdk"); 39 | await downloadApiJsonFiles(allDependentOpenUI5Libs, version, targetDir); 40 | log.info(`Wrote api.json files to: ${targetDir}`); 41 | } 42 | 43 | // CLI support for generation of (optionally) both globals and modules flavor in one go 44 | async function main() { 45 | const start = Date.now(); 46 | const { args } = await import("./utils/arguments-download-apijson.js"); 47 | 48 | const { libs, version, targetDir } = args; 49 | 50 | log.info(`Download api.json files for`); 51 | log.info(` libs: ${libs}`); 52 | log.info(` version: ${version}`); 53 | log.info(` targetDir: ${targetDir}`); 54 | log.info(``); 55 | 56 | await download( 57 | libs.split(",").map((lib) => lib.trim()), 58 | version, 59 | targetDir, 60 | ); 61 | 62 | const end = Date.now(); 63 | log.info( 64 | `Download completed in ${((end - start) / 1000).toFixed(1)} seconds.`, 65 | ); 66 | } 67 | 68 | // if called as CLI, parse arguments and trigger generation 69 | if (esMain(import.meta)) { 70 | main().then( 71 | () => { 72 | log.info(`Done.`); 73 | }, 74 | (err) => { 75 | log.error("An error occurred", err); 76 | process.exit(1); 77 | }, 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /packages/dts-generator/src/generate-from-paths.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from "@ui5/logger"; 2 | const log = getLogger("@ui5/dts-generator/runDTSGenerator"); 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | const readdir = fs.promises.readdir; 6 | import { generate } from "./generate.js"; 7 | 8 | async function findFiles(dir, extension) { 9 | if (dir == null) { 10 | return []; 11 | } 12 | const files = await readdir(dir).catch((err) => { 13 | if (err.code === "ENOENT") { 14 | return []; 15 | } 16 | log.error(`failed to read content of directory ${dir}:`, err); 17 | throw err; 18 | }); 19 | 20 | return files 21 | .filter((file) => file.endsWith(extension)) 22 | .map((file) => path.join(dir, file)); 23 | } 24 | 25 | const existingFile = (file) => (fs.existsSync(file) ? file : null); 26 | 27 | /** 28 | * The configuration for a generateFromPaths call. 29 | * @public 30 | */ 31 | export interface GenerateFromPathsConfig { 32 | /** 33 | * File path and name of the api.json file for the library for which the d.ts file should be generated. 34 | */ 35 | apiFile: string; 36 | 37 | /** 38 | * Directory where the api.json files are located for the libraries on which the currently to-be-built library depends. 39 | */ 40 | dependenciesApiPath: string; 41 | 42 | /** 43 | * Directory where the .dtsgenrc files for the libraries (current and dependencies) are located. 44 | */ 45 | directivesPath: string; 46 | 47 | /** 48 | * Directory where the d.ts files are located of the libraries on which the currently to-be-built library depends. 49 | * 50 | * This is meant for other libraries which belong to the same project and are built in the same build run, as opposed to external 51 | * libraries. E.g. when the OpenUI5 types are built, then the core library is built first and its resulting d.ts file is a dependency 52 | * for building the types of other libraries like sap.m. 53 | * 54 | * Only needed for the check. 55 | */ 56 | dependenciesDTSPathForCheck: string; 57 | 58 | /** 59 | * Comma-separated list of package names of the libraries on which the currently to-be-built types depends. 60 | * 61 | * This is meant for entire npm packages developed separately (often by others), not sibling libraries built in the same batch. 62 | * E.g. when a custom UI5 control library is built by an application team, then it usually depends on the OpenUI5 types because 63 | * those define the base classes like Control. 64 | * Setting this has the effect that for the TS compilation check, the `types` field of the package.json file will be set to the 65 | * respective package names and any other type packages are no longer considered. 66 | * 67 | * Only needed for the check. 68 | */ 69 | dependenciesTypePackagesForCheck?: string; 70 | 71 | /** 72 | * File path and name of the target d.ts file to write. 73 | */ 74 | targetFile: string; 75 | 76 | /** 77 | * Whether a test compilation should be executed. 78 | */ 79 | runCheckCompile?: boolean; 80 | 81 | /** 82 | * Whether *.d.ts files with globals should be generated instead of ES modules. 83 | */ 84 | generateGlobals?: boolean; 85 | 86 | /** 87 | * Whether the console output should be verbose. 88 | */ 89 | verbose?: boolean; 90 | } 91 | 92 | /** 93 | * Generate the *.d.ts file for a UI5 library from the given apiFile and directory paths of other needed resources. 94 | * 95 | * @param config - The configuration for a generateFromPaths call. 96 | * @returns 97 | * 98 | * @public 99 | */ 100 | export async function generateFromPaths(config: GenerateFromPathsConfig) { 101 | const success = await generate({ 102 | generateGlobals: config.generateGlobals, 103 | apiFile: config.apiFile, 104 | dependencyApiFiles: [ 105 | ...(await findFiles(config.dependenciesApiPath, ".json")), 106 | ], 107 | directiveFiles: [ 108 | existingFile(config.directivesPath), 109 | ...(await findFiles(config.dependenciesDTSPathForCheck, ".dtsgenrc")), 110 | ].filter(Boolean), 111 | targetFile: config.targetFile, 112 | dependencyDTSFilesForCheck: await findFiles( 113 | config.dependenciesDTSPathForCheck, 114 | "d.ts", 115 | ), 116 | dependenciesTypePackagesForCheck: config.dependenciesTypePackagesForCheck 117 | ? config.dependenciesTypePackagesForCheck.split(",") 118 | : [], 119 | runCheckCompile: config.runCheckCompile, 120 | }); 121 | return success; 122 | } 123 | -------------------------------------------------------------------------------- /packages/dts-generator/src/generate.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from "@ui5/logger"; 2 | const log = getLogger("@ui5/dts-generator/generate"); 3 | import * as path from "path"; 4 | import ts from "typescript"; 5 | import { generateFromObjects, Directives } from "./generate-from-objects.js"; 6 | import { default as checkCompile } from "./checkCompile/check-compile.js"; 7 | import { writeFileSafe, loadJSON } from "./utils/file-utils.js"; 8 | 9 | const loadedCache = new Map(); 10 | 11 | async function loadAPIJSON(file: string) { 12 | log.info(`load api.json from ${path.basename(file)}`); 13 | let apijson = loadedCache.get(file); 14 | if (apijson == null) { 15 | apijson = await loadJSON(file); 16 | loadedCache.set(file, apijson); 17 | } 18 | return JSON.parse(JSON.stringify(apijson)); // return a clone 19 | } 20 | 21 | /** 22 | * @param directivesPaths - Array of path names of directives files 23 | * @returns 24 | */ 25 | async function loadDirectives(directivesPaths: string[]) { 26 | const directives: Directives = { 27 | badSymbols: [], 28 | badMethods: [], 29 | badInterfaces: [], 30 | typeTyposMap: {}, 31 | namespacesToInterfaces: {}, 32 | forwardDeclarations: {}, 33 | fqnToIgnore: {}, 34 | overlays: {}, 35 | deprecatedEnumAliases: {}, 36 | }; 37 | 38 | function mergeDirectives(loadedDirectives: Directives) { 39 | Object.keys(loadedDirectives).forEach((key: keyof Directives) => { 40 | if (Array.isArray(directives[key])) { 41 | directives[key] = (directives[key] as Array).concat( 42 | loadedDirectives[key], 43 | ) as any; 44 | } else if (directives[key]) { 45 | Object.assign(directives[key], loadedDirectives[key]); 46 | } else { 47 | directives[key] = loadedDirectives[key] as any; 48 | } 49 | }); 50 | } 51 | 52 | await Promise.all( 53 | directivesPaths.map(async (file) => 54 | mergeDirectives((await loadJSON(file)) as Directives), 55 | ), 56 | ); 57 | 58 | log.verbose(`merged directives: ${JSON.stringify(directives, null, "\t")}`); 59 | return directives; 60 | } 61 | 62 | /** 63 | * The configuration for *.d.ts file generation from an existing api.json file. 64 | * 65 | * @public 66 | */ 67 | export type GenerateConfig = { 68 | /** 69 | * File path and name of the api.json file for the library for which the d.ts file should be generated. 70 | */ 71 | apiFile: string; 72 | 73 | /** 74 | * An array of file paths and names of the api.json files for the libraries on which the currently to-be-built library depends. 75 | */ 76 | dependencyApiFiles: string[]; 77 | 78 | /** 79 | * An array of file paths and names of the .dtsgenrc files for the libraries (current and dependencies). 80 | */ 81 | directiveFiles: string[]; 82 | 83 | /** 84 | * File path and name of the target d.ts file to write. 85 | */ 86 | targetFile: string; 87 | 88 | /** 89 | * An array of file paths and names of the d.ts files of the libraries on which the currently to-be-built library depends. 90 | * 91 | * This is meant for other libraries which belong to the same project and are built in the same build run, as opposed to external 92 | * libraries. E.g. when the OpenUI5 types are built, then the core library is built first and its resulting d.ts file is a dependency 93 | * for building the types of other libraries like sap.m. 94 | * 95 | * Only needed for the check. 96 | */ 97 | dependencyDTSFilesForCheck: string[]; 98 | 99 | /** 100 | * Array of package names of the libraries on which the currently to-be-built types depends. 101 | * 102 | * This is meant for entire npm packages developed separately (often by others), not sibling libraries built in the same batch. 103 | * E.g. when a custom UI5 control library is built by an application team, then it usually depends on the OpenUI5 types because 104 | * those define the base classes like Control. 105 | * Setting this has the effect that for the TS compilation check, the `types` field of the package.json file will be set to the 106 | * respective package names and any other type packages are no longer considered. 107 | * 108 | * Only needed for the check. 109 | */ 110 | dependenciesTypePackagesForCheck?: string[]; 111 | 112 | /** 113 | * Whether types for deprecated globals (instead of ES modules) should be generated. 114 | */ 115 | generateGlobals?: boolean; 116 | 117 | /** 118 | * Whether a test compilation should be executed. 119 | */ 120 | runCheckCompile?: boolean; 121 | 122 | /** 123 | * Path and name of a file into which the test compilation errors should be written. 124 | */ 125 | errorOutputFile?: string; 126 | }; 127 | 128 | /** 129 | * Generate the *.d.ts file content for a UI5 library. 130 | * 131 | * @returns a Promise which resolves with true if the generation and the optional test (enabled with the runCheckCompile parameter) was successful 132 | * 133 | * @public 134 | */ 135 | export async function generate({ 136 | apiFile, 137 | dependencyApiFiles, 138 | directiveFiles, 139 | targetFile, 140 | dependencyDTSFilesForCheck, 141 | dependenciesTypePackagesForCheck, 142 | generateGlobals, 143 | runCheckCompile, 144 | errorOutputFile, 145 | }: GenerateConfig): Promise { 146 | const start = Date.now(); 147 | const loadedDirectives = await loadDirectives(directiveFiles); 148 | 149 | const ownApiJson = await loadAPIJSON(apiFile); 150 | const dependenciesApiJsons = await Promise.all( 151 | dependencyApiFiles.map((apiFile) => loadAPIJSON(apiFile)), 152 | ); 153 | 154 | const dtsResult = await generateFromObjects({ 155 | apiObject: ownApiJson, 156 | directives: loadedDirectives, 157 | dependencyApiObjects: dependenciesApiJsons, 158 | generateGlobals, 159 | }); 160 | 161 | await writeFileSafe(targetFile, dtsResult.dtsText); 162 | 163 | if (runCheckCompile) { 164 | log.info( 165 | `running a test compile for ${[ 166 | ...dependencyDTSFilesForCheck, 167 | targetFile, 168 | ]}`, 169 | ); 170 | 171 | // set up the tsconfig for the test compile 172 | const tsOptions: ts.BuildOptions = { 173 | noEmit: true, 174 | noImplicitAny: true, 175 | strict: true, 176 | target: ts.ScriptTarget.ES2015, 177 | module: ts.ModuleKind.ES2015, 178 | lib: ["lib.es2015.d.ts", "lib.dom.d.ts"], 179 | }; 180 | 181 | // if type dependencies are set, use them 182 | if ( 183 | dependenciesTypePackagesForCheck && 184 | dependenciesTypePackagesForCheck.length > 0 185 | ) { 186 | tsOptions.types = dependenciesTypePackagesForCheck; 187 | } 188 | 189 | const success = checkCompile({ 190 | mainFile: targetFile, 191 | dependencyFiles: dependencyDTSFilesForCheck, 192 | tsOptions, 193 | errorOutputFile: errorOutputFile, 194 | }); 195 | const end = Date.now(); 196 | log.info( 197 | `Generation and check of ${path.basename(targetFile)} completed in ${( 198 | (end - start) / 199 | 1000 200 | ).toFixed(1)} seconds.`, 201 | ); 202 | return success; 203 | } else { 204 | const end = Date.now(); 205 | log.info( 206 | `Generation of ${path.basename(targetFile)} (using ${ 207 | generateGlobals ? "globals" : "modules" 208 | }) completed in ${((end - start) / 1000).toFixed(1)} seconds.`, 209 | ); 210 | return true; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /packages/dts-generator/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { getLogger } from "@ui5/logger"; 4 | const log = getLogger("@ui5/dts-generator/index"); 5 | import esMain from "es-main"; 6 | import { generateFromPaths } from "./generate-from-paths.js"; 7 | 8 | // re-exported APIs 9 | export { 10 | default as checkCompile, 11 | CheckCompileConfig, 12 | ScriptTarget, 13 | ModuleKind, 14 | ModuleResolutionKind, 15 | } from "./checkCompile/check-compile.js"; 16 | export { 17 | default as checkDtslint, 18 | CheckDtslintConfig, 19 | } from "./checkDtslint/check-dtslint.js"; 20 | export { ApiJSON } from "./types/api-json.js"; 21 | export { 22 | generateFromObjects, 23 | GenerateFromObjectsConfig, 24 | Directives, 25 | } from "./generate-from-objects.js"; 26 | export { generate, GenerateConfig } from "./generate.js"; 27 | export { 28 | generateFromPaths, 29 | GenerateFromPathsConfig, 30 | } from "./generate-from-paths.js"; 31 | export { download as downloadApiJson } from "./download-apijson.js"; 32 | 33 | // CLI support for generation of (optionally) both globals and modules flavor in one go 34 | async function main() { 35 | const start = Date.now(); 36 | const { args } = await import("./utils/arguments-index.js"); 37 | 38 | const { 39 | apiFile, 40 | dependenciesApiPath, 41 | dependenciesDTSPathForCheckForGlobals, 42 | dependenciesDTSPathForCheck, 43 | dependenciesTypePackagesForCheck, 44 | directivesPath, 45 | targetFileForGlobals, 46 | targetFile, 47 | verbose, 48 | skipCheckCompile, 49 | } = args; 50 | 51 | log.info(`Transform api.json to TypeScript definitions`); 52 | log.info(` own api.json file: ${apiFile}`); 53 | log.info(` own directives file: ${directivesPath || "(none)"}`); 54 | log.info(` dependency dir (api.json): ${dependenciesApiPath || "(none)"}`); 55 | log.info( 56 | ` dependency dir for globals (d.ts and directives): ${ 57 | dependenciesDTSPathForCheckForGlobals || "(none)" 58 | }`, 59 | ); 60 | log.info( 61 | ` dependency dir for modules (d.ts and directives): ${ 62 | dependenciesDTSPathForCheck || "(none)" 63 | }`, 64 | ); 65 | log.info( 66 | ` dependency type packages: ${ 67 | dependenciesTypePackagesForCheck || "(none)" 68 | }`, 69 | ); 70 | log.info(` target file for globals: ${targetFileForGlobals}`); 71 | log.info(` target file for modules: ${targetFile}`); 72 | log.info(` verbose: ${verbose}`); 73 | log.info(` skipCheckCompile: ${skipCheckCompile}`); 74 | log.info(``); 75 | 76 | const runCheckCompile = !skipCheckCompile; 77 | let success = true, 78 | successESM = true; 79 | 80 | if (targetFileForGlobals) { 81 | // if this required parameter for *globals* generation is given, do the globals generation 82 | success = await generateFromPaths({ 83 | generateGlobals: true, 84 | apiFile, 85 | dependenciesApiPath, 86 | directivesPath, 87 | dependenciesDTSPathForCheck: dependenciesDTSPathForCheckForGlobals, 88 | dependenciesTypePackagesForCheck, // would actually need to be a separate config for globals, but as they are deprecated we don't want to extend the API 89 | targetFile: targetFileForGlobals, 90 | runCheckCompile, 91 | verbose, 92 | }); 93 | } 94 | 95 | if (targetFile) { 96 | // if this required parameter for *modules* generation is given, do the modules generation 97 | successESM = await generateFromPaths({ 98 | generateGlobals: false, 99 | apiFile, 100 | dependenciesApiPath, 101 | directivesPath, 102 | dependenciesDTSPathForCheck, 103 | dependenciesTypePackagesForCheck, 104 | targetFile, 105 | runCheckCompile, 106 | verbose, 107 | }); 108 | } 109 | 110 | if (!success || !successESM) { 111 | throw new Error("TypeScript compilation failed, check log for errors"); 112 | } 113 | 114 | const end = Date.now(); 115 | log.info( 116 | `Generation completed in ${((end - start) / 1000).toFixed(1)} seconds.`, 117 | ); 118 | } 119 | 120 | // if called as CLI, parse arguments and trigger generation 121 | if (esMain(import.meta)) { 122 | main().then( 123 | () => { 124 | log.info(`Done.`); 125 | }, 126 | (err) => { 127 | log.error("An error occurred", err); 128 | process.exit(1); 129 | }, 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /packages/dts-generator/src/js-utils/write-url-to-file.js: -------------------------------------------------------------------------------- 1 | import fsextra from "fs-extra"; 2 | const { writeFile } = fsextra; 3 | import { getLogger } from "@ui5/logger"; 4 | const log = getLogger("@ui5/dts-generator/write-url-to-file"); 5 | 6 | /** 7 | * @param {string} url 8 | * @param {string} file 9 | * @returns {Promise} True if successful 10 | */ 11 | export default async function writeUrlToFile(url, file) { 12 | const fetch = (await import("node-fetch")).default; 13 | log.info(`fetching: ${url}`); 14 | const response = await fetch(url, { 15 | headers: { 16 | "User-Agent": "@ui5-dts-generator", 17 | }, 18 | }); 19 | if (!response.ok) { 20 | log.error(`error fetching from ${url}`); 21 | return false; 22 | } 23 | const text = await response.text(); 24 | if (text === "{}") { 25 | // These files don't add anything to the model but they return an error in strict mode 26 | log.info(`empty object returned from ${url}`); 27 | return true; 28 | } 29 | log.info(`writing: ${file}`); 30 | await writeFile(file, text); 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /packages/dts-generator/src/phases/ast-transform.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { 3 | AstNode, 4 | AstSymbol, 5 | Class, 6 | Enum, 7 | Interface, 8 | Namespace, 9 | UI5AstRoot, 10 | } from "../types/ast.js"; 11 | 12 | export function transformAst( 13 | ast: UI5AstRoot, 14 | symbolTable: { [key: string]: AstSymbol }, 15 | libraryName: string, 16 | ) { 17 | filterNonePublicApis(ast); 18 | 19 | if (libraryName === "sap.ui.core") { 20 | updateDefineArrayDepsTypes(symbolTable); 21 | } 22 | 23 | // adding the parent property must be done last after any other 24 | // ast transformations that add ast nodes 25 | addParentProp(ast); 26 | 27 | return ast; 28 | } 29 | 30 | function addParentProp(node: AstNode | UI5AstRoot) { 31 | _.forEach(node, (child: AstNode, key) => { 32 | // sub-node in the AST 33 | if (_.has(child, "kind") && key !== "parent") { 34 | addParentProp(child); 35 | child.parent = node as AstNode; 36 | } else if (_.isArray(child)) { 37 | _.forEach(child, (childElemNode) => { 38 | if (_.has(childElemNode, "kind") && key !== "parent") { 39 | addParentProp(childElemNode); 40 | childElemNode.parent = node; 41 | } 42 | }); 43 | } 44 | // This code assumes we never have a dictionary/map as a childNode. 45 | // Only direct children or children as elements in an array. 46 | }); 47 | } 48 | 49 | /** 50 | * @param ast 51 | */ 52 | function filterNonePublicApis(ast: UI5AstRoot) { 53 | filterNamespace(ast.topLevelNamespace); 54 | } 55 | 56 | /** 57 | * @param ast 58 | */ 59 | function filterNamespace(ast: Namespace) { 60 | ast.namespaces = ast.namespaces.filter(isPublic); 61 | ast.variables = ast.variables.filter(isPublic); 62 | ast.functions = ast.functions.filter(isPublic); 63 | ast.classes = ast.classes.filter(isPublic); 64 | ast.interfaces = ast.interfaces.filter(isPublic); 65 | ast.enums = ast.enums.filter(isPublic); 66 | 67 | _.forEach(ast.namespaces, filterNamespace); 68 | _.forEach(ast.classes, filterClass); 69 | _.forEach(ast.interfaces, filterInterface); 70 | _.forEach(ast.enums, filterEnum); 71 | } 72 | 73 | /** 74 | * @param ast 75 | */ 76 | function filterClass(ast: Class) { 77 | ast.fields = ast.fields.filter(isPublic); 78 | ast.methods = ast.methods.filter(isPublic); 79 | } 80 | 81 | /** 82 | * @param ast 83 | */ 84 | function filterInterface(ast: Interface) { 85 | ast.methods = ast.methods.filter(isPublic); 86 | } 87 | 88 | /** 89 | * @param ast 90 | */ 91 | function filterEnum(ast: Enum) { 92 | ast.values = ast.values.filter(isPublic); 93 | } 94 | 95 | function isPublic(ast: Symbol): boolean { 96 | return ( 97 | ast.visibility === "public" || 98 | // "protected" APIs are kind of public 99 | ast.visibility === "protected" || 100 | ast.visibility === undefined 101 | ); 102 | } 103 | 104 | /** 105 | * Modify the "aDependencies" parameter of the "sap.ui.define" and "sap.ui.require" 106 | * function (for all signature variants) to add code completion support for the known UI5 module names. 107 | * 108 | * TODO better introduce a dedicated type in UI5 for module names and replace that type in the generator 109 | * @param symbolTable 110 | */ 111 | function updateDefineArrayDepsTypes(symbolTable: { [key: string]: AstSymbol }) { 112 | const sapUiNs = symbolTable["sap.ui"] as Namespace; 113 | 114 | // sap.ui.define 115 | _.filter( 116 | sapUiNs && sapUiNs.functions, 117 | (func) => func.name === "define", 118 | ).forEach((defineFunction) => { 119 | const aDependenciesParam = _.find( 120 | defineFunction.parameters, 121 | (param) => param.name === "aDependencies", 122 | ); 123 | if (aDependenciesParam) { 124 | aDependenciesParam.type = { 125 | kind: "ArrayType", 126 | elementType: { 127 | kind: "NativeTSTypeExpression", 128 | type: "keyof IUI5DefineDependencyNames | (string & {IGNORE_ME?:never})", 129 | }, 130 | }; 131 | } 132 | }); 133 | 134 | // sap.ui.require 135 | _.filter( 136 | sapUiNs && sapUiNs.functions, 137 | (func) => func.name === "require", 138 | ).forEach((requireFunction) => { 139 | const vDependenciesParam = _.find( 140 | requireFunction.parameters, 141 | (param) => param.name === "vDependencies", 142 | ); 143 | if ( 144 | vDependenciesParam && 145 | vDependenciesParam.type && 146 | vDependenciesParam.type.kind === "UnionType" 147 | ) { 148 | vDependenciesParam.type.types.forEach((subType, index, unionTypes) => { 149 | if (subType && subType.kind === "ArrayType") { 150 | unionTypes[index] = { 151 | kind: "ArrayType", 152 | elementType: { 153 | kind: "NativeTSTypeExpression", 154 | type: "keyof IUI5DefineDependencyNames | (string & {IGNORE_ME?:never})", 155 | }, 156 | }; 157 | } 158 | }); 159 | } 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /packages/dts-generator/src/phases/post-process.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from "@ui5/logger"; 2 | const log = getLogger("@ui5/dts-generator/post-process"); 3 | import * as fs from "fs"; 4 | import prettier from "prettier"; 5 | const { format } = prettier; 6 | 7 | export async function postProcess( 8 | dtsResult: { library: string; dtsText: string }, 9 | options: { generateGlobals?: boolean }, 10 | ) { 11 | switch (dtsResult.library) { 12 | case "sap.ui.core": 13 | // TODO rather implement a "preamble" feature in the directives 14 | 15 | let preamble = fs 16 | .readFileSync( 17 | new URL("../resources/core-preamble.d.ts", import.meta.url), 18 | ) 19 | .toString(); 20 | 21 | if (options.generateGlobals) { 22 | preamble = preamble.replace( 23 | /Array/g, 24 | "sap.ui.core.Control[]", 25 | ); 26 | preamble = preamble.replace( 27 | /import\("sap\/ui\/core\/Control"\).default/g, 28 | "sap.ui.core.Control", 29 | ); 30 | } 31 | 32 | dtsResult.dtsText = await reformat(preamble + dtsResult.dtsText); 33 | break; 34 | 35 | case "sap.ui.export": 36 | // TODO rather manage "forbidden" names in the DTS generator 37 | // But how to know a good replacement then? 38 | // The Kyrillic 'o' was picked to have the same appearance in Intellisense tooltips 39 | dtsResult.dtsText = await reformat( 40 | dtsResult.dtsText.replace( 41 | "namespace export", 42 | "export { exp\u043ert as export }\nnamespace exp\u043ert", 43 | ), 44 | ); 45 | break; 46 | } 47 | } 48 | 49 | async function reformat(text: string): Promise { 50 | try { 51 | const formattedText = await format(text, { 52 | parser: "typescript", 53 | trailingComma: "es5", 54 | }); 55 | return formattedText; 56 | } catch (e) { 57 | // if we can't format successfully, at least we can manually inspect the un-formatted 58 | // definitions and find the error... 59 | log.error(e); 60 | return text; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/dts-generator/src/phases/symbols.ts: -------------------------------------------------------------------------------- 1 | import { AstSymbol, Namespace, SymbolTable, UI5AstRoot } from "../types/ast.js"; 2 | 3 | /** 4 | * @param ast 5 | * @return the resulting SymbolTable 6 | */ 7 | export function buildSymbolTable(ast: UI5AstRoot) { 8 | return buildNamespaceSymbols(ast.topLevelNamespace, ""); 9 | } 10 | 11 | /** 12 | * @param ast 13 | * @return the resulting SymbolTable 14 | */ 15 | function buildNamespaceSymbols(ast: Namespace, fqnPrefix: string): SymbolTable { 16 | const prefix = fqnPrefix ? fqnPrefix + "." + ast.name : ast.name; 17 | const nsSymbolEntry: { [prefix: string]: Namespace } = {}; 18 | nsSymbolEntry[prefix] = ast; 19 | 20 | const nestedNsSymsArr = ast.namespaces.map((nestedNs) => 21 | buildNamespaceSymbols(nestedNs, prefix), 22 | ); 23 | const nestedNsSyms = mergeSymbolTables.apply(null, nestedNsSymsArr); 24 | 25 | const classesSyms = buildGenericSymbols(ast.classes, prefix); 26 | const interfacesSyms = buildGenericSymbols(ast.interfaces, prefix); 27 | const enumsSyms = buildGenericSymbols(ast.enums, prefix); 28 | 29 | return mergeSymbolTables( 30 | nsSymbolEntry, 31 | nestedNsSyms, 32 | classesSyms, 33 | interfacesSyms, 34 | enumsSyms, 35 | ); 36 | } 37 | 38 | /** 39 | * @param symTables 40 | * @return the merged symbol table 41 | */ 42 | function mergeSymbolTables(...symTables: SymbolTable[]): SymbolTable { 43 | // TODO: add validations about duplicate names 44 | return Object.assign.apply(null, [{}].concat(symTables)); 45 | } 46 | 47 | /** 48 | * @param asts 49 | * @param prefix 50 | */ 51 | function buildGenericSymbols(asts: AstSymbol[], prefix: string) { 52 | const entries: { [name: string]: AstSymbol } = {}; 53 | asts.forEach((currAst) => { 54 | entries[prefix + "." + currAst.name] = currAst; 55 | }); 56 | 57 | return entries; 58 | } 59 | -------------------------------------------------------------------------------- /packages/dts-generator/src/runCheck.ts: -------------------------------------------------------------------------------- 1 | import { getLogger, setLogLevel } from "@ui5/logger"; 2 | const log = getLogger("@ui5/dts-generator/runCheck"); 3 | import esMain from "es-main"; 4 | 5 | import * as path from "path"; 6 | import { promises as fsp } from "fs"; 7 | const readdir = fsp.readdir; 8 | 9 | import { 10 | checkCompile, 11 | checkDtslint as checkDtslintApi, 12 | ScriptTarget, 13 | ModuleKind, 14 | ModuleResolutionKind, 15 | } from "./index.js"; 16 | 17 | async function findFiles(dir: string, extension: string) { 18 | if (dir == null) { 19 | return []; 20 | } 21 | const files = await readdir(dir).catch((err) => { 22 | if (err.code === "ENOENT") { 23 | return []; 24 | } 25 | log.error(`failed to read content of directory ${dir}:`, err); 26 | throw err; 27 | }); 28 | 29 | return files 30 | .filter((file) => file.endsWith(extension)) 31 | .map((file) => path.join(dir, file)); 32 | } 33 | 34 | // CLI support for checking types (both, checkCompile and checkDtslint, if requested) 35 | async function main() { 36 | const start = Date.now(); 37 | const { args } = await import("./utils/arguments-runCheck.js"); 38 | 39 | const { dtsDir, checkDtslint, verbose } = args; 40 | 41 | setLogLevel(verbose ? "verbose" : "info"); 42 | 43 | log.info(`Run a check on TypeScript definitions`); 44 | log.info(` d.ts dir: ${dtsDir}`); 45 | log.info(` verbose: ${verbose}`); 46 | log.info(` checkDtslint: ${checkDtslint}`); 47 | log.info(``); 48 | 49 | const dtsFiles = await findFiles(dtsDir, "d.ts"); 50 | 51 | log.verbose(`Running a compile check for ${dtsFiles}`); 52 | const success = checkCompile({ 53 | dependencyFiles: dtsFiles, 54 | tsOptions: { 55 | noEmit: true, 56 | noImplicitAny: true, 57 | strict: true, 58 | target: ScriptTarget.ES2015, 59 | module: ModuleKind.ES2015, 60 | moduleResolution: ModuleResolutionKind.NodeJs, 61 | }, 62 | }); 63 | 64 | if (!success) { 65 | throw new Error("TypeScript compilation failed, check log for errors"); 66 | } 67 | 68 | if (checkDtslint) { 69 | log.verbose(`Running a dtslint check for ${dtsFiles}`); 70 | checkDtslintApi(dtsDir); 71 | } 72 | 73 | const end = Date.now(); 74 | log.info(`Check completed in ${((end - start) / 1000).toFixed(1)} seconds.`); 75 | } 76 | 77 | // if called as CLI, parse arguments and trigger generation 78 | if (esMain(import.meta)) { 79 | main().then( 80 | () => { 81 | log.info(`Done.`); 82 | }, 83 | (err) => { 84 | log.error("An error occurred", err); 85 | process.exit(1); 86 | }, 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /packages/dts-generator/src/types/ast.d.ts: -------------------------------------------------------------------------------- 1 | export type UI5Visibility = "restricted" | "protected" | "public" | "private"; 2 | 3 | export type Kinds = 4 | | "Module" 5 | | "Export" 6 | | "FunctionDesc" 7 | | "Parameter" 8 | | "TypeReference" 9 | | "UnionType" 10 | | "Class" 11 | | "ReturnDesc" 12 | | "TypeLiteral" 13 | | "ArrayType" 14 | | "LiteralType" 15 | | "TypeAliasDeclaration" 16 | | "Interface" 17 | | "Enum" 18 | | "Variable" 19 | | "DeprecatedDesc" 20 | | "Namespace" 21 | | "ExperimentalDesc" 22 | | "TypeParameter" 23 | | "FunctionType" 24 | | "NativeTSTypeExpression" 25 | | "Import" 26 | | "Property"; 27 | 28 | export interface UI5AstRoot { 29 | version: string; 30 | topLevelNamespace: Namespace; 31 | modules: Module[]; 32 | } 33 | 34 | export interface AstNode { 35 | kind: Kinds; 36 | parent?: AstNode; 37 | } 38 | 39 | export interface AstSymbol extends AstNode { 40 | name: string; 41 | visibility: UI5Visibility; 42 | } 43 | 44 | // Symbols 45 | 46 | export interface Namespace extends AstSymbol, UI5JSDocs { 47 | kind: "Namespace"; 48 | namespaces: Namespace[]; 49 | variables: Variable[]; 50 | functions: FunctionDesc[]; 51 | classes: Class[]; 52 | interfaces: Interface[]; 53 | enums: Enum[]; 54 | typedefs: TypeAliasDeclaration[]; 55 | export: boolean; 56 | } 57 | 58 | export interface Interface extends AstSymbol, UI5JSDocs { 59 | kind: "Interface"; 60 | extends: string[]; 61 | methods: FunctionDesc[]; 62 | props: Variable[]; // TODO: a bit odd, but changed from "Property" based on the actual implementation 63 | isStaticObject?: boolean; 64 | fqname?: string; 65 | } 66 | 67 | export interface Class extends AstSymbol, UI5JSDocs { 68 | kind: "Class"; 69 | typeParameters?: TypeParameter[]; 70 | extends: string; 71 | implements: string[]; 72 | implementsFQName: string[]; 73 | constructors: FunctionDesc[]; 74 | fields: Variable[]; 75 | methods: FunctionDesc[]; 76 | isAbstract: boolean; 77 | isUI5Control?: boolean; 78 | } 79 | 80 | export interface Enum extends AstSymbol, UI5JSDocs { 81 | kind: "Enum"; 82 | values: VariableWithValue[]; 83 | withValues: true; 84 | isLibraryEnum: boolean; 85 | /** 86 | * When set, this enum is a deprecated alias for another enum whose name is given by this property 87 | */ 88 | deprecatedAliasFor?: string; 89 | } 90 | 91 | // Other Nodes 92 | 93 | export interface Module extends UI5JSDocs { 94 | // TODO: this has been reconstructed from the implementation, but should be re-checked 95 | kind: "Module"; 96 | name: string; 97 | imports: Import[]; 98 | exports: Export[]; 99 | namespaces: Namespace[]; 100 | } 101 | 102 | export interface Import extends AstNode { 103 | // TODO: reconstructed from code 104 | kind: "Import"; 105 | mappings: { [importNamespace: string]: string }; 106 | module: string; 107 | } 108 | 109 | export interface Export extends AstNode { 110 | kind: "Export"; 111 | export: boolean; 112 | asDefault: boolean; 113 | expression: Expression; 114 | parent?: Module; // TODO: re-check 115 | } 116 | 117 | export interface TypeAliasDeclaration extends AstNode, UI5JSDocs { 118 | // TODO: this has been reconstructed from the implementation, but should be re-checked 119 | kind: "TypeAliasDeclaration"; 120 | name: string; 121 | typeParameters?: TypeParameter[]; 122 | properties?: Property[]; 123 | type: Type; // TODO: re-check 124 | } 125 | 126 | export interface Variable extends AstNode, UI5JSDocs { 127 | kind: "Variable"; 128 | name: string; 129 | static?: boolean; 130 | type: Type; 131 | visibility: UI5Visibility; 132 | readonly?: boolean; 133 | optional: boolean; 134 | } 135 | 136 | export interface VariableWithValue extends Variable { 137 | value: string | number; 138 | } 139 | 140 | export interface FunctionDesc extends AstNode, UI5JSDocs { 141 | kind: "FunctionDesc"; 142 | name: string; 143 | static?: boolean; 144 | overwrite?: boolean; 145 | typeParameters?: TypeParameter[]; 146 | parameters: Parameter[]; 147 | genericType?: NativeTSTypeExpression; 148 | returns: ReturnDesc; 149 | // descriptions of potential errors being thrown 150 | throws?: { type?: string; description?: string }[]; 151 | visibility: UI5Visibility; 152 | optional: boolean; 153 | } 154 | 155 | export interface Property extends AstNode, UI5JSDocs { 156 | kind: "Property"; 157 | name: string; 158 | type: Type; 159 | optional: boolean; 160 | } 161 | 162 | export interface TypeParameter extends UI5JSDocs { 163 | kind: "TypeParameter"; 164 | name: string; 165 | constraint?: Type; 166 | default?: Type; 167 | } 168 | 169 | export interface Parameter extends UI5JSDocs { 170 | kind: "Parameter"; 171 | name: string; 172 | type: Type; 173 | defaultValue?: any; 174 | optional?: boolean; 175 | omissible?: boolean; 176 | repeatable?: boolean; 177 | } 178 | 179 | export interface Description { 180 | description?: string; 181 | } 182 | export interface ReturnDesc extends Description { 183 | kind: "ReturnDesc"; 184 | type?: Type; 185 | } 186 | 187 | export interface DeprecatedDesc extends Description { 188 | kind: "DeprecatedDesc"; 189 | since: string; 190 | } 191 | 192 | export interface ExperimentalDesc extends Description { 193 | kind: "ExperimentalDesc"; 194 | since: string; 195 | } 196 | 197 | // Types 198 | 199 | export type Type = 200 | | TypeReference 201 | | TypeLiteral 202 | | UnionType 203 | | IntersectionType 204 | | ArrayType 205 | | FunctionType 206 | | LiteralType 207 | | NativeTSTypeExpression; 208 | 209 | export interface TypeReference { 210 | kind: "TypeReference"; 211 | typeName: string; 212 | nullable?: boolean; 213 | typeArguments?: Type[]; 214 | isStandardEnum?: boolean; // only set when generating esm types 215 | } 216 | 217 | export interface TypeLiteral { 218 | kind: "TypeLiteral"; 219 | members: Parameter[]; 220 | } 221 | 222 | export interface UnionType { 223 | kind: "UnionType"; 224 | types: Type[]; 225 | } 226 | 227 | export interface IntersectionType { 228 | kind: "IntersectionType"; 229 | types: Type[]; 230 | } 231 | 232 | export interface ArrayType { 233 | kind: "ArrayType"; 234 | elementType: Type; 235 | } 236 | 237 | export interface FunctionType { 238 | kind: "FunctionType"; 239 | parameters: Parameter[]; 240 | typeParameters?: TypeParameter[]; 241 | type?: Type; 242 | isConstructor?: boolean; 243 | } 244 | 245 | export interface LiteralType { 246 | kind: "LiteralType"; 247 | literal: string; 248 | } 249 | 250 | export interface NativeTSTypeExpression { 251 | kind: "NativeTSTypeExpression"; 252 | type: string; 253 | } 254 | 255 | export type Expression = 256 | | Class 257 | | FunctionDesc 258 | | TypeAliasDeclaration 259 | | Interface 260 | | Enum 261 | | Variable 262 | | Namespace; // TODO: just listed by debugging 263 | 264 | //export type AstNode = AstSymbol | Variable | FunctionDesc | Import | Export | TypeAliasDeclaration; // TODO: or all others? 265 | 266 | export type SymbolTable = { [id: string]: AstSymbol }; 267 | 268 | export interface UI5JSDocs { 269 | description?: string; 270 | since?: string; 271 | deprecated?: DeprecatedDesc; 272 | experimental?: ExperimentalDesc; 273 | isProtected?: boolean; // "protected" is a reserved word 274 | additionalDocs?: string[]; 275 | } 276 | -------------------------------------------------------------------------------- /packages/dts-generator/src/types/ui5-logger-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@ui5/logger/Logger" { 2 | /** 3 | * Standard logging module for UI5 Tooling and extensions. 4 | *

5 | * Emits `ui5.log` events on the [`process`]{@link https://nodejs.org/api/process.html} object, 6 | * which can be handled by dedicated writers, 7 | * like [@ui5/logger/writers/Console]{@link @ui5/logger/writers/Console}. 8 | *

9 | * If no listener is attached to an event, messages are written directly to the `process.stderr` stream. 10 | */ 11 | export default class Logger { 12 | /** 13 | * Available log levels, ordered by priority: 14 | *
15 | *
    16 | *
  1. silly
  2. 17 | *
  3. verbose
  4. 18 | *
  5. perf
  6. 19 | *
  7. info (default)
  8. 20 | *
  9. warn
  10. 21 | *
  11. error
  12. 22 | *
  13. silent
  14. 23 | *
24 | * 25 | * Log level `silent` is special in the sense that no messages can be submitted with that level. 26 | * It can be used to suppress all logging. 27 | */ 28 | static LOG_LEVELS: string[]; 29 | 30 | /** 31 | * Event name used for emitting new log-message event on the 32 | * [`process`]{@link https://nodejs.org/api/process.html} object 33 | */ 34 | static LOG_EVENT_NAME: string; 35 | 36 | /** 37 | * Sets the standard log level. 38 | *
39 | * Example: Setting it to `perf` would suppress all `silly` and `verbose` 40 | * logging, and only show `perf`, `info`, `warn` and `error` logs. 41 | * 42 | * @param levelName New log level 43 | */ 44 | static setLevel(levelName: string); 45 | 46 | /** 47 | * Gets the current log level 48 | * 49 | * @returns The current log level. Defaults to `info` 50 | */ 51 | static getLevel(): string; 52 | 53 | /** 54 | * Tests whether the provided log level is enabled by the current log level 55 | * 56 | * @param levelName Log level to test 57 | * @returns True if the provided level is enabled 58 | */ 59 | static isLevelEnabled(levelName: string): boolean; 60 | 61 | /** 62 | * @param moduleName Identifier for messages created by this logger. 63 | * Example: `module:submodule:Class` 64 | */ 65 | constructor(moduleName: string); 66 | 67 | /** 68 | * Tests whether the provided log level is enabled by the current log level 69 | * 70 | * @param levelName Log level to test 71 | * @returns True if the provided level is enabled 72 | */ 73 | isLevelEnabled(levelName: string): boolean; 74 | 75 | /** 76 | * Create a log entry with the `silly` level 77 | * 78 | * @param message Messages to log. An automatic string conversion is applied if necessary 79 | */ 80 | silly(...message: any); 81 | 82 | /** 83 | * Create a log entry with the `verbose` level 84 | * 85 | * @param message Messages to log. An automatic string conversion is applied if necessary 86 | */ 87 | verbose(...message: any): void; 88 | 89 | /** 90 | * Create a log entry with the `perf` level 91 | * 92 | * @param message Messages to log. An automatic string conversion is applied if necessary 93 | */ 94 | perf(...message: any): void; 95 | 96 | /** 97 | * Create a log entry with the `info` level 98 | * 99 | * @param message Messages to log. An automatic string conversion is applied if necessary 100 | */ 101 | info(...message: any): void; 102 | 103 | /** 104 | * Create a log entry with the `warn` level 105 | * 106 | * @param message Messages to log. An automatic string conversion is applied if necessary 107 | */ 108 | warn(...message: any): void; 109 | 110 | /** 111 | * Create a log entry with the `error` level 112 | * 113 | * @param message Messages to log. An automatic string conversion is applied if necessary 114 | */ 115 | error(...message: any): void; 116 | } 117 | } 118 | 119 | /** 120 | * Interface for the UI5 Tooling logging module 121 | */ 122 | declare module "@ui5/logger" { 123 | import Logger from "@ui5/logger/Logger"; 124 | 125 | /** 126 | * Convenience function to create an instance of [@ui5/logger/Logger]{@link @ui5/logger/Logger} 127 | * 128 | * @param moduleName Identifier for messages created by the logger. 129 | * Example: `module:submodule:Class` 130 | * @returns 131 | */ 132 | export function getLogger(moduleName: string): Logger; 133 | 134 | /** 135 | * Tests whether the provided log level is enabled by the current log level 136 | * 137 | * @param levelName Log level to test 138 | * @returns True if the provided level is enabled 139 | */ 140 | export const isLogLevelEnabled: typeof Logger.isLevelEnabled; 141 | 142 | /** 143 | * Sets the standard log level. 144 | * 145 | * Example: Setting it to `perf` would suppress all `silly` and `verbose` 146 | * logging, and only show `perf`, `info`, `warn` and `error` logs. 147 | * 148 | * @param levelName New log level 149 | */ 150 | export const setLogLevel: typeof Logger.setLevel; 151 | } 152 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/arguments-download-apijson.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentParser } from "argparse"; 2 | 3 | export const args = (() => { 4 | const parser = new ArgumentParser({ 5 | description: 6 | "ui5-download-apijson: download the api.json files of library dependencies.", 7 | }); 8 | 9 | parser.add_argument("libs", { 10 | help: "Comma-separated list of library names for which the api.json files shall be downloaded, like: sap.ui.core,sap.m", 11 | }); 12 | parser.add_argument("version", { 13 | help: "The UI5 version for which the api.json files shall be downloaded (this is the version on which the custom library depends). This version must be available on the OpenUI5 CDN. Like: 1.120.2", 14 | }); 15 | parser.add_argument("--targetDir", { 16 | help: "The directory into which the api.json files shall be downloaded. Optional; if not given, the following path is used: ./temp/dependency-apijson", 17 | }); 18 | 19 | const args = parser.parse_args(); 20 | return args; 21 | })(); 22 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/arguments-index.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentParser } from "argparse"; 2 | 3 | export const args = (() => { 4 | const parser = new ArgumentParser({ 5 | description: 6 | "@ui5/dts-generator: Generate the *.d.ts type definition file for a UI5 library.", 7 | }); 8 | 9 | parser.add_argument("apiFile", { 10 | help: "File path and name of the api.json file for the library for which the d.ts file should be generated.", 11 | }); 12 | parser.add_argument("--dependenciesApiPath", { 13 | help: "Directory where the api.json files are located for the libraries on which the currently to-be-built library depends.", 14 | }); 15 | parser.add_argument("--dependenciesDTSPathForCheck", { 16 | help: 17 | "Directory where the d.ts files are located of the libraries on which the currently to-be-built library depends. Typically used for" + 18 | " other UI5 libraries for which types are being generated in the same build run. Only needed for the check.", 19 | }); 20 | parser.add_argument("--dependenciesTypePackagesForCheck", { 21 | help: 22 | "Comma-separated list of package names of the libraries on which the currently to-be-built types depends. This is meant for entire npm packages" + 23 | " developed separately (often by others), not sibling libraries built in the same generation run. E.g. when a custom UI5 control library is built by an" + 24 | " application team, then it usually depends on the OpenUI5 types because those define the base classes like Control. Setting this has the effect" + 25 | " that for the TS compilation check, the `types` field of the package.json file will be set to the respective package names and any other type packages" + 26 | " are no longer considered. Only needed for the check.", 27 | }); 28 | parser.add_argument("--directivesPath", { 29 | help: "Directory where the .dtsgenrc files for the libraries (current and dependencies) are located.", 30 | }); 31 | parser.add_argument("--targetFile", { 32 | help: "File path and name of the target d.ts file to write.", 33 | required: true, 34 | }); 35 | parser.add_argument("--verbose", { 36 | help: "Set when the console output should be verbose.", 37 | action: "store_true", 38 | }); 39 | parser.add_argument("--skipCheckCompile", { 40 | help: "Set when the test compilation should be skipped.", 41 | action: "store_true", 42 | }); 43 | parser.add_argument("--dependenciesDTSPathForCheckForGlobals", { 44 | help: "Directory where the d.ts files (using globals, not ES modules) are located of the libraries on which the currently to-be-built library depends. Only needed when globals are generated and the check is run.", 45 | }); 46 | parser.add_argument("--targetFileForGlobals", { 47 | help: "File path and name of the target d.ts file to write for the type definitions with globals (not ES modules). Only needed when globals should be generated.", 48 | }); 49 | 50 | const args = parser.parse_args(); 51 | return args; 52 | })(); 53 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/arguments-runCheck.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentParser } from "argparse"; 2 | 3 | export const args = (() => { 4 | const parser = new ArgumentParser({ 5 | description: 6 | "@ui5/dts-generator runCheck: check *.d.ts type definition files.", 7 | }); 8 | 9 | parser.add_argument("dtsDir", { 10 | help: "File path and name of the api.json file for the library for which the d.ts file should be generated.", 11 | }); 12 | parser.add_argument("--verbose", { 13 | help: "Set when the console output should be verbose.", 14 | action: "store_true", 15 | }); 16 | parser.add_argument("--checkDtslint", { 17 | help: "Set when the dtslint check should be executed.", 18 | action: "store_true", 19 | }); 20 | 21 | const args = parser.parse_args(); 22 | return args; 23 | })(); 24 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/ast-utils.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { AstNode, AstSymbol } from "../types/ast.js"; 3 | 4 | export function getFqn(ast: AstNode) { 5 | const hierarchy = [ast]; 6 | let currAncestor = ast.parent; 7 | while (currAncestor !== undefined) { 8 | hierarchy.push(currAncestor); 9 | currAncestor = currAncestor.parent; 10 | } 11 | 12 | hierarchy.reverse(); 13 | const fqnParts = _.map(hierarchy, "name"); 14 | return _.drop(fqnParts).join("."); 15 | } 16 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/base-utils.ts: -------------------------------------------------------------------------------- 1 | export function splitName(name: string, separator = ".") { 2 | const p = name.lastIndexOf(separator); 3 | return p < 0 ? ["", name] : [name.slice(0, p), name.slice(p + 1)]; 4 | } 5 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from "fs"; 2 | const { mkdir, readFile, writeFile } = fsp; 3 | import * as path from "path"; 4 | import stripJsonComments from "strip-json-comments"; 5 | 6 | export async function writeFileSafe(file: string, content: string) { 7 | await mkdir(path.dirname(file), { recursive: true }); 8 | return writeFile(file, content, "utf8"); 9 | } 10 | 11 | export async function loadJSON(file: string): Promise { 12 | let content = await readFile(file, "utf8"); 13 | if (file.endsWith(".dtsgenrc")) { 14 | // allow comments in .dtsgenrc files 15 | content = stripJsonComments(String(content)); 16 | } 17 | return JSON.parse(content); 18 | } 19 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/runtime-checks.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | const commonIrrelevantProps = [ 4 | // JSDoc Related 5 | "since", 6 | "description", 7 | "deprecated", 8 | "references", 9 | "experimental", 10 | // TODO: do we care about "examples" in JSDocs or do we ignore them? 11 | "examples", 12 | 13 | // Common 14 | "resource", 15 | "module", 16 | "export", 17 | "kind", 18 | "ui5-metadata", 19 | "ui5-metamodel", 20 | "static", 21 | "visibility", 22 | "allowedFor", 23 | "tsSkip", 24 | ]; 25 | 26 | export function assertKnownProps(expectedPropNames: string[], obj: object) { 27 | const objProps = _.keys(obj); 28 | const allExpectedPropNames = expectedPropNames.concat(commonIrrelevantProps); 29 | const unexpectedProps = objProps.filter( 30 | (x) => !allExpectedPropNames.includes(x), 31 | ); 32 | 33 | if (_.isEmpty(unexpectedProps) === false) { 34 | throw Error(`Unexpected Properties: ` + JSON.stringify(unexpectedProps)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/dts-generator/src/utils/ts-ast-type-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Type, 3 | ArrayType, 4 | FunctionType, 5 | LiteralType, 6 | TypeReference, 7 | UnionType, 8 | TypeLiteral, 9 | Parameter, 10 | } from "../types/ast.js"; 11 | 12 | /** 13 | * Standard type mappings that have to be applied to the API of any UI5 library. 14 | */ 15 | const STANDARD_TYPE_MAPPINGS: { [nonStandardTypeName: string]: string } = { 16 | // JSDoc uses the asterisk as synonym for 'any', so do some developers 17 | // (should be normalized in api.json already) 18 | "*": "any", 19 | 20 | // UI5 prefers 'int' and 'float' instead of 'number' to describe the value range 21 | // "long" and "double" are also used sometimes, although not (yet) standardized by UI5 22 | long: "int", 23 | double: "float", 24 | 25 | // TypeScript requires a full signature or type 'Function' 26 | function: "Function", 27 | 28 | // typos etc. 29 | "jQuery.promise": "jQuery.Promise", 30 | "jQuery.Deferred.promise": "jQuery.Promise", 31 | }; 32 | 33 | // generic types that require type parameters 34 | const DEFAULT_TYPE_ARGUMENTS: { 35 | [typeName: string]: { typeName: string; typeArguments: string[] }; 36 | } = { 37 | array: { 38 | typeName: "Array", 39 | typeArguments: ["any"], 40 | }, 41 | Array: { 42 | typeName: "Array", 43 | typeArguments: ["any"], 44 | }, 45 | Promise: { 46 | typeName: "Promise", 47 | typeArguments: ["any"], 48 | }, 49 | Set: { 50 | typeName: "Set", 51 | typeArguments: ["any"], 52 | }, 53 | Map: { 54 | typeName: "Map", 55 | typeArguments: ["any", "any"], 56 | }, 57 | // a legacy type name (should be fixed in sources) 58 | // (should be normalized in api.json already) 59 | map: { 60 | typeName: "Object", 61 | typeArguments: ["string", "any"], 62 | }, 63 | }; 64 | 65 | export class TSASTTypeBuilder { 66 | literal(str: string): LiteralType { 67 | return { 68 | kind: "LiteralType", 69 | literal: str, 70 | }; 71 | } 72 | simpleType(type: string): TypeReference { 73 | return { 74 | kind: "TypeReference", 75 | typeName: type, 76 | // no typeArguments 77 | }; 78 | } 79 | array(componentType: Type): ArrayType { 80 | return { 81 | kind: "ArrayType", 82 | elementType: componentType, 83 | }; 84 | } 85 | object(keyType: Type, valueType: Type): TypeReference { 86 | return { 87 | kind: "TypeReference", 88 | typeName: "Record", 89 | typeArguments: [keyType, valueType], 90 | }; 91 | } 92 | set(elementType: Type): TypeReference { 93 | // NOTE: this branch is never hit in the debugger 94 | return { 95 | kind: "TypeReference", 96 | typeName: "Set", 97 | typeArguments: [elementType], 98 | }; 99 | } 100 | promise(fulfillmentType: Type): TypeReference { 101 | return { 102 | kind: "TypeReference", 103 | typeName: "Promise", 104 | typeArguments: [fulfillmentType], 105 | }; 106 | } 107 | function( 108 | paramTypes: Type[], 109 | returnType: Type, 110 | thisType: Type, 111 | constructorType: Type, 112 | ): FunctionType { 113 | const parameters: Parameter[] = paramTypes.map((param, idx) => ({ 114 | kind: "Parameter", 115 | name: "p" + (idx + 1), // JSDoc function types don't allow parameter names -> generate names 116 | type: param, 117 | optional: (param as any).optional, 118 | })); 119 | if (thisType != null) { 120 | // for TS, a 'this' type is specified as the first parameter type of a function 121 | parameters.unshift({ 122 | kind: "Parameter", 123 | name: "this", 124 | type: thisType, 125 | }); 126 | } 127 | return { 128 | kind: "FunctionType", 129 | parameters, 130 | type: constructorType ?? returnType, 131 | isConstructor: constructorType != null, 132 | }; 133 | } 134 | structure(structure: { 135 | [propName: string]: { type: Type; optional: boolean }; 136 | }): TypeLiteral { 137 | return { 138 | kind: "TypeLiteral", 139 | members: Object.entries(structure).map(([name, { type, optional }]) => ({ 140 | name, 141 | type, 142 | optional, 143 | kind: "Parameter", 144 | })), 145 | }; 146 | } 147 | union(types: Type[]): UnionType { 148 | return { 149 | kind: "UnionType", 150 | types, 151 | }; 152 | } 153 | synthetic(type: Type) { 154 | ( 155 | type as any 156 | ) /* NOTE: this branch is never hit in the debugger */.synthetic = true; 157 | return type; 158 | } 159 | nullable(type: TypeReference) { 160 | type.nullable = true; 161 | return type; 162 | } 163 | mandatory(type: Type) { 164 | ( 165 | type as any 166 | ) /* NOTE: this branch is never hit in the debugger */.mandatory = true; 167 | return type; 168 | } 169 | optional(type: Type) { 170 | ( 171 | type as any 172 | ) /* NOTE: this branch is never hit in the debugger */.optional = true; 173 | } 174 | repeatable(type: Type) { 175 | ( 176 | type as any 177 | ) /* NOTE: this branch is never hit in the debugger */.repeatable = true; 178 | return type; 179 | } 180 | typeApplication(type: TypeReference, templateTypes: Type[]): TypeReference { 181 | return { 182 | kind: "TypeReference", 183 | typeName: type.typeName, 184 | typeArguments: templateTypes, 185 | }; 186 | } 187 | normalizeType(type: Type) { 188 | if (type.kind === "TypeReference") { 189 | if (DEFAULT_TYPE_ARGUMENTS[type.typeName]) { 190 | const replacement = DEFAULT_TYPE_ARGUMENTS[type.typeName]; 191 | if ( 192 | replacement.typeName === "Array" && 193 | replacement.typeArguments && 194 | replacement.typeArguments.length == 1 195 | ) { 196 | (type as unknown as ArrayType).kind = "ArrayType"; 197 | (type as unknown as ArrayType).elementType = this.simpleType( 198 | replacement.typeArguments[0], 199 | ); 200 | } else { 201 | type.typeName = replacement.typeName || type.typeName; 202 | if (replacement.typeArguments) { 203 | type.typeArguments = type.typeArguments || []; 204 | replacement.typeArguments.forEach((defaultType, index) => { 205 | if (type.typeArguments[index] == null) { 206 | type.typeArguments[index] = this.simpleType(defaultType); 207 | } 208 | }); 209 | } 210 | } 211 | } 212 | if (STANDARD_TYPE_MAPPINGS[type.typeName]) { 213 | type.typeName = STANDARD_TYPE_MAPPINGS[type.typeName]; 214 | } 215 | } else if (type.kind === "ArrayType") { 216 | type.elementType = this.normalizeType(type.elementType); 217 | } 218 | return type; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /packages/dts-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "moduleResolution": "Node16", 5 | "target": "es2022", 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "noImplicitAny": false, 11 | "resolveJsonModule": true, 12 | "allowJs": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": [ 16 | "./dist/**/*", 17 | "./node_modules/**/*", 18 | "./src/checkDtslint/dtslintConfig/openui5-tests.ts", 19 | "./src/checkDtslint/dtslintConfig/tsconfig.json", 20 | "./src/resources/core-preamble.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | ], 12 | parser: "@typescript-eslint/parser", 13 | parserOptions: { 14 | project: [ 15 | "./tsconfig.json", 16 | "./tsconfig-testcontrol.json", 17 | "./tsconfig-tests.json", 18 | ], 19 | tsconfigRootDir: __dirname, 20 | sourceType: "module", 21 | }, 22 | plugins: ["@typescript-eslint"], 23 | rules: { 24 | "@typescript-eslint/ban-ts-comment": [ 25 | "error", 26 | { 27 | "ts-ignore": "allow-with-description", 28 | minimumDescriptionLength: 10, 29 | }, 30 | ], 31 | }, 32 | ignorePatterns: [ 33 | ".eslintrc.js", 34 | "someFile.js", 35 | "*.gen.d.ts", 36 | "src/test/samples/sampleWebComponent/**/*", 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/ts-interface-generator/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Development Notes 2 | 3 | This file keeps internal information for the development of this tool. 4 | 5 | ### Structure 6 | 7 | The generation consists of the following parts: 8 | 9 | - Initializing the TypeScript watch mode with the given or found tsconfig file, so the same type world is known as during normal compilation 10 | - Whenever the watch mode detects file changes (or exactly once on startup in non-watch mode), the interface generation is triggered 11 | - A huge map of all UI5 global names (`allKnownGlobals`) is created which contains information in which module the respective entity resides (important to decide whether a certain enum is in a module of its own or inside a library module) 12 | - All real source files in this list (i.e. not type definitions) are checked for definitions of classes which are derived from `sap.ui.base.ManagedObject` 13 | - For each such class definition, a TypeScript AST is created which represents a file with an interface definition containing all UI5 accessor methods for the properties, aggregations etc. in the class. It also contains the required imports. The interface has the same name as the class to which it is related, so the TypeScript merging can happen which lets TypeScript know that these additional methods also exist. 14 | - This AST is transformed to a nicely formatted string and written to a \*.gen.d.ts file next to the file in which the respective class was defined. 15 | 16 | ### npm Publishing 17 | 18 | ### Watch mode 19 | 20 | #### In Theory 21 | 22 | Main task: re-generate control interfaces once 23 | 24 | 1. a file with metadata has been added 25 | 2. metadata has been added to a file not covered so far 26 | 3. metadata in an already covered file has changed 27 | 4. metadata has been deleted from a file covered so far 28 | 5. a file with metadata has been deleted 29 | 30 | Files to watch: 31 | 32 | 1. new ts file creation 33 | 2. content change in known ts file without metadata 34 | 3. content change in known ts file with metadata 35 | 4. content change in known ts file with metadata 36 | 5. ts file deletion 37 | 38 | ### In Practice 39 | 40 | The TypeScript compiler API offers a watch mode (or rather 2-3 flavors) which do not offer detailed change information, but are an apparently really optimized way of avoiding the multi-second TS initialization on every change. There are several callback hooks which need to be used in combination: 41 | 42 | 1. reportWatchStatusChanged is called with initially NO program, later an old program, and undefined error count and e.g. "message TS6032: File change detected. Starting incremental compilation..." 43 | 2. createProgram notifies that a change was detected 44 | 3. afterProgramCreate is called with the new Program (needed to create a TypeChecker!), but it is not yet known whether there are errors 45 | 4. reportDiagnostic is called for each error; the file name can be derived for each 46 | 5. reportWatchStatusChanged is then called with an error summary, the error count and e.g. "message TS6194: Found 4 errors. Watching for file changes." and can still access the new updated program 47 | 48 | There does not seem to be a way to get informed about deleted files.
49 | New files and changed files can be enumerated by calling `program.getSemanticDiagnosticsOfNextAffectedFile()`. But this only works in the `afterProgramCreate` callback. By the time `reportWatchStatusChanged` is called, the "affected files" have already been handled and can no longer be accessed like this. 50 | 51 | The program object does have a `getState()` method and the state has a `changedFilesSet` property which contains the changed files, but those are not a public API, so they are not used. 52 | 53 | So the information about file changes is limited, but it is anyway questionable whether only changed files need to be considered when re-generating the interfaces: a change in a base class or in the UI5 type definitions could also influence the interface of an unchanged control implementation file. As performance for the overall standard flow is REALLY good (~20ms for a re-scan of all modules in UI5 and analyzing the types in the project files), there does not seem to be a need to restrict interface generation to those which MIGHT have an actual change. 54 | 55 | Open: should all interfaces be deleted before generation to get rid of those which are no longer needed? 56 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | testMatch: [ 4 | "**/__tests__/**/*.+(ts|tsx|js)", 5 | "**/?(*.)+(spec|test).+(ts|tsx|js)", 6 | ], 7 | transform: { 8 | "^.+\\.(ts|tsx)$": "ts-jest", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5/ts-interface-generator", 3 | "version": "0.10.0", 4 | "description": "Generator for TypeScript type definitions for custom UI5 controls implemented in TypeScript", 5 | "author": "SAP SE", 6 | "license": "Apache-2.0", 7 | "main": "dist/generateTSInterfaces.js", 8 | "bin": "dist/generateTSInterfaces.js", 9 | "scripts": { 10 | "start": "node dist/generateTSInterfaces.js", 11 | "try": "npm run build && node dist/generateTSInterfaces.js --config tsconfig-testcontrol.json", 12 | "try-watch": "npm run build && node dist/generateTSInterfaces.js --watch --config tsconfig-testcontrol.json", 13 | "try-watch-managedobject": "npm run build && node dist/generateTSInterfaces.js --watch --config tsconfig-testmanagedobject.json", 14 | "try-watch-webcomponent": "npm run build && node dist/generateTSInterfaces.js --watch --config tsconfig-testwebcomponent.json", 15 | "build": "tsc", 16 | "watch": "tsc --watch", 17 | "test": "jest", 18 | "lint": "eslint src", 19 | "ci": "npm-run-all build test lint" 20 | }, 21 | "homepage": "https://github.com/SAP/ui5-typescript/tree/main/packages/ts-interface-generator", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/SAP/ui5-typescript.git", 25 | "directory": "packages/ts-interface-generator" 26 | }, 27 | "keywords": [ 28 | "UI5", 29 | "TypeScript", 30 | "SAPUI5", 31 | "OpenUI5", 32 | "Controls" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "devDependencies": { 38 | "@types/hjson": "2.4.6", 39 | "@types/jest": "29.5.12", 40 | "@types/node": "20.14.9", 41 | "@types/openui5": "1.127.0", 42 | "@types/yargs": "17.0.32", 43 | "@typescript-eslint/eslint-plugin": "7.14.1", 44 | "@typescript-eslint/parser": "7.14.1", 45 | "eslint": "8.57.0", 46 | "jest": "29.7.0", 47 | "npm-run-all": "4.1.5", 48 | "ts-jest": "29.1.5", 49 | "typescript": "5.5.2" 50 | }, 51 | "dependencies": { 52 | "hjson": "3.2.2", 53 | "loglevel": "1.9.1", 54 | "yargs": "17.7.2" 55 | }, 56 | "peerDependencies": { 57 | "typescript": ">=5.2.0" 58 | }, 59 | "files": [ 60 | "dist" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/addSourceExports.ts: -------------------------------------------------------------------------------- 1 | import path = require("path"); 2 | import ts = require("typescript"); 3 | 4 | /** 5 | * This function aims to find the UI5-style global name for all exports of the given source file 6 | * and to add an entry for each such export to allExports. 7 | * The entry maps the UI5-style global name to a module path and an optional export name. The resulting map 8 | * is used later to find out which module needs to be loaded (and optionally: which named export) for a given global name. 9 | * 10 | * @param sourceFile 11 | * @param basePath 12 | * @param typeChecker 13 | * @param allPathMappings 14 | * @param allExports 15 | * @returns 16 | */ 17 | function addSourceExports( 18 | sourceFile: ts.SourceFile, 19 | basePath: string, 20 | typeChecker: ts.TypeChecker, 21 | allPathMappings: { target: string; sourcePattern: string }[], 22 | allExports: GlobalToModuleMapping, 23 | ) { 24 | const fileName = sourceFile.fileName; 25 | const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); 26 | if (!moduleSymbol) { 27 | // not a module -> ignore 28 | return; 29 | } 30 | 31 | // find out the module path of the current file; this is done by comparing the physical file name to 32 | // the list of the program's path mappings (logical path to physical path). If a physical path matches, 33 | // we know the locical path that can be used to address the module. 34 | // Also, deduce a UI5-style global name for the module. 35 | // Caution: this is full of heuristics and guesses. 36 | const filePath = path.normalize(fileName); // e.g. 'c:\SAPDevelop\git\ui5-typescript-control-library\src-ts\com\myorg\myUI5Library\Example.ts 37 | let globalName: string, moduleFileName: string; 38 | for (let i = 0; i < allPathMappings.length; i++) { 39 | const fullTargetPath = path.resolve(basePath, allPathMappings[i].target); 40 | if (filePath.indexOf(fullTargetPath) === 0) { 41 | const restPath = filePath.replace(fullTargetPath, ""); 42 | moduleFileName = path 43 | .join(allPathMappings[i].sourcePattern, restPath) 44 | .replace(/\\/g, "/") 45 | .replace(/\.ts$/, ""); 46 | globalName = moduleFileName.replace(/\//g, "."); 47 | } 48 | } 49 | if (!globalName) { 50 | // log.warn("No module name could be found for file " + filePath + "\nIs this a problem?"); 51 | } else if (globalName.endsWith(".library")) { 52 | // heuristics: library.ts files usually use the parent path as library name 53 | globalName = globalName.slice(0, -".library".length); 54 | } 55 | 56 | // ask tsc for all exports of the file 57 | const exports = typeChecker.getExportsOfModule(moduleSymbol); 58 | // for each export, add an entry with the assumed global name to the allExports array 59 | exports.forEach((exp) => { 60 | const exportName = exp.getName(); 61 | const globalExportName = 62 | globalName + (exportName === "default" ? "" : "." + exportName); 63 | 64 | // TODO: once annotation is supported, ignore duplicate named export when annotated 65 | allExports[globalExportName] = { 66 | moduleName: moduleFileName, 67 | exportName: exportName === "default" ? undefined : exportName, 68 | }; 69 | }); 70 | } 71 | 72 | export { addSourceExports }; 73 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/astToString.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | function astToString(ast: ts.Node[]) { 4 | const file = ts.createSourceFile( 5 | "_.d.ts", 6 | "", 7 | ts.ScriptTarget.Latest, 8 | false, 9 | ts.ScriptKind.TS, 10 | ); 11 | // @ts-ignore this assignment works 12 | file.statements = ts.factory.createNodeArray(ast); 13 | 14 | // using a *printer* for simplicity; TODO: the TypeScript formatter API would be more appropriate 15 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 16 | let result = printer.printNode(ts.EmitHint.Unspecified, file, undefined); 17 | result = result.replace(/\s*\/\/\n/g, "\n"); // replace dummy comments with just a linebreak 18 | return result; 19 | } 20 | 21 | export default astToString; 22 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/generateTSInterfaces.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // @ts-ignore as the "rootDir" in tsconfig.json is set to "src", this file is outside the source tree. But that's fine and we don't want to make "." the root because this would include more files and cause build result path problems (files going to "dist/src"). 4 | import pkgJson from "../package.json"; 5 | import { Args, main } from "./generateTSInterfacesAPI"; 6 | 7 | import yargs from "yargs"; 8 | 9 | // configure yargs with the cli options as launcher 10 | const version = `${pkgJson.version} (from ${__filename})`; 11 | yargs.version(version); 12 | yargs 13 | .option({ 14 | config: { 15 | alias: "c", 16 | type: "string", 17 | description: "Path to the configuration file to use", 18 | }, 19 | watch: { 20 | alias: "w", 21 | type: "boolean", 22 | description: "Run in watch mode", 23 | }, 24 | loglevel: { 25 | choices: ["error", "warn", "info", "debug", "trace"], 26 | description: "Set the console logging verbosity", 27 | }, 28 | jsdoc: { 29 | choices: ["none", "minimal", "verbose"], 30 | default: "verbose", 31 | description: 32 | "Determines the amount of JSDoc that is generated for the methods; 'minimal' only adds JSDoc present in the source class, 'verbose' also the standard boilerplate texts.", 33 | }, 34 | }) 35 | .default("watch", false) 36 | .default("loglevel", "info"); 37 | 38 | const appArgs = yargs.argv as Args; 39 | main(appArgs); 40 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/generateTSInterfacesAPI.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import ts from "typescript"; 4 | import { generateInterfaces } from "./interfaceGenerationHelper"; 5 | import { initialize } from "./typeScriptEnvironment"; 6 | import { addSourceExports } from "./addSourceExports"; 7 | import Preferences from "./preferences"; 8 | import log from "loglevel"; 9 | log.setDefaultLevel("info"); 10 | 11 | export interface Args { 12 | watch?: boolean; 13 | config?: string; 14 | loglevel?: "debug" | "info" | "warn" | "error"; 15 | jsdoc?: "none" | "minimal" | "verbose"; 16 | } 17 | 18 | // main entry point 19 | export function main(args: Args) { 20 | const watchMode = args.watch; 21 | 22 | Preferences.set({ 23 | jsdoc: args.jsdoc, 24 | }); 25 | 26 | const level = args.loglevel; 27 | if ( 28 | level === "error" || 29 | level === "warn" || 30 | level === "info" || 31 | level === "debug" || 32 | level === "trace" 33 | ) { 34 | log.setDefaultLevel(level); 35 | log.info(`Log level set to: ${level}`); 36 | } 37 | 38 | let tsconfig = args.config; 39 | let logFound = ""; 40 | if ( 41 | !tsconfig || 42 | !fs.existsSync(tsconfig) || 43 | fs.lstatSync(tsconfig).isDirectory() 44 | ) { 45 | // eslint-disable-next-line @typescript-eslint/unbound-method 46 | tsconfig = ts.findConfigFile("./", ts.sys.fileExists, "tsconfig.json"); 47 | if (!tsconfig) { 48 | throw new Error( 49 | "Could not find a valid 'tsconfig.json'. Please specify it using the '--config' parameter.", 50 | ); 51 | } else { 52 | logFound = " (automatically found, as none was given)"; 53 | } 54 | } 55 | log.info( 56 | `Using the following TypeScript configuration file${logFound}: ${tsconfig}`, 57 | ); 58 | 59 | initialize(tsconfig, onTSProgramUpdate, { watchMode }); 60 | } 61 | 62 | /** 63 | * Extracts known local exports and relevant source files from program. 64 | * 65 | * @param program 66 | * @param typeChecker 67 | */ 68 | export function getProgramInfo( 69 | program: ts.Program, 70 | typeChecker: ts.TypeChecker, 71 | ) { 72 | // this block collects all path mappings from the compiler configuration, so we can find out the logical name for a concrete file path 73 | const paths = program.getCompilerOptions().paths; 74 | const allPathMappings: { target: string; sourcePattern: string }[] = []; // contains target/sourcePattern couples; built as array, not a map, to make sure duplicate targets for different patterns work as well 75 | if (paths) { 76 | for (let sourcePattern in paths) { 77 | // e.g. 'com/myorg/myUI5Library/*': [ 'src-ts/com/myorg/myUI5Library\*' ] 78 | const targets = paths[sourcePattern]; 79 | sourcePattern = path.normalize(sourcePattern); // e.g. 'com\myorg\myUI5Library\*' 80 | if (sourcePattern === "*") { 81 | sourcePattern = "."; 82 | } else if (sourcePattern.endsWith("*")) { 83 | sourcePattern = sourcePattern.slice(0, -1); 84 | } 85 | for (let i = 0; i < targets.length; i++) { 86 | let target = path.normalize(targets[i]); // e.g. 'src-ts\com\myorg\myUI5Library\*' 87 | if (target === "*") { 88 | target = "."; 89 | } else if (target.endsWith("*")) { 90 | target = target.slice(0, -1); 91 | } 92 | allPathMappings.push({ target, sourcePattern }); 93 | } 94 | } 95 | } 96 | 97 | const allRelevantSourceFiles: ts.SourceFile[] = []; 98 | const allKnownLocalExports: GlobalToModuleMapping = {}; 99 | 100 | // path mappings are relative to the "baseUrl", so find out what it is 101 | let basePath = path.normalize(program.getCurrentDirectory()); // e.g. 'c:\SAPDevelop\git\ui5-typescript-control-library\' 102 | const options = program.getCompilerOptions(); 103 | if (options.baseUrl) { 104 | basePath = path.normalize(options.baseUrl); 105 | } 106 | 107 | // loop all files, filter for relevant ones, and extract knowledge about all their module exports 108 | program 109 | .getSourceFiles() 110 | .filter((sourceFile) => { 111 | return ( 112 | sourceFile.fileName.indexOf("@types") === -1 && 113 | sourceFile.fileName.indexOf("node_modules/") === -1 && 114 | !program.isSourceFileFromExternalLibrary(sourceFile) 115 | ); // do not generate interfaces for dependencies; the last check is needed because source files inside node_modules do not have "node_modules" in their file name when symlinked; probably this last check would also be sufficient (and better) than guessing via name 116 | }) 117 | .forEach((sourceFile: ts.SourceFile) => { 118 | allRelevantSourceFiles.push(sourceFile); 119 | addSourceExports( 120 | sourceFile, 121 | basePath, 122 | typeChecker, 123 | allPathMappings, 124 | allKnownLocalExports, 125 | ); // extract all local exports 126 | }); 127 | 128 | return { allRelevantSourceFiles, allKnownLocalExports }; 129 | } 130 | 131 | /** 132 | * Whenever the code changes, this is called, in oder to re-generate the interfaces 133 | * 134 | * 135 | * TODO: can we use the knowledge about the changed files to limit the scope here? 136 | * A Problem is that also a change in a different file could influence the generation of an unchanged file. E.g. when an update of the 137 | * UI5 type definitions changes the base class of sap.m.Button to something where API methods are not generated, then 138 | * the next generation run would no longer create a *.gen.d.ts file for Controls deriving from Button (ok, extreme example...) 139 | * @param program 140 | * @param typeChecker 141 | * @param changedFiles 142 | * @param allKnownGlobals 143 | */ 144 | function onTSProgramUpdate( 145 | program: ts.Program, 146 | typeChecker: ts.TypeChecker, 147 | changedFiles: string[], // is an empty array in non-watch case; is at least one file in watch case - but overall not reliable! 148 | allKnownGlobals: GlobalToModuleMapping, 149 | ) { 150 | const { allRelevantSourceFiles, allKnownLocalExports } = getProgramInfo( 151 | program, 152 | typeChecker, 153 | ); 154 | 155 | // now actually generate the interface files for all source files 156 | allRelevantSourceFiles.forEach((sourceFile) => { 157 | generateInterfaces( 158 | sourceFile, 159 | typeChecker, 160 | Object.assign(allKnownLocalExports, allKnownGlobals), 161 | ); // don't modify the ambient globals here, as they might have a different lifecycle and we don't want to keep adding the same properties again 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/preferences.ts: -------------------------------------------------------------------------------- 1 | import type { Args } from "./generateTSInterfacesAPI"; 2 | 3 | class Preferences { 4 | private static instance: Preferences; 5 | private static prefs: Args = { 6 | jsdoc: "verbose", 7 | }; 8 | 9 | constructor() { 10 | if (!Preferences.instance) { 11 | Preferences.instance = this; 12 | } 13 | return Preferences.instance; 14 | } 15 | 16 | set(prefs: Args) { 17 | Preferences.prefs = prefs; 18 | } 19 | 20 | get() { 21 | return Preferences.prefs; 22 | } 23 | } 24 | 25 | export default new Preferences(); 26 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/generateMethods.test.ts: -------------------------------------------------------------------------------- 1 | import astToString from "../astToString"; 2 | import { generateMethods } from "../astGenerationHelper"; 3 | 4 | test("Generate methods for property", () => { 5 | const expected = `// property: text 6 | getText(): string; 7 | setText(text: string): this;`; 8 | 9 | const requiredImports: RequiredImports = {}; 10 | const knownGlobals: { 11 | [key: string]: { moduleName: string; exportName?: string }; 12 | } = {}; 13 | const classInfo: ClassInfo = { 14 | name: "MyTestClass", 15 | properties: { 16 | text: { 17 | name: "text", 18 | type: "string", 19 | methods: { get: "getText", set: "setText" }, 20 | }, 21 | }, 22 | defaultProperty: "text", 23 | }; 24 | 25 | const methods = generateMethods( 26 | classInfo, 27 | requiredImports, 28 | knownGlobals, 29 | {}, 30 | {}, 31 | ); 32 | 33 | const interfaceText = astToString(methods); 34 | 35 | expect(interfaceText.trim()).toEqual(expected); 36 | }); 37 | 38 | test("Generate methods for aggregation", () => { 39 | const expected = `// aggregation: content 40 | getContent(): Control[]; 41 | addContent(content: Control): this; 42 | insertContent(content: Control, index: number): this; 43 | removeContent(content: number | string | Control): Control | null; 44 | removeAllContent(): Control[]; 45 | indexOfContent(content: Control): number; 46 | destroyContent(): this; 47 | bindContent(bindingInfo: AggregationBindingInfo): this; 48 | unbindContent(): this;`; 49 | 50 | const requiredImports: RequiredImports = {}; 51 | const knownGlobals: { 52 | [key: string]: { moduleName: string; exportName?: string }; 53 | } = {}; 54 | const classInfo: ClassInfo = { 55 | name: "MyTestClass", 56 | aggregations: { 57 | content: { 58 | name: "content", 59 | type: "Control", 60 | cardinality: "0..n", 61 | altTypes: null, 62 | bindable: true, 63 | singularName: "content", 64 | methods: { 65 | get: "getContent", 66 | add: "addContent", 67 | insert: "insertContent", 68 | remove: "removeContent", 69 | removeAll: "removeAllContent", 70 | indexOf: "indexOfContent", 71 | bind: "bindContent", 72 | unbind: "unbindContent", 73 | destroy: "destroyContent", 74 | }, 75 | }, 76 | }, 77 | defaultAggregation: "content", 78 | }; 79 | 80 | const methods = generateMethods( 81 | classInfo, 82 | requiredImports, 83 | knownGlobals, 84 | {}, 85 | {}, 86 | ); 87 | 88 | const interfaceText = astToString(methods); 89 | 90 | expect(interfaceText.trim()).toEqual(expected); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleControl/SampleControl.ts: -------------------------------------------------------------------------------- 1 | import Button from "sap/m/Button"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * A SampleControl is a control and this is its documentation. 7 | * 8 | * @namespace ui5tssampleapp.control 9 | */ 10 | export default class SampleControl extends Button { 11 | 12 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 13 | constructor(idOrSettings?: string | $SampleControlSettings); 14 | constructor(id?: string, settings?: $SampleControlSettings); 15 | constructor(id?: string, settings?: $SampleControlSettings) { super(id, settings); } 16 | 17 | static readonly metadata: MetadataOptions = { 18 | properties: { 19 | /** 20 | * The text that appears below the main text. 21 | * @since 1.0 22 | */ 23 | subtext: "string", 24 | 25 | /** 26 | * Determines the text color of the SampleControl. 27 | * 28 | * @experimental 29 | */ 30 | textColor: { type: "sap.ui.core.CSSColor", defaultValue: "" }, 31 | }, 32 | aggregations: { 33 | /** 34 | * Determines the content of the SampleControl. 35 | */ 36 | content: { multiple: true, type: "sap.ui.core.Control", bindable: true }, 37 | header: { multiple: false, type: "sap.ui.core.Control" }, 38 | tooltip: { multiple: false, type: "sap.ui.core.TooltipBase", altTypes : ["string"]} 39 | }, 40 | defaultAggregation: "content", 41 | associations: { 42 | partnerControl: "SampleControl", 43 | /** 44 | * This is an association. 45 | */ 46 | alsoLabelledBy: { type: "sap.ui.core.Control", multiple: true }, 47 | }, 48 | events: { 49 | /** 50 | * Fired when single-clicked. This event has no parameters, which requires an eslint-disable in the generated code. 51 | */ 52 | singlePress: {}, 53 | /** 54 | * Fired when double-clicked. 55 | */ 56 | doublePress: { 57 | allowPreventDefault: true, 58 | parameters: { 59 | /** 60 | * The amount of milliseconds between the first and second press 61 | */ 62 | delay: { type: "int" } 63 | } 64 | } 65 | }, 66 | }; 67 | 68 | static renderer = { 69 | apiVersion: 2, 70 | render: function (rm: RenderManager, control: SampleControl) { 71 | rm.openStart("div", control); 72 | rm.openEnd(); 73 | 74 | rm.text(control.getText()); 75 | // @ts-ignore this only works with the generated interface 76 | rm.text(control.getSubtext()); 77 | // @ts-ignore this only works with the generated interface 78 | const content = control.getContent(); 79 | for (let i = 0; i < content.length; i++) { 80 | rm.renderControl(content[i]); 81 | } 82 | rm.close("div"); 83 | } 84 | }; 85 | 86 | doit() { 87 | this.fireSinglePress(); 88 | this.fireDoublePress({ delay: 100 }); 89 | alert("Hello"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnotherManagedObject.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $SampleManagedObjectSettings } from "./SampleManagedObject"; 3 | 4 | declare module "./SampleAnotherManagedObject" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $SampleAnotherManagedObjectSettings extends $SampleManagedObjectSettings { 10 | anotherTest?: number | PropertyBindingInfo | `{${string}}`; 11 | } 12 | 13 | export default interface SampleAnotherManagedObject { 14 | 15 | // property: anotherTest 16 | 17 | /** 18 | * Gets current value of property "anotherTest". 19 | * 20 | * @returns Value of property "anotherTest" 21 | */ 22 | getAnotherTest(): number; 23 | 24 | /** 25 | * Sets a new value for property "anotherTest". 26 | * 27 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 28 | * 29 | * @param anotherTest New value for property "anotherTest" 30 | * @returns Reference to "this" in order to allow method chaining 31 | */ 32 | setAnotherTest(anotherTest: number): this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnotherManagedObject.ts: -------------------------------------------------------------------------------- 1 | import { MetadataOptions } from "sap/ui/core/Element"; 2 | import SampleManagedObject from "./SampleManagedObject"; 3 | 4 | /** 5 | * @namespace ui5tssampleapp.control 6 | */ 7 | export default class SampleAnotherManagedObject extends SampleManagedObject { 8 | 9 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 10 | constructor(idOrSettings?: string | $SampleAnotherManagedObjectSettings); 11 | constructor(id?: string, settings?: $SampleAnotherManagedObjectSettings); 12 | constructor(id?: string, settings?: $SampleAnotherManagedObjectSettings) { super(id, settings); } 13 | 14 | static readonly metadata: MetadataOptions = { 15 | properties: { 16 | anotherTest: { 17 | type: "float" 18 | } 19 | } 20 | }; 21 | 22 | init() { 23 | this.setTest(123); 24 | this.setAnotherTest(123); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleManagedObject.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ManagedObjectSettings } from "sap/ui/base/ManagedObject"; 3 | 4 | declare module "./SampleManagedObject" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $SampleManagedObjectSettings extends $ManagedObjectSettings { 10 | test?: number | PropertyBindingInfo | `{${string}}`; 11 | } 12 | 13 | export default interface SampleManagedObject { 14 | 15 | // property: test 16 | 17 | /** 18 | * Gets current value of property "test". 19 | * 20 | * @returns Value of property "test" 21 | */ 22 | getTest(): number; 23 | 24 | /** 25 | * Sets a new value for property "test". 26 | * 27 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 28 | * 29 | * @param test New value for property "test" 30 | * @returns Reference to "this" in order to allow method chaining 31 | */ 32 | setTest(test: number): this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleManagedObject.ts: -------------------------------------------------------------------------------- 1 | import ManagedObject from "sap/ui/base/ManagedObject"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | 4 | /** 5 | * @namespace ui5tssampleapp.control 6 | */ 7 | export default class SampleManagedObject extends ManagedObject { 8 | 9 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 10 | constructor(idOrSettings?: string | $SampleManagedObjectSettings); 11 | constructor(id?: string, settings?: $SampleManagedObjectSettings); 12 | constructor(id?: string, settings?: $SampleManagedObjectSettings) { super(id, settings); } 13 | 14 | static readonly metadata: MetadataOptions = { 15 | properties: { 16 | test: { 17 | type: "float" 18 | } 19 | } 20 | }; 21 | 22 | init() { 23 | this.setTest(123); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleWebComponent/App.codetest.ts: -------------------------------------------------------------------------------- 1 | import SampleWebComponent from "./SampleWebComponent"; 2 | 3 | const webcInstance = new SampleWebComponent(); 4 | 5 | const propertyValue = webcInstance.getSubtext(); 6 | const methodValue = webcInstance.somePublicMethod(); 7 | const getterValue = webcInstance.somePublicGetter; 8 | 9 | console.log(`Property returned: ${propertyValue}`); 10 | console.log(`Method returned: ${methodValue}`); 11 | console.log(`Getter returned: ${getterValue}`); 12 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/sampleWebComponent/SampleWebComponent.ts: -------------------------------------------------------------------------------- 1 | import WebComponent, { MetadataOptions } from "sap/ui/core/webc/WebComponent"; 2 | 3 | /** 4 | * A SampleWebComponent is a control wrapper for a Web Component and this is its documentation. 5 | * 6 | * @namespace ui5tssampleapp.control 7 | */ 8 | export default class SampleWebComponent extends WebComponent { 9 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 10 | constructor(idOrSettings?: string | $SampleWebComponentSettings); 11 | constructor(id?: string, settings?: $SampleWebComponentSettings); 12 | constructor(id?: string, settings?: $SampleWebComponentSettings) { 13 | super(id, settings); 14 | } 15 | 16 | static readonly metadata: MetadataOptions = { 17 | tag: "sample-webcomponent", 18 | properties: { 19 | /** 20 | * The text that appears below the main text. 21 | * @since 1.0 22 | */ 23 | subtext: "string", 24 | 25 | /** 26 | * Determines the text color of the SampleWebComponent. 27 | * 28 | * @experimental 29 | */ 30 | textColor: { type: "sap.ui.core.CSSColor", defaultValue: "" }, 31 | 32 | /** 33 | * Usage of mapping 34 | */ 35 | text: { 36 | type: "string", 37 | mapping: { 38 | type: "textContent", 39 | }, 40 | }, 41 | }, 42 | aggregations: { 43 | /** 44 | * Determines the content of the SampleWebComponent. 45 | */ 46 | content: { multiple: true, type: "sap.ui.core.Control", bindable: true }, 47 | header: { multiple: false, type: "sap.ui.core.webc.WebComponent" }, 48 | tooltip: { 49 | multiple: false, 50 | type: "sap.ui.core.TooltipBase", 51 | altTypes: ["string"], 52 | }, 53 | }, 54 | defaultAggregation: "content", 55 | associations: { 56 | partnerControl: "SampleWebComponent", 57 | /** 58 | * This is an association. 59 | */ 60 | alsoLabelledBy: { type: "sap.ui.core.Control", multiple: true }, 61 | }, 62 | events: { 63 | /** 64 | * Fired when double-clicked. 65 | */ 66 | doublePress: { allowPreventDefault: true }, 67 | }, 68 | methods: ["somePublicMethod"], 69 | getters: ["somePublicGetter"], 70 | }; 71 | } 72 | 73 | /** 74 | * implement the methods and getters 75 | */ 76 | declare module "./SampleWebComponent" { 77 | export default interface SampleWebComponent { 78 | /** 79 | * Some public method returning a "string" 80 | * 81 | * @since 1.0 82 | * 83 | * @returns Value of property "subtext" 84 | */ 85 | somePublicMethod(): string; 86 | 87 | /** 88 | * Some public getter being a "string" 89 | */ 90 | somePublicGetter: string; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/tsfiles/someFile.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-typescript/b6c0fe52c1603042439df035b5ba8f5439b732f8/packages/ts-interface-generator/src/test/samples/tsfiles/someFile.js -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/samples/tsfiles/someFile.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/ui5-typescript/b6c0fe52c1603042439df035b5ba8f5439b732f8/packages/ts-interface-generator/src/test/samples/tsfiles/someFile.ts -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/instance-exported/MyControl.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ControlSettings } from "sap/ui/core/Control"; 3 | 4 | declare module "./MyControl" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $MyControlSettings extends $ControlSettings { 10 | text?: string | PropertyBindingInfo; 11 | } 12 | 13 | interface MyControl { 14 | 15 | // property: text 16 | 17 | /** 18 | * Gets current value of property "text". 19 | * 20 | * @returns Value of property "text" 21 | */ 22 | getText(): string; 23 | 24 | /** 25 | * Sets a new value for property "text". 26 | * 27 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 28 | * 29 | * @param text New value for property "text" 30 | * @returns Reference to "this" in order to allow method chaining 31 | */ 32 | setText(text: string): this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/instance-exported/MyControl.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * @namespace my 7 | */ 8 | export class MyControl extends Control { 9 | static readonly metadata: MetadataOptions = { 10 | properties: { 11 | text: "string", 12 | }, 13 | }; 14 | 15 | static renderer = { 16 | apiVersion: 2, 17 | render: function (rm: RenderManager, control: MyControl) { 18 | rm.openStart("div", control); 19 | rm.openEnd(); 20 | // @ts-ignore this only works with the generated interface 21 | rm.text(control.getText()); 22 | rm.close("div"); 23 | }, 24 | }; 25 | } 26 | 27 | export default new MyControl(); 28 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/separate-export/MyControl.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ControlSettings } from "sap/ui/core/Control"; 3 | 4 | declare module "./MyControl" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $MyControlSettings extends $ControlSettings { 10 | text?: string | PropertyBindingInfo; 11 | } 12 | 13 | export default interface MyControl { 14 | 15 | // property: text 16 | 17 | /** 18 | * Gets current value of property "text". 19 | * 20 | * @returns Value of property "text" 21 | */ 22 | getText(): string; 23 | 24 | /** 25 | * Sets a new value for property "text". 26 | * 27 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 28 | * 29 | * @param text New value for property "text" 30 | * @returns Reference to "this" in order to allow method chaining 31 | */ 32 | setText(text: string): this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/separate-export/MyControl.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * @namespace my 7 | */ 8 | class MyControl extends Control { 9 | static readonly metadata: MetadataOptions = { 10 | properties: { 11 | text: "string", 12 | }, 13 | }; 14 | 15 | static renderer = { 16 | apiVersion: 2, 17 | render: function (rm: RenderManager, control: MyControl) { 18 | rm.openStart("div", control); 19 | rm.openEnd(); 20 | // @ts-ignore this only works with the generated interface 21 | rm.text(control.getText()); 22 | rm.close("div"); 23 | }, 24 | }; 25 | } 26 | 27 | export default MyControl; -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/simple-control/MyControl.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ControlSettings } from "sap/ui/core/Control"; 3 | 4 | declare module "./MyControl" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $MyControlSettings extends $ControlSettings { 10 | text?: string | PropertyBindingInfo; 11 | } 12 | 13 | export default interface MyControl { 14 | 15 | // property: text 16 | 17 | /** 18 | * Gets current value of property "text". 19 | * 20 | * @returns Value of property "text" 21 | */ 22 | getText(): string; 23 | 24 | /** 25 | * Sets a new value for property "text". 26 | * 27 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 28 | * 29 | * @param text New value for property "text" 30 | * @returns Reference to "this" in order to allow method chaining 31 | */ 32 | setText(text: string): this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/simple-control/MyControl.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * @namespace my 7 | */ 8 | export default class MyControl extends Control { 9 | static readonly metadata: MetadataOptions = { 10 | properties: { 11 | text: "string", 12 | }, 13 | }; 14 | 15 | static renderer = { 16 | apiVersion: 2, 17 | render: function (rm: RenderManager, control: MyControl) { 18 | rm.openStart("div", control); 19 | rm.openEnd(); 20 | // @ts-ignore this only works with the generated interface 21 | rm.text(control.getText()); 22 | rm.close("div"); 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/testcaseRunner.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import ts from "typescript"; 4 | import log from "loglevel"; 5 | import { generateInterfaces } from "../../interfaceGenerationHelper"; 6 | import { getAllKnownGlobals, GlobalToModuleMapping } from "../../typeScriptEnvironment"; 7 | import { getProgramInfo } from "../../generateTSInterfacesAPI"; 8 | 9 | const testCasesDir = path.resolve(__dirname); 10 | 11 | const standardTsConfig: ts.CompilerOptions = { 12 | target: ts.ScriptTarget.ES2022, 13 | module: ts.ModuleKind.CommonJS, 14 | strict: true, 15 | moduleResolution: ts.ModuleResolutionKind.Node16, 16 | esModuleInterop: true, 17 | skipLibCheck: true, 18 | forceConsistentCasingInFileNames: true, 19 | }; 20 | 21 | describe("Single Testcases", () => { 22 | beforeAll(() => { 23 | jest.spyOn(log, "warn").mockImplementation(() => {}); 24 | }); 25 | 26 | afterAll(() => { 27 | // Restore the original console.warn method 28 | jest.restoreAllMocks(); 29 | }); 30 | 31 | fs.readdirSync(testCasesDir).forEach((testCase) => { 32 | const testCaseDir = path.join(testCasesDir, testCase); 33 | // abort if not a directory 34 | if (!fs.lstatSync(testCaseDir).isDirectory()) { 35 | return; 36 | } 37 | 38 | function parseConfig(tsConfigPath: string) { 39 | let config: ts.CompilerOptions; 40 | if (fs.existsSync(tsConfigPath)) { 41 | const json: unknown = ts.parseConfigFileTextToJson(tsConfigPath, fs.readFileSync(tsConfigPath).toString()).config || {}; 42 | config = ts.parseJsonConfigFileContent(json, ts.sys, path.dirname(tsConfigPath)).options; 43 | } else { 44 | config = {...standardTsConfig}; 45 | } 46 | config.baseUrl = testCaseDir; 47 | return config; 48 | } 49 | 50 | test(`Interface generation for ${testCase}`, async () => { 51 | // setup TypeScript program 52 | const tsConfigPath = path.join(testCaseDir, "tsconfig.json"); 53 | const tsFiles = fs 54 | .readdirSync(testCaseDir) 55 | .filter((file) => file.endsWith(".ts") && !file.endsWith(".d.ts")) 56 | .map((file) => path.join(testCaseDir, file)); 57 | 58 | const program = ts.createProgram(tsFiles, parseConfig(tsConfigPath)); 59 | const typeChecker = program.getTypeChecker(); 60 | const programInfo = getProgramInfo(program, typeChecker); 61 | const allKnownGlobals: GlobalToModuleMapping = getAllKnownGlobals(typeChecker); 62 | 63 | const sourceFiles = program.getSourceFiles().filter((sourceFile) => { 64 | return !sourceFile.isDeclarationFile && path.basename(sourceFile.fileName) !== "library.ts"; 65 | }); 66 | 67 | const runGenerateInterfaces = async ( 68 | sourceFile: ts.SourceFile, 69 | ): Promise => { 70 | return new Promise((resolve) => { 71 | const resultProcessor = ( 72 | sourceFileName: string, 73 | className: string, 74 | interfaceText: string, 75 | ) => { 76 | resolve(interfaceText); 77 | }; 78 | 79 | generateInterfaces( 80 | sourceFile, 81 | typeChecker, 82 | Object.assign({}, programInfo.allKnownLocalExports, allKnownGlobals), 83 | resultProcessor, 84 | ); 85 | }); 86 | }; 87 | 88 | for (const sourceFile of sourceFiles) { 89 | const generatedInterfaces = await runGenerateInterfaces(sourceFile); 90 | 91 | const expectedOutputPath = sourceFile.fileName.replace( 92 | /\.ts$/, 93 | ".gen.d.ts", 94 | ); 95 | 96 | if (!fs.existsSync(expectedOutputPath)) { 97 | // write the generated output to the file if it does not exist 98 | fs.writeFileSync(expectedOutputPath, generatedInterfaces); 99 | console.log(`Generated output written to ${expectedOutputPath}`); 100 | } else { 101 | const expectedOutput = fs.readFileSync(expectedOutputPath, "utf-8"); 102 | expect(generatedInterfaces).toEqual(expectedOutput); 103 | } 104 | } 105 | }, 15000); // typical run takes a second, so increase the 5000 ms default timeout to be safe 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/MyControl.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { MyJSEnum } from "my/library"; 2 | import { MyTSEnum } from "my/library"; 3 | import MyDependency from "my/MyDependency"; 4 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 5 | import { $ControlSettings } from "sap/ui/core/Control"; 6 | 7 | declare module "./MyControl" { 8 | 9 | /** 10 | * Interface defining the settings object used in constructor calls 11 | */ 12 | interface $MyControlSettings extends $ControlSettings { 13 | myJSEnumVal?: MyJSEnum | PropertyBindingInfo | `{${string}}`; 14 | myTSEnumVal?: MyTSEnum | PropertyBindingInfo | `{${string}}`; 15 | myDependency?: MyDependency | PropertyBindingInfo | `{${string}}`; 16 | } 17 | 18 | export default interface MyControl { 19 | 20 | // property: myJSEnumVal 21 | 22 | /** 23 | * Gets current value of property "myJSEnumVal". 24 | * 25 | * Default value is: "MyJSEnum.Foo," 26 | * @returns Value of property "myJSEnumVal" 27 | */ 28 | getMyJSEnumVal(): MyJSEnum; 29 | 30 | /** 31 | * Sets a new value for property "myJSEnumVal". 32 | * 33 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 34 | * 35 | * Default value is: "MyJSEnum.Foo," 36 | * @param [myJSEnumVal="MyJSEnum.Foo,"] New value for property "myJSEnumVal" 37 | * @returns Reference to "this" in order to allow method chaining 38 | */ 39 | setMyJSEnumVal(myJSEnumVal: MyJSEnum): this; 40 | 41 | // property: myTSEnumVal 42 | 43 | /** 44 | * Gets current value of property "myTSEnumVal". 45 | * 46 | * Default value is: "MyTSEnum.Foo," 47 | * @returns Value of property "myTSEnumVal" 48 | */ 49 | getMyTSEnumVal(): MyTSEnum; 50 | 51 | /** 52 | * Sets a new value for property "myTSEnumVal". 53 | * 54 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 55 | * 56 | * Default value is: "MyTSEnum.Foo," 57 | * @param [myTSEnumVal="MyTSEnum.Foo,"] New value for property "myTSEnumVal" 58 | * @returns Reference to "this" in order to allow method chaining 59 | */ 60 | setMyTSEnumVal(myTSEnumVal: MyTSEnum): this; 61 | 62 | // property: myDependency 63 | 64 | /** 65 | * Gets current value of property "myDependency". 66 | * 67 | * @returns Value of property "myDependency" 68 | */ 69 | getMyDependency(): MyDependency; 70 | 71 | /** 72 | * Sets a new value for property "myDependency". 73 | * 74 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 75 | * 76 | * @param myDependency New value for property "myDependency" 77 | * @returns Reference to "this" in order to allow method chaining 78 | */ 79 | setMyDependency(myDependency: MyDependency): this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/MyControl.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import type { MetadataOptions } from "sap/ui/core/Element"; 3 | import { MyJSEnum, MyTSEnum } from "./library"; 4 | 5 | export default class MyControl extends Control { 6 | static readonly metadata: MetadataOptions = { 7 | properties: { 8 | myJSEnumVal: { 9 | type: "my.MyJSEnum", 10 | defaultValue: MyJSEnum.Foo, 11 | }, 12 | myTSEnumVal: { 13 | type: "my.MyTSEnum", 14 | defaultValue: MyTSEnum.Foo, 15 | }, 16 | myDependency: { 17 | type: "my.MyDependency", 18 | }, 19 | }, 20 | }; 21 | constructor(idOrSettings?: string | $MyControlSettings); 22 | constructor(id?: string, settings?: $MyControlSettings); 23 | constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/MyDependency.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ControlSettings } from "sap/ui/core/Control"; 3 | 4 | declare module "./MyDependency" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $MyDependencySettings extends $ControlSettings { 10 | foo?: string | PropertyBindingInfo; 11 | } 12 | 13 | export default interface MyDependency { 14 | 15 | // property: foo 16 | 17 | /** 18 | * Gets current value of property "foo". 19 | * 20 | * Default value is: "bar" 21 | * @returns Value of property "foo" 22 | */ 23 | getFoo(): string; 24 | 25 | /** 26 | * Sets a new value for property "foo". 27 | * 28 | * When called with a value of "null" or "undefined", the default value of the property will be restored. 29 | * 30 | * Default value is: "bar" 31 | * @param [foo="bar"] New value for property "foo" 32 | * @returns Reference to "this" in order to allow method chaining 33 | */ 34 | setFoo(foo: string): this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/MyDependency.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import type { MetadataOptions } from "sap/ui/core/Element"; 3 | 4 | export default class MyDependency extends Control { 5 | static readonly metadata: MetadataOptions = { 6 | properties: { 7 | foo: { 8 | type: "string", 9 | defaultValue: "bar", 10 | }, 11 | }, 12 | }; 13 | 14 | constructor(idOrSettings?: string | $MyDependencySettings); 15 | constructor(id?: string, settings?: $MyDependencySettings); 16 | constructor(id?: string, settings?: $MyDependencySettings) { super(id, settings); } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/library.ts: -------------------------------------------------------------------------------- 1 | import DataType from "sap/ui/base/DataType"; 2 | import Lib from "sap/ui/core/Lib"; 3 | 4 | const thisLib: object&{MyJSEnum?: object; MyTSEnum?: object} = Lib.init({ 5 | name: "my", 6 | version: "${version}", 7 | dependencies: ["sap.ui.core"], 8 | types: [], 9 | interfaces: [], 10 | controls: [], 11 | elements: [], 12 | noLibraryCSS: false, 13 | }); 14 | 15 | export const MyJSEnum = { 16 | Foo: "foo", 17 | Bar: "bar", 18 | } as const; 19 | thisLib.MyJSEnum = MyJSEnum; 20 | DataType.registerEnum("mylib.MyJSEnum", thisLib.MyJSEnum); 21 | 22 | export enum MyTSEnum { 23 | Foo = "foo", 24 | Bar = "bar", 25 | } 26 | thisLib.MyTSEnum = MyTSEnum; 27 | DataType.registerEnum("mylib.MyTSEnum", thisLib.MyTSEnum); 28 | 29 | export default thisLib; 30 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/tsconfig-path-relative/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "strict": true, 6 | "moduleResolution": "Node16", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "paths": { 12 | "my/*": ["${configDir}/*"] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/type-used-in-api/MyControl.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { $ControlSettings } from "sap/ui/core/Control"; 2 | 3 | declare module "./MyControl" { 4 | 5 | /** 6 | * Interface defining the settings object used in constructor calls 7 | */ 8 | interface $MyControlSettings extends $ControlSettings { 9 | otherControl?: MyControl; 10 | } 11 | 12 | export default interface MyControl { 13 | 14 | // aggregation: otherControl 15 | 16 | /** 17 | * Gets content of aggregation "otherControl". 18 | */ 19 | getOtherControl(): MyControl; 20 | 21 | /** 22 | * Sets the aggregated otherControl. 23 | * 24 | * @param otherControl The otherControl to set 25 | * @returns Reference to "this" in order to allow method chaining 26 | */ 27 | setOtherControl(otherControl: MyControl): this; 28 | 29 | /** 30 | * Destroys the otherControl in the aggregation "otherControl". 31 | * 32 | * @returns Reference to "this" in order to allow method chaining 33 | */ 34 | destroyOtherControl(): this; 35 | } 36 | } 37 | 38 | // this duplicate interface without export is needed to avoid "Cannot find name 'MyControl'" TypeScript errors above 39 | declare module "./MyControl" { 40 | interface MyControl { 41 | 42 | // aggregation: otherControl 43 | 44 | /** 45 | * Gets content of aggregation "otherControl". 46 | */ 47 | getOtherControl(): MyControl; 48 | 49 | /** 50 | * Sets the aggregated otherControl. 51 | * 52 | * @param otherControl The otherControl to set 53 | * @returns Reference to "this" in order to allow method chaining 54 | */ 55 | setOtherControl(otherControl: MyControl): this; 56 | 57 | /** 58 | * Destroys the otherControl in the aggregation "otherControl". 59 | * 60 | * @returns Reference to "this" in order to allow method chaining 61 | */ 62 | destroyOtherControl(): this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/type-used-in-api/MyControl.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * This is my control. 7 | * 8 | * @namespace my 9 | */ 10 | export default class MyControl extends Control { 11 | static readonly metadata: MetadataOptions = { 12 | aggregations: { 13 | otherControl: { multiple: false, type: "MyControl" }, 14 | }, 15 | }; 16 | 17 | static renderer = { 18 | apiVersion: 2, 19 | render: function (rm: RenderManager, control: MyControl) { 20 | rm.openStart("div", control); 21 | rm.openEnd(); 22 | // @ts-ignore this only works with the generated interface 23 | rm.renderControl(control.getOtherControl()); 24 | rm.close("div"); 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/web-component/SampleWebComponent.ts: -------------------------------------------------------------------------------- 1 | import WebComponent, { MetadataOptions } from "sap/ui/core/webc/WebComponent"; 2 | 3 | /** 4 | * A SampleWebComponent is a control wrapper for a Web Component and this is its documentation. 5 | * 6 | * @namespace ui5tssampleapp.control 7 | */ 8 | export default class SampleWebComponent extends WebComponent { 9 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 10 | constructor(idOrSettings?: string | $SampleWebComponentSettings); 11 | constructor(id?: string, settings?: $SampleWebComponentSettings); 12 | constructor(id?: string, settings?: $SampleWebComponentSettings) { 13 | super(id, settings); 14 | } 15 | 16 | static readonly metadata: MetadataOptions = { 17 | tag: "sample-webcomponent", 18 | properties: { 19 | /** 20 | * The text that appears below the main text. 21 | * @since 1.0 22 | */ 23 | subtext: "string", 24 | 25 | /** 26 | * Determines the text color of the SampleWebComponent. 27 | * 28 | * @experimental 29 | */ 30 | textColor: { type: "sap.ui.core.CSSColor", defaultValue: "" }, 31 | 32 | /** 33 | * Usage of mapping 34 | */ 35 | text: { 36 | type: "string", 37 | mapping: { 38 | type: "textContent", 39 | }, 40 | }, 41 | }, 42 | aggregations: { 43 | /** 44 | * Determines the content of the SampleWebComponent. 45 | */ 46 | content: { multiple: true, type: "sap.ui.core.Control", bindable: true }, 47 | header: { multiple: false, type: "sap.ui.core.webc.WebComponent" }, 48 | tooltip: { 49 | multiple: false, 50 | type: "sap.ui.core.TooltipBase", 51 | altTypes: ["string"], 52 | }, 53 | }, 54 | defaultAggregation: "content", 55 | associations: { 56 | partnerControl: "SampleWebComponent", 57 | /** 58 | * This is an association. 59 | */ 60 | alsoLabelledBy: { type: "sap.ui.core.Control", multiple: true }, 61 | }, 62 | events: { 63 | /** 64 | * Fired when double-clicked. 65 | */ 66 | doublePress: { allowPreventDefault: true }, 67 | }, 68 | methods: ["somePublicMethod"], 69 | getters: ["somePublicGetter"], 70 | }; 71 | } 72 | 73 | /** 74 | * implement the methods and getters 75 | */ 76 | declare module "./SampleWebComponent" { 77 | export default interface SampleWebComponent { 78 | /** 79 | * Some public method returning a "string" 80 | * 81 | * @since 1.0 82 | * 83 | * @returns Value of property "subtext" 84 | */ 85 | somePublicMethod(): string; 86 | 87 | /** 88 | * Some public getter being a "string" 89 | */ 90 | somePublicGetter: string; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/test/testcases/xl-control-with-all-features/SampleControl.ts: -------------------------------------------------------------------------------- 1 | import Button from "sap/m/Button"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import RenderManager from "sap/ui/core/RenderManager"; 4 | 5 | /** 6 | * A SampleControl is a control and this is its documentation. 7 | * 8 | * @namespace ui5tssampleapp.control 9 | */ 10 | export default class SampleControl extends Button { 11 | 12 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 13 | constructor(idOrSettings?: string | $SampleControlSettings); 14 | constructor(id?: string, settings?: $SampleControlSettings); 15 | constructor(id?: string, settings?: $SampleControlSettings) { super(id, settings); } 16 | 17 | static readonly metadata: MetadataOptions = { 18 | properties: { 19 | /** 20 | * The text that appears below the main text. 21 | * @since 1.0 22 | */ 23 | subtext: "string", 24 | 25 | /** 26 | * Determines the text color of the SampleControl. 27 | * 28 | * @experimental 29 | */ 30 | textColor: { type: "sap.ui.core.CSSColor", defaultValue: "" }, 31 | }, 32 | aggregations: { 33 | /** 34 | * Determines the content of the SampleControl. 35 | */ 36 | content: { multiple: true, type: "sap.ui.core.Control", bindable: true }, 37 | /** 38 | * The header - there can be only one 39 | */ 40 | header: { multiple: false, type: "sap.ui.core.Control" }, 41 | /** 42 | * The tooltip - either a string or a TooltipBase 43 | */ 44 | tooltip: { multiple: false, type: "sap.ui.core.TooltipBase", altTypes : ["string"]} 45 | }, 46 | defaultAggregation: "content", 47 | associations: { 48 | /** 49 | * Another control belonging to this one 50 | */ 51 | partnerControl: "SampleControl", 52 | /** 53 | * This is an association. 54 | */ 55 | alsoLabelledBy: { type: "sap.ui.core.Control", multiple: true }, 56 | }, 57 | events: { 58 | /** 59 | * Fired when single-clicked. This event has no parameters. 60 | */ 61 | singlePress: {}, 62 | /** 63 | * Fired when double-clicked. 64 | */ 65 | doublePress: { 66 | allowPreventDefault: true, 67 | parameters: { 68 | /** 69 | * The amount of milliseconds between the first and second press 70 | */ 71 | delay: { type: "int" } 72 | } 73 | } 74 | }, 75 | }; 76 | 77 | static renderer = { 78 | apiVersion: 2, 79 | render: function (rm: RenderManager, control: SampleControl) { 80 | rm.openStart("div", control); 81 | rm.openEnd(); 82 | 83 | rm.text(control.getText()); 84 | // @ts-ignore this only works with the generated interface 85 | rm.text(control.getSubtext()); 86 | // @ts-ignore this only works with the generated interface 87 | const content = control.getContent(); 88 | for (let i = 0; i < content.length; i++) { 89 | rm.renderControl(content[i]); 90 | } 91 | rm.close("div"); 92 | } 93 | }; 94 | 95 | doit() { 96 | this.fireDoublePress({ 97 | delay: 100 98 | }); 99 | this.fireSinglePress(); 100 | alert("Hello"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/src/types.d.ts: -------------------------------------------------------------------------------- 1 | interface GlobalToModuleMapping { 2 | [key: string]: { moduleName: string; exportName?: string }; 3 | } 4 | 5 | interface ManagedObjectInfo { 6 | sourceFile: ts.SourceFile; 7 | className: string; 8 | classDeclaration: ts.ClassDeclaration; 9 | isDefaultExport: boolean; 10 | settingsTypeFullName: string; 11 | interestingBaseClass: 12 | | "ManagedObject" 13 | | "WebComponent" 14 | | "Element" 15 | | "Control" 16 | | undefined; 17 | constructorSignaturesAvailable: boolean; 18 | metadata: ts.PropertyDeclaration[]; 19 | } 20 | 21 | interface RequiredImports { 22 | selfIsUsed?: boolean; 23 | [key: string]: { localName: string; moduleName: string; exportName?: string }; 24 | } 25 | 26 | interface APIMember { 27 | name: string; 28 | doc?: string; 29 | since?: string; 30 | deprecation?: string; 31 | experimental?: string; 32 | visibility?: string; 33 | generatedJSDoc?: string; 34 | } 35 | 36 | interface APIMemberWithMethods extends APIMember { 37 | methods: { [key: string]: string }; 38 | } 39 | 40 | interface APIMemberWithType extends APIMember { 41 | type: string; 42 | } 43 | 44 | interface Property extends APIMemberWithMethods, APIMemberWithType { 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | defaultValue?: any; 47 | bindable?: boolean; 48 | } 49 | 50 | interface Aggregation extends APIMemberWithMethods, APIMemberWithType { 51 | cardinality: "0..1" | "0..n"; 52 | altTypes: [string] | null; 53 | //dnd: any, 54 | singularName: string; 55 | bindable: boolean; 56 | } 57 | 58 | interface AggregationMetadata extends Aggregation { 59 | multiple: true; 60 | } 61 | 62 | interface Association extends APIMemberWithMethods, APIMemberWithType { 63 | cardinality: "0..1" | "0..n"; 64 | singularName: string; 65 | } 66 | 67 | interface AssociationMetadata extends Association { 68 | multiple: true; 69 | } 70 | 71 | interface UI5Event extends APIMemberWithMethods { 72 | allowPreventDefault: boolean; 73 | enableEventBubbling: boolean; 74 | parameters: { [key: string]: EventParameter }; 75 | } 76 | 77 | interface EventParameter { 78 | name: string; 79 | doc: string; 80 | deprecation: string; 81 | since: string; 82 | experimental: string; 83 | type: string; 84 | } 85 | 86 | type SpecialSetting = APIMemberWithType; 87 | 88 | interface ClassInfo { 89 | name?: string; 90 | interfaces?: string[]; 91 | doc?: string; 92 | deprecation?: string; 93 | since?: string; 94 | experimental?: string; 95 | specialSettings?: { [key: string]: SpecialSetting }; 96 | properties?: { [key: string]: Property }; 97 | defaultProperty?: string; 98 | aggregations?: { [key: string]: Aggregation }; 99 | defaultAggregation?: string; 100 | associations?: { [key: string]: Association }; 101 | events?: { [key: string]: UI5Event }; 102 | methods?: Array; // TODO (used for sap/ui/core/webc/WebComponent) 103 | getters?: Array; // TODO (used for sap/ui/core/webc/WebComponent) 104 | annotations?: Record; // TODO 105 | designtime?: boolean | string; 106 | designTime?: boolean | string; 107 | stereotype?: null; 108 | metadataClass?: undefined; 109 | library?: string; 110 | //dnd: any, 111 | 112 | abstract?: boolean; 113 | final?: boolean; 114 | 115 | generatedJSDoc?: GeneratedJSDoc; 116 | } 117 | 118 | type TypeForMetadataSectionName = { 119 | properties: Property; 120 | aggregations: Aggregation; 121 | associations: Association; 122 | events: UI5Event; 123 | }; 124 | 125 | const enum MethodType { 126 | PropertySet = "PropertySet", 127 | PropertyGet = "PropertyGet", 128 | PropertyBind = "PropertyBind", 129 | PropertyUnbind = "PropertyUnbind", 130 | 131 | AggregationGet = "AggregationGet", 132 | AggregationInsert = "AggregationInsert", 133 | AggregationAdd = "AggregationAdd", 134 | AggregationRemove = "AggregationRemove", 135 | AggregationRemoveAll = "AggregationRemoveAll", 136 | AggregationIndexOf = "AggregationIndexOf", 137 | AggregationSet = "AggregationSet", 138 | AggregationDestroy = "AggregationDestroy", 139 | AggregationBind = "AggregationBind", 140 | AggregationUnbind = "AggregationUnbind", 141 | 142 | AssociationGet = "AssociationGet", 143 | AssociationAdd = "AssociationAdd", 144 | AssociationRemove = "AssociationRemove", 145 | AssociationRemoveAll = "AssociationRemoveAll", 146 | AssociationSet = "AssociationSet", 147 | 148 | EventAttach = "EventAttach", 149 | EventAttachWithData = "EventAttachWithData", 150 | EventDetach = "EventDetach", 151 | EventFire = "EventFire", 152 | } 153 | type GeneratedJSDoc = Partial<{ 154 | [name in MethodType]: { [name: string]: string }; 155 | }>; 156 | 157 | interface Doclet { 158 | description: string; 159 | deprecated: string; 160 | since: string; 161 | experimental: string; 162 | augments: string; 163 | } 164 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/tsconfig-testcontrol.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src/test/samples/sampleControl"], 4 | "outDir": "./src/test/dist", 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "sourceMap": true /* Generates corresponding '.map' file. */, 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "strictNullChecks": false /* Enable strict null checks. */, 10 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "skipLibCheck": true /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 14 | }, 15 | "include": ["./src/test/samples/sampleControl/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/tsconfig-testmanagedobject.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src/test/samples/sampleManagedObject"], 4 | "outDir": "./src/test/dist", 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "sourceMap": true /* Generates corresponding '.map' file. */, 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "strictNullChecks": false /* Enable strict null checks. */, 10 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "skipLibCheck": true /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 14 | }, 15 | "include": ["./src/test/samples/sampleManagedObject/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/tsconfig-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src/test"], 4 | "outDir": "./src/test/dist", 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "sourceMap": true /* Generates corresponding '.map' file. */, 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "strictNullChecks": false /* Enable strict null checks. */, 10 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "skipLibCheck": true /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 14 | }, 15 | "include": ["./src/test/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/tsconfig-testwebcomponent.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["./src/test/samples/sampleWebComponent"], 4 | "outDir": "./src/test/dist", 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "sourceMap": true /* Generates corresponding '.map' file. */, 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "strictNullChecks": false /* Enable strict null checks. */, 10 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "skipLibCheck": true /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 14 | }, 15 | "include": ["./src/test/samples/sampleWebComponent/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ts-interface-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "dist", 5 | "target": "es6", 6 | "module": "commonjs", 7 | "sourceMap": true, 8 | "strict": true, 9 | "strictNullChecks": false, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["./src/**/*"], 17 | "exclude": ["./src/test"] 18 | } 19 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/.gitignore: -------------------------------------------------------------------------------- 1 | # temp artifacts generated during snapshot testing 2 | /test/output-dts-temp 3 | temp-dtslint 4 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/README.md: -------------------------------------------------------------------------------- 1 | # @ui5/openui5-snapshot-test 2 | 3 | This is a test package for the dts-generator package. This test package is used in two development flows: 4 | 5 | ## Local Playground 6 | 7 | By running the `re-generate` npm script you can inspect how changes to the `dts-generator` 8 | would affect the output d.ts files (in `output-dts` dir). 9 | 10 | ## Snapshot Testing 11 | 12 | On each CI build, the d.ts files would be re-generated and compared 13 | to the source controlled contents in `output-dts` dir. This has two benefits: 14 | 1. When a code change is not supposed to have an effect on the generated `*.d.ts` files (e.g. refactoring), then the test is likely to reveal any unintended effect. 15 | 2. This forces the `output-dts` contents to be synced in any PR 16 | which means the expected changes to the .d.ts files will also be **reviewed** in new PRs. 17 | 18 | Note that this means that if you have changed the `dts-generator` package's output 19 | you will need to run the `re-generate` script in this package to update the "expected d.ts results". 20 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/input-sdk/openui5-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "sap.ui.core": { 3 | "npmPackageName": "@openui5/sap.ui.core", 4 | "version": "1.120.30", 5 | "gav": "com.sap.ui5:core:1.120.30:jar", 6 | "dependencies": [], 7 | "optionalDependencies": [] 8 | }, 9 | "sap.m": { 10 | "npmPackageName": "@openui5/sap.m", 11 | "version": "1.120.30", 12 | "gav": "com.sap.ui5:mobile:1.120.30:jar", 13 | "dependencies": ["sap.ui.core", "sap.ui.layout", "sap.ui.unified"], 14 | "optionalDependencies": [] 15 | }, 16 | "sap.f": { 17 | "npmPackageName": "@openui5/sap.f", 18 | "version": "1.120.30", 19 | "gav": "com.sap.ui5:fiori:1.120.30:jar", 20 | "dependencies": ["sap.m", "sap.ui.core", "sap.ui.layout"], 21 | "optionalDependencies": [] 22 | }, 23 | "sap.tnt": { 24 | "npmPackageName": "@openui5/sap.tnt", 25 | "version": "1.120.30", 26 | "gav": "com.sap.ui5:tnt:1.120.30:jar", 27 | "dependencies": ["sap.m", "sap.ui.core"], 28 | "optionalDependencies": [] 29 | }, 30 | "sap.ui.layout": { 31 | "npmPackageName": "@openui5/sap.ui.layout", 32 | "version": "1.120.30", 33 | "gav": "com.sap.ui5:layout:1.120.30:jar", 34 | "dependencies": ["sap.ui.core"], 35 | "optionalDependencies": [] 36 | }, 37 | "sap.ui.unified": { 38 | "npmPackageName": "@openui5/sap.ui.unified", 39 | "version": "1.120.30", 40 | "gav": "com.sap.ui5:unified:1.120.30:jar", 41 | "dependencies": ["sap.ui.core"], 42 | "optionalDependencies": [] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/input-sdk/sap.f.dtsgenrc: -------------------------------------------------------------------------------- 1 | { 2 | "forwardDeclarations": { 3 | "sap.f": [ 4 | { 5 | "kind": "interface", 6 | "name": "sap.tnt.IToolHeader", 7 | "module": "sap/tnt/library", 8 | "export": "IToolHeader" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/input-sdk/sap.m.dtsgenrc: -------------------------------------------------------------------------------- 1 | { 2 | "forwardDeclarations": { 3 | "sap.m": [ 4 | { 5 | "kind": "interface", 6 | "name": "sap.f.IDynamicPageStickyContent", 7 | "module": "sap/f/library", 8 | "export": "IDynamicPageStickyContent" 9 | }, 10 | { 11 | "kind": "interface", 12 | "name": "sap.f.IShellBar", 13 | "module": "sap/f/library", 14 | "export": "IShellBar" 15 | } 16 | ] 17 | }, 18 | "namespacesToInterfaces": { 19 | "InstanceManager": true, 20 | "URLHelper": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/lib/download-sdk.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { pick } = require("lodash"); 3 | const { writeJsonSync } = require("fs-extra"); 4 | const { log } = require("./utils/logger"); 5 | 6 | /* 7 | * This file is the entry point for the download of SAPUI5 libraries for the test in this sub-project. It: 8 | * 1. Reads the version of the snapshot from the local package.json file. 9 | * 2. Fetches the metadata of SAPUI5 libraries. 10 | * 3. Fetches the possible OpenUI5 library names and filters out the metadata of OpenUI5 libraries. 11 | * 4. Expands the transitive dependencies of the OpenUI5 libraries. 12 | * 5. Downloads the api.json files of the libraries. 13 | */ 14 | 15 | async function main() { 16 | const { 17 | downloadApiJsonFiles, 18 | downloadDtsgenrcFiles, 19 | expandTransitiveDeps, 20 | getOpenUI5PossibleLibNames, 21 | getSAPUI5LibsMeta, 22 | } = await import("@ui5/dts-generator/src/js-utils/ui5-metadata.js"); 23 | const pkgJson = require("../package.json"); 24 | const version = pkgJson.snapshot.version; 25 | const ui5LibsMeta = await getSAPUI5LibsMeta(version); 26 | const possibleOpenUI5LibNames = await getOpenUI5PossibleLibNames(); 27 | const openUI5Meta = pick(ui5LibsMeta, possibleOpenUI5LibNames); 28 | const allDependentOpenUI5Libs = expandTransitiveDeps( 29 | pkgJson.snapshot.libs, 30 | openUI5Meta 31 | ); 32 | 33 | const inputSdkDir = resolve(__dirname, "..", "input-sdk"); 34 | await downloadApiJsonFiles(allDependentOpenUI5Libs, version, inputSdkDir); 35 | await downloadDtsgenrcFiles(allDependentOpenUI5Libs, version, inputSdkDir); 36 | const openUI5MetaPath = resolve(inputSdkDir, "openui5-meta.json"); 37 | log(`writing: ${openUI5MetaPath}`); 38 | writeJsonSync(openUI5MetaPath, pick(openUI5Meta, allDependentOpenUI5Libs)); 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/lib/generate-dts.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { genDtsToDir } = require("./utils/dts-gen-wrapper"); 3 | 4 | const inputDir = resolve(__dirname, "..", "input-sdk"); 5 | const outputDir = resolve(__dirname, "..", "./output-dts"); 6 | 7 | genDtsToDir({ inputDir, outputDir }); 8 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/lib/utils/dts-gen-wrapper.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { readdirSync } = require('fs'); 3 | const { emptyDirSync, readJsonSync, writeFileSync } = require("fs-extra"); 4 | const { map, keys, mapValues, uniq, flatMap } = require("lodash"); 5 | 6 | /** 7 | * 8 | * @param {string} inputDir 9 | * @param {string} outputDir 10 | */ 11 | async function genDtsToDir({ inputDir, outputDir }) { 12 | const { generateFromObjects } = await import("@ui5/dts-generator"); 13 | const { loadDirectives } = await import("../../../../packages/dts-generator/src/js-utils/ui5-metadata.js"); // not exposed as api of the package, hence referenced directly 14 | 15 | function readJsonApi(libName) { 16 | const libJsonPath = resolve(inputDir, libName + ".api.json"); 17 | const libJsonData = readJsonSync(libJsonPath); 18 | // Generating the api.jsons using to ui5-cli seems to lose the `library` property 19 | libJsonData.library = libName; 20 | return libJsonData; 21 | } 22 | 23 | function getTransitiveDeps(deps) { 24 | return uniq( 25 | flatMap(deps, (currDep) => { 26 | return [currDep].concat( 27 | getTransitiveDeps(librariesDirectDeps[currDep]) 28 | ); 29 | }) 30 | ); 31 | } 32 | 33 | // const outputDir = resolve(__dirname, "..", "./output-dts"); 34 | emptyDirSync(outputDir); 35 | 36 | const relevantUI5MetaPath = resolve(inputDir, "openui5-meta.json"); 37 | const relevantOpenUI5Meta = readJsonSync(relevantUI5MetaPath); 38 | 39 | const librariesDirectDeps = mapValues( 40 | relevantOpenUI5Meta, 41 | (_) => _.dependencies 42 | ); 43 | 44 | const librariesAllDeps = mapValues(librariesDirectDeps, getTransitiveDeps); 45 | const allInputDirFiles = readdirSync(inputDir).map((file) => resolve(inputDir, file)); 46 | const directiveFiles = allInputDirFiles.filter(file => file.endsWith(".dtsgenrc")); 47 | const directives = await loadDirectives(directiveFiles); 48 | 49 | // use for...of to allow for an async function 50 | for (const [libName, deps] of Object.entries(librariesAllDeps)) { 51 | console.log(`Generating type definitions for <${libName}> library.`); 52 | const depsJsonsData = map(deps, readJsonApi); 53 | const libJsonData = readJsonApi(libName); 54 | const libDTSResult = await generateFromObjects({ 55 | apiObject: libJsonData, 56 | directives, 57 | dependencyApiObjects: depsJsonsData, 58 | generateGlobals: false 59 | }); 60 | 61 | writeFileSync( 62 | resolve(outputDir, libDTSResult.library + ".d.ts"), 63 | libDTSResult.dtsText 64 | ); 65 | } 66 | 67 | const imports = map( 68 | keys(librariesDirectDeps), 69 | (depLibName) => `/// ` 70 | ); 71 | 72 | writeFileSync( 73 | resolve(outputDir, "index.d.ts"), 74 | imports.join("\n") + "\n", 75 | "UTF8" 76 | ); 77 | } 78 | 79 | module.exports = { 80 | genDtsToDir, 81 | }; 82 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/lib/utils/logger.js: -------------------------------------------------------------------------------- 1 | const { noop } = require("lodash"); 2 | 3 | const silent = false; 4 | const log = silent ? noop : (_) => console.log(_); 5 | const error = silent ? noop : (_) => console.error(_); 6 | 7 | module.exports = { 8 | log, 9 | error, 10 | }; 11 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/output-dts/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5/openui5-snapshot-test", 3 | "private": true, 4 | "version": "3.2.7", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:SAP/ui5-typescript.git", 9 | "directory": "test-packages/openui5-snapshot-test" 10 | }, 11 | "dependencies": { 12 | "@ui5/dts-generator": "link:../../packages/dts-generator", 13 | "fs-extra": "11.2.0", 14 | "lodash": "4.17.21", 15 | "typescript": "5.5.2" 16 | }, 17 | "devDependencies": { 18 | "@types/jquery": "3.5.13", 19 | "@types/qunit": "2.5.4" 20 | }, 21 | "scripts": { 22 | "ci": "npm-run-all test", 23 | "test": "mocha \"./test/**/*spec.js\"", 24 | "re-generate": "npm-run-all sdk:* dts:*", 25 | "sdk:download": "node ./lib/download-sdk.js", 26 | "sdk:format": "prettier --write \"input-sdk/*.json\"", 27 | "dts:generate": "node ./lib/generate-dts.js", 28 | "dts:verify": "tsc -p ." 29 | }, 30 | "mocha": { 31 | "diff": false 32 | }, 33 | "snapshot": { 34 | "version": "1.120.30", 35 | "libs": [ 36 | "sap.ui.core", 37 | "sap.m", 38 | "sap.f", 39 | "sap.tnt" 40 | ] 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/playground/playground.js: -------------------------------------------------------------------------------- 1 | sap.ui.define( 2 | ["sap/ui/core/mvc/Controller"], 3 | /** 4 | * @param {typeof import('sap/ui/core/mvc/Controller').default} Controller 5 | */ 6 | function (Controller) { 7 | Controller.extend("foo", { 8 | // bar: function () { 9 | // this.getVlah(); 10 | // } 11 | }); 12 | const ui5Instance = new Controller("666"); 13 | 14 | // "own" instance methods calls 15 | ui5Instance.createId("bamba"); 16 | ui5Instance.onBeforeRendering(); 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/test/snapshot-spec.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, readFileSync } = require("fs"); 2 | const { resolve, relative } = require("path"); 3 | const { forEach, map } = require("lodash"); 4 | const { expect } = require("chai"); 5 | const { emptyDirSync } = require("fs-extra"); 6 | 7 | const { genDtsToDir } = require("../lib/utils/dts-gen-wrapper"); 8 | 9 | describe("The OpenUI5 d.ts snapshots", async () => { 10 | const projectRootDir = resolve(__dirname, ".."); 11 | const apiJsonDir = resolve(__dirname, "..", "input-sdk"); 12 | const snapshotsDir = resolve(projectRootDir, "output-dts"); 13 | const tempOutDir = resolve(__dirname, "./output-dts-temp"); 14 | 15 | const apiJsonSDKFiles = readdirSync(apiJsonDir); 16 | // Only interested in the actual api.json files 17 | const relevantApiJsonSDKFiles = apiJsonSDKFiles.filter((_) => _.endsWith(".api.json")); 18 | const relevantDTSFiles = map(relevantApiJsonSDKFiles, (_) => 19 | _.replace(".api.json", ".d.ts") 20 | ); 21 | 22 | before( function () { 23 | this.timeout(60000); 24 | return genDtsToDir({ inputDir: apiJsonDir, outputDir: tempOutDir }); // return the Promise to signal the test has to wait 25 | }); 26 | 27 | forEach(relevantDTSFiles, (_) => { 28 | it(`"${_}" is up-to-date`, () => { 29 | const snapshotPath = resolve(snapshotsDir, _); 30 | const reGenPath = resolve(tempOutDir, _); 31 | const snapshotText = readFileSync(snapshotPath, "UTF-8"); 32 | const reGenText = readFileSync(reGenPath, "UTF-8"); 33 | expect( 34 | snapshotText === reGenText, 35 | `${relative(projectRootDir, snapshotPath)} !== ${relative( 36 | projectRootDir, 37 | reGenPath 38 | )} you may need to run the "re-generate" script` 39 | ).to.be.true; 40 | }); 41 | }); 42 | 43 | after(() => { 44 | emptyDirSync(tempOutDir); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test-packages/openui5-snapshot-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["playground", "output-dts"], 3 | "compilerOptions": { 4 | "module": "ES2022", 5 | "target": "ES2022", 6 | "noEmit": true, 7 | "checkJs": true, 8 | "allowJs": true, 9 | "types": [ 10 | "../../node_modules/@types/jquery", 11 | "../../node_modules/@types/qunit" 12 | ] 13 | } 14 | } 15 | --------------------------------------------------------------------------------