From de6134f9dfe0ae9892f7d5e22f7cfcffe0703137 Mon Sep 17 00:00:00 2001 From: Peter Muessig Date: Mon, 7 Aug 2023 22:27:29 +0200 Subject: [PATCH] feat(ui5-tooling-transpile): integrate ts-interface-generator If a TypeScript project adds the dependency to the ts-interface-generator the ui5-tooling-transpile-middleware tries to start the watch mode so that changes to control files will automatically update the interfaces and that code completion can work properly. For now this only works for the local project, not for dependencies. --- .prettierignore | 1 + packages/ui5-tooling-transpile/README.md | 3 ++ .../ui5-tooling-transpile/lib/middleware.js | 32 +++++++++++++++-- packages/ui5-tooling-transpile/lib/task.js | 2 +- packages/ui5-tooling-transpile/lib/util.js | 22 ++++++++++-- packages/ui5-tooling-transpile/package.json | 8 +++++ pnpm-lock.yaml | 35 ++++++++++-------- showcases/ui5-tsapp-simple/package.json | 3 +- .../webapp/control/SimpleControl.gen.d.ts | 19 ++++++++++ .../webapp/control/SimpleControl.ts | 36 +++++++++++++++++++ showcases/ui5-tslib/package.json | 2 +- 11 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 showcases/ui5-tsapp-simple/webapp/control/SimpleControl.gen.d.ts create mode 100644 showcases/ui5-tsapp-simple/webapp/control/SimpleControl.ts diff --git a/.prettierignore b/.prettierignore index 045e6342a..e0d8f6813 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,3 +21,4 @@ pnpm-lock.yaml /**/*.svg /**/*.png /**/*.md +/**/*.gen.d.ts diff --git a/packages/ui5-tooling-transpile/README.md b/packages/ui5-tooling-transpile/README.md index dd0ec75bc..8582a5351 100644 --- a/packages/ui5-tooling-transpile/README.md +++ b/packages/ui5-tooling-transpile/README.md @@ -41,6 +41,9 @@ npm install ui5-tooling-transpile --save-dev - transformTypeScript: `boolean` (old alias: transpileTypeScript) if enabled, the tooling extension transforms TypeScript sources; the default value is derived from the existence of a `tsconfig.json` in the root folder of the project - if the file exists the configuration option is `true` otherwise `false`; setting this configuration option overrules the automatic determination +- generateTsInterfaces: `boolean|undefined` (*experimental feature*) + option requires a dependency to the `@ui5/ts-interface-generator` when either the value of the option is `true` or `undefined` and the project is a TypeScript-based project or the `transformTypeScript` option is set to `true` - can be forced to be inactive by setting the option to `false` (only relevant for the middleware) + - generateDts: `boolean` if enabled, the tooling extension will generate type definitions (`.d.ts`) files; by default for projects of type `library` this option is considered as `true` and for other projects such as `application` this option is considered as `false` by default (is only relevant in case of transformTypeScript is `true`) diff --git a/packages/ui5-tooling-transpile/lib/middleware.js b/packages/ui5-tooling-transpile/lib/middleware.js index 8bfc6d21b..aa7526be2 100644 --- a/packages/ui5-tooling-transpile/lib/middleware.js +++ b/packages/ui5-tooling-transpile/lib/middleware.js @@ -1,4 +1,5 @@ /* eslint-disable jsdoc/check-param-names */ +const path = require("path"); const parseurl = require("parseurl"); /** @@ -17,7 +18,7 @@ const parseurl = require("parseurl"); * [MiddlewareUtil]{@link module:@ui5/server.middleware.MiddlewareUtil} instance * @param {object} parameters.options Options * @param {string} [parameters.options.configuration] Custom server middleware configuration if given in ui5.yaml - * @returns {function} Middleware function to use + * @returns {Function} Middleware function to use */ module.exports = async function ({ log, resources, options, middlewareUtil }) { const { @@ -26,7 +27,8 @@ module.exports = async function ({ log, resources, options, middlewareUtil }) { normalizeLineFeeds, determineResourceFSPath, transformAsync, - shouldHandlePath + shouldHandlePath, + resolveNodeModule } = require("./util")(log); const cwd = middlewareUtil.getProject().getRootPath() || process.cwd(); @@ -86,6 +88,32 @@ module.exports = async function ({ log, resources, options, middlewareUtil }) { ); } + // if the TypeScript interfaces should be created, launch the ts-interface-generator in watch mode + if (config.generateTsInterfaces) { + const generateTSInterfacesAPI = resolveNodeModule( + "@ui5/ts-interface-generator/dist/generateTSInterfacesAPI", + cwd + ); + if (generateTSInterfacesAPI) { + const { main } = require(generateTSInterfacesAPI); + try { + config.debug && log.info(`Starting "@ui5/ts-interface-generator" in watch mode...`); + main({ + watch: true, + logLevel: config.debug ? log.constructor.getLevel() : "error", + config: path.join(cwd, "tsconfig.json") + }); + } catch (e) { + log.error(e); + } + } else { + config.debug && + log.warn( + `Missing dependency "@ui5/ts-interface-generator"! TypeScript interfaces will not be generated until dependency has been added...` + ); + } + } + return async (req, res, next) => { const pathname = parseurl(req)?.pathname; if (pathname.endsWith(".js") && shouldHandlePath(pathname, config.excludes, config.includes)) { diff --git a/packages/ui5-tooling-transpile/lib/task.js b/packages/ui5-tooling-transpile/lib/task.js index fe0caada0..ce6cc35ed 100644 --- a/packages/ui5-tooling-transpile/lib/task.js +++ b/packages/ui5-tooling-transpile/lib/task.js @@ -290,7 +290,7 @@ module.exports = async function ({ log, workspace /*, dependencies*/, taskUtil, config.debug && log.info(` + [.d.ts] index.d.ts`); const pckgJsonFile = path.join(cwd, "package.json"); if (fs.existsSync(pckgJsonFile)) { - const pckgJson = require(pckgJsonFile); + const pckgJson = JSON.parse(fs.readFileSync(pckgJsonFile, { encoding: "utf8" })); if (!pckgJson.types) { log.warn( ` /!\\ package.json has no "types" property! Add it and point to "index.d.ts" in build destination!` diff --git a/packages/ui5-tooling-transpile/lib/util.js b/packages/ui5-tooling-transpile/lib/util.js index 4f9e8e7b1..9462573b8 100644 --- a/packages/ui5-tooling-transpile/lib/util.js +++ b/packages/ui5-tooling-transpile/lib/util.js @@ -198,8 +198,8 @@ module.exports = function (log) { const tscJsonPath = path.join(cwd, "tsconfig.json"); const isTypeScriptProject = fs.existsSync(tscJsonPath); - // read package.json and tsconfig.json to determine whether to transpile dependencies or not - if (isTypeScriptProject && !config.transpileDependencies) { + // read tsconfig.json to determine whether to transpile dependencies or not + if (isTypeScriptProject && !config.transpileDependencies && fs.existsSync(tscJsonPath)) { const tscJson = JSONC.parse(fs.readFileSync(tscJsonPath, { encoding: "utf8" })); const tsDeps = tscJson?.compilerOptions?.types?.filter((typePkgName) => { try { @@ -221,6 +221,21 @@ module.exports = function (log) { // derive whether TypeScript should be transformed or not const transformTypeScript = config.transformTypeScript ?? config.transpileTypeScript ?? isTypeScriptProject; + // load the pkgJson to determine the existence of the @ui5/ts-interface-generator + // to automatically set the config option generateTsInterfaces (if this is a ts project) + let generateTsInterfaces = config.generateTsInterfaces; + const pkgJsonPath = path.join(cwd, "package.json"); + if (transformTypeScript && generateTsInterfaces === undefined && fs.existsSync(pkgJsonPath)) { + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, { encoding: "utf8" })); + const deps = [ + ...Object.keys(pkgJson?.dependencies || {}), + ...Object.keys(pkgJson?.devDependencies || {}) + ]; + if (deps.indexOf("@ui5/ts-interface-generator") !== -1) { + generateTsInterfaces = true; + } + } + // derive the includes/excludes from the configuration const includes = config.includes || config.includePatterns || []; const defaultExcludes = [".png", ".jpeg", ".jpg"]; // still needed? @@ -244,6 +259,7 @@ module.exports = function (log) { excludes, filePattern, omitTSFromBuildResult: config.omitTSFromBuildResult, + generateTsInterfaces, generateDts: config.generateDts, transpileDependencies: config.transpileDependencies, transformAtStartup: config.transformAtStartup, @@ -448,7 +464,7 @@ module.exports = function (log) { * @param {string} pathname the path name * @param {Array} excludes exclude paths * @param {Array} includes include paths - * @returns true, if the path should be handled + * @returns {boolean} true, if the path should be handled */ shouldHandlePath: function shouldHandlePath(pathname, excludes = [], includes = []) { return ( diff --git a/packages/ui5-tooling-transpile/package.json b/packages/ui5-tooling-transpile/package.json index b4ea0515c..71c24733b 100644 --- a/packages/ui5-tooling-transpile/package.json +++ b/packages/ui5-tooling-transpile/package.json @@ -23,6 +23,14 @@ "devDependencies": { "ava": "^5.3.1" }, + "peerDependencies": { + "@ui5/ts-interface-generator": ">=0.8.0" + }, + "peerDependenciesMeta": { + "@ui5/ts-interface-generator": { + "optional": true + } + }, "scripts": { "lint": "eslint lib", "test": "ava --no-worker-threads" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d39a97086..98cf759f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,9 @@ importers: '@babel/preset-typescript': specifier: ^7.22.5 version: 7.22.5(@babel/core@7.22.10) + '@ui5/ts-interface-generator': + specifier: '>=0.8.0' + version: 0.8.0(typescript@5.1.6) babel-plugin-transform-async-to-promises: specifier: ^0.8.18 version: 0.8.18 @@ -757,8 +760,11 @@ importers: '@ui5/cli': specifier: ^3.4.0 version: 3.4.0 + '@ui5/ts-interface-generator': + specifier: ^0.8.0 + version: 0.8.0(typescript@5.1.6) eslint: - specifier: ^8.47.0 + specifier: ^8.46.0 version: 8.47.0 rimraf: specifier: ^5.0.1 @@ -791,8 +797,8 @@ importers: specifier: ^3.4.0 version: 3.4.0 '@ui5/ts-interface-generator': - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.8.0 + version: 0.8.0(typescript@5.1.6) eslint: specifier: ^8.47.0 version: 8.47.0 @@ -3197,7 +3203,7 @@ packages: resolution: {integrity: sha512-YXmxzxXTP3u9HQpSXvK8qqoAm7VWQIFria3FVMQKkOSkWkph1TNnvt3Q1JvKT7/Jgd1HfTc3QrK09a2FND9+8A==} engines: {node: ^14.17.0 || >=16.0.0} dependencies: - chalk: 4.1.0 + chalk: 4.1.2 execa: 5.0.0 strong-log-transformer: 2.1.0 dev: true @@ -5097,14 +5103,16 @@ packages: transitivePeerDependencies: - supports-color - /@ui5/ts-interface-generator@0.7.0: - resolution: {integrity: sha512-3dCDGr05UnnjMCFm89Mj7m5q2rBtI+zDZ3p3aSbWD463XkC7G3xwXcJ/ynFa8cdWH64H7M+66qQmStuYQ/zHXg==} + /@ui5/ts-interface-generator@0.8.0(typescript@5.1.6): + resolution: {integrity: sha512-V6djTW+IZ3ScOoXQvAIqwSb09nfPZniAkjTlqAFSAD7JsTLK4RwjMD4u/MpUKYewBC5RIElSqz2jScNH+ttLiw==} hasBin: true + peerDependencies: + typescript: '>=4.4.3' dependencies: hjson: 3.2.2 loglevel: 1.8.1 + typescript: 5.1.6 yargs: 17.7.2 - dev: true /@wdio/cli@7.32.0(typescript@5.1.6): resolution: {integrity: sha512-JT6h5Tk7ZjaMWVFNCZFz+Y4vQRVmtUAlZjx6ECqDQuumItu9Fqf9NEmoFHOoyFJqz6mJ/85x/IQb9nfQf7HH+w==} @@ -9841,7 +9849,7 @@ packages: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.5 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 dev: true @@ -9852,7 +9860,7 @@ packages: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.8 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 dev: true @@ -10177,7 +10185,6 @@ packages: /hjson@3.2.2: resolution: {integrity: sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q==} hasBin: true - dev: true /homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} @@ -11138,7 +11145,7 @@ packages: resolution: {integrity: sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.0 + chalk: 4.1.2 diff-sequences: 29.4.3 jest-get-type: 29.4.3 pretty-format: 29.6.2 @@ -12190,7 +12197,6 @@ packages: /loglevel@1.8.1: resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} engines: {node: '>= 0.6.0'} - dev: true /long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} @@ -13097,7 +13103,7 @@ packages: array-differ: 3.0.0 array-union: 2.1.0 arrify: 2.0.1 - minimatch: 3.0.5 + minimatch: 3.1.2 dev: true /mustache@2.2.1: @@ -13596,7 +13602,7 @@ packages: '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.6 axios: 1.4.0 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 7.0.4 @@ -16924,7 +16930,6 @@ packages: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true - dev: true /ua-parser-js@0.7.35: resolution: {integrity: sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==} diff --git a/showcases/ui5-tsapp-simple/package.json b/showcases/ui5-tsapp-simple/package.json index c5baf3c19..6fe77984e 100644 --- a/showcases/ui5-tsapp-simple/package.json +++ b/showcases/ui5-tsapp-simple/package.json @@ -24,8 +24,9 @@ "@types/openui5": "1.117.0", "@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/parser": "^6.3.0", + "@ui5/ts-interface-generator": "^0.8.0", "@ui5/cli": "^3.4.0", - "eslint": "^8.47.0", + "eslint": "^8.46.0", "rimraf": "^5.0.1", "typescript": "^5.1.6", "ui5-middleware-livereload": "workspace:^", diff --git a/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.gen.d.ts b/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.gen.d.ts new file mode 100644 index 000000000..a055b5322 --- /dev/null +++ b/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.gen.d.ts @@ -0,0 +1,19 @@ +import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; +import { $ControlSettings } from "sap/ui/core/Control"; + +declare module "./SimpleControl" { + + /** + * Interface defining the settings object used in constructor calls + */ + interface $SimpleControlSettings extends $ControlSettings { + text?: string | PropertyBindingInfo; + } + + export default interface SimpleControl { + + // property: text + getText(): string; + setText(text: string): this; + } +} diff --git a/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.ts b/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.ts new file mode 100644 index 000000000..edf6c7674 --- /dev/null +++ b/showcases/ui5-tsapp-simple/webapp/control/SimpleControl.ts @@ -0,0 +1,36 @@ +import Control from "sap/ui/core/Control"; +import RenderManager from "sap/ui/core/RenderManager"; +import type { MetadataOptions } from "sap/ui/core/Element"; + +/** + * @namespace ui5.ecosystem.demo.simpletsapp.control + */ +export default class SimpleControl extends Control { + static readonly metadata: MetadataOptions = { + properties: { + text: "string", + }, + }; + + // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures + constructor(idOrSettings?: string | $SimpleControlSettings); + constructor(id?: string, settings?: $SimpleControlSettings); + constructor(id?: string, settings?: $SimpleControlSettings) { + super(id, settings); + } + + renderer = { + apiVersion: 2, + render: (rm: RenderManager, control: SimpleControl) => { + rm.openStart("div", control); + rm.style("font-size", "2rem"); + rm.style("width", "2rem"); + rm.style("height", "2rem"); + rm.style("display", "inline-block"); + rm.style("color", "blue"); + rm.openEnd(); + rm.text(control.getText()); + rm.close("div"); + }, + }; +} diff --git a/showcases/ui5-tslib/package.json b/showcases/ui5-tslib/package.json index 0e419b1f5..57af72707 100644 --- a/showcases/ui5-tslib/package.json +++ b/showcases/ui5-tslib/package.json @@ -32,7 +32,7 @@ "@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/parser": "^6.3.0", "@ui5/cli": "^3.4.0", - "@ui5/ts-interface-generator": "^0.7.0", + "@ui5/ts-interface-generator": "^0.8.0", "eslint": "^8.47.0", "karma": "^6.4.2", "karma-chrome-launcher": "^3.2.0",