diff --git a/packages/ai/custom-runner.cjs b/packages/ai/custom-runner.cjs new file mode 100644 index 000000000000..0be90b173cbd --- /dev/null +++ b/packages/ai/custom-runner.cjs @@ -0,0 +1,29 @@ +const getScripts = require("@ui5/webcomponents-tools/components-package/custom-runner-commands.js"); + +const options = { + port: 8082, + portStep: 2, + aiPackage: true, + noWatchTS: true, + dev: true, + internal: { + cypress_code_coverage: false, + cypress_acc_tests: false, + }, +}; + +const scripts = getScripts(options); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/ai/package.json b/packages/ai/package.json index 310f08a43dab..8060fdca3fde 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -16,19 +16,19 @@ "ui5" ], "scripts": { - "clean": "wc-dev clean", - "lint": "wc-dev lint", - "lint:scope": "nps scope.lint", - "start": "wc-dev start", - "build": "wc-dev build", - "watch": "wc-dev watch", - "generate": "nps generate", - "generateAPI": "nps generateAPI", - "bundle": "nps build.bundle", - "test": "nps test-cy-ci", - "test:cypress": "nps test-cy-ci", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "lint:scope": "node ./custom-runner.cjs scope:lint", + "start": "node ./custom-runner.cjs start", + "build": "node ./custom-runner.cjs build", + "watch": "node ./custom-runner.cjs watch", + "generate": "node ./custom-runner.cjs generate", + "generateAPI": "node ./custom-runner.cjs generateAPI", + "bundle": "node ./custom-runner.cjs build:bundle", + "test": "node ./custom-runner.cjs test-cy-ci", + "test:cypress": "node ./custom-runner.cjs test-cy-ci", "test:cypress:single": "npx cypress run --component --browser chrome --spec", - "test:cypress:open": "nps test-cy-open", + "test:cypress:open": "node ./custom-runner.cjs test-cy-open", "test:ssr": "node -e \"import('./test/ssr/component-imports.js')\"", "create-ui5-element": "wc-create-ui5-element", "prepublishOnly": "tsc -b" diff --git a/packages/base/custom-runner.cjs b/packages/base/custom-runner.cjs new file mode 100644 index 000000000000..5e0e72ffb9a5 --- /dev/null +++ b/packages/base/custom-runner.cjs @@ -0,0 +1,281 @@ +const resolve = require("resolve"); +const path = require("path"); +const BuildRunner = require("@ui5/webcomponents-tools/task-runner/build-runner"); +const buildI18nJson = require("@ui5/webcomponents-tools/lib/i18n/toJSON"); +const buildI18nDefaultsjs = require("@ui5/webcomponents-tools/lib/i18n/defaults"); +const buildJsonImportsI18n = require("@ui5/webcomponents-tools/lib/generate-json-imports/i18n"); +const amdToES6 = require("@ui5/webcomponents-tools/lib/amd-to-es6/index"); +const noRequire = require("@ui5/webcomponents-tools/lib/amd-to-es6/no-remaining-require"); +const versionScript = require("./lib/generate-version-info/index.cjs"); +const assetParametersScript = require("./lib/generate-asset-parameters/index.cjs"); +const copyAndWatch = require("@ui5/webcomponents-tools/lib/copy-and-watch/index.js").copyAndWatch; +const validate = require("@ui5/webcomponents-tools/lib/cem/validate"); +const copyUsedModules = require("@ui5/webcomponents-tools/lib/copy-list/index.js"); +const removeDevMode = require("@ui5/webcomponents-tools/lib/remove-dev-mode/remove-dev-mode.mjs").default; +const stylesScript = require("./lib/generate-styles/index.js").default; // +const fontFaceScript = require("./lib/css-processors/css-processor-font-face.mjs").default; // + +const runner = new BuildRunner(); + +const LIB = path.join(__dirname, `../tools/lib/`); + +const viteConfig = `-c "${require.resolve("@ui5/webcomponents-tools/components-package/vite.config.js")}"`; + +runner.addTask("clean", { + dependencies: [ + "rimraf src/generated", + "rimraf dist", + "rimraf .port", + ], +}); + +runner.addTask("lint", { + dependencies: [ + "eslint .", + ], +}); + +runner.addTask("generate", { + dependencies: [ + "clean", + "build:i18n", + "integrate", + "copy", + "generateAssetParameters", + "generateVersionInfo", + "generateStyles", + "generateFontFace", + "build:jsonImports", + ], + crossEnv: { + UI5_TS: true, + }, +}); + +runner.addTask("prepare", { + dependencies: [ + "clean", + "build:i18n", + "integrate", + "copy", + "generateAssetParameters", + "generateVersionInfo", + "generateStyles", + "generateFontFace", + "typescript", + "integrate:no-remaining-require", + "build:jsonImports", + ], + crossEnv: { + UI5_TS: true, + }, +}); + +runner.addTask("typescript", { + dependencies: [ + "tsc -b", + ], +}); + +runner.addTask("integrate", { + dependencies: [ + "integrate:copy-used-modules", + "integrate:amd-to-es6", + "integrate:no-remaining-require", + "integrate:third-party", + ], + parallel: false, // ??? +}); + +runner.addTask("integrate:copy-used-modules", { + callback: async () => { + await copyUsedModules("./used-modules.txt", "dist/"); + return "Used modules copied."; + }, +}); + +runner.addTask("integrate:amd-to-es6", { + callback: async () => { + await amdToES6("dist/"); + return ""; + }, +}); + +runner.addTask("integrate:no-remaining-require", { + callback: async () => { + await noRequire("dist/"); + return ""; + }, +}); + +runner.addTask("integrate:third-party", { + dependencies: [ + "integrate:third-party:copy", + "integrate:third-party:fix", + ], + parallel: false, // ??? +}); + +runner.addTask("integrate:third-party:copy", { + callback: async () => { + await copyAndWatch("../../node_modules/@openui5/sap.ui.core/src/sap/ui/thirdparty/caja-html-sanitizer.js", "dist/sap/ui/thirdparty/", { silent: true }); + return "Third party files copied."; + }, +}); + +runner.addTask("integrate:third-party:fix", { + dependencies: [ + "replace-in-file 240 xA0 dist/sap/ui/thirdparty/caja-html-sanitizer.js", + ], +}); + +runner.addTask("build", { + dependencies: [ + "prepare", + ], +}); + +runner.addTask("build:bundle", { + dependencies: [ + `vite build ${viteConfig}`, + ], +}); + +runner.addTask("build:i18n", { + dependencies: [ + "build:i18n:defaultsjs", + "build:i18n:json", + ], + parallel: true, +}); + +runner.addTask("build:i18n:defaultsjs", { + callback: async () => { + await buildI18nDefaultsjs("src/i18n", "src/generated/i18n", true); + return "i18n default file generated."; + }, +}); + +runner.addTask("build:i18n:json", { + callback: async () => { + await buildI18nJson("src/i18n", "dist/generated/assets/i18n"); + return "Message bundle JSON files generated."; + }, +}); + +runner.addTask("build:jsonImports", { + dependencies: [ + "build:jsonImports:i18n", + ], +}); + +runner.addTask("build:jsonImports:i18n", { + callback: async () => { + await buildJsonImportsI18n("dist/generated/assets/i18n", "src/generated/json-imports", true); + return "Generated i18n JSON imports."; + }, +}); + +runner.addTask("copy", { + dependencies: [ + "copy:src", + ], +}); + +runner.addTask("copy:src", { + callback: async () => { + await copyAndWatch("src/**/*.{js,css,d.ts}", "dist/", { silent: true }); + return "Source files copied."; + }, +}); + +runner.addTask("generateAssetParameters", { + callback: async () => { + await assetParametersScript(); + return "Assets parameters generated."; + }, +}); + +runner.addTask("generateVersionInfo", { + callback: async () => { + await versionScript(); + return "Version info file generated."; + }, +}); + +runner.addTask("generateStyles", { + callback: async () => { + await stylesScript(); + return "Styles files generated."; + }, +}); + +runner.addTask("generateFontFace", { + callback: async () => { + await fontFaceScript(); + return "FontFace CSS generated."; + }, +}); + +runner.addTask("generateProd", { + dependencies: [ + "generateProd:remove-dev-mode", + "generateProd:copy-prod", + ], + parallel: false, // ??? +}); + +runner.addTask("generateProd:remove-dev-mode", { + callback: async () => { + await removeDevMode(); + return "Dev mode removed."; + }, +}); + +runner.addTask("generateProd:copy-prod", { + callback: async () => { + Promise.all([ + copyAndWatch("dist/sap/**/*", "dist/prod/sap/", { silent: true }), + copyAndWatch("dist/thirdparty/preact/**/*.js", "dist/prod/thirdparty/preact/", { silent: true }), + copyAndWatch("dist/generated/assets/**/*.json", "dist/prod/generated/assets/", { silent: true }), + ]); + return "Production files copied."; + }, +}); + +runner.addTask("generateAPI", { + dependencies: [ + "generateAPI:generateCEM", + "generateAPI:validateCEM", + ], +}); + +runner.addTask("generateAPI:generateCEM", { + dependencies: [ + `cem analyze --config "${LIB}/cem/custom-elements-manifest.config.mjs"`, + ], + crossEnv: { + UI5_CEM_MODE: "dev", + }, +}); + +runner.addTask("generateAPI:validateCEM", { + callback: async () => { + await validate({ devMode: "dev" }); + return "CEM validation completed."; + } +}); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log("Available tasks:", Array.from(runner.tasks.keys()).join(", ")); + process.exit(1); + } + + runner.run(taskName).catch(error => { + console.error("Task failed:", error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/base/lib/css-processors/css-processor-font-face.mjs b/packages/base/lib/css-processors/css-processor-font-face.mjs index 4c5b234a33f2..3cc1d577b6ab 100644 --- a/packages/base/lib/css-processors/css-processor-font-face.mjs +++ b/packages/base/lib/css-processors/css-processor-font-face.mjs @@ -3,62 +3,161 @@ import * as path from "path"; import { readFile, writeFile } from "fs/promises"; import { fileURLToPath } from "url"; -const themeBasePackage = JSON.parse(await readFile(fileURLToPath(import.meta.resolve("@sap-theming/theming-base-content/package.json", "utf-8")))); +/** + * Processes font-face CSS declarations by updating URLs and filtering content + * @param {string} text - CSS text containing @font-face declarations + * @param {Object} options - Processing options + * @param {string} options.cdnUrl - CDN URL template for fonts + * @param {string} options.version - Package version for CDN URL + * @param {string[]} options.excludeFonts - Font families to exclude (default: ["SAP-icons"]) + * @param {boolean} options.removeWoff - Whether to remove woff format URLs (default: true) + * @returns {string} Processed CSS text + */ +export const processFontFace = (text, options = {}) => { + const { + cdnUrl = "https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content@{version}/content/Base/baseLib/baseTheme/fonts", + version, + excludeFonts = ["SAP-icons"], + removeWoff = true + } = options; -const processFontFace = (text) => { const declarationExpr = /@font-face\s*{[^}]*}/g; // change font-face src - text = text.replaceAll("../baseTheme/fonts", `https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content@${themeBasePackage.version}/content/Base/baseLib/baseTheme/fonts`); + if (version) { + const finalCdnUrl = cdnUrl.replace("{version}", version); + text = text.replaceAll("../baseTheme/fonts", finalCdnUrl); + } // extract declarations for separate usage let fontFaceDeclarations = [...text.matchAll(declarationExpr)].map(x => x[0]); - // remove SAP-icons - fontFaceDeclarations = fontFaceDeclarations.filter(decl => !decl.includes("SAP-icons")); + // remove excluded fonts + if (excludeFonts.length > 0) { + fontFaceDeclarations = fontFaceDeclarations.filter(decl => + !excludeFonts.some(font => decl.includes(font)) + ); + } - // remove woff urls - fontFaceDeclarations = fontFaceDeclarations.map(decl => { - // @font-face { - // src: url(../baseTheme/fonts/72-Semibold.woff2) format("woff2"), url(../baseTheme/fonts/72-Semibold.woff) format("woff"), local("72-Semibold"); - return decl.replace(/,url\(([^)]+)\.woff\)\ format\("woff"\)/, ''); - }); + // remove woff urls if requested + if (removeWoff) { + fontFaceDeclarations = fontFaceDeclarations.map(decl => { + // @font-face { + // src: url(../baseTheme/fonts/72-Semibold.woff2) format("woff2"), url(../baseTheme/fonts/72-Semibold.woff) format("woff"), local("72-Semibold"); + return decl.replace(/,url\(([^)]+)\.woff\)\ format\("woff"\)/, ''); + }); + } return fontFaceDeclarations.join("\n"); -} - -let fontfacePlugin = { - name: 'fontface', - setup(build) { - build.initialOptions.write = false; - - build.onEnd(result => { - result.outputFiles.forEach(async f => { - let newText = processFontFace(f.text); - const tsPath = path.join(process.cwd(), "src/generated/css/FontFace.css.ts"); - const tsContent = `export default \`${newText}\``; - await writeFile(tsPath, tsContent); +}; + +/** + * Creates an esbuild plugin for processing font-face CSS + * @param {Object} options - Plugin options + * @param {string} options.outputPath - Output file path for generated CSS + * @param {Object} options.processingOptions - Options passed to processFontFace function + * @param {boolean} options.generateTypeScript - Whether to generate TypeScript export (default: true) + * @returns {Object} esbuild plugin + */ +export const createFontFacePlugin = (options = {}) => { + const { + outputPath = path.join(process.cwd(), "src/generated/css/FontFace.css.ts"), + processingOptions = {}, + generateTypeScript = true + } = options; + + return { + name: 'fontface-processor', + setup(build) { + build.initialOptions.write = false; + + build.onEnd(result => { + result.outputFiles.forEach(async f => { + const processedText = processFontFace(f.text, processingOptions); + + if (generateTypeScript) { + const tsContent = `export default \`${processedText}\``; + await writeFile(outputPath, tsContent); + } else { + const cssPath = outputPath.replace(/\.ts$/, ''); + await writeFile(cssPath, processedText); + } + }); }); - }) - }, -} - -// esbuild cannot resolve the node module format when passed as stdin, so resolve the actual file via node resolve -const themeBaseFile = fileURLToPath(import.meta.resolve("@sap-theming/theming-base-content/content/Base/baseLib/sap_horizon/css_variables.css")); - -const config = { - stdin: { - contents: `@import ${JSON.stringify(themeBaseFile)};`, // windows paths contain a backslash which has to be escaped because this will be treated as a string - resolveDir: './', - sourcefile: 'virtual-font-face.css', - loader: 'css', - }, - bundle: true, - minify: true, - plugins: [ - fontfacePlugin, - ], - external: ["*.ttf", "*.woff", "*.woff2"], + }, + }; }; -const result = await esbuild.build(config); +/** + * Creates esbuild configuration for font-face processing + * @param {Object} options - Configuration options + * @param {string} options.themePackage - Theme package name (default: "@sap-theming/theming-base-content") + * @param {string} options.cssFile - CSS file path within the package (default: "content/Base/baseLib/sap_horizon/css_variables.css") + * @param {boolean} options.bundle - Whether to bundle (default: true) + * @param {boolean} options.minify - Whether to minify (default: true) + * @param {string[]} options.external - External dependencies (default: ["*.ttf", "*.woff", "*.woff2"]) + * @param {Object} options.pluginOptions - Options for the font-face plugin + * @returns {Promise} esbuild configuration + */ +export const createFontFaceConfig = async (options = {}) => { + const { + themePackage = "@sap-theming/theming-base-content", + cssFile = "content/Base/baseLib/sap_horizon/css_variables.css", + bundle = true, + minify = true, + external = ["*.ttf", "*.woff", "*.woff2"], + pluginOptions = {} + } = options; + + // Get theme package version + const themeBasePackage = JSON.parse( + await readFile(fileURLToPath(import.meta.resolve(`${themePackage}/package.json`)), "utf-8") + ); + + // Resolve the actual CSS file path + const themeBaseFile = fileURLToPath(import.meta.resolve(`${themePackage}/${cssFile}`)); + + // Set up processing options with version + const processingOptions = { + version: themeBasePackage.version, + ...pluginOptions.processingOptions + }; + + return { + stdin: { + contents: `@import ${JSON.stringify(themeBaseFile)};`, + resolveDir: './', + sourcefile: 'virtual-font-face.css', + loader: 'css', + }, + bundle, + minify, + plugins: [ + createFontFacePlugin({ + ...pluginOptions, + processingOptions + }), + ], + external, + }; +}; + +/** + * Processes font-face CSS and generates TypeScript output + * @param {Object} options - Processing options (same as createFontFaceConfig) + * @returns {Promise} esbuild result + */ +export const processFontFaceCSS = async (options = {}) => { + const config = await createFontFaceConfig(options); + return await esbuild.build(config); +}; + +// Default export for backward compatibility +export default processFontFaceCSS; + +// // If this file is run directly, execute with default configuration +// if (import.meta.url === `file://${process.argv[1]}`) { +// const config = await createFontFaceConfig(); +// const result = await esbuild.build(config); +// console.log('Font-face CSS processing completed:', result); +// } \ No newline at end of file diff --git a/packages/base/lib/generate-asset-parameters/index.js b/packages/base/lib/generate-asset-parameters/index.cjs similarity index 74% rename from packages/base/lib/generate-asset-parameters/index.js rename to packages/base/lib/generate-asset-parameters/index.cjs index d51d4784a021..680168dc06c1 100644 --- a/packages/base/lib/generate-asset-parameters/index.js +++ b/packages/base/lib/generate-asset-parameters/index.cjs @@ -1,5 +1,6 @@ -import fs from "fs/promises"; -import assets from "@ui5/webcomponents-tools/assets-meta.js"; +const fs = require("fs").promises; +const assets = require("@ui5/webcomponents-tools/assets-meta.js"); + const fileContent = `const assetParameters = ${JSON.stringify(assets)}; @@ -22,6 +23,8 @@ const generate = async () => { return fs.writeFile("src/generated/AssetParameters.ts", fileContent); } -generate().then(() => { - console.log("Assets parameters generated."); -}); +// generate().then(() => { +// console.log("Assets parameters generated."); +// }); + +module.exports = generate; \ No newline at end of file diff --git a/packages/base/lib/generate-styles/index.js b/packages/base/lib/generate-styles/index.js index 60cd1639cb95..507e14227d51 100644 --- a/packages/base/lib/generate-styles/index.js +++ b/packages/base/lib/generate-styles/index.js @@ -16,6 +16,8 @@ const generate = async () => { return Promise.all(filesPromises); }; -generate().then(() => { - console.log("Styles files generated."); -}); +// generate().then(() => { +// console.log("Styles files generated."); +// }); + +export default generate; \ No newline at end of file diff --git a/packages/base/lib/generate-version-info/index.js b/packages/base/lib/generate-version-info/index.cjs similarity index 83% rename from packages/base/lib/generate-version-info/index.js rename to packages/base/lib/generate-version-info/index.cjs index df86c5d3e966..6ad40b87b4e6 100644 --- a/packages/base/lib/generate-version-info/index.js +++ b/packages/base/lib/generate-version-info/index.cjs @@ -1,4 +1,4 @@ -import fs from "fs/promises"; +const fs = require("fs").promises; const generate = async () => { const version = JSON.parse(await fs.readFile("package.json")).version; @@ -27,6 +27,8 @@ export default VersionInfo;`; await fs.writeFile("src/generated/VersionInfo.ts", fileContent); } -generate().then(() => { - console.log("Version info file generated."); -}); +// generate().then(() => { +// console.log("Version info file generated."); +// }); + +module.exports = generate diff --git a/packages/base/package.json b/packages/base/package.json index a4914858e51d..d45c50468443 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -43,16 +43,16 @@ }, "types": "./dist", "scripts": { - "clean": "nps clean", - "lint": "nps lint", - "start": "nps start", - "build": "nps build", - "generate": "nps generate", - "generateAPI": "nps generateAPI", - "generateProd": "nps generateProd", - "bundle": "nps build.bundle", - "test": "nps test", - "test:cypress:open": "nps test.test-cy-open", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "start": "node ./custom-runner.cjs start", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", + "generateAPI": "node ./custom-runner.cjs generateAPI", + "generateProd": "node ./custom-runner.cjs generateProd", + "bundle": "node ./custom-runner.cjs build.bundle", + "test": "node ./custom-runner.cjs test", + "test:cypress:open": "node ./custom-runner.cjs test.test-cy-open", "prepublishOnly": "tsc -b" }, "dependencies": { diff --git a/packages/compat/custom-runner.cjs b/packages/compat/custom-runner.cjs new file mode 100644 index 000000000000..1e2848440e0e --- /dev/null +++ b/packages/compat/custom-runner.cjs @@ -0,0 +1,29 @@ +const getScripts = require("@ui5/webcomponents-tools/components-package/custom-runner-commands.js"); + +const options = { + port: 8082, + portStep: 2, + compatPackage: true, + noWatchTS: true, + dev: true, + internal: { + cypress_code_coverage: false, + cypress_acc_tests: false, + }, +}; + +const scripts = getScripts(options); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/compat/package.json b/packages/compat/package.json index da109575ace0..7622498ecfc8 100644 --- a/packages/compat/package.json +++ b/packages/compat/package.json @@ -16,19 +16,19 @@ "ui5" ], "scripts": { - "clean": "wc-dev clean", - "lint": "wc-dev lint", - "lint:scope": "nps scope.lint", - "start": "wc-dev start", - "build": "wc-dev build", - "watch": "wc-dev watch", - "generate": "nps generate", - "generateAPI": "nps generateAPI", - "bundle": "nps build.bundle", - "test": "wc-dev test", - "test:cypress": "nps test-cy-ci", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "lint:scope": "node ./custom-runner.cjs scope:lint", + "start": "node ./custom-runner.cjs start", + "build": "node ./custom-runner.cjs build", + "watch": "node ./custom-runner.cjs watch", + "generate": "node ./custom-runner.cjs generate", + "generateAPI": "node ./custom-runner.cjs generateAPI", + "bundle": "node ./custom-runner.cjs build:bundle", + "test": "node ./custom-runner.cjs test", + "test:cypress": "node ./custom-runner.cjs test-cy-ci", "test:cypress:single": "npx cypress run --component --browser chrome --spec", - "test:cypress:open": "nps test-cy-open", + "test:cypress:open": "node ./custom-runner.cjs test-cy-open", "test:ssr": "node -e \"import('./test/ssr/component-imports.js')\"", "create-ui5-element": "wc-create-ui5-element", "prepublishOnly": "tsc -b" diff --git a/packages/fiori/package-scripts.cjs b/packages/fiori/custom-runner.cjs similarity index 83% rename from packages/fiori/package-scripts.cjs rename to packages/fiori/custom-runner.cjs index 9255a2af5ace..a7be2819921f 100644 --- a/packages/fiori/package-scripts.cjs +++ b/packages/fiori/custom-runner.cjs @@ -1,4 +1,4 @@ -const getScripts = require("@ui5/webcomponents-tools/components-package/nps.js"); +const getScripts = require("@ui5/webcomponents-tools/components-package/custom-runner-commands.js"); const filterOut = [ "sapIllus-Dot", @@ -80,6 +80,16 @@ const options = { const scripts = getScripts(options); -module.exports = { - scripts -}; +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/fiori/package.json b/packages/fiori/package.json index 680ff711b2a6..c0369d7b1732 100644 --- a/packages/fiori/package.json +++ b/packages/fiori/package.json @@ -31,20 +31,20 @@ "./*": "./dist/*" }, "scripts": { - "clean": "wc-dev clean", - "lint": "wc-dev lint", - "lint:scope": "nps scope.lint", - "start": "wc-dev start", - "watch": "wc-dev watch", - "build": "wc-dev build", - "generate": "nps generate", - "generateAPI": "nps generateAPI", - "bundle": "nps build.bundle", - "test": "wc-dev test", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "lint:scope": "node ./custom-runner.cjs scope:lint", + "start": "node ./custom-runner.cjs start", + "watch": "node ./custom-runner.cjs watch", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", + "generateAPI": "node ./custom-runner.cjs generateAPI", + "bundle": "node ./custom-runner.cjs build:bundle", + "test": "node ./custom-runner.cjs test", "test:ssr": "node -e \"import('./test/ssr/component-imports.js')\"", - "test:cypress": "nps test-cy-ci", + "test:cypress": "node ./custom-runner.cjs test-cy-ci", "test:cypress:single": "npx cypress run --component --browser chrome --spec", - "test:cypress:open": "nps test-cy-open", + "test:cypress:open": "node ./custom-runner.cjs test-cy-open", "create-ui5-element": "wc-create-ui5-element", "prepublishOnly": "tsc -b" }, diff --git a/packages/icons-business-suite/custom-runner.cjs b/packages/icons-business-suite/custom-runner.cjs new file mode 100644 index 000000000000..18c64ac45b6b --- /dev/null +++ b/packages/icons-business-suite/custom-runner.cjs @@ -0,0 +1,29 @@ +const getScripts = require("@ui5/webcomponents-tools/icons-collection/custom-runner-commands.js"); + +const options = { + collectionName: "SAP-icons-business-suite", + versions: ["v1", "v2"], +}; + +const scripts = getScripts(options); + +scripts.emptyTask("build:i18n", { + skip: true, +}); +scripts.emptyTask("build:jsonImports", { + skip: true, +}); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/icons-business-suite/package.json b/packages/icons-business-suite/package.json index 480958aedd10..993638f78fb6 100644 --- a/packages/icons-business-suite/package.json +++ b/packages/icons-business-suite/package.json @@ -17,9 +17,9 @@ "./*": "./dist/*" }, "scripts": { - "clean": "wc-dev clean", - "build": "wc-dev build", - "generate": "nps generate", + "clean": "node ./custom-runner.cjs clean", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", "prepublishOnly": "tsc -b" }, "repository": { diff --git a/packages/icons-tnt/custom-runner.cjs b/packages/icons-tnt/custom-runner.cjs new file mode 100644 index 000000000000..f8624462fbe4 --- /dev/null +++ b/packages/icons-tnt/custom-runner.cjs @@ -0,0 +1,29 @@ +const getScripts = require("@ui5/webcomponents-tools/icons-collection/custom-runner-commands.js"); + +const options = { + collectionName: "SAP-icons-TNT", + versions: ["v2", "v3"], +}; + +const scripts = getScripts(options); + +scripts.emptyTask("build:i18n", { + skip: true, +}); +scripts.emptyTask("build:jsonImports", { + skip: true, +}); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/icons-tnt/package.json b/packages/icons-tnt/package.json index 998c205c0d36..99b1aad3cec0 100644 --- a/packages/icons-tnt/package.json +++ b/packages/icons-tnt/package.json @@ -17,9 +17,9 @@ "./*": "./dist/*" }, "scripts": { - "clean": "wc-dev clean", - "build": "wc-dev build", - "generate": "nps generate", + "clean": "node ./custom-runner.cjs clean", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", "prepublishOnly": "tsc -b" }, "repository": { diff --git a/packages/icons/custom-runner.cjs b/packages/icons/custom-runner.cjs new file mode 100644 index 000000000000..856dab28ba88 --- /dev/null +++ b/packages/icons/custom-runner.cjs @@ -0,0 +1,22 @@ +const getScripts = require("@ui5/webcomponents-tools/icons-collection/custom-runner-commands.js"); + +const options = { + collectionName: "SAP-icons", + versions: ["v4", "v5"], +}; + +const scripts = getScripts(options); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/icons/package.json b/packages/icons/package.json index 61d57b297b12..0f0a0e8fc32e 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -17,9 +17,9 @@ "./*": "./dist/*" }, "scripts": { - "clean": "wc-dev clean", - "generate": "nps generate", - "build": "wc-dev build", + "clean": "node ./custom-runner.cjs clean", + "generate": "node ./custom-runner.cjs generate", + "build": "node ./custom-runner.cjs build", "prepublishOnly": "tsc -b" }, "repository": { diff --git a/packages/localization/custom-runner.cjs b/packages/localization/custom-runner.cjs new file mode 100644 index 000000000000..b8a84b8ead7a --- /dev/null +++ b/packages/localization/custom-runner.cjs @@ -0,0 +1,117 @@ +const resolve = require("resolve"); +const BuildRunner = require("@ui5/webcomponents-tools/task-runner/build-runner"); +const amdToES6 = require("@ui5/webcomponents-tools/lib/amd-to-es6/index"); +const noRequire = require("@ui5/webcomponents-tools/lib/amd-to-es6/no-remaining-require"); +const cldr = require("./lib/generate-json-imports/cldr").default; +const copyUsedModules = require("@ui5/webcomponents-tools/lib/copy-list/index.js"); +const copyAndWatch = require("@ui5/webcomponents-tools/lib/copy-and-watch/index.js").copyAndWatch; + +const runner = new BuildRunner(); + +runner.addTask("clean", { + dependencies: [ + "rimraf src/generated", + "rimraf dist", + ], +}); + +runner.addTask("lint", { + dependencies: [ + "eslint .", + ], +}); + +runner.addTask("generate", { + dependencies: [ + "clean", + "copy", + "build:amd-to-es6", + "build:jsonImports", + ], +}); + +runner.addTask("build", { + dependencies: [ + "clean", + "copy", + "build:amd-to-es6", + "build:jsonImports", + "build:typescript", + "build:no-remaining-require", + ], +}); + +runner.addTask("build:amd-to-es6", { + callback: async () => { + await amdToES6("dist/"); + return ""; + }, +}); + +runner.addTask("build:no-remaining-require", { + callback: async () => { + await noRequire("dist/"); + return ""; + }, +}); + +runner.addTask("build:typescript", { + dependencies: "tsc --build", +}); + +runner.addTask("build:jsonImports", { + callback: async () => { + await cldr(); + return ""; + }, +}); + +runner.addTask("typescript", { + dependencies: [ + "tsc --build", + ], +}); + +runner.addTask("copy", { + dependencies: [ + "copy:used-modules", + "copy:cldr", + "copy:overlay", + ], + parallel: true, +}); + +runner.addTask("copy:used-modules", { + callback: async () => { + await copyUsedModules("./used-modules.txt", "dist/"); + return "Used modules copied."; + }, +}); + +runner.addTask("copy:cldr", { + callback: async () => { + await copyAndWatch("../../node_modules/@openui5/sap.ui.core/src/sap/ui/core/cldr/*", "dist/generated/assets/cldr/", { silent: true }); + return "CLDR JSON files generated."; + } +}); + +runner.addTask("copy:overlay", { + callback: async () => { + await copyAndWatch("overlay/**/*.js", "dist/", { silent: true }); + return "Overlay files copied."; + } +}); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log("Available tasks:", Array.from(runner.tasks.keys()).join(", ")); + process.exit(1); + } + + runner.run(taskName).catch(error => { + console.error("Task failed:", error.message); + process.exit(1); + }); +} diff --git a/packages/localization/lib/generate-json-imports/cldr.js b/packages/localization/lib/generate-json-imports/cldr.js index 39d528a68f8e..44ff9319a850 100644 --- a/packages/localization/lib/generate-json-imports/cldr.js +++ b/packages/localization/lib/generate-json-imports/cldr.js @@ -42,6 +42,8 @@ const generate = async () => { ]); } -generate().then(() => { - console.log("CLDR files generated."); -}); +// generate().then(() => { +// console.log("CLDR files generated."); +// }); + +export default generate; \ No newline at end of file diff --git a/packages/localization/package.json b/packages/localization/package.json index 6a82eab2ab66..5a333a346a2f 100644 --- a/packages/localization/package.json +++ b/packages/localization/package.json @@ -21,11 +21,11 @@ "./*": "./dist/*" }, "scripts": { - "clean": "nps clean", - "lint": "nps lint", - "start": "nps start", - "build": "nps build", - "generate": "nps generate", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "start": "node ./custom-runner.cjs start", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", "prepublishOnly": "tsc -b" }, "devDependencies": { diff --git a/packages/main/custom-runner.cjs b/packages/main/custom-runner.cjs new file mode 100644 index 000000000000..447ae03dd5d1 --- /dev/null +++ b/packages/main/custom-runner.cjs @@ -0,0 +1,28 @@ +const getScripts = require("@ui5/webcomponents-tools/components-package/custom-runner-commands.js"); + +const options = { + port: 8080, + portStep: 2, + noWatchTS: true, + dev: true, + internal: { + cypress_code_coverage: false, + cypress_acc_tests: false, + }, +}; + +const scripts = getScripts(options); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(scripts.tasks.keys()).join(', ')); + process.exit(1); + } + + scripts.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/main/package.json b/packages/main/package.json index 74a7668e7707..e8eeaeadd2ec 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -16,22 +16,22 @@ "ui5" ], "scripts": { - "clean": "wc-dev clean", - "lint": "wc-dev lint", - "lint:scope": "nps scope.lint", - "start": "wc-dev start", - "watch": "wc-dev watch", - "generate": "nps generate", - "generateAPI": "nps generateAPI", - "build": "wc-dev build", - "bundle": "nps build.bundle", - "test": "wc-dev test", - "test:suite-1": "wc-dev test-suite-1", - "test:suite-2": "wc-dev test-suite-2", - "test:cypress": "nps test-cy-ci", - "test:cypress:suite-1": "nps test-cy-ci-suite-1", - "test:cypress:suite-2": "nps test-cy-ci-suite-2", - "test:cypress:open": "nps test-cy-open", + "clean": "node ./custom-runner.cjs clean", + "lint": "node ./custom-runner.cjs lint", + "lint:scope": "node ./custom-runner.cjs scope:lint", + "start": "node ./custom-runner.cjs start", + "watch": "node ./custom-runner.cjs watch", + "generate": "node ./custom-runner.cjs generate", + "generateAPI": "node ./custom-runner.cjs generateAPI", + "build": "node ./custom-runner.cjs build", + "bundle": "node ./custom-runner.cjs build:bundle", + "test": "node ./custom-runner.cjs test", + "test:suite-1": "node ./custom-runner.cjs test-suite-1", + "test:suite-2": "node ./custom-runner.cjs test-suite-2", + "test:cypress": "node ./custom-runner.cjs test-cy-ci", + "test:cypress:suite-1": "node ./custom-runner.cjs test-cy-ci-suite-1", + "test:cypress:suite-2": "node ./custom-runner.cjs test-cy-ci-suite-2", + "test:cypress:open": "node ./custom-runner.cjs test-cy-open", "test:cypress:single": "npx cypress run --component --browser chrome --spec", "test:vitest": "yarn vitest run", "test:ssr": "node -e \"import('./test/ssr/component-imports.js')\"", @@ -69,4 +69,4 @@ "lit": "^2.0.0", "vitest": "^3.0.2" } -} +} \ No newline at end of file diff --git a/packages/theming/custom-runner.cjs b/packages/theming/custom-runner.cjs new file mode 100644 index 000000000000..83d644d96169 --- /dev/null +++ b/packages/theming/custom-runner.cjs @@ -0,0 +1,82 @@ +const path = require('path'); +const BuildRunner = require('@ui5/webcomponents-tools/task-runner/build-runner'); +const buildJsonImportsThemes = require("@ui5/webcomponents-tools/lib/generate-json-imports/themes"); +const generateReport = require("./lib/generate-css-vars-usage-report/index.cjs"); +const cssProcessorThemes = require("@ui5/webcomponents-tools/lib/css-processors/css-processor-themes.mjs").default; +const copyAndWatch = require("@ui5/webcomponents-tools/lib/copy-and-watch/index.js").copyAndWatch; + +const runner = new BuildRunner(); + +runner.addTask("clean", { + dependencies: [ + "rimraf dist", + "rimraf src/generated", + ] +}); + +runner.addTask("generate", { + dependencies: [ + "build:postcss", + "build:jsonImports", + ] +}); + +runner.addTask("build", { + dependencies: [ + "clean", + "build:src", + "build:postcss", + "build:jsonImports", + "build:typescript", + "generateReport", + ] +}); + +runner.addTask("build:src", { + callback: async () => { + await copyAndWatch("src/**/*.{json}", "dist/", { silent: true }); + return "Source files copied."; + } +}); + +runner.addTask("build:typescript", { + dependencies: [ + "tsc" + ] +}); + +runner.addTask("build:postcss", { + callback: async () => { + await cssProcessorThemes({ tsMode: true }); + return "" + } +}); + +runner.addTask("build:jsonImports", { + callback: async () => { + await buildJsonImportsThemes("dist/generated/assets/themes", "src/generated/json-imports", true); + return "Generated themes JSON imports."; + }, +}); + +runner.addTask("generateReport", { + callback: async () => { + await generateReport(); + return "CSS Vars usage report generated."; + }, +}); + + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(runner.tasks.keys()).join(', ')); + process.exit(1); + } + + runner.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/theming/lib/generate-css-vars-usage-report/index.js b/packages/theming/lib/generate-css-vars-usage-report/index.cjs similarity index 77% rename from packages/theming/lib/generate-css-vars-usage-report/index.js rename to packages/theming/lib/generate-css-vars-usage-report/index.cjs index 81ee14b9ca43..d4cb92d578eb 100644 --- a/packages/theming/lib/generate-css-vars-usage-report/index.js +++ b/packages/theming/lib/generate-css-vars-usage-report/index.cjs @@ -1,10 +1,6 @@ -import fs from "fs/promises"; -import path from "path"; -import beautify from "json-beautify"; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const fs = require("fs/promises"); +const path = require("path"); +const beautify = require("json-beautify"); const vars = new Set(); @@ -18,7 +14,7 @@ const processFile = async file => { }; const generate = async () => { - const { globby } = await import("globby"); + const { globby } = require("globby"); const mainFiles = await globby(path.join(__dirname, "../../../main/src/themes/**/*.css").replace(/\\/g, "/")); const fioriFiles = await globby(path.join(__dirname, "../../../fiori/src/themes/**/*.css").replace(/\\/g, "/")); @@ -33,6 +29,8 @@ const generate = async () => { return fs.writeFile(path.join(__dirname, "../../css-vars-usage.json"), beautify(result, null, 2, 100)); } -generate().then(() => { - console.log("CSS Vars usage report generated."); -}); +// generate().then(() => { +// console.log("CSS Vars usage report generated."); +// }); + +module.exports = generate; diff --git a/packages/theming/package.json b/packages/theming/package.json index a2190610cb6c..d3e50c7c48cc 100644 --- a/packages/theming/package.json +++ b/packages/theming/package.json @@ -17,10 +17,10 @@ "./*": "./dist/*" }, "scripts": { - "clean": "nps clean", - "build": "nps build", - "generate": "nps generate", - "start": "nps start", + "clean": "node ./custom-runner.cjs clean", + "build": "node ./custom-runner.cjs build", + "generate": "node ./custom-runner.cjs generate", + "start": "node ./custom-runner.cjs start", "verify": "node ./lib/verify-vars/index.js", "prepublishOnly": "tsc -b" }, diff --git a/packages/tools/components-package/custom-runner-commands.js b/packages/tools/components-package/custom-runner-commands.js new file mode 100644 index 000000000000..005bca836020 --- /dev/null +++ b/packages/tools/components-package/custom-runner-commands.js @@ -0,0 +1,306 @@ +const BuildRunner = require('../task-runner/build-runner'); +const path = require("path"); +const fs = require("fs"); +const LIB = path.join(__dirname, `../lib/`); +const buildI18nJson = require("../lib/i18n/toJSON"); +const buildI18nDefaultsjs = require("../lib/i18n/defaults"); +const buildJsonImportsI18n = require("../lib/generate-json-imports/i18n"); +const buildJsonImportsThemes = require("../lib/generate-json-imports/themes"); +const cssProcessorThemes = require("../lib/css-processors/css-processor-themes.mjs").default; +const cssProcessorComponents = require("../lib/css-processors/css-processor-components.mjs").default; +const copyAndWatch = require("../lib/copy-and-watch/index.js").copyAndWatch; +const validate = require("../lib/cem/validate"); +const createIllustrations = require("../lib/create-illustrations"); +const generateJsImportsIllustrations = require("../lib/generate-js-imports/illustrations"); + +let websiteBaseUrl = "/"; + +if (process.env.DEPLOY) { + websiteBaseUrl = "/ui5-webcomponents/"; +} else if (process.env.DEPLOY_NIGHTLY) { + websiteBaseUrl = "/ui5-webcomponents/nightly/"; +} + +const getScripts = (options) => { + const runner = new BuildRunner(); + + // The script creates all JS modules (dist/illustrations/{illustrationName}.js) out of the existing SVGs + const illustrationsData = options.illustrationsData || []; + const illustrations = illustrationsData.map(illustration => createIllustrations(illustration.path, illustration.defaultText, illustration.illustrationsPrefix, illustration.set, illustration.destinationPath, illustration.collection)); + + // The script creates the "src/generated/js-imports/Illustration.js" file that registers loaders (dynamic JS imports) for each illustration + const createIllustrationsLoadersScript = illustrationsData.map(illustrations => + generateJsImportsIllustrations(illustrations.destinationPath, illustrations.dynamicImports.outputFile, illustrations.set, illustrations.collection, illustrations.dynamicImports.location, illustrations.dynamicImports.filterOut)) + + let viteConfig; + if (fs.existsSync("config/vite.config.js")) { + // old project setup where config file is in separate folder + viteConfig = "-c config/vite.config.js"; + } else if (fs.existsSync("vite.config.js")) { + // preferred way of custom configuration in root project folder + viteConfig = ""; + } else { + // no custom configuration - use default from tools project + viteConfig = `-c "${require.resolve("@ui5/webcomponents-tools/components-package/vite.config.js")}"`; + } + + let eslintConfig; + if (fs.existsSync(".eslintrc.js") || fs.existsSync(".eslintrc.cjs")) { + // preferred way of custom configuration in root project folder + eslintConfig = ""; + } else { + // no custom configuration - use default from tools project + eslintConfig = `--config "${require.resolve("@ui5/webcomponents-tools/components-package/eslint.js")}"`; + } + + + runner.addTask("clean", { + dependencies: [ + "rimraf src/generated", + "rimraf dist", + "scope:testPages:clean" + ], + parallel: true, + }); + + runner.addTask("lint", { + dependencies: [ + `eslint . ${eslintConfig}` + ] + }); + + runner.addTask("lintfix", { + dependencies: [ + `eslint . ${eslintConfig} --fix` + ] + }); + + runner.addTask("generate", { + dependencies: [ + "prepare:all" + ], + }) + + runner.addTask("generate:all", { + dependencies: [ + "build:i18n", + "prepare:styleRelated", + "copyProps", + "build:illustrations" + ], + parallel: true, + }) + + runner.addTask("generate:styleRelated", { + dependencies: [ + "build:styles", + "build:jsonImports", + "build:jsImports" + ], + parallel: true, + }) + + runner.addTask("prepare", { + dependencies: [ + "clean", + "prepare:all", + "copyProps", + "prepare:typescript", + "generateAPI" + ], + }) + + runner.addTask("prepare:all", { + dependencies: [ + "build:i18n", + "prepare:styleRelated", + "build:illustrations" + ], + parallel: true, + }); + + runner.addTask("prepare:styleRelated", { + dependencies: [ + "build:styles", + "build:jsonImports", + "build:jsImports" + ], + }) + + runner.addTask("prepare:typescript", { + dependencies: [ + "tsc --build", + ] + }); + + runner.addTask("build", { + dependencies: [ + "prepare", + "lint", + "build:bundle", // "build:bundle2" + ] + }) + + runner.addTask("build:styles", { + dependencies: [ + "build:styles:themes", + "build:styles:components" + ], + parallel: true, + }) + + runner.addTask("build:styles:themes", { + callback: async () => { + await cssProcessorThemes({ tsMode: true }); + return "" + } + }); + + runner.addTask("build:styles:components", { + callback: async () => { + await cssProcessorComponents({ tsMode: true }); + return "" + } + }); + + runner.addTask("build:i18n", { + dependencies: [ + "build:i18n:defaultsjs", + "build:i18n:json" + ], + parallel: true, + }); + + runner.addTask("build:i18n:defaultsjs", { + callback: async () => { + await buildI18nDefaultsjs("src/i18n", "src/generated/i18n", true); + return "i18n default file generated." + } + }); + + runner.addTask("build:i18n:json", { + callback: async () => { + await buildI18nJson("src/i18n", "dist/generated/assets/i18n"); + console.log("Message bundle JSON files generated."); + return "Message bundle JSON files generated." + }, + }); + + runner.addTask("build:jsonImports", { + dependencies: [ + "build:jsonImports:themes", + "build:jsonImports:i18n" + ], + parallel: true, + }); + + runner.addTask("build:jsonImports:themes", { + callback: async () => { + await buildJsonImportsThemes("dist/generated/assets/themes", "src/generated/json-imports", true); + console.log("Generated themes JSON imports."); + return "Generated themes JSON imports."; + }, + }); + + runner.addTask("build:jsonImports:i18n", { + callback: async () => { + await buildJsonImportsI18n("dist/generated/assets/i18n", "src/generated/json-imports", true); + console.log("Generated i18n JSON imports."); + return "Generated i18n JSON imports."; + }, + }); + + runner.addTask("build:jsImports", { + dependencies: [ + "build:jsImports:illustrationsLoaders" + ], + parallel: false, // ??? + }); + + runner.addTask("build:jsImports:illustrationsLoaders", { + callback: async () => { + await Promise.all(createIllustrationsLoadersScript); + console.log("Generated illustrations JS imports."); + return "Generated illustrations JS imports."; + } + }); + + runner.addTask("build:bundle", { + dependencies: [ + `vite build ${viteConfig} --mode testing --base ${websiteBaseUrl}` + ] + }); + + runner.addTask("build:bundle2", { + dependencies: [ + `` + ] + }); + + runner.addTask("build:illustrations", { + callback: async () => { + await Promise.all(illustrations); + console.log("Illustrations generated."); + // just a placeholder for the dependencies to work + return "Illustrations generated."; + } + }); + + runner.addTask("copyProps", { + callback: async () => { + await copyAndWatch("src/i18n/*.properties", "dist/", { silent: true }); + return "Properties files copied."; + } + }); + + runner.addTask("copy", { + dependencies: [ + "copy:src", + "copy:props" + ], + }); + + runner.addTask("copy:src", { + callback: async () => { + await copyAndWatch("src/**/*.{js,json}", "dist/", { silent: true }); + return "Source files copied."; + } + }); + + runner.addTask("copy:props", { + callback: async () => { + await copyAndWatch("src/i18n/*.properties", "dist/", { silent: true }); + return "Properties files copied."; + } + }); + + runner.addTask("generateAPI", { + dependencies: [ + "generateAPI:generateCEM", + "generateAPI:validateCEM", + ] + }); + + runner.addTask("generateAPI:generateCEM", { + dependencies: [ + `${options.dev ? "cross-env UI5_CEM_MODE='dev'" : ""} cem analyze --config "${LIB}/cem/custom-elements-manifest.config.mjs"` + ] + }); + + runner.addTask("generateAPI:validateCEM", { + callback: async () => { + await validate({ devMode: options.dev }); + return "CEM validation completed."; + } + }); + + runner.addTask("scope:testPages:clean", { + dependencies: [ + "" + ] + }) + + return runner; +}; + +module.exports = getScripts; \ No newline at end of file diff --git a/packages/tools/components-package/nps.js b/packages/tools/components-package/nps.js index 4b34a6e1ab7c..3261a3a626b1 100644 --- a/packages/tools/components-package/nps.js +++ b/packages/tools/components-package/nps.js @@ -13,7 +13,7 @@ const cypressEnvVariables = (options, predefinedVars) => { let variables = []; const { cypress_code_coverage, cypress_acc_tests } = options.internal ?? {}; - // Handle environment variables like TEST_SUITE + // Handle environment variables like TEST_SUITE if (predefinedVars) { variables = [...predefinedVars]; } diff --git a/packages/tools/icons-collection/custom-runner-commands.js b/packages/tools/icons-collection/custom-runner-commands.js new file mode 100644 index 000000000000..79f1d0bfa865 --- /dev/null +++ b/packages/tools/icons-collection/custom-runner-commands.js @@ -0,0 +1,174 @@ +const path = require("path"); +const BuildRunner = require('../task-runner/build-runner'); +const buildI18nJson = require("../lib/i18n/toJSON"); +const buildI18nDefaultsjs = require("../lib/i18n/defaults"); +const buildJsonImportsI18n = require("../lib/generate-json-imports/i18n"); +const buildIcons = require("../lib/create-icons/index"); +const copyAndWatch = require("../lib/copy-and-watch/index.js").copyAndWatch; + +const createIconImportsCommand = (options, runner) => { + if (!options.versions) { + runner.addTask("build:icons", { + callback: async () => { + await buildIcons(options.collectionName); + return "Icons created." + } + }) + return; + } + + const dependencies = [] + + options.versions.forEach((v) => { + dependencies.push(`build:icons:create${v}`); + + runner.addTask(`build:icons:create${v}`, { + callback: async () => { + await buildIcons(options.collectionName, v); + return "Icons created." + } + }) + }); + + runner.addTask("build:icons", { + dependencies, + parallel: true, + }) +} + +const copyIconAssetsCommand = (options, runner) => { + if (!options.versions) { + runner.addTask("copy", { + dependencies: [ + "copy:json-imports", + "copy:icon-collection" + ], + parallel: true, + }) + + runner.addTask("copy:json-imports", { + callback: async () => { + await copyAndWatch("src/**/*.js", "dist/", { silent: true }); + return "JSON imports copied."; + } + }); + runner.addTask("copy:icon-collection", { + callback: async () => { + await copyAndWatch("src/*.json", "src/generated/assets/", { silent: true }); + return "Icon collection JSON files copied."; + } + }); + + return; + } + + const dependencies = ["copy:json-imports"] + + + options.versions.forEach((v) => { + dependencies.push(`copy:icon-collection${v}`); + + runner.addTask(`copy:icon-collection${v}`, { + callback: async () => { + await copyAndWatch(`src/${v}/*.json`, `src/generated/assets/${v}/`, { silent: true }); + return `Icon collection ${v} JSON files copied.`; + } + }) + }); + + runner.addTask(`copy:json-imports`, { + callback: async () => { + await copyAndWatch("src/**/*.js", "dist/", { silent: true }); + return "JSON imports copied."; + } + }); + + runner.addTask("copy", { + dependencies, + parallel: true, + }) +} + +const getScripts = (options) => { + const runner = new BuildRunner(); + + createIconImportsCommand(options, runner); + copyIconAssetsCommand(options, runner); + + runner.addTask("clean", "rimraf dist && rimraf src/generated"); + + runner.addTask("generate", { + dependencies: [ + "clean", + "copy", + "build:i18n", + "build:icons", + "build:jsonImports", + "copyjson" + ] + }) + + runner.addTask("copyjson", { + callback: async () => { + await copyAndWatch("src/**/*.json", "dist/generated/", { silent: true }); + return "JSON files copied."; + }, + }); + + runner.addTask("build", { + dependencies: [ + "clean", + "copy", + "build:i18n", + "typescript", + "build:icons", + "build:jsonImports", + ] + }) + + runner.addTask("build:i18n", { + dependencies: [ + "build:i18n:defaultsjs", + "build:i18n:json" + ], + parallel: true, + }); + + runner.addTask("build:i18n:defaultsjs", { + callback: async () => { + await buildI18nDefaultsjs("src/i18n", "src/generated/i18n", true); + return "i18n default file generated." + } + }); + + runner.addTask("build:i18n:json", { + callback: async () => { + await buildI18nJson("src/i18n", "dist/generated/assets/i18n"); + return "Message bundle JSON files generated." + }, + }); + + runner.addTask("build:jsonImports", { + dependencies: [ + "build:jsonImports:i18n" + ] + }) + + + runner.addTask("build:jsonImports:i18n", { + callback: async () => { + await buildJsonImportsI18n("src/generated/assets/i18n", "src/generated/json-imports", true); + return "Generated i18n JSON imports."; + }, + }); + + runner.addTask("typescript", { + dependencies: [ + "tsc --build", + ] + }); + + return runner; +}; + +module.exports = getScripts; diff --git a/packages/tools/lib/amd-to-es6/index.js b/packages/tools/lib/amd-to-es6/index.js index 3ddd7930f412..c4a13f1750d0 100644 --- a/packages/tools/lib/amd-to-es6/index.js +++ b/packages/tools/lib/amd-to-es6/index.js @@ -1,12 +1,11 @@ const fs = require("fs").promises; const path = require("path"); -const basePath = process.argv[2]; const babelCore = require("@babel/core"); const babelParser = require("@babel/parser"); const babelGenerator = require("@babel/generator").default; const replaceAsync = require('replace-in-file'); -const convertSAPUIDefineToDefine = async (filePath) => { +const convertSAPUIDefineToDefine = async (filePath, basePath) => { return replaceAsync({ files: filePath, processor: (input) => { @@ -15,13 +14,13 @@ const convertSAPUIDefineToDefine = async (filePath) => { }) } -const convertAmdToEs6 = async (code) => { +const convertAmdToEs6 = async (code, basePath) => { return (await babelCore.transformAsync(code, { plugins: [['babel-plugin-amd-to-esm', {}]] })).code; } -const convertAbsImportsToRelative = (filePath, code) => { +const convertAbsImportsToRelative = (filePath, code, basePath) => { let changed = false; // console.log("File processing started: ", srcPath); @@ -68,8 +67,8 @@ const convertAbsImportsToRelative = (filePath, code) => { return changed ? babelGenerator(tree).code : code; } -const replaceGlobalCoreUsage = (filePath, code) => { - if (!filePath.includes("Configuration")) { +const replaceGlobalCoreUsage = (filePath, code, basePath) => { + if (!filePath.includes("Configuration")) { const replaced = code.replace(/sap\.ui\.getCore\(\)/g, `Core`); return code !== replaced ? `import Core from 'sap/ui/core/Core';${replaced}` : code; } @@ -77,26 +76,31 @@ const replaceGlobalCoreUsage = (filePath, code) => { return code; }; -const transformAmdToES6Module = async (filePath) => { - await convertSAPUIDefineToDefine(filePath); +const transformAmdToES6Module = async (filePath, basePath) => { + await convertSAPUIDefineToDefine(filePath, basePath); let code = (await fs.readFile(filePath)).toString(); - code = await convertAmdToEs6(code); + code = await convertAmdToEs6(code, basePath); - code = replaceGlobalCoreUsage(filePath, code); + code = replaceGlobalCoreUsage(filePath, code, basePath); - code = convertAbsImportsToRelative(filePath, code); + code = convertAbsImportsToRelative(filePath, code, basePath); return fs.writeFile(filePath, code); } -const transformAmdToES6Modules = async () => { +const transformAmdToES6Modules = async (distFolder) => { + const basePath = distFolder; + + const { globby } = await import("globby"); const fileNames = await globby(basePath.replace(/\\/g, "/") + "**/*.js"); - return Promise.all(fileNames.map(transformAmdToES6Module).filter(x => !!x)); + return Promise.all(fileNames.map(fileName => transformAmdToES6Module(fileName, basePath)).filter(x => !!x)); }; -transformAmdToES6Modules().then(() => { - console.log("Success: all amd modules are transformed to es6!"); -}); \ No newline at end of file +// transformAmdToES6Modules().then(() => { +// console.log("Success: all amd modules are transformed to es6!"); +// }); + +module.exports = transformAmdToES6Modules; \ No newline at end of file diff --git a/packages/tools/lib/amd-to-es6/no-remaining-require.js b/packages/tools/lib/amd-to-es6/no-remaining-require.js index 7ab6d8de633e..92558653f1f6 100644 --- a/packages/tools/lib/amd-to-es6/no-remaining-require.js +++ b/packages/tools/lib/amd-to-es6/no-remaining-require.js @@ -1,6 +1,5 @@ const fs = require("fs").promises; const path = require("path"); -const basePath = process.argv[2]; const babelCore = require("@babel/core"); const babelParser = require("@babel/parser"); const babelGenerator = require("@babel/generator").default; @@ -12,11 +11,11 @@ const checkHasRequire = (filePath, code) => { const tree = babelParser.parse(code, { sourceType: "module" }); walk(tree, { CallExpression: function (node) { - if (node.type === "CallExpression" && node?.callee?.name === "unhandledRequire") { - throw new Error(`sap.ui.require found in ${filePath}`); - } - } - }); + if (node.type === "CallExpression" && node?.callee?.name === "unhandledRequire") { + throw new Error(`sap.ui.require found in ${filePath}`); + } + } + }); } const checkFile = async (filePath) => { @@ -24,10 +23,14 @@ const checkFile = async (filePath) => { checkHasRequire(filePath, code); } -const checkAll = async () => { +const checkAll = async (distFolder) => { + const basePath = distFolder; + const { globby } = await import("globby"); const fileNames = await globby(basePath.replace(/\\/g, "/") + "**/*.js"); return Promise.all(fileNames.map(checkFile).filter(x => !!x)); }; -checkAll(); +// checkAll(); + +module.exports = checkAll; diff --git a/packages/tools/lib/cem/validate.js b/packages/tools/lib/cem/validate.js index 1da039400ebd..87296f21ae36 100644 --- a/packages/tools/lib/cem/validate.js +++ b/packages/tools/lib/cem/validate.js @@ -1,21 +1,11 @@ const fs = require('fs'); const Ajv = require('ajv'); const path = require('path'); + // Load your JSON schema -const extenalSchema = require('./schema.json'); +const externalSchema = require('./schema.json'); const internalSchema = require('./schema-internal.json'); -// Load your JSON data from the input file -const inputFilePath = path.join(process.cwd(), "dist/custom-elements.json"); // Update with your file path -const customManifest = fs.readFileSync(inputFilePath, 'utf8'); -const inputDataInternal = JSON.parse(customManifest); -const devMode = process.env.UI5_CEM_MODE === "dev"; - -inputDataInternal.modules.forEach(moduleDoc => { - moduleDoc.exports = moduleDoc.exports. - filter(e => moduleDoc.declarations.find(d => d.name === e.declaration.name && ["class", "function", "variable", "enum"].includes(d.kind)) || e.name === "default"); -}) - const clearProps = (data) => { if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { @@ -39,29 +29,82 @@ const clearProps = (data) => { } return data; -} +}; + +const filterExports = (moduleDoc) => { + moduleDoc.exports = moduleDoc.exports + .filter(e => moduleDoc.declarations.find(d => d.name === e.declaration.name && ["class", "function", "variable", "enum"].includes(d.kind)) || e.name === "default"); +}; + +const validateCEM = (options = {}) => { + const { + inputFilePath = path.join(process.cwd(), "dist/custom-elements.json"), + devMode = "", + writeFiles = true, + silent = false + } = options; + + // Load your JSON data from the input file + const customManifest = fs.readFileSync(inputFilePath, 'utf8'); + const inputDataInternal = JSON.parse(customManifest); + + // Filter exports + inputDataInternal.modules.forEach(filterExports); + + const ajv = new Ajv({ allowUnionTypes: true, allError: true }); + let validate = ajv.compile(internalSchema); -const ajv = new Ajv({ allowUnionTypes: true, allError: true }) -let validate = ajv.compile(internalSchema) + // Validate the JSON data against the internal schema + if (devMode) { + if (validate(inputDataInternal)) { + !silent && console.log('Internal custom element manifest is validated successfully'); + } else { + console.log(validate.errors); + throw new Error(`Validation of internal custom elements manifest failed: ${validate.errors}`); + } + } + + // Create external data by clearing internal properties + const inputDataExternal = clearProps(JSON.parse(JSON.stringify(inputDataInternal))); + validate = ajv.compile(externalSchema); -// Validate the JSON data against the schema -if (devMode) { - if (validate(inputDataInternal)) { - console.log('Internal custom element manifest is validated successfully'); + // Validate the JSON data against the external schema + if (validate(inputDataExternal)) { + !silent && console.log('Custom element manifest is validated successfully'); + + if (writeFiles) { + fs.writeFileSync(inputFilePath, JSON.stringify(inputDataExternal, null, 2), 'utf8'); + fs.writeFileSync(inputFilePath.replace("custom-elements", "custom-elements-internal"), JSON.stringify(inputDataInternal, null, 2), 'utf8'); + } + + return { + external: inputDataExternal, + internal: inputDataInternal, + valid: true + }; + } else if (devMode) { + throw new Error(`Validation of public custom elements manifest failed: ${validate.errors}`); } else { - console.log(validate.errors) - throw new Error(`Validation of internal custom elements manifest failed: ${validate.errors}`); + return { + external: inputDataExternal, + internal: inputDataInternal, + valid: false, + errors: validate.errors + }; } -} +}; -const inputDataExternal = clearProps(JSON.parse(JSON.stringify(inputDataInternal))); -validate = ajv.compile(extenalSchema) +// If this file is run directly (not required as a module) +if (require.main === module) { + try { + validateCEM(); + } catch (error) { + console.error('Validation failed:', error.message); + process.exit(1); + } +} -// Validate the JSON data against the schema -if (validate(inputDataExternal)) { - console.log('Custom element manifest is validated successfully'); - fs.writeFileSync(inputFilePath, JSON.stringify(inputDataExternal, null, 2), 'utf8'); - fs.writeFileSync(inputFilePath.replace("custom-elements", "custom-elements-internal"), JSON.stringify(inputDataInternal, null, 2), 'utf8'); -} else if (devMode) { - throw new Error(`Validation of public custom elements manifest failed: ${validate.errors}`); -} \ No newline at end of file +module.exports = validateCEM; +module.exports.validateCEM = validateCEM; +module.exports.clearProps = clearProps; +module.exports.filterExports = filterExports; \ No newline at end of file diff --git a/packages/tools/lib/copy-and-watch/index.js b/packages/tools/lib/copy-and-watch/index.js index ae974f511b95..8ba1bebb3b15 100644 --- a/packages/tools/lib/copy-and-watch/index.js +++ b/packages/tools/lib/copy-and-watch/index.js @@ -30,38 +30,14 @@ const globParent = require('glob-parent'); /* CODE */ -const args = process.argv.slice(2); -const options = {}; - -['watch', 'clean', 'skip-initial-copy', 'safe', 'silent'].forEach(key => { - const index = args.indexOf(`--${key}`); - if (index >= 0) { - options[key] = true; - args.splice(index, 1); - } -}); - -if (args.length < 2) { - console.error('Not enough arguments: copy-and-watch [options] '.red); - process.exit(1); -} - -if (options['skip-initial-copy'] && !options['watch']) { - console.error('--skip-initial-copy argument is meant to be used with --watch, otherwise no files will be copied'.red); - process.exit(1); -} - -const target = args.pop(); -const sources = args; -const parents = [...new Set(sources.map(globParent))]; - -const findTarget = from => { +const findTarget = (from, parents, target) => { const parent = parents .filter(p => from.indexOf(p) >= 0) .sort() .reverse()[0]; return path.join(target, path.relative(parent, from)); }; + const createDirIfNotExist = to => { 'use strict'; @@ -79,21 +55,26 @@ const createDirIfNotExist = to => { } }); }; -const copy = from => { - const to = findTarget(from); + +const copy = (from, parents, target, silent = false) => { + const to = findTarget(from, parents, target); createDirIfNotExist(to); const stats = fs.statSync(from); if (stats.isDirectory()) { return; } fs.writeFileSync(to, fs.readFileSync(from)); - options.silent || console.log('[COPY]'.yellow, from, 'to'.yellow, to); + silent || console.log('[COPY]'.yellow, from, 'to'.yellow, to); }; -const remove = from => { - const to = findTarget(from); - fs.unlinkSync(to); - options.silent || console.log('[DELETE]'.yellow, to); + +const remove = (from, parents, target, silent = false) => { + const to = findTarget(from, parents, target); + if (fs.existsSync(to)) { + fs.unlinkSync(to); + silent || console.log('[DELETE]'.yellow, to); + } }; + const rimraf = dir => { if (fs.existsSync(dir)) { fs.readdirSync(dir).forEach(entry => { @@ -108,38 +89,67 @@ const rimraf = dir => { } }; -// clean -if (options.clean) { - rimraf(target); -} - -// initial copy -if (!options['skip-initial-copy']) { - sources.forEach(s => glob.sync(s).forEach(copy)); -} - -// watch -if (options.watch) { - const chokidarOptions = { - ignoreInitial: true - }; - - if (options.safe) { - chokidarOptions.awaitWriteFinish = { - stabilityThreshold: 500, - pollInterval: 100 +const copyAndWatch = (sources, target, options = {}) => { + const { + watch = false, + clean = false, + skipInitialCopy = false, + safe = false, + silent = false + } = options; + + if (!sources || !target) { + throw new Error('Sources and target are required'); + } + + const sourceArray = Array.isArray(sources) ? sources : [sources]; + const parents = [...new Set(sourceArray.map(globParent))]; + + // clean + if (clean) { + rimraf(target); + } + + // initial copy + if (!skipInitialCopy) { + sourceArray.forEach(s => glob.sync(s).forEach(file => copy(file, parents, target, silent))); + } + + // watch + if (watch) { + const chokidarOptions = { + ignoreInitial: true }; + + if (safe) { + chokidarOptions.awaitWriteFinish = { + stabilityThreshold: 500, + pollInterval: 100 + }; + } + + const watcher = chokidar + .watch(sourceArray, chokidarOptions) + .on('ready', () => sourceArray.forEach(s => { + silent || console.log('[WATCH]'.yellow, s); + })) + .on('add', file => copy(file, parents, target, silent)) + .on('addDir', file => copy(file, parents, target, silent)) + .on('change', file => copy(file, parents, target, silent)) + .on('unlink', file => remove(file, parents, target, silent)) + .on('unlinkDir', file => remove(file, parents, target, silent)) + .on('error', e => console.log('[ERROR]'.red, e)); + + return watcher; } - chokidar - .watch(sources, chokidarOptions) - .on('ready', () => sources.forEach(s => { - options.silent || console.log('[WATCH]'.yellow, s); - })) - .on('add', copy) - .on('addDir', copy) - .on('change', copy) - .on('unlink', remove) - .on('unlinkDir', remove) - .on('error', e => console.log('[ERROR]'.red, e)); -} + return null; +}; + +module.exports = copyAndWatch; +module.exports.copyAndWatch = copyAndWatch; +module.exports.rimraf = rimraf; +module.exports.copy = copy; +module.exports.remove = remove; +module.exports.createDirIfNotExist = createDirIfNotExist; +module.exports.findTarget = findTarget; diff --git a/packages/tools/lib/copy-list/index.js b/packages/tools/lib/copy-list/index.js index b999c7b749c0..ec7d363d0462 100644 --- a/packages/tools/lib/copy-list/index.js +++ b/packages/tools/lib/copy-list/index.js @@ -1,11 +1,9 @@ const fs = require("fs").promises; const path = require("path"); -const fileList = process.argv[2]; -const dest = process.argv[3]; const src = "@openui5/sap.ui.core/src/"; -const generate = async () => { +const generate = async (fileList, dest) => { const filesToCopy = (await fs.readFile(fileList)).toString(); // console.log(filesToCopy); @@ -15,7 +13,7 @@ const generate = async () => { const trimFile = file => file.trim(); return filesToCopy.split("\n").map(trimFile).filter(shouldCopy).map(async moduleName => { - const srcPath = require.resolve(path.join(src, moduleName), {paths: [process.cwd()]}); + const srcPath = require.resolve(path.join(src, moduleName), { paths: [process.cwd()] }); const destPath = path.join(dest, moduleName); await fs.mkdir(path.dirname(destPath), { recursive: true }); @@ -23,6 +21,4 @@ const generate = async () => { }); }; -generate().then(() => { - console.log("Files copied."); -}); +module.exports = generate; diff --git a/packages/tools/lib/create-icons/index.js b/packages/tools/lib/create-icons/index.js index a51bc5a6f429..7cbdf6c5a74f 100644 --- a/packages/tools/lib/create-icons/index.js +++ b/packages/tools/lib/create-icons/index.js @@ -1,12 +1,11 @@ const fs = require("fs").promises; const path = require("path"); -const collectionName = process.argv[2] || "SAP-icons-v4"; -const collectionVersion = process.argv[3]; -const srcFile = collectionVersion ? path.normalize(`src/${collectionVersion}/${collectionName}.json`) : path.normalize(`src/${collectionName}.json`); -const destDir = collectionVersion ? path.normalize(`dist/${collectionVersion}/`) : path.normalize("dist/"); +const generate = async (collectionName, collectionVersion) => { + const srcFile = collectionVersion ? path.normalize(`src/${collectionVersion}/${collectionName}.json`) : path.normalize(`src/${collectionName}.json`); + const destDir = collectionVersion ? path.normalize(`dist/${collectionVersion}/`) : path.normalize("dist/"); -const iconTemplate = (name, pathData, ltr, collection, packageName) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; + const iconTemplate = (name, pathData, ltr, collection, packageName) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; const name = "${name}"; const pathData = "${pathData}"; @@ -21,7 +20,7 @@ export default "${collection}/${name}"; export { pathData, ltr, accData };`; -const iconAccTemplate = (name, pathData, ltr, accData, collection, packageName, versioned) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; + const iconAccTemplate = (name, pathData, ltr, accData, collection, packageName, versioned) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; import { ${accData.key} } from "${versioned ? "../" : "./"}generated/i18n/i18n-defaults.js"; const name = "${name}"; @@ -38,7 +37,7 @@ export { pathData, ltr, accData };`; -const collectionTemplate = (name, versions, fullName) => `import { isLegacyThemeFamilyAsync } from "@ui5/webcomponents-base/dist/config/Theme.js"; + const collectionTemplate = (name, versions, fullName) => `import { isLegacyThemeFamilyAsync } from "@ui5/webcomponents-base/dist/config/Theme.js"; import { pathData as pathData${versions[0]}, ltr, accData } from "./${versions[0]}/${name}.js"; import { pathData as pathData${versions[1]} } from "./${versions[1]}/${name}.js"; @@ -50,7 +49,7 @@ export default "${fullName}"; export { getPathData, ltr, accData };`; -const typeDefinitionTemplate = (name, accData, collection) => `declare const pathData: string; + const typeDefinitionTemplate = (name, accData, collection) => `declare const pathData: string; declare const ltr: boolean; declare const accData: ${accData ? '{ key: string; defaultText: string; }' : null} declare const _default: "${collection}/${name}"; @@ -58,7 +57,7 @@ declare const _default: "${collection}/${name}"; export default _default; export { pathData, ltr, accData };` -const collectionTypeDefinitionTemplate = (name, accData) => `declare const getPathData: () => Promise; + const collectionTypeDefinitionTemplate = (name, accData) => `declare const getPathData: () => Promise; declare const ltr: boolean; declare const accData: ${accData ? '{ key: string; defaultText: string; }' : null} declare const _default: "${name}"; @@ -67,61 +66,62 @@ export default _default; export { getPathData, ltr, accData };` -const svgTemplate = (pathData) => ` + const svgTemplate = (pathData) => ` `; -const createIcons = async (file) => { - await fs.mkdir(destDir, { recursive: true }); - - const json = JSON.parse(await fs.readFile(file)); - - const promises = []; - for (let name in json.data) { - const iconData = json.data[name]; - const pathData = iconData.path; - const ltr = !!iconData.ltr; - const acc = iconData.acc; - const packageName = json.packageName; - const collection = json.collection; - const versioned = json.version; - - const content = acc ? iconAccTemplate(name, pathData, ltr, acc, collection, packageName, versioned) : iconTemplate(name, pathData, ltr, collection, packageName); - - promises.push(fs.writeFile(path.join(destDir, `${name}.js`), content)); - promises.push(fs.writeFile(path.join(destDir, `${name}.svg`), svgTemplate(pathData))); - promises.push(fs.writeFile(path.join(destDir, `${name}.d.ts`), typeDefinitionTemplate(name, acc, collection))); - - // For versioned icons collections, the script creates top level (unversioned) module that internally imports the versioned ones. - // For example, the top level "@ui5/ui5-webcomponents-icons/dist/accept.js" imports: - // - "@ui5/ui5-webcomponents-icons/dist/v5/accept.js" - // - "@ui5/ui5-webcomponents-icons/dist/v4/accept.js" - - if (versioned) { - // The exported value from the top level (unversioned) icon module depends on whether the collection is the default, - // to add or not the collection name to the exported value: - // For the default collection (SAPIcons) we export just the icon name - "export default { 'accept' }" - // For non-default collections (SAPTNTIcons and SAPBSIcons) we export the full name - "export default { 'tnt/actor' }" - const effectiveName = isDefaultCollection(collection) ? name : getUnversionedFullIconName(name, collection); - promises.push(fs.writeFile(path.join(path.normalize("dist/"), `${name}.js`), collectionTemplate(name, json.versions, effectiveName))); - promises.push(fs.writeFile(path.join(path.normalize("dist/"), `${name}.d.ts`), collectionTypeDefinitionTemplate(effectiveName, acc))); + const createIcons = async (file) => { + await fs.mkdir(destDir, { recursive: true }); + + const json = JSON.parse(await fs.readFile(file)); + + const promises = []; + for (let name in json.data) { + const iconData = json.data[name]; + const pathData = iconData.path; + const ltr = !!iconData.ltr; + const acc = iconData.acc; + const packageName = json.packageName; + const collection = json.collection; + const versioned = json.version; + + const content = acc ? iconAccTemplate(name, pathData, ltr, acc, collection, packageName, versioned) : iconTemplate(name, pathData, ltr, collection, packageName); + + promises.push(fs.writeFile(path.join(destDir, `${name}.js`), content)); + promises.push(fs.writeFile(path.join(destDir, `${name}.svg`), svgTemplate(pathData))); + promises.push(fs.writeFile(path.join(destDir, `${name}.d.ts`), typeDefinitionTemplate(name, acc, collection))); + + // For versioned icons collections, the script creates top level (unversioned) module that internally imports the versioned ones. + // For example, the top level "@ui5/ui5-webcomponents-icons/dist/accept.js" imports: + // - "@ui5/ui5-webcomponents-icons/dist/v5/accept.js" + // - "@ui5/ui5-webcomponents-icons/dist/v4/accept.js" + + if (versioned) { + // The exported value from the top level (unversioned) icon module depends on whether the collection is the default, + // to add or not the collection name to the exported value: + // For the default collection (SAPIcons) we export just the icon name - "export default { 'accept' }" + // For non-default collections (SAPTNTIcons and SAPBSIcons) we export the full name - "export default { 'tnt/actor' }" + const effectiveName = isDefaultCollection(collection) ? name : getUnversionedFullIconName(name, collection); + promises.push(fs.writeFile(path.join(path.normalize("dist/"), `${name}.js`), collectionTemplate(name, json.versions, effectiveName))); + promises.push(fs.writeFile(path.join(path.normalize("dist/"), `${name}.d.ts`), collectionTypeDefinitionTemplate(effectiveName, acc))); + } } - } - return Promise.all(promises); -}; + return Promise.all(promises); + }; -const isDefaultCollection = collectionName => collectionName === "SAP-icons-v4" || collectionName === "SAP-icons-v5"; -const getUnversionedFullIconName = (name, collection) => `${getUnversionedCollectionName(collection)}/${name}`; -const getUnversionedCollectionName = collectionName => CollectionVersionedToUnversionedMap[collectionName] || collectionName; + const isDefaultCollection = collectionName => collectionName === "SAP-icons-v4" || collectionName === "SAP-icons-v5"; + const getUnversionedFullIconName = (name, collection) => `${getUnversionedCollectionName(collection)}/${name}`; + const getUnversionedCollectionName = collectionName => CollectionVersionedToUnversionedMap[collectionName] || collectionName; -const CollectionVersionedToUnversionedMap = { - "tnt-v2": "tnt", - "tnt-v3": "tnt", - "business-suite-v1": "business-suite", - "business-suite-v2": "business-suite", -}; + const CollectionVersionedToUnversionedMap = { + "tnt-v2": "tnt", + "tnt-v3": "tnt", + "business-suite-v1": "business-suite", + "business-suite-v2": "business-suite", + }; + + await createIcons(srcFile) +} -createIcons(srcFile).then(() => { - console.log("Icons created."); -}); +module.exports = generate; \ No newline at end of file diff --git a/packages/tools/lib/create-illustrations/index.js b/packages/tools/lib/create-illustrations/index.js index e15bbfe1487a..e37d310a471a 100644 --- a/packages/tools/lib/create-illustrations/index.js +++ b/packages/tools/lib/create-illustrations/index.js @@ -1,12 +1,7 @@ const fs = require("fs").promises; const path = require("path"); -if (process.argv.length < 7) { - throw new Error("Not enough arguments"); -} - -const generate = async () => { - +const generate = async (srcPath, defaultText, illustrationsPrefix, illustrationSet, destPath, collection) => { const ORIGINAL_TEXTS = { UnableToLoad: "UnableToLoad", UnableToUpload: "UnableToUpload", @@ -70,12 +65,6 @@ const generate = async () => { SuccessHighFive: ORIGINAL_TEXTS.BalloonSky }; - const srcPath = process.argv[2]; - const defaultText = process.argv[3] === "true"; - const illustrationsPrefix = process.argv[4]; - const illustrationSet = process.argv[5]; - const destPath = process.argv[6]; - const collection = process.argv[7]; const fileNamePattern = new RegExp(`${illustrationsPrefix}-.+-(.+).svg`); // collect each illustration name because each one should have Sample.js file const fileNames = new Set(); @@ -126,8 +115,7 @@ const generate = async () => { import dialogSvg from "./${illustrationsPrefix}-Dialog-${illustrationName}.js"; import sceneSvg from "./${illustrationsPrefix}-Scene-${illustrationName}.js"; import spotSvg from "./${illustrationsPrefix}-Spot-${illustrationName}.js"; -import dotSvg from "./${illustrationsPrefix}-${hasDot}-${illustrationName}.js";${ - defaultText ? `import { +import dotSvg from "./${illustrationsPrefix}-${hasDot}-${illustrationName}.js";${defaultText ? `import { IM_TITLE_${illustrationNameUpperCase}, IM_SUBTITLE_${illustrationNameUpperCase}, } from "../generated/i18n/i18n-defaults.js";` : ``} @@ -195,6 +183,4 @@ export { dialogSvg, sceneSvg, spotSvg, dotSvg };` }); }; -generate().then(() => { - console.log("Illustrations generated."); -}); +module.exports = generate; \ No newline at end of file diff --git a/packages/tools/lib/css-processors/css-processor-components.mjs b/packages/tools/lib/css-processors/css-processor-components.mjs index 8ec38052dff5..a813721006b4 100644 --- a/packages/tools/lib/css-processors/css-processor-components.mjs +++ b/packages/tools/lib/css-processors/css-processor-components.mjs @@ -7,72 +7,90 @@ import chokidar from "chokidar"; import scopeVariables from "./scope-variables.mjs"; import { writeFileIfChanged, getFileContent } from "./shared.mjs"; -const tsMode = process.env.UI5_TS === "true"; -const extension = tsMode ? ".css.ts" : ".css.js"; +const createCustomPlugin = (packageJSON, tsMode = false) => { + const extension = tsMode ? ".css.ts" : ".css.js"; -const packageJSON = JSON.parse(fs.readFileSync("./package.json")) -const inputFilesGlob = "src/themes/*.css"; -const restArgs = process.argv.slice(2); + return { + name: 'ui5-tools', + setup(build) { + build.initialOptions.write = false; -let customPlugin = { - name: 'ui5-tools', - setup(build) { - build.initialOptions.write = false; + build.onEnd(async (result) => { + const fileProcessingPromises = result.outputFiles.map(async (f) => { + let newText = scopeVariables(f.text, packageJSON); + newText = newText.replaceAll(/\\/g, "\\\\"); // Escape backslashes as they might appear in css rules - build.onEnd(result => { - result.outputFiles.forEach(async f => { - // scoping - let newText = scopeVariables(f.text, packageJSON); - newText = newText.replaceAll(/\\/g, "\\\\"); // Escape backslashes as they might appear in css rules - await mkdir(path.dirname(f.path), {recursive: true}); - writeFile(f.path, newText); + // JS/TS + const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension); + const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`", true); - // JS/TS - const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension); - const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`", true); - writeFileIfChanged(jsPath, jsContent); - }); - }) - }, -} + await mkdir(path.dirname(f.path), { recursive: true }); -const getConfig = async () => { - const config = { - entryPoints: await globby(inputFilesGlob), - bundle: true, - minify: true, - outdir: 'dist/css', - outbase: 'src', - plugins: [ - customPlugin, - ] - }; - return config; -} + Promise.all([ + await writeFile(f.path, newText), + await writeFileIfChanged(jsPath, jsContent), + ]); + }); -if (restArgs.includes("-w")) { - let ready; - let config = await getConfig(); - let ctx = await esbuild.context(config); - await ctx.watch() - console.log('watching...') + await Promise.all(fileProcessingPromises); + }) + }, + }; +}; - // when new component css files are added, they do not trigger a build as no one directly imports them - // restart the watch mode with the new entry points if a css file is added. - const watcher = chokidar.watch(inputFilesGlob); - watcher.on("ready", () => { - ready = true; // Initial scan is over -> waiting for new files - }); - watcher.on("add", async path => { - if (ready) { - // new file - ctx.dispose(); - config = await getConfig(); - ctx = await esbuild.context(config); - ctx.watch(); - } - }); -} else { - const config = await getConfig(); - const result = await esbuild.build(config); -} +const getConfig = async (inputFilesGlob, tsMode) => { + const packageJSON = JSON.parse(fs.readFileSync("./package.json")); + + const config = { + entryPoints: await globby([inputFilesGlob]), + bundle: true, + minify: true, + outdir: 'dist/css', + outbase: 'src', + plugins: [ + createCustomPlugin(packageJSON, tsMode), + ] + }; + return config; +}; + +const processComponents = async (options = {}) => { + const { + watch = false, + tsMode = false + } = options; + + const inputFilesGlob = "src/themes/*.css"; + + if (watch) { + let ready; + let config = await getConfig(inputFilesGlob, tsMode); + let ctx = await esbuild.context(config); + await ctx.watch(); + console.log('watching...'); + + // when new component css files are added, they do not trigger a build as no one directly imports them + // restart the watch mode with the new entry points if a css file is added. + const watcher = chokidar.watch(inputFilesGlob); + watcher.on("ready", () => { + ready = true; // Initial scan is over -> waiting for new files + }); + watcher.on("add", async path => { + if (ready) { + // new file + ctx.dispose(); + config = await getConfig(inputFilesGlob, tsMode); + ctx = await esbuild.context(config); + ctx.watch(); + } + }); + + return { ctx, watcher }; + } else { + const config = await getConfig(inputFilesGlob, tsMode); + const result = await esbuild.build(config); + return result; + } +}; + +export default processComponents; diff --git a/packages/tools/lib/css-processors/css-processor-themes.mjs b/packages/tools/lib/css-processors/css-processor-themes.mjs index 927552c5ee65..da5e6688b6c9 100644 --- a/packages/tools/lib/css-processors/css-processor-themes.mjs +++ b/packages/tools/lib/css-processors/css-processor-themes.mjs @@ -8,16 +8,6 @@ import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/ import { writeFileIfChanged, getFileContent } from "./shared.mjs"; import scopeVariables from "./scope-variables.mjs"; -const tsMode = process.env.UI5_TS === "true"; -const extension = tsMode ? ".css.ts" : ".css.js"; - -const packageJSON = JSON.parse(fs.readFileSync("./package.json")) - -const inputFiles = await globby([ - "src/**/parameters-bundle.css", -]); -const restArgs = process.argv.slice(2); - const processThemingPackageFile = async (f) => { const selector = ':root'; const result = await postcss().process(f.text); @@ -35,54 +25,79 @@ const processThemingPackageFile = async (f) => { return newRule.toString(); }; -const processComponentPackageFile = async (f) => { +const processComponentPackageFile = async (f, packageJSON) => { const result = await postcss(combineDuplicatedSelectors).process(f.text); return scopeVariables(result.css, packageJSON, f.path); } -let scopingPlugin = { - name: 'scoping', - setup(build) { - build.initialOptions.write = false; - - build.onEnd(result => { - result.outputFiles.forEach(async f => { - let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f); - - await mkdir(path.dirname(f.path), { recursive: true }); - writeFile(f.path, newText); - - // JSON - const jsonPath = f.path.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", ".css.json"); - await mkdir(path.dirname(jsonPath), { recursive: true }); - writeFileIfChanged(jsonPath, JSON.stringify(newText)); - - // JS/TS - const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension); - const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`"); - writeFileIfChanged(jsPath, jsContent); - }); - }) - }, -} +const createScopingPlugin = (packageJSON, tsMode = false) => { + const extension = tsMode ? ".css.ts" : ".css.js"; + + return { + name: 'scoping', + setup(build) { + build.initialOptions.write = false; + + build.onEnd(async (result) => { + const fileProcessingPromises = result.outputFiles.map(async (f) => { + let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f, packageJSON); + + // JSON + const jsonPath = f.path.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", ".css.json"); + + // JS/TS + const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension); + const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`"); + + await Promise.all([ + await mkdir(path.dirname(f.path), { recursive: true }), + await mkdir(path.dirname(jsonPath), { recursive: true }) + ]); + + await Promise.all([ + await writeFile(f.path, newText), + await writeFileIfChanged(jsonPath, JSON.stringify(newText)), + await writeFileIfChanged(jsPath, jsContent), + ]) + }); + + await Promise.all(fileProcessingPromises); + }) + }, + }; +}; -const config = { - entryPoints: inputFiles, - bundle: true, - minify: true, - outdir: 'dist/css', - outbase: 'src', - plugins: [ - scopingPlugin, - ], - external: ["*.ttf", "*.woff", "*.woff2"], +const processThemes = async (options = {}) => { + const { + watch = false, + tsMode = false, + } = options; + + const packageJSON = JSON.parse(fs.readFileSync("./package.json")); + const inputFiles = await globby(["src/**/parameters-bundle.css"]); + + const config = { + entryPoints: inputFiles, + bundle: true, + minify: true, + outdir: 'dist/css', + outbase: 'src', + plugins: [ + createScopingPlugin(packageJSON, tsMode), + ], + external: ["*.ttf", "*.woff", "*.woff2"], + }; + + if (watch) { + let ctx = await esbuild.context(config); + await ctx.watch(); + console.log('watching...'); + return ctx; + } else { + const result = await esbuild.build(config); + return result; + } }; -if (restArgs.includes("-w")) { - let ctx = await esbuild.context(config); - await ctx.watch() - console.log('watching...') -} else { - const result = await esbuild.build(config); -} \ No newline at end of file +export default processThemes; \ No newline at end of file diff --git a/packages/tools/lib/generate-js-imports/illustrations.js b/packages/tools/lib/generate-js-imports/illustrations.js index d977e40aaab6..48dac358de58 100644 --- a/packages/tools/lib/generate-js-imports/illustrations.js +++ b/packages/tools/lib/generate-js-imports/illustrations.js @@ -2,27 +2,27 @@ const fs = require("fs").promises; const path = require("path"); const generateDynamicImportLines = async (fileNames, location, exclusionPatterns = []) => { - const packageName = JSON.parse(await fs.readFile("package.json")).name; - return fileNames - .filter((fileName) => !exclusionPatterns.some((pattern) => fileName.startsWith(pattern))) - .map((fileName) => { - const illustrationName = fileName.replace(".js", ""); - const illustrationPath = `${location}/${illustrationName}`; - return `\t\tcase "${fileName.replace('.js', '')}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${illustrationName.toLowerCase()}" */ "${illustrationPath}.js")).default;`; - }) - .join("\n"); + const packageName = JSON.parse(await fs.readFile("package.json")).name; + return fileNames + .filter((fileName) => !exclusionPatterns.some((pattern) => fileName.startsWith(pattern))) + .map((fileName) => { + const illustrationName = fileName.replace(".js", ""); + const illustrationPath = `${location}/${illustrationName}`; + return `\t\tcase "${fileName.replace('.js', '')}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${illustrationName.toLowerCase()}" */ "${illustrationPath}.js")).default;`; + }) + .join("\n"); }; const generateAvailableIllustrationsArray = (fileNames, exclusionPatterns = []) => { - return JSON.stringify( - fileNames - .filter((fileName) => !exclusionPatterns.some((pattern) => fileName.startsWith(pattern))) - .map((fileName) => fileName.replace(".js", "")) - ); + return JSON.stringify( + fileNames + .filter((fileName) => !exclusionPatterns.some((pattern) => fileName.startsWith(pattern))) + .map((fileName) => fileName.replace(".js", "")) + ); }; const generateDynamicImportsFileContent = (dynamicImports, availableIllustrations, collection, set, prefix = "") => { - return `// @ts-nocheck + return `// @ts-nocheck import { registerIllustrationLoader } from "@ui5/webcomponents-base/dist/asset-registries/Illustrations.js"; export const loadIllustration = async (illustrationName) => { @@ -47,40 +47,26 @@ ${availableIllustrations}.forEach((illustrationName) => }; const getMatchingFiles = async (folder, pattern) => { - const dir = await fs.readdir(folder); - return dir.filter((fileName) => fileName.match(pattern)); + const dir = await fs.readdir(folder); + return dir.filter((fileName) => fileName.match(pattern)); }; -const generateIllustrations = async (config) => { - const { inputFolder, outputFile, collection, location, prefix, filterOut, set } = config; +const generateIllustrations = async (inputFolder, outputFile, set, collection, location, filterOut) => { - const normalizedInputFolder = path.normalize(inputFolder); - const normalizedOutputFile = path.normalize(outputFile); + const normalizedInputFolder = path.normalize(inputFolder); + const normalizedOutputFile = path.normalize(outputFile); - const illustrations = await getMatchingFiles(normalizedInputFolder, /^.*\.js$/); + const illustrations = await getMatchingFiles(normalizedInputFolder, /^.*\.js$/); - const dynamicImports = await generateDynamicImportLines(illustrations, location, filterOut); - const availableIllustrations = generateAvailableIllustrationsArray(illustrations, filterOut); + const dynamicImports = await generateDynamicImportLines(illustrations, location, filterOut); + const availableIllustrations = generateAvailableIllustrationsArray(illustrations, filterOut); - const contentDynamic = generateDynamicImportsFileContent(dynamicImports, availableIllustrations, collection, set, prefix); + const contentDynamic = generateDynamicImportsFileContent(dynamicImports, availableIllustrations, collection, set); - await fs.mkdir(path.dirname(normalizedOutputFile), { recursive: true }); - await fs.writeFile(normalizedOutputFile, contentDynamic); + await fs.mkdir(path.dirname(normalizedOutputFile), { recursive: true }); + await fs.writeFile(normalizedOutputFile, contentDynamic); - console.log(`Generated ${normalizedOutputFile}`); + console.log(`Generated ${normalizedOutputFile}`); }; -// Parse configuration from command-line arguments -const config = { - inputFolder: process.argv[2], - outputFile: process.argv[3], - set: process.argv[4], - collection: process.argv[5], - location: process.argv[6], - filterOut: process.argv.slice[7], -}; - -// Run the generation process -generateIllustrations(config).catch((error) => { - console.error("Error generating illustrations:", error); -}); +module.exports = generateIllustrations; diff --git a/packages/tools/lib/generate-json-imports/i18n.js b/packages/tools/lib/generate-json-imports/i18n.js index 4facdd417c0a..01b01a1d1cbe 100644 --- a/packages/tools/lib/generate-json-imports/i18n.js +++ b/packages/tools/lib/generate-json-imports/i18n.js @@ -1,11 +1,7 @@ const fs = require("fs").promises; const path = require('path'); -const isTypeScript = process.env.UI5_TS; -const ext = isTypeScript ? 'ts' : 'js'; - - -const getContent = function(caseLines, languagesKeysStringArray, packageName) { +const getContent = function (caseLines, languagesKeysStringArray, packageName) { return `// @ts-nocheck import { registerI18nLoader } from "@ui5/webcomponents-base/dist/asset-registries/i18n.js"; @@ -27,19 +23,19 @@ const importAndCheck = async (localeId) => { const localeIds = [${languagesKeysStringArray}]; localeIds.forEach(localeId => { - registerI18nLoader(${ packageName.split("").map(c => `"${c}"`).join (" + ") }, localeId, importAndCheck); + registerI18nLoader(${packageName.split("").map(c => `"${c}"`).join(" + ")}, localeId, importAndCheck); }); `; } -const generate = async () => { - +const generate = async (inputFolder, distFolder, isTypeScript) => { + const ext = isTypeScript ? 'ts' : 'js'; const packageName = JSON.parse(await fs.readFile("package.json")).name; - const inputFolder = path.normalize(process.argv[2]); - const outputFileDynamic = path.normalize(`${process.argv[3]}/i18n.${ext}`); - const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/i18n-fetch.${ext}`); - const outputFileDynamicImportJSONImport = path.normalize(`${process.argv[3]}/i18n-node.${ext}`); + inputFolder = path.normalize(inputFolder); + const outputFileDynamic = path.normalize(`${distFolder}/i18n.${ext}`); + const outputFileFetchMetaResolve = path.normalize(`${distFolder}/i18n-fetch.${ext}`); + const outputFileDynamicImportJSONImport = path.normalize(`${distFolder}/i18n-node.${ext}`); // All languages present in the file system const files = await fs.readdir(inputFolder); @@ -57,7 +53,7 @@ const generate = async () => { contentDynamic = ""; contentFetchMetaResolve = ""; contentDynamicImportJSONAttr = ""; - // There is i18n - generate the full file + // There is i18n - generate the full file } else { // Keys for the array const languagesKeysStringArray = languages.map(key => `"${key}",`).join("\n\t"); @@ -82,6 +78,4 @@ const generate = async () => { ]); } -generate().then(() => { - console.log("Generated i18n JSON imports."); -}); +module.exports = generate \ No newline at end of file diff --git a/packages/tools/lib/generate-json-imports/themes.js b/packages/tools/lib/generate-json-imports/themes.js index a3fc84f1f39a..7905258aa66f 100644 --- a/packages/tools/lib/generate-json-imports/themes.js +++ b/packages/tools/lib/generate-json-imports/themes.js @@ -2,14 +2,13 @@ const fs = require("fs").promises; const path = require('path'); const assets = require("../../assets-meta.js"); -const isTypeScript = process.env.UI5_TS; -const ext = isTypeScript ? 'ts' : 'js'; +const generate = async (inputFolder, distFolder, isTypeScript) => { + const ext = isTypeScript ? 'ts' : 'js'; -const generate = async () => { - const inputFolder = path.normalize(process.argv[2]); - const outputFileDynamic = path.normalize(`${process.argv[3]}/Themes.${ext}`); - const outputFileDynamicImportJSONAttr = path.normalize(`${process.argv[3]}/Themes-node.${ext}`); - const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/Themes-fetch.${ext}`); + inputFolder = path.normalize(inputFolder); + const outputFileDynamic = path.normalize(`${distFolder}/Themes.${ext}`); + const outputFileDynamicImportJSONAttr = path.normalize(`${distFolder}/Themes-node.${ext}`); + const outputFileFetchMetaResolve = path.normalize(`${distFolder}/Themes-fetch.${ext}`); // All supported optional themes const allThemes = assets.themes.all; @@ -49,7 +48,7 @@ const loadAndCheck = async (themeName) => { }; ${availableThemesArray} - .forEach(themeName => registerThemePropertiesLoader(${ packageName.split("").map(c => `"${c}"`).join (" + ") }, themeName, loadAndCheck)); + .forEach(themeName => registerThemePropertiesLoader(${packageName.split("").map(c => `"${c}"`).join(" + ")}, themeName, loadAndCheck)); `; } @@ -61,6 +60,4 @@ ${availableThemesArray} ]); }; -generate().then(() => { - console.log("Generated themes JSON imports."); -}); +module.exports = generate; \ No newline at end of file diff --git a/packages/tools/lib/i18n/defaults.js b/packages/tools/lib/i18n/defaults.js index dc909a12b713..9db247d7c096 100644 --- a/packages/tools/lib/i18n/defaults.js +++ b/packages/tools/lib/i18n/defaults.js @@ -3,14 +3,14 @@ const path = require('path'); const PropertiesReader = require('properties-reader'); const assets = require('../../assets-meta.js'); -const generate = async () => { +const generate = async (inputFolder, distFolder, ts) => { const defaultLanguage = assets.languages.default; - const messageBundle = path.normalize(`${process.argv[2]}/messagebundle.properties`); - const messageBundleDefaultLanguage = path.normalize(`${process.argv[2]}/messagebundle_${defaultLanguage}.properties`); - const tsMode = process.env.UI5_TS === "true"; // In Typescript mode, we output .ts files and set the required types, otherwise - output pure .js files + const messageBundle = path.normalize(`${inputFolder}/messagebundle.properties`); + const messageBundleDefaultLanguage = path.normalize(`${inputFolder}/messagebundle_${defaultLanguage}.properties`); + const tsMode = ts; // In Typescript mode, we output .ts files and set the required types, otherwise - output pure .js files - const outputFile = path.normalize(`${process.argv[3]}/i18n-defaults.${tsMode ? "ts": "js"}`); + const outputFile = path.normalize(`${distFolder}/i18n-defaults.${tsMode ? "ts": "js"}`); if (!messageBundle || !outputFile) { return; @@ -78,6 +78,4 @@ export {${textKeys.join()}};`; await fs.writeFile(outputFile, getOutputFileContent(properties, defaultLanguageProperties)); }; -generate().then(() => { - console.log("i18n default file generated."); -}); +module.exports = generate \ No newline at end of file diff --git a/packages/tools/lib/i18n/toJSON.js b/packages/tools/lib/i18n/toJSON.js index 80ff49856475..d8cd4b72528b 100644 --- a/packages/tools/lib/i18n/toJSON.js +++ b/packages/tools/lib/i18n/toJSON.js @@ -14,10 +14,7 @@ const assets = require('../../assets-meta.js'); const allLanguages = assets.languages.all; -const messagesBundles = path.normalize(`${process.argv[2]}/messagebundle_*.properties`); -const messagesJSONDist = path.normalize(`${process.argv[3]}`); - -const convertToJSON = async (file) => { +const convertToJSON = async (file, messagesJSONDist) => { const properties = PropertiesReader(file)._properties; const filename = path.basename(file, path.extname(file)); const language = filename.match(/^messagebundle_(.*?)$/)[1]; @@ -31,13 +28,13 @@ const convertToJSON = async (file) => { // console.log(`[i18n]: "${filename}.json" has been generated!`); }; -const generate = async () => { +const generate = async (inputFolder, distFolder) => { const { globby } = await import("globby"); + const messagesBundles = path.normalize(`${inputFolder}/messagebundle_*.properties`); + const messagesJSONDist = path.normalize(`${distFolder}`); await fs.mkdir(messagesJSONDist, { recursive: true }); const files = await globby(messagesBundles.replace(/\\/g, "/")); - return Promise.all(files.map(convertToJSON)); + return Promise.all(files.map((file => convertToJSON(file, messagesJSONDist)))); }; -generate().then(() => { - console.log("Message bundle JSON files generated."); -}); +module.exports = generate; diff --git a/packages/tools/lib/remove-dev-mode/remove-dev-mode.mjs b/packages/tools/lib/remove-dev-mode/remove-dev-mode.mjs index d46f2ae7360a..1cfe3bf6a3bb 100644 --- a/packages/tools/lib/remove-dev-mode/remove-dev-mode.mjs +++ b/packages/tools/lib/remove-dev-mode/remove-dev-mode.mjs @@ -3,35 +3,39 @@ import * as esbuild from 'esbuild' import * as fs from "fs"; let customPlugin = { - name: 'ui5-tools', - setup(build) { - build.onLoad({ filter: /UI5Element.ts$/ }, async (args) => { - let text = await fs.promises.readFile(args.path, 'utf8'); - text = text.replaceAll(/const DEV_MODE = true/g, ""); - text = text.replaceAll(/if \(DEV_MODE\)/g, "if (false)"); - return { - contents: text, - loader: 'ts', - } - }) - }, + name: 'ui5-tools', + setup(build) { + build.onLoad({ filter: /UI5Element.ts$/ }, async (args) => { + let text = await fs.promises.readFile(args.path, 'utf8'); + text = text.replaceAll(/const DEV_MODE = true/g, ""); + text = text.replaceAll(/if \(DEV_MODE\)/g, "if (false)"); + return { + contents: text, + loader: 'ts', + } + }) + }, } const getConfig = async () => { - const config = { - entryPoints: await globby("src/**/*.ts"), - bundle: false, - minify: true, - sourcemap: true, - outdir: 'dist/prod', - outbase: 'src', - plugins: [ - customPlugin, - ] - }; - return config; + const config = { + entryPoints: await globby("src/**/*.ts"), + bundle: false, + minify: true, + sourcemap: true, + outdir: 'dist/prod', + outbase: 'src', + plugins: [ + customPlugin, + ] + }; + return config; } +const removeDevMode = async () => { + const config = await getConfig(); + const result = await esbuild.build(config); + return result; +} -const config = await getConfig(); -const result = await esbuild.build(config); \ No newline at end of file +export default removeDevMode; \ No newline at end of file diff --git a/packages/tools/task-runner/build-runner.js b/packages/tools/task-runner/build-runner.js new file mode 100644 index 000000000000..a7e495e56b6f --- /dev/null +++ b/packages/tools/task-runner/build-runner.js @@ -0,0 +1,187 @@ +const { execSync } = require('child_process'); + +const times = new Map(); + +function buildCrossEnvCommand(task) { + if (!task.crossEnv || Object.keys(task.crossEnv).length === 0) { + return task.command; + } + + const envVars = Object.entries(task.crossEnv) + .map(([key, value]) => `${key}=${value}`) + .join(' '); + + return `cross-env ${envVars} ${task.command}`; +} + +class BuildRunner { + constructor() { + this.tasks = new Map(); + this.runningTasks = new Set(); + } + + // Register a task + addTask(name, options = {}) { + const hasFunction = typeof options.callback === 'function'; + + this.tasks.set(name, { + callback: options.callback, + isFunction: hasFunction, + cwd: options.cwd || process.cwd(), + crossEnv: { ...options.crossEnv }, + env: { ...process.env, ...options.env }, + parallel: options.parallel || false, + skip: options.skip || false, + dependencies: options.dependencies || [] + }); + } + + // Register a task + emptyTask(taskName) { + let task = this.tasks.get(taskName); + + if (task) { + task.callback = null; + task.isFunction = false; + task.dependencies = []; + + this.tasks.set(taskName, task) + } + } + + // Helper method to run either a registered task or a direct command + async runTaskOrCommand(nameOrCommand, parentEnv = {}, parentCrossEnv = {}) { + // Skip empty strings or null/undefined commands + if (!nameOrCommand || nameOrCommand.trim() === '') { + console.log('Skipping empty command'); + return Promise.resolve(); + } + + // Check if it's a registered task + if (this.tasks.has(nameOrCommand)) { + return this.runTask(nameOrCommand, parentEnv, parentCrossEnv); + } else { + // Execute as direct shell command + console.log(`Running command: ${nameOrCommand}`); + return new Promise((resolve, reject) => { + const start = Date.now() + try { + // Create a temporary task object for buildCrossEnvCommand + const tempTask = { + command: nameOrCommand, + crossEnv: parentCrossEnv + }; + + const commandToExecute = buildCrossEnvCommand(tempTask); + + const result = execSync(commandToExecute, { + cwd: process.cwd(), + env: { ...process.env, ...parentEnv }, + stdio: 'inherit' + }); + resolve(result); + } catch (error) { + reject(error); + } finally { + const end = Date.now(); + if (end - start > 150) { + times.set(nameOrCommand, `Execution time: ${(end - start) / 1000} seconds`); + } + + if (times.size > 0) { + console.log(`======= Completed command: ${nameOrCommand}`); + console.log(times); + } + } + }); + } + } + + // Run a single task + async runTask(taskName, parentEnv = {}, parentCrossEnv = {}) { + if (this.runningTasks.has(taskName)) { + return; // Already running + } + + let task = this.tasks.get(taskName); + + if (!task) { + // Only execute if it's not a registered command - create a default task for direct command execution + this.addTask(taskName, { dependencies: [taskName] }); + task = this.tasks.get(taskName); + } + + if ((!task.callback && !task.isFunction && (!task.dependencies || task.dependencies.length === 0)) || task.skip) { + return; + } + + // Merge parent environment variables with task environment + const mergedEnv = { ...task.env, ...parentEnv }; + const mergedCrossEnv = { ...task.crossEnv, ...parentCrossEnv }; + task = { ...task, env: mergedEnv, crossEnv: mergedCrossEnv }; + + if (task.parallel) { + const promises = []; + + // Run dependencies first, passing along the merged environment + for (const dep of task.dependencies) { + if (dep && dep.trim() !== '') { // Skip empty dependencies + promises.push(this.runTaskOrCommand(dep, mergedEnv, mergedCrossEnv)); + } + } + + await Promise.all(promises); + } else { + // Run dependencies first, passing along the merged environment + for (const dep of task.dependencies) { + if (dep && dep.trim() !== '') { // Skip empty dependencies + await this.runTaskOrCommand(dep, mergedEnv, mergedCrossEnv); + } + } + } + + this.runningTasks.add(taskName); + + try { + console.log(`Running: ${taskName}`); + + if (task.isFunction) { + // Execute function directly + return new Promise(async (resolve, reject) => { + const start = Date.now() + + try { + const result = await task.callback(); + resolve(result); + } catch (error) { + reject(error); + } finally { + const end = Date.now(); + if (end - start > 150) { + times.set(taskName + start, `Execution time: ${(end - start) / 1000} seconds`); + } + + if (times.size > 0) { + console.log(`======= Completed command: ${taskName}`); + console.log(times); + } + } + }); + } else { + // If no function, the task is completed by running its dependencies + return Promise.resolve(); + } + } finally { + this.runningTasks.delete(taskName); + } + } + + // Run multiple tasks + async run(...taskNames) { + for (const taskName of taskNames) { + await this.runTask(taskName); + } + } +} + +module.exports = BuildRunner; \ No newline at end of file diff --git a/packages/tools/task-runner/runner.js b/packages/tools/task-runner/runner.js new file mode 100644 index 000000000000..e3b8f0d57c7d --- /dev/null +++ b/packages/tools/task-runner/runner.js @@ -0,0 +1,42 @@ +const BuildRunner = require('./build-runner'); + +const runner = new BuildRunner(); + +// Define your tasks (replace these with your actual NPS script equivalents) +runner.addTask('clean', 'rm -rf dist', { + cwd: process.cwd() +}); + +runner.addTask('lint', 'eslint src --ext .js,.ts', { + cwd: process.cwd() +}); + +runner.addTask('build:components', 'rollup -c', { + cwd: process.cwd(), + dependencies: ['clean'] +}); + +runner.addTask('test', 'jest', { + cwd: process.cwd() +}); + +runner.addTask('build', 'echo "Building project"', { + dependencies: ['lint', 'build:components'], + parallel: false +}); + +// Export for CLI usage +if (require.main === module) { + const taskName = process.argv[2]; + if (!taskName) { + console.log('Available tasks:', Array.from(runner.tasks.keys()).join(', ')); + process.exit(1); + } + + runner.run(taskName).catch(error => { + console.error('Task failed:', error.message); + process.exit(1); + }); +} + +module.exports = runner; \ No newline at end of file