From 035ee094a5e5a82e154dde882bd804b9704c3926 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Mon, 20 Apr 2026 10:49:10 +0300 Subject: [PATCH 01/12] chore(deps): patch npm audit vulnerabilities Update root and test lockfiles to pull non-vulnerable axios, follow-redirects, and next versions so repo-wide audits pass. --- package-lock.json | 92 ++++++++++++------------- tests/test-commonjs/package-lock.json | 12 ++-- tests/test-nextjs-cjs/package-lock.json | 90 ++++++++++++------------ tests/test-nextjs/package-lock.json | 90 ++++++++++++------------ 4 files changed, 142 insertions(+), 142 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3513ca..631155b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "svgo": "^4.0.1", "svgtofont": "^6.5.1", "typescript": "^6.0.2", - "vite": "^8.0.3", + "vite": "^8.0.5", "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-zip-pack": "^1.2.4", "winston": "^3.19.0" @@ -1529,16 +1529,16 @@ } }, "node_modules/@next/env": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", - "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", "dev": true, "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", - "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", "cpu": [ "arm64" ], @@ -1553,9 +1553,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", - "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", "cpu": [ "x64" ], @@ -1570,9 +1570,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", - "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", "cpu": [ "arm64" ], @@ -1587,9 +1587,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", - "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", "cpu": [ "arm64" ], @@ -1604,9 +1604,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", - "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", "cpu": [ "x64" ], @@ -1621,9 +1621,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", - "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", "cpu": [ "x64" ], @@ -1638,9 +1638,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", - "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", "cpu": [ "arm64" ], @@ -1655,9 +1655,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", - "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", "cpu": [ "x64" ], @@ -4224,9 +4224,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -6461,9 +6461,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8646,13 +8646,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", - "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "16.2.2", + "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -8666,14 +8666,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.2", - "@next/swc-darwin-x64": "16.2.2", - "@next/swc-linux-arm64-gnu": "16.2.2", - "@next/swc-linux-arm64-musl": "16.2.2", - "@next/swc-linux-x64-gnu": "16.2.2", - "@next/swc-linux-x64-musl": "16.2.2", - "@next/swc-win32-arm64-msvc": "16.2.2", - "@next/swc-win32-x64-msvc": "16.2.2", + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/tests/test-commonjs/package-lock.json b/tests/test-commonjs/package-lock.json index 670a3f9..d801c90 100644 --- a/tests/test-commonjs/package-lock.json +++ b/tests/test-commonjs/package-lock.json @@ -2613,9 +2613,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -3929,9 +3929,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", diff --git a/tests/test-nextjs-cjs/package-lock.json b/tests/test-nextjs-cjs/package-lock.json index 405219f..b67ffc0 100644 --- a/tests/test-nextjs-cjs/package-lock.json +++ b/tests/test-nextjs-cjs/package-lock.json @@ -2062,9 +2062,9 @@ } }, "node_modules/@next/env": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", - "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2078,9 +2078,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", - "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", "cpu": [ "arm64" ], @@ -2094,9 +2094,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", - "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", "cpu": [ "x64" ], @@ -2110,9 +2110,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", - "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", "cpu": [ "arm64" ], @@ -2126,9 +2126,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", - "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", "cpu": [ "arm64" ], @@ -2142,9 +2142,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", - "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", "cpu": [ "x64" ], @@ -2158,9 +2158,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", - "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", "cpu": [ "x64" ], @@ -2174,9 +2174,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", - "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", "cpu": [ "arm64" ], @@ -2190,9 +2190,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", - "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", "cpu": [ "x64" ], @@ -4586,9 +4586,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -6759,9 +6759,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8951,12 +8951,12 @@ } }, "node_modules/next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", - "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", "license": "MIT", "dependencies": { - "@next/env": "16.2.2", + "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -8970,14 +8970,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.2", - "@next/swc-darwin-x64": "16.2.2", - "@next/swc-linux-arm64-gnu": "16.2.2", - "@next/swc-linux-arm64-musl": "16.2.2", - "@next/swc-linux-x64-gnu": "16.2.2", - "@next/swc-linux-x64-musl": "16.2.2", - "@next/swc-win32-arm64-msvc": "16.2.2", - "@next/swc-win32-x64-msvc": "16.2.2", + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/tests/test-nextjs/package-lock.json b/tests/test-nextjs/package-lock.json index d16ec7a..3ed9b58 100644 --- a/tests/test-nextjs/package-lock.json +++ b/tests/test-nextjs/package-lock.json @@ -2011,9 +2011,9 @@ } }, "node_modules/@next/env": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", - "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2027,9 +2027,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", - "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", "cpu": [ "arm64" ], @@ -2043,9 +2043,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", - "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", "cpu": [ "x64" ], @@ -2059,9 +2059,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", - "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", "cpu": [ "arm64" ], @@ -2075,9 +2075,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", - "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", "cpu": [ "arm64" ], @@ -2091,9 +2091,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", - "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", "cpu": [ "x64" ], @@ -2107,9 +2107,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", - "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", "cpu": [ "x64" ], @@ -2123,9 +2123,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", - "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", "cpu": [ "arm64" ], @@ -2139,9 +2139,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", - "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", "cpu": [ "x64" ], @@ -4704,9 +4704,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -6820,9 +6820,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -9052,12 +9052,12 @@ } }, "node_modules/next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", - "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", "license": "MIT", "dependencies": { - "@next/env": "16.2.2", + "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -9071,14 +9071,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.2", - "@next/swc-darwin-x64": "16.2.2", - "@next/swc-linux-arm64-gnu": "16.2.2", - "@next/swc-linux-arm64-musl": "16.2.2", - "@next/swc-linux-x64-gnu": "16.2.2", - "@next/swc-linux-x64-musl": "16.2.2", - "@next/swc-win32-arm64-msvc": "16.2.2", - "@next/swc-win32-x64-msvc": "16.2.2", + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { From 4de05ed2469933007aa654e370a84d90aecf3df3 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Mon, 20 Apr 2026 12:05:56 +0300 Subject: [PATCH 02/12] fix(cli): harden shortcut cleanup and add dts trace logging Improve stdin shortcut lifecycle cleanup to avoid stale bindings and preserve terminal state across server restarts. Add trace hooks to the dts rollup build to pinpoint where type generation can stall. --- rollup.config.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++- src/shortcuts.ts | 42 ++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/rollup.config.ts b/rollup.config.ts index 7e1a1eb..3f8e992 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, RollupOptions } from 'rollup'; +import { defineConfig, Plugin, RollupOptions } from 'rollup'; import typescript from '@rollup/plugin-typescript'; import dts from 'rollup-plugin-dts'; import terser from '@rollup/plugin-terser'; @@ -32,15 +32,69 @@ const defaultConfig: RollupOptions = { if (warning.code === 'CIRCULAR_DEPENDENCY') { return; } + if (warning.code === 'UNUSED_EXTERNAL_IMPORT') { return; } + warn(warning); }, // Preserve Node.js globals context: 'globalThis', }; +function typeDefsTracePlugin(): Plugin { + let transformCount = 0; + const startedAt = Date.now(); + const logTrace = (message: string) => { + const elapsedMs = Date.now() - startedAt; + console.info(`[rollup:dts:trace] +${elapsedMs}ms ${message}`); + }; + + return { + name: 'type-defs-trace', + buildStart() { + logTrace('buildStart'); + }, + resolveId(source, importer) { + if (source === 'src/index.ts') { + logTrace(`resolveId source=${source} importer=${importer ?? 'entry'}`); + } + return null; + }, + transform(_, id) { + transformCount += 1; + if (transformCount === 1 || transformCount % 25 === 0) { + logTrace(`transform count=${transformCount} file=${id}`); + } + return null; + }, + buildEnd(error) { + if (error) { + logTrace(`buildEnd with error=${error.message}`); + } else { + logTrace('buildEnd'); + } + }, + outputOptions() { + logTrace('outputOptions'); + return null; + }, + renderStart() { + logTrace('renderStart'); + }, + generateBundle() { + logTrace('generateBundle'); + }, + writeBundle() { + logTrace('writeBundle'); + }, + closeBundle() { + logTrace('closeBundle'); + }, + }; +} + const configs: RollupOptions[] = [ // ESM Build defineConfig({ @@ -141,6 +195,7 @@ const configs: RollupOptions[] = [ defineConfig({ input: 'src/index.ts', plugins: [ + typeDefsTracePlugin(), dts({ tsconfig: './tsconfig.types.json', compilerOptions: { diff --git a/src/shortcuts.ts b/src/shortcuts.ts index 796560c..2c4517b 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -20,6 +20,8 @@ export type CLIShortcut = { action(server: ViteDevServer): void | Promise; }; +let cleanupActiveShortcutBinding: (() => void) | null = null; + export function bindShortcuts( server: ViteDevServer, opts: BindShortcutsOptions, @@ -44,12 +46,27 @@ export function bindShortcuts( .filter(isDefined) .concat(BASE_SHORTCUTS); + // Ensure previous server bindings are fully detached before rebinding. + cleanupActiveShortcutBinding?.(); + cleanupActiveShortcutBinding = null; + let actionRunning = false; + const wasPaused = process.stdin.isPaused(); + const stdinWithRawMode = process.stdin as NodeJS.ReadStream & { + isRaw?: boolean; + }; + const hadRawModeEnabled = Boolean(stdinWithRawMode.isRaw); const onInput = async (input: string) => { // ctrl+c or ctrl+d - if (input === '\x03' || input === '\x04') { - await server.close().finally(() => process.exit(1)); + if (input === '\x03') { + // Re-emit SIGINT so the CLI graceful shutdown handlers run. + process.kill(process.pid, 'SIGINT'); + return; + } + + if (input === '\x04') { + await server.close().finally(() => process.exit(0)); return; } @@ -88,9 +105,24 @@ export function bindShortcuts( process.stdin.on('data', onInput).setEncoding('utf8').resume(); - server.httpServer.on('close', () => { - process.stdin.off('data', onInput).pause(); - }); + const cleanupBinding = () => { + process.stdin.off('data', onInput); + + if (!hadRawModeEnabled) { + process.stdin.setRawMode(false); + } + + if (wasPaused) { + process.stdin.pause(); + } + + if (cleanupActiveShortcutBinding === cleanupBinding) { + cleanupActiveShortcutBinding = null; + } + }; + + cleanupActiveShortcutBinding = cleanupBinding; + server.httpServer.on('close', cleanupBinding); } const BASE_SHORTCUTS: CLIShortcut[] = [ From 8edd46558b09fba381e6f17b100c263fb53dc531 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Mon, 20 Apr 2026 12:06:10 +0300 Subject: [PATCH 03/12] chore(test): refresh commonjs fixture lockfile Update the CommonJS test fixture lockfile to the latest local pp-dev package and aligned dependency metadata so fixture installs stay reproducible. --- tests/test-commonjs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-commonjs/package-lock.json b/tests/test-commonjs/package-lock.json index d801c90..21a7184 100644 --- a/tests/test-commonjs/package-lock.json +++ b/tests/test-commonjs/package-lock.json @@ -1230,9 +1230,9 @@ } }, "node_modules/@metricinsights/pp-dev": { - "version": "0.17.0-beta.1", + "version": "0.18.0-beta.1", "resolved": "file:../../metricinsights-pp-dev-latest.tgz", - "integrity": "sha512-nJbTMF46s3lLkg+nJ41kr3ShhV7Hh38fl4kOeAHbxBE2A3264OCNu50z+tri3toI4hbPkaTImYSsVQFkpbuiwQ==", + "integrity": "sha512-yTfu4UHFVFdm0s46d+Wx82eqzDzdJRZBxLa5dQonQ6xsbWMMvIV7i1sMouKKal3cMVh9ZX3zJ891IFGx7izRsA==", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -1260,7 +1260,7 @@ "svgo": "^4.0.1", "svgtofont": "^6.5.1", "typescript": "^6.0.2", - "vite": "^8.0.3", + "vite": "^8.0.5", "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-zip-pack": "^1.2.4", "winston": "^3.19.0" From 42d52b349a0175ba0a06dd59d2261c4458e0c49e Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:13:44 +0300 Subject: [PATCH 04/12] chore(tooling): add Prettier configuration --- .prettierignore | 6 ++++++ .prettierrc.cjs | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc.cjs diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..26979ba --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +dist-zip +coverage +*.tgz +tests/test-nextjs/.next diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 0000000..b0648ab --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + tabWidth: 2, + semi: true, + singleQuote: true, + trailingComma: 'all', + printWidth: 120, +}; From d4780d9ce777daf5dd58d9f49a40f5e96cdec2ea Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:14:15 +0300 Subject: [PATCH 05/12] chore(ci): update Dependabot configuration --- .github/dependabot.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3ea3a9f..53e2a10 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,10 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "daily" - - package-ecosystem: "docker" - directory: "/" + interval: 'daily' + - package-ecosystem: 'docker' + directory: '/' schedule: - interval: "daily" + interval: 'daily' From caf2b98fa734c6f2b4fe705024c32139a732cfc7 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:14:26 +0300 Subject: [PATCH 06/12] chore(build): update dependencies, bundler configs, scripts, and test runners --- esbuild.config.ts | 8 +- playwright.config.ts | 15 ++- pp-dev.d.ts | 1 + rollup.config.ts | 126 ++++++++++-------- scripts/build-optimizer.js | 43 +++--- scripts/build-parallel.js | 11 +- scripts/patch-npm-bundled-vulnerabilities.mjs | 19 ++- scripts/postbuild.js | 2 +- scripts/run-tests.js | 16 ++- scripts/startup-optimizer.js | 36 ++--- tsconfig.cjs.json | 4 +- tsconfig.esm.json | 2 +- tsconfig.json | 11 +- vitest.config.ts | 6 +- vitest.integration.config.ts | 6 +- 15 files changed, 158 insertions(+), 148 deletions(-) diff --git a/esbuild.config.ts b/esbuild.config.ts index bcd10b8..2ede34f 100644 --- a/esbuild.config.ts +++ b/esbuild.config.ts @@ -12,10 +12,7 @@ const commonOptions = { sourcemap: true, target: 'node20' as const, platform: 'node' as const, - external: [ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ], + external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], outdir: 'dist/esbuild', format: 'esm' as const, splitting: true, @@ -27,7 +24,7 @@ const commonOptions = { async function buildWithEsbuild() { try { console.log('šŸš€ Starting esbuild build...'); - + // Build main package const result = await build({ ...commonOptions, @@ -48,7 +45,6 @@ async function buildWithEsbuild() { console.log('āœ… Esbuild build completed successfully!'); console.log('šŸ“Š Build metadata:', result.metafile); - } catch (error) { console.error('āŒ Esbuild build failed:', error); process.exit(1); diff --git a/playwright.config.ts b/playwright.config.ts index 129fb1a..c584b65 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -23,11 +23,14 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ - ['html', { - outputFolder: `playwright-report/${process.env.TEST_TYPE || 'all'}`, - open: 'never' - }], - ['list'] + [ + 'html', + { + outputFolder: `playwright-report/${process.env.TEST_TYPE || 'all'}`, + open: 'never', + }, + ], + ['list'], ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { @@ -36,7 +39,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - + /* Take screenshot on both success and failure */ screenshot: 'on', }, diff --git a/pp-dev.d.ts b/pp-dev.d.ts index eb5a44f..382878d 100644 --- a/pp-dev.d.ts +++ b/pp-dev.d.ts @@ -56,5 +56,6 @@ declare module '.pp-watch.config.json' { declare module '*.scss' { const content: Record; + export default content; } diff --git a/rollup.config.ts b/rollup.config.ts index 3f8e992..0bcf121 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -1,9 +1,11 @@ -import { defineConfig, Plugin, RollupOptions } from 'rollup'; +// @ts-nocheck +import { defineConfig } from 'rollup'; import typescript from '@rollup/plugin-typescript'; import dts from 'rollup-plugin-dts'; import terser from '@rollup/plugin-terser'; import * as path from 'path'; import * as fs from 'fs'; +import { builtinModules } from 'module'; // Read package.json safely const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8')); @@ -15,7 +17,26 @@ const externalDeps = [ // Exclude dev dependencies from external as they shouldn't be bundled ]; -const defaultConfig: RollupOptions = { +const nodeBuiltins = new Set(builtinModules.flatMap((mod) => [mod, `node:${mod}`])); +const additionalExternal = new Set(['postcss', 'rollup', 'vite', 'estree']); + +const isExternal = (id) => { + if (nodeBuiltins.has(id)) { + return true; + } + + if (externalDeps.includes(id)) { + return true; + } + + if (additionalExternal.has(id)) { + return true; + } + + return externalDeps.some((dep) => id.startsWith(`${dep}/`)); +}; + +const defaultConfig = { input: { index: 'src/index.ts', plugin: 'src/plugin.ts', @@ -36,66 +57,72 @@ const defaultConfig: RollupOptions = { if (warning.code === 'UNUSED_EXTERNAL_IMPORT') { return; } - + warn(warning); }, // Preserve Node.js globals context: 'globalThis', }; -function typeDefsTracePlugin(): Plugin { - let transformCount = 0; - const startedAt = Date.now(); - const logTrace = (message: string) => { - const elapsedMs = Date.now() - startedAt; - console.info(`[rollup:dts:trace] +${elapsedMs}ms ${message}`); +function typeDefsMonitorPlugin() { + const timeoutMs = Number(process.env.PP_DEV_DTS_TIMEOUT_MS ?? 120_000); + const heartbeatMs = Number(process.env.PP_DEV_DTS_HEARTBEAT_MS ?? 10_000); + + let buildStartedAt = 0; + let heartbeat = null; + let timeout = null; + + const clearTimers = () => { + if (heartbeat) { + clearInterval(heartbeat); + heartbeat = null; + } + + if (timeout) { + clearTimeout(timeout); + timeout = null; + } }; return { - name: 'type-defs-trace', + name: 'type-defs-monitor', buildStart() { - logTrace('buildStart'); - }, - resolveId(source, importer) { - if (source === 'src/index.ts') { - logTrace(`resolveId source=${source} importer=${importer ?? 'entry'}`); - } - return null; - }, - transform(_, id) { - transformCount += 1; - if (transformCount === 1 || transformCount % 25 === 0) { - logTrace(`transform count=${transformCount} file=${id}`); - } - return null; + buildStartedAt = Date.now(); + console.info(`[rollup:dts] build started (timeout=${timeoutMs}ms heartbeat=${heartbeatMs}ms)`); + + heartbeat = setInterval(() => { + const elapsedMs = Date.now() - buildStartedAt; + + console.info(`[rollup:dts] still building... elapsed=${elapsedMs}ms`); + }, heartbeatMs); + heartbeat.unref?.(); + + timeout = setTimeout(() => { + const elapsedMs = Date.now() - buildStartedAt; + + console.error(`[rollup:dts] build exceeded timeout after ${elapsedMs}ms, aborting`); + process.exit(1); + }, timeoutMs); + timeout.unref?.(); }, buildEnd(error) { + clearTimers(); + + const elapsedMs = Date.now() - buildStartedAt; + if (error) { - logTrace(`buildEnd with error=${error.message}`); + console.error(`[rollup:dts] build failed after ${elapsedMs}ms: ${error.message}`); } else { - logTrace('buildEnd'); + console.info(`[rollup:dts] build completed in ${elapsedMs}ms`); } }, - outputOptions() { - logTrace('outputOptions'); - return null; - }, - renderStart() { - logTrace('renderStart'); - }, - generateBundle() { - logTrace('generateBundle'); - }, - writeBundle() { - logTrace('writeBundle'); - }, closeBundle() { - logTrace('closeBundle'); + clearTimers(); }, }; } -const configs: RollupOptions[] = [ +const configs = [ // ESM Build defineConfig({ ...defaultConfig, @@ -137,7 +164,7 @@ const configs: RollupOptions[] = [ chunkFileNames: '[name]-[hash].js', entryFileNames: '[name].js', }, - external: externalDeps, + external: isExternal, // Better tree-shaking preserveEntrySignatures: 'strict', }), @@ -173,7 +200,7 @@ const configs: RollupOptions[] = [ format: 'cjs', assetFileNames: '[name][extname]', sourcemap: true, - exports: 'auto', + exports: 'named', interop: 'compat', // Fix: avoid _interopNamespaceCompat crash when processing Node.js built-ins // (path, child_process, etc.) that have inherited prototype properties. @@ -187,7 +214,7 @@ const configs: RollupOptions[] = [ chunkFileNames: '[name]-[hash].js', entryFileNames: '[name].js', }, - external: externalDeps, + external: isExternal, preserveEntrySignatures: 'strict', }), @@ -195,7 +222,7 @@ const configs: RollupOptions[] = [ defineConfig({ input: 'src/index.ts', plugins: [ - typeDefsTracePlugin(), + typeDefsMonitorPlugin(), dts({ tsconfig: './tsconfig.types.json', compilerOptions: { @@ -211,14 +238,7 @@ const configs: RollupOptions[] = [ format: 'esm', assetFileNames: '[name][extname]', }, - external: [ - ...externalDeps, - // Add problematic dependencies to external - 'postcss', - 'rollup', - 'vite', - 'estree', - ], + external: isExternal, }), ]; diff --git a/scripts/build-optimizer.js b/scripts/build-optimizer.js index e709866..8b92e87 100644 --- a/scripts/build-optimizer.js +++ b/scripts/build-optimizer.js @@ -16,13 +16,13 @@ class BuildOptimizer { // Analyze bundle sizes async analyzeBundleSizes() { console.log('šŸ“Š Analyzing bundle sizes...'); - + try { // Check if rollup-plugin-visualizer is available if (this.packageJson.devDependencies['rollup-plugin-visualizer']) { - execSync('npm run build:analyze', { - cwd: this.projectRoot, - stdio: 'inherit' + execSync('npm run build:analyze', { + cwd: this.projectRoot, + stdio: 'inherit', }); console.log('āœ… Bundle analysis completed'); } else { @@ -36,26 +36,21 @@ class BuildOptimizer { // Optimize TypeScript compilation optimizeTypeScript() { console.log('⚔ Optimizing TypeScript configuration...'); - - const tsConfigs = [ - 'tsconfig.json', - 'tsconfig.esm.json', - 'tsconfig.cjs.json', - 'tsconfig.types.json' - ]; - - tsConfigs.forEach(configFile => { + + const tsConfigs = ['tsconfig.json', 'tsconfig.esm.json', 'tsconfig.cjs.json', 'tsconfig.types.json']; + + tsConfigs.forEach((configFile) => { const configPath = resolve(this.projectRoot, configFile); if (existsSync(configPath)) { const config = JSON.parse(readFileSync(configPath, 'utf-8')); - + // Add performance optimizations if (config.compilerOptions) { config.compilerOptions.incremental = true; config.compilerOptions.tsBuildInfoFile = './dist/.tsbuildinfo'; config.compilerOptions.assumeChangesOnlyAffectDirectDependencies = true; } - + writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log(`āœ… Optimized ${configFile}`); } @@ -65,7 +60,7 @@ class BuildOptimizer { // Generate build performance report generateBuildReport() { console.log('šŸ“ˆ Generating build performance report...'); - + const report = { timestamp: new Date().toISOString(), package: this.packageJson.name, @@ -82,39 +77,39 @@ class BuildOptimizer { 'Optimized TypeScript compilation', 'Bundle analysis support', 'Improved source maps', - 'Better external dependency handling' + 'Better external dependency handling', ], recommendations: [ 'Use `npm run build:fast` for quick builds', 'Use `npm run build:parallel` for production builds', 'Use `npm run build:analyze` to analyze bundle sizes', - 'Consider using `npm run build:watch` for development' - ] + 'Consider using `npm run build:watch` for development', + ], }; const reportPath = resolve(this.projectRoot, 'dist/build-report.json'); writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log(`āœ… Build report generated: ${reportPath}`); - + return report; } // Run all optimizations async optimize() { console.log('šŸš€ Starting build optimization...\n'); - + this.optimizeTypeScript(); await this.analyzeBundleSizes(); const report = this.generateBuildReport(); - + console.log('\nšŸŽ‰ Build optimization completed!'); console.log('\nšŸ“‹ Summary:'); console.log(` Package: ${report.package}@${report.version}`); console.log(` Dependencies: ${report.dependencies.total} runtime, ${report.dependencies.dev} dev`); console.log(` Optimizations applied: ${report.buildOptimizations.length}`); - + console.log('\nšŸ’” Quick start:'); - report.recommendations.forEach(rec => console.log(` ${rec}`)); + report.recommendations.forEach((rec) => console.log(` ${rec}`)); } } diff --git a/scripts/build-parallel.js b/scripts/build-parallel.js index 495d536..595044d 100644 --- a/scripts/build-parallel.js +++ b/scripts/build-parallel.js @@ -10,7 +10,7 @@ async function runCommand(command, args, cwd) { const child = spawn(command, args, { cwd, stdio: 'inherit', - shell: true + shell: true, }); child.on('close', (code) => { @@ -29,19 +29,18 @@ async function runCommand(command, args, cwd) { async function buildParallel() { const projectRoot = resolve(__dirname, '..'); - + try { console.log('šŸš€ Starting parallel build...'); - + // Start both builds concurrently const nodeBuild = runCommand('npm', ['run', 'build:node'], projectRoot); const clientBuild = runCommand('npm', ['run', 'build:client'], projectRoot); - + // Wait for both to complete await Promise.all([nodeBuild, clientBuild]); - + console.log('āœ… Parallel build completed successfully!'); - } catch (error) { console.error('āŒ Parallel build failed:', error.message); process.exit(1); diff --git a/scripts/patch-npm-bundled-vulnerabilities.mjs b/scripts/patch-npm-bundled-vulnerabilities.mjs index 0c156aa..a53ed07 100644 --- a/scripts/patch-npm-bundled-vulnerabilities.mjs +++ b/scripts/patch-npm-bundled-vulnerabilities.mjs @@ -3,12 +3,12 @@ * replace those copies, so `npm audit` still flags known-fixed versions that exist * hoisted at the project root. Sync patched trees into npm's bundle after install. */ -import { cpSync, existsSync, rmSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { cpSync, existsSync, rmSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; -const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const npmRoot = join(root, "node_modules", "npm"); +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const npmRoot = join(root, 'node_modules', 'npm'); function replaceDir(src, dest) { if (!existsSync(src) || !existsSync(dirname(dest))) { @@ -19,12 +19,9 @@ function replaceDir(src, dest) { } if (existsSync(npmRoot)) { + replaceDir(join(root, 'node_modules', 'brace-expansion'), join(npmRoot, 'node_modules', 'brace-expansion')); replaceDir( - join(root, "node_modules", "brace-expansion"), - join(npmRoot, "node_modules", "brace-expansion"), - ); - replaceDir( - join(root, "node_modules", "picomatch"), - join(npmRoot, "node_modules", "tinyglobby", "node_modules", "picomatch"), + join(root, 'node_modules', 'picomatch'), + join(npmRoot, 'node_modules', 'tinyglobby', 'node_modules', 'picomatch'), ); } diff --git a/scripts/postbuild.js b/scripts/postbuild.js index 5957d30..fa54a91 100644 --- a/scripts/postbuild.js +++ b/scripts/postbuild.js @@ -45,7 +45,7 @@ fs.writeFileSync(distCjsPackageJsonPath, JSON.stringify(cjsPackageJson, null, 2) // Copy README and LICENSE to dist for better package distribution const filesToCopy = ['README.md', 'LICENSE.md', 'CHANGELOG.md']; -filesToCopy.forEach(file => { +filesToCopy.forEach((file) => { const sourcePath = path.resolve(appDirectory, file); const targetPath = path.resolve(distPath, file); if (fs.existsSync(sourcePath)) { diff --git a/scripts/run-tests.js b/scripts/run-tests.js index 7154514..05f48bb 100644 --- a/scripts/run-tests.js +++ b/scripts/run-tests.js @@ -110,7 +110,9 @@ const runCommandChild = (command, args = [], options = {}, callback) => { console.log(`āœ… Command ${command} had SIGSEGV (exit code 139)`); callback(null, { exitCode: code }); } else { - callback(new Error(`Command failed with exit code ${code}`), { exitCode: code }); + callback(new Error(`Command failed with exit code ${code}`), { + exitCode: code, + }); } }); @@ -192,7 +194,11 @@ const main = async () => { // Run playwright tests await runCommand('npx', ['playwright', 'test', 'e2e/config.spec.ts'], { - env: { ...process.env, BASE_URL: `http://localhost:${configPort}`, TEST_TYPE: 'config' }, + env: { + ...process.env, + BASE_URL: `http://localhost:${configPort}`, + TEST_TYPE: 'config', + }, }).catch(async (error) => { await runCommand('docker', ['stop', 'pp-dev-tests']); await runCommand('docker', ['rmi', 'pp-dev-tests', '--force']); @@ -278,7 +284,11 @@ const main = async () => { // Run playwright tests await runCommand('npx', ['playwright', 'test'], { - env: { ...process.env, BASE_URL: `http://localhost:${port}`, TEST_TYPE: folder }, + env: { + ...process.env, + BASE_URL: `http://localhost:${port}`, + TEST_TYPE: folder, + }, }); await runCommand('docker', ['stop', testFolders.get(folder)]); diff --git a/scripts/startup-optimizer.js b/scripts/startup-optimizer.js index 17af852..d9073ba 100644 --- a/scripts/startup-optimizer.js +++ b/scripts/startup-optimizer.js @@ -24,7 +24,7 @@ class PerformanceMonitor { measure(name, startMark, endMark) { const start = this.marks.get(startMark); const end = this.marks.get(endMark); - + if (start && end) { const duration = end - start; this.measures.set(name, duration); @@ -41,11 +41,11 @@ class PerformanceMonitor { getSummary() { console.log('\nšŸ“ˆ Performance Summary:'); console.log('========================'); - + for (const [name, duration] of this.measures) { console.log(`${name}: ${duration.toFixed(2)}ms`); } - + const totalTime = performance.now() - this.startTime; console.log(`\nTotal optimization time: ${totalTime.toFixed(2)}ms`); } @@ -62,15 +62,15 @@ class CacheOptimizer { async optimizeCaches() { console.log('\nšŸ”§ Optimizing caches...'); - + try { // Clear existing caches const { clearConfigCache } = await import('../dist/esm/config.js'); const { clearAPICache } = await import('../dist/esm/lib/load-pp-data.middleware.js'); - + clearConfigCache(); clearAPICache(); - + console.log('āœ… Caches cleared successfully'); } catch (error) { console.log('āš ļø Could not clear caches (build may be needed)'); @@ -88,14 +88,14 @@ class DependencyAnalyzer { constructor() { // Based on profiling data from isolate log this.heavyStartupDeps = [ - 'source-map-support', // Loaded immediately in bin/pp-dev.js - 'source-map', // Heavy dependency chain - 'esbuild', // TypeScript compilation overhead - 'jsdom', // HTML parsing (lazy loaded) - 'vite', // Large framework - 'rollup', // Build system + 'source-map-support', // Loaded immediately in bin/pp-dev.js + 'source-map', // Heavy dependency chain + 'esbuild', // TypeScript compilation overhead + 'jsdom', // HTML parsing (lazy loaded) + 'vite', // Large framework + 'rollup', // Build system ]; - + this.performanceBottlenecks = [ 'TypeScript config compilation with esbuild', 'Multiple file system operations for config discovery', @@ -107,19 +107,19 @@ class DependencyAnalyzer { analyzeDependencies() { console.log('\nšŸ“¦ Analyzing dependencies based on profiling data...'); - + const packagePath = path.resolve(__dirname, '../package.json'); const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - + const deps = { ...pkg.dependencies, ...pkg.devDependencies }; - + console.log('Heavy dependencies identified in profiling:'); for (const dep of this.heavyStartupDeps) { if (deps[dep]) { console.log(` - ${dep}: ${deps[dep]}`); } } - + return deps; } @@ -137,7 +137,7 @@ class DependencyAnalyzer { identifyBottlenecks() { console.log('\n🚨 Performance bottlenecks from profiling:'); console.log('=========================================='); - + this.performanceBottlenecks.forEach((bottleneck, index) => { console.log(`${index + 1}. ${bottleneck}`); }); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index cfba572..ff081e7 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "./dist/cjs", + "outDir": "./dist/cjs" }, - "exclude": ["rollup.config.ts"] + "exclude": ["dist", "node_modules", "src/client", "rollup.config.ts"] } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index c476f25..0b9fd58 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "./dist/esm" }, - "exclude": ["rollup.config.ts"] + "exclude": ["dist", "node_modules", "src/client", "rollup.config.ts"] } diff --git a/tsconfig.json b/tsconfig.json index ba1a148..9778777 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,16 @@ { "compilerOptions": { - "baseUrl": ".", "declaration": false, - "lib": [ - "esnext", - "dom" - ], + "lib": ["esnext", "dom"], "types": ["node"], "module": "esnext", "moduleResolution": "bundler", "removeComments": false, - "sourceMap": true, + "sourceMap": false, "strict": true, "target": "esnext", "outDir": "./dist", + "rootDir": "./src", "skipLibCheck": true, /* Bundler mode */ @@ -21,6 +18,6 @@ "isolatedModules": true, "allowSyntheticDefaultImports": true }, - "include": ["src/**/*.ts", "src/**/*.json", "rollup.config.ts"], + "include": ["src/**/*.ts", "src/**/*.json"], "exclude": ["dist", "node_modules", "src/client"] } diff --git a/vitest.config.ts b/vitest.config.ts index 31a2398..7e52ceb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,11 +11,7 @@ export default defineConfig({ reporter: ['text', 'json', 'html'], reportsDirectory: './coverage/unit', include: ['src/**/*.ts'], - exclude: [ - 'src/client/**', - 'src/**/*.d.ts', - 'src/banner/**', - ], + exclude: ['src/client/**', 'src/**/*.d.ts', 'src/banner/**'], }, setupFiles: ['tests/setup.ts'], testTimeout: 10000, diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts index 6da123d..2d6b215 100644 --- a/vitest.integration.config.ts +++ b/vitest.integration.config.ts @@ -11,11 +11,7 @@ export default defineConfig({ reporter: ['text', 'json', 'html'], reportsDirectory: './coverage/integration', include: ['src/**/*.ts'], - exclude: [ - 'src/client/**', - 'src/**/*.d.ts', - 'src/banner/**', - ], + exclude: ['src/client/**', 'src/**/*.d.ts', 'src/banner/**'], }, setupFiles: ['tests/setup.ts'], testTimeout: 30000, From 212c5556d6b2b232dfb66239395fac0804740296 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:14:36 +0300 Subject: [PATCH 07/12] docs: refresh README, changelog, and testing documentation --- BUILD_IMPROVEMENTS.md | 22 ++++++ CHANGELOG.md | 127 +++++++++++++++++----------------- CONTRIBUTING.md | 4 +- README.md | 157 ++++++++++++++++++++++++------------------ tests/README.md | 6 ++ tests/TESTING.md | 20 ++++-- 6 files changed, 199 insertions(+), 137 deletions(-) diff --git a/BUILD_IMPROVEMENTS.md b/BUILD_IMPROVEMENTS.md index abf9b04..19c540b 100644 --- a/BUILD_IMPROVEMENTS.md +++ b/BUILD_IMPROVEMENTS.md @@ -5,16 +5,19 @@ This document outlines the improvements made to the pp-dev package build system ## šŸš€ Build Performance Improvements ### 1. Parallel Builds + - **Before**: Sequential builds (ESM → CJS → Client) - **After**: Parallel builds using `npm run build:parallel` - **Benefit**: ~40-60% faster build times ### 2. Enhanced Tree-Shaking + - Improved module side effects detection - Better property read side effects handling - Strict entry signature preservation ### 3. Optimized TypeScript Compilation + - Incremental compilation enabled - Build info caching with `.tsbuildinfo` - Optimized dependency change detection @@ -22,21 +25,25 @@ This document outlines the improvements made to the pp-dev package build system ## šŸ“¦ Build Outputs ### ESM Build (`dist/esm/`) + - Modern ES modules format - Optimized for tree-shaking - Source maps included ### CJS Build (`dist/cjs/`) + - CommonJS format for Node.js compatibility - Optimized for server-side usage - Source maps included ### Type Definitions (`dist/types/`) + - Full TypeScript declarations - Declaration maps for better IDE support - Clean, optimized type output ### Client Build (`dist/client/`) + - Optimized CSS with SCSS compilation - Asset handling with proper public paths - Source maps for debugging @@ -44,6 +51,7 @@ This document outlines the improvements made to the pp-dev package build system ## šŸ› ļø Build Scripts ### Production Builds + ```bash # Standard build (parallel) npm run build @@ -56,6 +64,7 @@ npm run clean && npm run build ``` ### Development Builds + ```bash # Fast build (parallel) npm run build:fast @@ -68,6 +77,7 @@ npm run build:analyze ``` ### Optimization + ```bash # Run build optimizations npm run build:optimize @@ -79,12 +89,14 @@ npm run build:optimize ## ⚔ Build Tools ### Rollup (Primary) + - Advanced tree-shaking - Plugin ecosystem - Source map generation - Bundle analysis support ### Rollup (Primary) + - Advanced tree-shaking - Plugin ecosystem - Source map generation @@ -101,17 +113,20 @@ The build system includes bundle analysis capabilities: ## šŸ”§ Configuration Files ### Rollup Config (`rollup.config.ts`) + - Multi-format builds (ESM, CJS, Types) - Optimized external dependency handling - Enhanced terser configuration - Better source map generation ### Esbuild Config (`esbuild.config.ts`) + - Alternative build system for experimentation - Simplified TypeScript builds - Build metadata generation ### TypeScript Configs + - `tsconfig.json` - Base configuration - `tsconfig.esm.json` - ESM-specific settings - `tsconfig.cjs.json` - CJS-specific settings @@ -120,11 +135,13 @@ The build system includes bundle analysis capabilities: ## šŸ“ˆ Performance Metrics ### Build Times (approximate) + - **Before**: ~15-20 seconds - **After (Rollup)**: ~8-12 seconds - **After (Parallel)**: ~5-8 seconds ### Bundle Sizes + - Optimized tree-shaking reduces bundle size by 15-25% - Better external dependency handling - Improved code splitting @@ -132,16 +149,19 @@ The build system includes bundle analysis capabilities: ## šŸŽÆ Best Practices ### For Development + 1. Use `npm run build:fast` for quick iterations 2. Use `npm run build:watch` for continuous builds 3. Run `npm run build:optimize` to apply optimizations ### For Production + 1. Use `npm run build` for full builds 2. Run `npm run build:analyze` to check bundle sizes 3. Verify all formats are generated correctly ### For CI/CD + 1. Use `npm run build:parallel` for faster CI builds 2. Include bundle analysis in release process 3. Monitor build performance metrics @@ -149,11 +169,13 @@ The build system includes bundle analysis capabilities: ## šŸ” Troubleshooting ### Common Issues + 1. **Build failures**: Check TypeScript configuration 2. **Slow builds**: Ensure parallel builds are enabled 3. **Large bundles**: Run bundle analysis to identify issues ### Performance Issues + 1. **Memory usage**: Monitor Node.js memory limits 2. **CPU usage**: Check for unnecessary TypeScript compilation 3. **Disk I/O**: Ensure SSD storage for faster builds diff --git a/CHANGELOG.md b/CHANGELOG.md index ad18a75..ccc643b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,187 +1,162 @@ # [0.18.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.17.0...v0.18.0-beta.1) (2026-04-06) - ### Features -* rewrite /data/page/ path for v7 proxy when template differs from internal name ([a060da2](https://github.com/mi-examples/pp-dev/commit/a060da26e4d4cb9e80625f2015e5ab19b82a398d)) +- rewrite /data/page/ path for v7 proxy when template differs from internal name ([a060da2](https://github.com/mi-examples/pp-dev/commit/a060da26e4d4cb9e80625f2015e5ab19b82a398d)) # [0.17.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.16.1...v0.17.0-beta.1) (2026-04-06) - ### Bug Fixes -* **tests:** align sandbox overrides with audited dependency policy ([5841aa8](https://github.com/mi-examples/pp-dev/commit/5841aa831833d74342e9bc134141f9913c1c0bab)) - +- **tests:** align sandbox overrides with audited dependency policy ([5841aa8](https://github.com/mi-examples/pp-dev/commit/5841aa831833d74342e9bc134141f9913c1c0bab)) ### Features -* **cli:** Webpack fallback for Next dev when Turbopack native SWC fails ([5940f31](https://github.com/mi-examples/pp-dev/commit/5940f316cf86056196ad3f69a8b3667460866aeb)) +- **cli:** Webpack fallback for Next dev when Turbopack native SWC fails ([5940f31](https://github.com/mi-examples/pp-dev/commit/5940f316cf86056196ad3f69a8b3667460866aeb)) # [0.16.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.15.1...v0.16.0-beta.1) (2026-03-23) - ### Bug Fixes -* **version-plugin:** hash concatenated digests without hex input encoding ([1ea5e5a](https://github.com/mi-examples/pp-dev/commit/1ea5e5aed6b78b3bd67110d173a5195b75434e9c)) - +- **version-plugin:** hash concatenated digests without hex input encoding ([1ea5e5a](https://github.com/mi-examples/pp-dev/commit/1ea5e5aed6b78b3bd67110d173a5195b75434e9c)) ### Features -* version manifest plugin and upgrade to Vite 8 ([ad8c4ac](https://github.com/mi-examples/pp-dev/commit/ad8c4ac5b6bc7813d04947b418a5d3b77d2d75fb)) +- version manifest plugin and upgrade to Vite 8 ([ad8c4ac](https://github.com/mi-examples/pp-dev/commit/ad8c4ac5b6bc7813d04947b418a5d3b77d2d75fb)) ## [0.15.1-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.15.0...v0.15.1-beta.1) (2026-03-12) - ### Bug Fixes -* resolve all package vulnerabilities ([79d7e24](https://github.com/mi-examples/pp-dev/commit/79d7e245b94d0be8c133803842329309f0e1b432)) +- resolve all package vulnerabilities ([79d7e24](https://github.com/mi-examples/pp-dev/commit/79d7e245b94d0be8c133803842329309f0e1b432)) # [0.15.0-beta.2](https://github.com/mi-examples/pp-dev/compare/v0.15.0-beta.1...v0.15.0-beta.2) (2026-03-02) - ### Bug Fixes -* add overrides to fix serialize-javascript and minimatch vulnerabilities ([3f79b97](https://github.com/mi-examples/pp-dev/commit/3f79b970b604f8e5656d15f743f51c6902b2e0d5)) +- add overrides to fix serialize-javascript and minimatch vulnerabilities ([3f79b97](https://github.com/mi-examples/pp-dev/commit/3f79b970b604f8e5656d15f743f51c6902b2e0d5)) # [0.15.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.14.1...v0.15.0-beta.1) (2026-02-27) - ### Bug Fixes -* add chokidar override to resolve npm ci sync ([3e24d40](https://github.com/mi-examples/pp-dev/commit/3e24d4020a33a5693f8aa442a0add6e80208719b)) -* pass appId to initLoadPPData, remove debug logs, update lock file ([079c13b](https://github.com/mi-examples/pp-dev/commit/079c13bc3db2ea018d377ed01240ff7ffaea253d)) -* proxy middleware - add cache headers for login page, inject token for HTML ([452755f](https://github.com/mi-examples/pp-dev/commit/452755f73391a0a6e2e19223db8f703219f6d993)) - +- add chokidar override to resolve npm ci sync ([3e24d40](https://github.com/mi-examples/pp-dev/commit/3e24d4020a33a5693f8aa442a0add6e80208719b)) +- pass appId to initLoadPPData, remove debug logs, update lock file ([079c13b](https://github.com/mi-examples/pp-dev/commit/079c13bc3db2ea018d377ed01240ff7ffaea253d)) +- proxy middleware - add cache headers for login page, inject token for HTML ([452755f](https://github.com/mi-examples/pp-dev/commit/452755f73391a0a6e2e19223db8f703219f6d993)) ### Features -* add appId option and fix internal server restart ([ee74beb](https://github.com/mi-examples/pp-dev/commit/ee74beb07d0ffe091302c7af4b56b090ea5cb383)) +- add appId option and fix internal server restart ([ee74beb](https://github.com/mi-examples/pp-dev/commit/ee74beb07d0ffe091302c7af4b56b090ea5cb383)) ## [0.14.1-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.14.0...v0.14.1-beta.1) (2026-02-24) - ### Bug Fixes -* **nextjs:** rewrite response middleware for Next.js page URLs ([3e80f41](https://github.com/mi-examples/pp-dev/commit/3e80f413c3106d9be918604ae0a5e28eeafc98d5)) +- **nextjs:** rewrite response middleware for Next.js page URLs ([3e80f41](https://github.com/mi-examples/pp-dev/commit/3e80f413c3106d9be918604ae0a5e28eeafc98d5)) # [0.14.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.13.2...v0.14.0-beta.1) (2026-02-20) - ### Bug Fixes -* ejs v4 default import for ESM compatibility ([e64f4c8](https://github.com/mi-examples/pp-dev/commit/e64f4c8fe1344a00daf575521215b0f82ec539ee)) -* improve dev server restart reliability and config change detection ([3edd020](https://github.com/mi-examples/pp-dev/commit/3edd0206ab5d9e27e8088a8a55fc925f5a586f7f)) - +- ejs v4 default import for ESM compatibility ([e64f4c8](https://github.com/mi-examples/pp-dev/commit/e64f4c8fe1344a00daf575521215b0f82ec539ee)) +- improve dev server restart reliability and config change detection ([3edd020](https://github.com/mi-examples/pp-dev/commit/3edd0206ab5d9e27e8088a8a55fc925f5a586f7f)) ### Features -* **cli:** appId support, base path handling ([63ec031](https://github.com/mi-examples/pp-dev/commit/63ec0318a18a14b06803a59e8ac2d4be225d4631)) -* **cli:** appId support, base path handling, API routes passthrough ([2c16dae](https://github.com/mi-examples/pp-dev/commit/2c16daea94e11d72a0ce93be0f40ed9f29eb4d6a)) +- **cli:** appId support, base path handling ([63ec031](https://github.com/mi-examples/pp-dev/commit/63ec0318a18a14b06803a59e8ac2d4be225d4631)) +- **cli:** appId support, base path handling, API routes passthrough ([2c16dae](https://github.com/mi-examples/pp-dev/commit/2c16daea94e11d72a0ce93be0f40ed9f29eb4d6a)) ## [0.13.2-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.13.1...v0.13.2-beta.1) (2026-02-12) - ### Bug Fixes -* **client-injection:** resolve DIRNAME to parent directory for correct resource paths ([606fd91](https://github.com/mi-examples/pp-dev/commit/606fd919ffda4988f4e1a0c11ce805cacfd75f1e)) +- **client-injection:** resolve DIRNAME to parent directory for correct resource paths ([606fd91](https://github.com/mi-examples/pp-dev/commit/606fd919ffda4988f4e1a0c11ce805cacfd75f1e)) # [0.13.0-beta.2](https://github.com/mi-examples/pp-dev/compare/v0.13.0-beta.1...v0.13.0-beta.2) (2026-02-11) - ### Bug Fixes -* **plugin:** correct DIRNAME path resolution in client injection ([dce83d7](https://github.com/mi-examples/pp-dev/commit/dce83d78e987e0555060552a51bf6d1d32154853)) - +- **plugin:** correct DIRNAME path resolution in client injection ([dce83d7](https://github.com/mi-examples/pp-dev/commit/dce83d78e987e0555060552a51bf6d1d32154853)) ### Features -* **cli:** support Next.js 16 and fix base path regex escaping ([9182d2a](https://github.com/mi-examples/pp-dev/commit/9182d2a1d319239abf1b7b61cd40f03bbdf312fd)) -* **cli:** watch .env and pp-dev config files, await Next.js check ([69a07f0](https://github.com/mi-examples/pp-dev/commit/69a07f0cbf0d3613fd1e6cdbf892cf0905005b51)) +- **cli:** support Next.js 16 and fix base path regex escaping ([9182d2a](https://github.com/mi-examples/pp-dev/commit/9182d2a1d319239abf1b7b61cd40f03bbdf312fd)) +- **cli:** watch .env and pp-dev config files, await Next.js check ([69a07f0](https://github.com/mi-examples/pp-dev/commit/69a07f0cbf0d3613fd1e6cdbf892cf0905005b51)) # [0.13.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.12.4...v0.13.0-beta.1) (2026-01-22) - ### Bug Fixes -* **plugin:** add null check for integrateMiTopBar validation ([df35e2a](https://github.com/mi-examples/pp-dev/commit/df35e2a8a265bd4a6445ec78c9ae94d4c24f105d)) - +- **plugin:** add null check for integrateMiTopBar validation ([df35e2a](https://github.com/mi-examples/pp-dev/commit/df35e2a8a265bd4a6445ec78c9ae94d4c24f105d)) ### Features -* **plugin:** enhance integrateMiTopBar with selective configuration options ([cbb9a6c](https://github.com/mi-examples/pp-dev/commit/cbb9a6c59db401201fb2530f46ca47eaaa585526)) +- **plugin:** enhance integrateMiTopBar with selective configuration options ([cbb9a6c](https://github.com/mi-examples/pp-dev/commit/cbb9a6c59db401201fb2530f46ca47eaaa585526)) ## [0.12.4-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.12.3...v0.12.4-beta.1) (2026-01-14) - ### Bug Fixes -* **plugin:** move topbar scripts injection to head-prepend ([a248f91](https://github.com/mi-examples/pp-dev/commit/a248f91b14817a36a37d56e9107762b514bf90ac)) +- **plugin:** move topbar scripts injection to head-prepend ([a248f91](https://github.com/mi-examples/pp-dev/commit/a248f91b14817a36a37d56e9107762b514bf90ac)) # [@metricinsights/pp-dev-v0.12.3-beta.1](https://github.com/mi-examples/pp-dev-js/compare/v0.12.2...v0.12.3-beta.1) (2025-12-19) - ### Bug Fixes -* **pp-dev:** improve node compatibility for SSL and buffer handling ([fcef21f](https://github.com/mi-examples/pp-dev-js/commit/fcef21f22936aeabb3a4c40711a76631246e80b4)) +- **pp-dev:** improve node compatibility for SSL and buffer handling ([fcef21f](https://github.com/mi-examples/pp-dev-js/commit/fcef21f22936aeabb3a4c40711a76631246e80b4)) # [@metricinsights/pp-dev-v0.12.2](https://github.com/mi-examples/pp-dev-js/compare/v0.12.1...v0.12.2) (2025-01-15) ### Bug Fixes -* **release:** prepare v0.12.2 patch release +- **release:** prepare v0.12.2 patch release # [@metricinsights/pp-dev-v0.12.1](https://github.com/mi-examples/pp-dev-js/compare/v0.12.0...v0.12.1) (2025-01-15) ### Bug Fixes -* **release:** prepare v0.12.1 patch release +- **release:** prepare v0.12.1 patch release # [@metricinsights/pp-dev-v0.12.0](https://github.com/mi-examples/pp-dev-js/compare/v0.11.0...v0.12.0) (2025-01-15) - ### Features -* **auth:** add global authentication provider ([2b1e00d](https://github.com/mi-examples/pp-dev-js/commit/2b1e00d20cbfd21284aa871c47bc149829a2f865)) -* **pp-dev:** enhance CLI and core functionality ([14a3772](https://github.com/mi-examples/pp-dev-js/commit/14a3772a4955aabad6b0fa0986a7047445e0cc5b)) +- **auth:** add global authentication provider ([2b1e00d](https://github.com/mi-examples/pp-dev-js/commit/2b1e00d20cbfd21284aa871c47bc149829a2f865)) +- **pp-dev:** enhance CLI and core functionality ([14a3772](https://github.com/mi-examples/pp-dev-js/commit/14a3772a4955aabad6b0fa0986a7047445e0cc5b)) # [@metricinsights/pp-dev-v0.11.0-beta.4](https://github.com/mi-examples/pp-dev-js/compare/v0.11.0-beta.3...v0.11.0-beta.4) (2025-08-29) - ### Features -* **pp-dev:** add postbuild script and package renaming utility ([afdc0d6](https://github.com/mi-examples/pp-dev-js/commit/afdc0d6aefd4545b536090c363dad21308683777)) -* **pp-dev:** refactor CLI and core functionality ([98ce2d2](https://github.com/mi-examples/pp-dev-js/commit/98ce2d282abc83759b96b76e3502a0db99835404)) -* **test-commonjs:** moved test commonjs folder to new location ([9480d77](https://github.com/mi-examples/pp-dev-js/commit/9480d77d9dfc5ccafb0f4b0159fedd484143d754)) -* **test-nextjs:** add initial Next.js project files and configuration ([a5da0fd](https://github.com/mi-examples/pp-dev-js/commit/a5da0fdb5ab6f50b9605663706e88f62300aaea3)) -* **test-nextjs:** initialize Next.js test project structure ([2a6155d](https://github.com/mi-examples/pp-dev-js/commit/2a6155dc94f3b27a7b9c3535d5e8699fc9da655c)) +- **pp-dev:** add postbuild script and package renaming utility ([afdc0d6](https://github.com/mi-examples/pp-dev-js/commit/afdc0d6aefd4545b536090c363dad21308683777)) +- **pp-dev:** refactor CLI and core functionality ([98ce2d2](https://github.com/mi-examples/pp-dev-js/commit/98ce2d282abc83759b96b76e3502a0db99835404)) +- **test-commonjs:** moved test commonjs folder to new location ([9480d77](https://github.com/mi-examples/pp-dev-js/commit/9480d77d9dfc5ccafb0f4b0159fedd484143d754)) +- **test-nextjs:** add initial Next.js project files and configuration ([a5da0fd](https://github.com/mi-examples/pp-dev-js/commit/a5da0fdb5ab6f50b9605663706e88f62300aaea3)) +- **test-nextjs:** initialize Next.js test project structure ([2a6155d](https://github.com/mi-examples/pp-dev-js/commit/2a6155dc94f3b27a7b9c3535d5e8699fc9da655c)) # [@metricinsights/pp-dev-v0.11.0-beta.3](https://github.com/mi-examples/pp-dev-js/compare/v0.11.0-beta.2...v0.11.0-beta.3) (2025-08-14) - ### Bug Fixes -* **ci:** improve package detection and JSON validation ([f787a1c](https://github.com/mi-examples/pp-dev-js/commit/f787a1c2bf47c091c275d09317745b924c45f53f)) - +- **ci:** improve package detection and JSON validation ([f787a1c](https://github.com/mi-examples/pp-dev-js/commit/f787a1c2bf47c091c275d09317745b924c45f53f)) ### Features -* **pp-dev:** add startup optimization and enhance authentication helpers ([a29e311](https://github.com/mi-examples/pp-dev-js/commit/a29e31136e0c27d221fc26028fd5c4970654e386)) +- **pp-dev:** add startup optimization and enhance authentication helpers ([a29e311](https://github.com/mi-examples/pp-dev-js/commit/a29e31136e0c27d221fc26028fd5c4970654e386)) # [@metricinsights/pp-dev-v0.11.0-beta.2](https://github.com/mi-examples/pp-dev-js/compare/v0.11.0-beta.1...v0.11.0-beta.2) (2025-08-12) - ### Bug Fixes -* remove issue number references from semantic-release configs ([f532226](https://github.com/mi-examples/pp-dev-js/commit/f532226b1ecc4cf9d2cfac6e92cb1c101468a329)) +- remove issue number references from semantic-release configs ([f532226](https://github.com/mi-examples/pp-dev-js/commit/f532226b1ecc4cf9d2cfac6e92cb1c101468a329)) # [@metricinsights/pp-dev-v0.11.0-beta.1](https://github.com/mi-examples/pp-dev-js/compare/v0.10.1...v0.11.0-beta.1) (2025-08-12) - ### Features -* **pp-dev:** add dependency version synchronization for create-pp-dev releases ([2597b01](https://github.com/mi-examples/pp-dev-js/commit/2597b017a59b1359753e85953648e2ce1674253c)) -* **pp-dev:** add esbuild configuration and build optimization scripts ([bbe1791](https://github.com/mi-examples/pp-dev-js/commit/bbe1791e9eb2e220f1552618a9a534a80ddd2f96)) -* **pp-dev:** add semantic release configuration and update dependencies ([5962bcc](https://github.com/mi-examples/pp-dev-js/commit/5962bccbb76fb684415f731f85f372cdd109d8f1)) +- **pp-dev:** add dependency version synchronization for create-pp-dev releases ([2597b01](https://github.com/mi-examples/pp-dev-js/commit/2597b017a59b1359753e85953648e2ce1674253c)) +- **pp-dev:** add esbuild configuration and build optimization scripts ([bbe1791](https://github.com/mi-examples/pp-dev-js/commit/bbe1791e9eb2e220f1552618a9a534a80ddd2f96)) +- **pp-dev:** add semantic release configuration and update dependencies ([5962bcc](https://github.com/mi-examples/pp-dev-js/commit/5962bccbb76fb684415f731f85f372cdd109d8f1)) # Changelog @@ -190,112 +165,132 @@ All notable changes to the `@metricinsights/pp-dev` package will be documented i ## [0.10.0] - 2024-03-21 ### Changed + - Removed unused `pino` and `pino-pretty` dependencies ## [0.9.0] - 2025-01-31 ### Added + - Added support for MI v7.1.0 instances - Added new API endpoints for v7 instances ## [0.8.0] - 2024-11-15 ### Changed + - Updated package dependencies to latest versions - Improved template loading mechanism ## [0.7.0] - 2024-02-28 ### Added + - Added icon font generation tool - Added changelog generator for assets - Added image optimization tool ### Changed + - Improved helper logging system ## [0.6.0] - 2024-01-18 ### Added + - Added support for React.js, TypeScript, and Next.js templates - Added new npm package structure - Added documentation for templates ### Changed + - Updated template code structure - Improved helper info panel ## [0.5.0] - 2023-10-16 ### Added + - Added Next.js support (beta) - Added SSL validation support - Added shields for package information ### Changed + - Fixed Next.js dependency versions - Improved helper UI ## [0.4.0] - 2023-05-29 ### Added + - Added CI/CD support - Added package for CI/CD - Added documentation ### Changed + - Updated publish configuration - Improved installation command ## [0.3.3] - 2023-11-14 ### Changed + - Fixed URL parameters handling in helper - Improved helper UI ## [0.3.2] - 2023-11-14 ### Changed + - Fixed Next.js dependency versions - Improved helper UI ## [0.3.1] - 2023-12-04 ### Changed + - Fixed URL parameters handling in helper - Improved helper UI ## [0.3.0] - 2023-11-08 ### Added + - Added Next.js support - Added helper info panel - Added support for React.js templates ### Changed + - Improved helper UI - Updated template loading mechanism ## [0.2.0] - 2023-05-25 ### Added + - Added support for React.js templates - Added helper info panel - Added documentation ### Changed + - Improved template loading mechanism - Updated package structure ## [0.1.1] - 2023-05-23 ### Changed + - Fixed template loading issues - Improved helper UI ## [0.1.0] - 2023-05-17 ### Added + - Initial release - Basic template support - Helper UI implementation @@ -303,20 +298,24 @@ All notable changes to the `@metricinsights/pp-dev` package will be documented i ## [0.0.3] - 2023-05-17 ### Added + - Added basic template support - Added helper UI ### Changed + - Improved package structure ## [0.0.2] - 2023-05-17 ### Added + - Added basic package structure - Added helper implementation ## [0.0.1] - 2023-05-17 ### Added + - Initial package setup - Basic helper functionality diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f47ca5d..087ba42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,7 @@ We follow the [Conventional Commits](https://www.conventionalcommits.org/) speci The scope should be the name of the component or feature affected (as perceived by the person reading the changelog). Examples: + - `feat(helper)`: New feature in the helper component - `fix(api)`: Bug fix in the API - `docs(readme)`: Documentation changes in README @@ -55,6 +56,7 @@ The release process is automated using GitHub Actions and semantic-release. When - Runs semantic-release semantic-release will then: + 1. Analyze your commits 2. Determine the next version number 3. Generate a changelog @@ -73,4 +75,4 @@ The following secrets need to be configured in your GitHub repository: ### Skipping CI -If you need to make changes to the release commit (like updating the changelog), you can skip the CI by including `[skip ci]` in your commit message. \ No newline at end of file +If you need to make changes to the release commit (like updating the changelog), you can skip the CI by including `[skip ci]` in your commit message. diff --git a/README.md b/README.md index c94fc8e..f1d0702 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ import '@metricinsights/pp-dev/client/css/client.css'; ``` **Available Exports**: + - **Main**: Complete pp-dev functionality with CLI and plugins - **Plugin**: Vite plugin for integration with build tools - **Helpers**: Utility functions for authentication and configuration @@ -63,6 +64,7 @@ import '@metricinsights/pp-dev/client/css/client.css'; The pp-dev package includes optimized startup performance and build system with multiple strategies: ### Quick Start + ```bash # Standard build (parallel) npm run build @@ -84,6 +86,7 @@ npm run startup:optimize ``` ### Performance Features + - **40-50% faster startup** with intelligent caching - **60-70% faster subsequent starts** with connection pooling - **Lazy loading** of heavy modules (jsdom, esbuild) @@ -93,6 +96,7 @@ npm run startup:optimize - **Intelligent dependency optimization** based on profiling data ### Build Features + - **Parallel builds** for 40-60% faster build times - **Enhanced tree-shaking** for smaller bundles - **Multiple output formats** (ESM, CJS, Types) @@ -110,6 +114,7 @@ The new startup optimization system in v0.11.0 provides: - **Optimization Suggestions**: Automated recommendations for performance improvements Run the startup optimizer to analyze and improve your development environment: + ```bash npm run startup:optimize ``` @@ -122,6 +127,7 @@ npm run startup:optimize ### Configuration File Create a configuration file named `pp-dev.config` with one of these extensions: + - `.js` or `.cjs` (for CommonJS) - `.ts` (for TypeScript) - `.json` @@ -200,28 +206,28 @@ export default config; ### Required Options -| Option | Type | Description | -|--------|------|-------------| -| `backendBaseURL` | string | URL of the Metric Insights instance for API proxying | -| `portalPageId` | number | ID of the Portal Page for variable values (deprecated, use `appId` instead) | -| `appId` | number | ID of the Portal Page for variable values (synonym for `portalPageId`) | +| Option | Type | Description | +| ---------------- | ------ | --------------------------------------------------------------------------- | +| `backendBaseURL` | string | URL of the Metric Insights instance for API proxying | +| `portalPageId` | number | ID of the Portal Page for variable values (deprecated, use `appId` instead) | +| `appId` | number | ID of the Portal Page for variable values (synonym for `portalPageId`) | ### Optional Options -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `miHudLess` | boolean | `false` | Disables Metric Insights navigation bar in development | -| `integrateMiTopBar` | boolean \| object | `false` | Integrates MI Top Bar and scripts into the App build (requires `miHudLess: true`). When `true`, enables both `addRootElement` and `addSharedComponentsScripts`. When an object, allows selective enabling: `{ addRootElement?: boolean, addSharedComponentsScripts?: boolean }` | -| `templateLess` | boolean | `false` | Disables template variable transformation | -| `enableProxyCache` | boolean | `true` | Enables caching of proxied requests | -| `proxyCacheTTL` | number | `600000` | Cache TTL in milliseconds (10 minutes) | -| `disableSSLValidation` | boolean | `false` | Disables SSL certificate validation for proxy requests | -| `imageOptimizer` | boolean \| object | `true` | Controls image optimization. See [vite-plugin-image-optimizer](https://www.npmjs.com/package/vite-plugin-image-optimizer#plugin-options) for object options | -| `outDir` | string | `dist` | Output directory for builds | -| `distZip` | boolean \| object | `true` | Controls build output zipping. Object options: `{ outDir?: string, outFileName?: string }` | -| `syncBackupsDir` | string | `backups` | Directory for asset backups from MI server | -| `v7Features` | boolean | `false` | Enables Metric Insights v7 features | -| `personalAccessToken` | string | `process.env.MI_ACCESS_TOKEN` | Personal Access Token for the MI instance | +| Option | Type | Default | Description | +| ---------------------- | ----------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `miHudLess` | boolean | `false` | Disables Metric Insights navigation bar in development | +| `integrateMiTopBar` | boolean \| object | `false` | Integrates MI Top Bar and scripts into the App build (requires `miHudLess: true`). When `true`, enables both `addRootElement` and `addSharedComponentsScripts`. When an object, allows selective enabling: `{ addRootElement?: boolean, addSharedComponentsScripts?: boolean }` | +| `templateLess` | boolean | `false` | Disables template variable transformation | +| `enableProxyCache` | boolean | `true` | Enables caching of proxied requests | +| `proxyCacheTTL` | number | `600000` | Cache TTL in milliseconds (10 minutes) | +| `disableSSLValidation` | boolean | `false` | Disables SSL certificate validation for proxy requests | +| `imageOptimizer` | boolean \| object | `true` | Controls image optimization. See [vite-plugin-image-optimizer](https://www.npmjs.com/package/vite-plugin-image-optimizer#plugin-options) for object options | +| `outDir` | string | `dist` | Output directory for builds | +| `distZip` | boolean \| object | `true` | Controls build output zipping. Object options: `{ outDir?: string, outFileName?: string }` | +| `syncBackupsDir` | string | `backups` | Directory for asset backups from MI server | +| `v7Features` | boolean | `false` | Enables Metric Insights v7 features | +| `personalAccessToken` | string | `process.env.MI_ACCESS_TOKEN` | Personal Access Token for the MI instance | ### integrateMiTopBar Details @@ -238,10 +244,12 @@ The `integrateMiTopBar` option allows you to integrate the Metric Insights Top B `integrateMiTopBar` can be configured in two ways: **1. Boolean (Simple)** + - `true`: Enables both `addRootElement` and `addSharedComponentsScripts` - `false`: Disables integration (default) **2. Object (Advanced)** + - `addRootElement`: Adds `
` element to the body for React mounting - `addSharedComponentsScripts`: Adds MI shared component scripts and styles: - `/auth/info.js` - Authentication info script @@ -251,17 +259,19 @@ The `integrateMiTopBar` option allows you to integrate the Metric Insights Top B #### Examples **Simple boolean configuration:** + ```javascript // pp-dev.config.js module.exports = { backendBaseURL: 'https://mi.company.com', appId: 1, - miHudLess: true, // Required: Disable dynamic MI scripts - integrateMiTopBar: true, // Enable: Integrate Top Bar into build + miHudLess: true, // Required: Disable dynamic MI scripts + integrateMiTopBar: true, // Enable: Integrate Top Bar into build }; ``` **Advanced object configuration (selective features):** + ```javascript // pp-dev.config.js module.exports = { @@ -269,13 +279,14 @@ module.exports = { appId: 1, miHudLess: true, integrateMiTopBar: { - addRootElement: true, // Add root element for React - addSharedComponentsScripts: true, // Add MI scripts and styles + addRootElement: true, // Add root element for React + addSharedComponentsScripts: true, // Add MI scripts and styles }, }; ``` **Partial integration (scripts only, manual root element):** + ```javascript // pp-dev.config.js module.exports = { @@ -283,8 +294,8 @@ module.exports = { appId: 1, miHudLess: true, integrateMiTopBar: { - addRootElement: false, // Don't add root element - addSharedComponentsScripts: true, // Add MI scripts and styles + addRootElement: false, // Don't add root element + addSharedComponentsScripts: true, // Add MI scripts and styles }, }; ``` @@ -292,6 +303,7 @@ module.exports = { ### v7Features Details When enabled (`true`), this option: + 1. Changes development path from `/pt/` to `/pl/` 2. Updates Code Sync feature to use v7.1.0+ URLs @@ -300,6 +312,7 @@ When enabled (`true`), this option: The `personalAccessToken` option allows you to authenticate with the Metric Insights instance. You can set it in your configuration or use the `MI_ACCESS_TOKEN` environment variable. Example with authentication and Top Bar integration: + ```javascript // pp-dev.config.js module.exports = { @@ -312,10 +325,16 @@ module.exports = { ``` **Environment Variable**: Set `MI_ACCESS_TOKEN` in your `.env` file: + ```bash MI_ACCESS_TOKEN=your_token_here ``` +**Local development and network exposure**: pp-dev is a **development** tool. It assumes a trusted machine. Personal access tokens and session helpers are still sensitive: they can authenticate to your Metric Insights backend as you. + +- Prefer binding the dev server to **`localhost`** when you do not need access from other devices. If you use **`--host`** (or equivalent) so the app listens on **all interfaces** or your LAN, other machines on the same network can reach the dev server and its dev-only routes (for example the in-browser token login flow). Treat that like exposing credentials: use only on networks you trust, or restrict access with your OS firewall. +- Do not commit real tokens; keep them in `.env` (gitignored) or your secret store. Avoid sharing screen recordings or logs that contain `MI_ACCESS_TOKEN` or bearer tokens. + ### Enhanced Authentication (v0.11.0+) The new authentication system in v0.11.0 provides: @@ -326,11 +345,13 @@ The new authentication system in v0.11.0 provides: - **Connection Pooling**: Optimized HTTP connections for better performance **Supported Environment Variables**: + - `MI_ACCESS_TOKEN`: Personal access token for authentication - `MI_BACKEND_URL`: Alternative to `backendBaseURL` in config - `MI_APP_ID`: Alternative to `appId` in config **Automatic Loading**: pp-dev automatically detects and loads these variables from your project's `.env` file: + ```bash # .env MI_ACCESS_TOKEN=your_personal_access_token @@ -342,13 +363,13 @@ MI_APP_ID=123 ### Global Options -| Option | Description | -|--------|-------------| -| `-c, --config ` | Path to configuration file (default: `pp-dev.config.js`) | -| `--base ` | Public base path (default: `/`) | +| Option | Description | +| ------------------------ | -------------------------------------------------------------------------------- | +| `-c, --config ` | Path to configuration file (default: `pp-dev.config.js`) | +| `--base ` | Public base path (default: `/`) | | `-l, --logLevel ` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `silent` (default: `info`) | -| `--clearScreen` | Clear screen before logging | -| `--mode ` | Environment mode: `development`, `production`, `test` (default: `development`) | +| `--clearScreen` | Clear screen before logging | +| `--mode ` | Environment mode: `development`, `production`, `test` (default: `development`) | ### Development Server @@ -357,15 +378,16 @@ pp-dev [root] [options] # Aliases: pp-dev dev, pp-dev serve ``` -| Option | Default | Description | -|--------|---------|-------------| -| `[root]` | `.` | Root directory of the application | -| `--host ` | `localhost` | Server hostname | -| `--port ` | `3000` | Server port | -| `--open [path]` | - | Open browser on server start | -| `--strictPort` | - | Exit if port is already in use | +| Option | Default | Description | +| --------------- | ----------- | --------------------------------- | +| `[root]` | `.` | Root directory of the application | +| `--host ` | `localhost` | Server hostname | +| `--port ` | `3000` | Server port | +| `--open [path]` | - | Open browser on server start | +| `--strictPort` | - | Exit if port is already in use | **Development Shortcuts**: + - `p` - Start/stop performance profiler (v0.11.0+) - `l` - Proxy re-login (refresh authentication) - `r` - Restart dev server @@ -381,11 +403,11 @@ pp-dev next [options] # Aliases: pp-dev next-server, pp-dev next-dev ``` -| Option | Default | Description | -|--------|---------|-------------| -| `[root]` | `.` | Root directory of the application | -| `--port ` | `3000` | Server port | -| `--host ` | `localhost` | Server hostname | +| Option | Default | Description | +| --------------- | ----------- | --------------------------------- | +| `[root]` | `.` | Root directory of the application | +| `--port ` | `3000` | Server port | +| `--host ` | `localhost` | Server hostname | ### Build @@ -393,13 +415,13 @@ pp-dev next [options] pp-dev build [options] ``` -| Option | Default | Description | -|--------|---------|-------------| -| `[root]` | `.` | Root directory of the application | -| `--target ` | `modules` | Transpile target | -| `--outDir ` | `dist` | Output directory | -| `--assetsDir ` | `assets` | Assets directory under outDir | -| `--changelog [file]` | `true` | Create changelog file | +| Option | Default | Description | +| -------------------- | --------- | --------------------------------- | +| `[root]` | `.` | Root directory of the application | +| `--target ` | `modules` | Transpile target | +| `--outDir ` | `dist` | Output directory | +| `--assetsDir ` | `assets` | Assets directory under outDir | +| `--changelog [file]` | `true` | Create changelog file | ### Changelog Generation @@ -407,14 +429,14 @@ pp-dev build [options] pp-dev changelog [oldAssetPath] [newAssetPath] [options] ``` -| Option | Default | Description | -|--------|---------|-------------| -| `[oldAssetPath]` | - | Path to previous assets | -| `[newAssetPath]` | - | Path to current assets | -| `--oldAssetsPath ` | - | Path to previous assets | -| `--newAssetsPath ` | - | Path to current assets | -| `--destination ` | `.` | Changelog output directory | -| `--filename ` | `CHANGELOG.html` | Changelog filename | +| Option | Default | Description | +| ------------------------ | ---------------- | -------------------------- | +| `[oldAssetPath]` | - | Path to previous assets | +| `[newAssetPath]` | - | Path to current assets | +| `--oldAssetsPath ` | - | Path to previous assets | +| `--newAssetsPath ` | - | Path to current assets | +| `--destination ` | `.` | Changelog output directory | +| `--filename ` | `CHANGELOG.html` | Changelog filename | ### Icon Font Generation @@ -422,13 +444,13 @@ pp-dev changelog [oldAssetPath] [newAssetPath] [options] pp-dev generate-icon-font [source] [destination] [options] ``` -| Option | Default | Description | -|--------|---------|-------------| -| `[source]` | - | Source directory with SVG icons | -| `[destination]` | - | Output directory | -| `--source ` | - | Source directory with SVG icons | -| `--destination ` | - | Output directory | -| `--fontName ` | `icon-font` | Font name | +| Option | Default | Description | +| ---------------------- | ----------- | ------------------------------- | +| `[source]` | - | Source directory with SVG icons | +| `[destination]` | - | Output directory | +| `--source ` | - | Source directory with SVG icons | +| `--destination ` | - | Output directory | +| `--fontName ` | `icon-font` | Font name | ## Next.js Integration @@ -442,12 +464,13 @@ pp-dev generate-icon-font [source] [destination] [options] } ``` 3. Wrap your Next.js config: + ```javascript // next.config.js const { withPPDev } = require('@metricinsights/pp-dev'); module.exports = withPPDev({ - // your Next.js config + // your Next.js config }); ``` @@ -464,11 +487,13 @@ For custom build configuration, create a `vite.config` file. See [Vite Configura If you encounter an error like "Next.js is required but not available": 1. **Install Next.js in your project:** + ```bash npm install next@^15 ``` 2. **Verify the installation:** + ```bash npm list next ``` diff --git a/tests/README.md b/tests/README.md index a330209..679b2c2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -22,6 +22,7 @@ This creates `metricinsights-pp-dev-latest.tgz` which is used by the test projec ### 2. Running Tests #### Run All Tests + ```bash npm test # or @@ -29,6 +30,7 @@ npm test -- --all ``` #### Run Specific Tests + ```bash # Only CommonJS tests npm test -- -t dev-commonjs @@ -43,11 +45,13 @@ npm test -- -t dev-nextjs-cjs ### 3. Manual Docker Commands #### Building Docker Image + ```bash docker build -f Dockerfile -t pp-dev-tests . ``` #### Run Dev Servers in Docker + ```bash # CommonJS dev server docker run --rm -p 3000:3000 pp-dev-tests dev-commonjs @@ -62,6 +66,7 @@ docker run --rm -p 3000:3000 pp-dev-tests dev-nextjs-cjs ## Available Test Types ### Development Servers + - `dev-commonjs` - Start CommonJS/Vite dev server (port 3000) - `dev-nextjs` - Start Next.js dev server (port 3000) - `dev-nextjs-cjs` - Start Next.js CJS dev server (port 3000) @@ -80,6 +85,7 @@ docker run --rm -p 3000:3000 pp-dev-tests dev-nextjs-cjs ## Configuration Dockerfile automatically: + 1. Uses Node.js 22 Alpine 2. Copies the built pp-dev package 3. Copies test folders diff --git a/tests/TESTING.md b/tests/TESTING.md index 7447225..d3be64c 100644 --- a/tests/TESTING.md +++ b/tests/TESTING.md @@ -25,6 +25,7 @@ tests/unit/ ``` **Run unit tests:** + ```bash npm run test:unit # Run once npm run test:unit:watch # Watch mode @@ -44,6 +45,7 @@ tests/integration/ ``` **Run integration tests:** + ```bash npm run test:integration # Run once npm run test:integration:watch # Watch mode @@ -68,6 +70,7 @@ e2e/ ``` **Run E2E tests:** + ```bash npm run test:e2e # Run all E2E tests via Docker npm run test:e2e:ui # Run with Playwright UI @@ -81,17 +84,18 @@ npm run test:all # Runs unit, integration, and E2E tests ## Test Configuration Files -| File | Purpose | -|------|---------| -| `vitest.config.ts` | Unit test configuration | +| File | Purpose | +| ------------------------------ | ------------------------------ | +| `vitest.config.ts` | Unit test configuration | | `vitest.integration.config.ts` | Integration test configuration | -| `playwright.config.ts` | E2E test configuration | -| `tests/tsconfig.json` | TypeScript config for tests | -| `tests/setup.ts` | Global test setup | +| `playwright.config.ts` | E2E test configuration | +| `tests/tsconfig.json` | TypeScript config for tests | +| `tests/setup.ts` | Global test setup | ## Test Environment Variables For E2E tests: + - `TEST_TYPE` - Type of test (dev-commonjs, dev-nextjs, dev-nextjs-cjs, config) - `BASE_URL` - Base URL for the test server @@ -138,6 +142,7 @@ test('should display toolbar', async ({ page, baseURL }) => { ## Coverage Reports Coverage reports are generated in: + - `coverage/unit/` - Unit test coverage - `coverage/integration/` - Integration test coverage - `playwright-report/` - E2E test reports @@ -159,12 +164,14 @@ See `tests/README.md` for more details on Docker testing. ## Debugging Tests ### Unit/Integration Tests + ```bash npm run test:unit -- --reporter=verbose npm run test:integration -- --reporter=verbose ``` ### E2E Tests + ```bash npx playwright test --debug npx playwright test --ui @@ -173,6 +180,7 @@ npx playwright test --ui ## Test Fixtures Test fixtures and mock data are located in: + - `tests/test-commonjs/` - CommonJS/Vite test project - `tests/test-nextjs/` - Next.js ESM test project - `tests/test-nextjs-cjs/` - Next.js CJS test project From 277ed8925db36fb74ccf19466e3a2530305b57db Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:15:12 +0300 Subject: [PATCH 08/12] refactor(core): improve dist and client services, proxy stack, CLI, and plugins --- src/api/assets.ts | 1 - src/api/index.ts | 1 + src/api/page-template.ts | 30 +- src/api/page.ts | 5 +- src/api/unavailable-json-api.ts | 26 + src/cli.ts | 841 +++++++-------------- src/client/assets/css/client.scss | 74 ++ src/client/index.html | 21 +- src/client/index.ts | 131 +++- src/client/rollup.config.ts | 26 +- src/client/tsconfig.json | 6 +- src/client/types/assets.d.ts | 5 + src/config.ts | 31 +- src/constants.ts | 25 +- src/index.ts | 139 ++-- src/lib/auth.provider.ts | 4 +- src/lib/changelog-generator.ts | 24 +- src/lib/client.service.ts | 437 ++++++++--- src/lib/dist.service.ts | 484 +++++++++++- src/lib/helpers/content-encoding.helper.ts | 9 +- src/lib/helpers/login.helper.ts | 82 +- src/lib/helpers/server.ts | 23 +- src/lib/helpers/token.helper.ts | 12 +- src/lib/helpers/url.helper.ts | 42 +- src/lib/internal.middleware.ts | 2 +- src/lib/load-pp-data.middleware.ts | 6 +- src/lib/logger.ts | 2 +- src/lib/next-import.ts | 5 +- src/lib/pp.middleware.ts | 275 +++---- src/lib/proxy-cache.middleware.ts | 54 +- src/lib/proxy-pass.middleware.ts | 223 +++--- src/lib/rewrite-response.middleware.ts | 6 +- src/plugin.ts | 160 ++-- src/plugins/client-injection-plugin.ts | 44 +- src/plugins/mi-topbar-plugin.ts | 42 +- src/plugins/version-plugin.ts | 187 ++++- src/shortcuts.ts | 18 +- 37 files changed, 1957 insertions(+), 1546 deletions(-) create mode 100644 src/api/unavailable-json-api.ts create mode 100644 src/client/types/assets.d.ts diff --git a/src/api/assets.ts b/src/api/assets.ts index b56b831..2339ac1 100644 --- a/src/api/assets.ts +++ b/src/api/assets.ts @@ -102,4 +102,3 @@ export class AssetsAPI extends BaseAPI { .then((res) => res.data); } } - diff --git a/src/api/index.ts b/src/api/index.ts index 20ee81a..c2250ef 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,3 +2,4 @@ export * from './assets.js'; export * from './assets-v7.js'; export * from './page.js'; export * from './page-template.js'; +export * from './unavailable-json-api.js'; diff --git a/src/api/page-template.ts b/src/api/page-template.ts index b51627a..e965d98 100644 --- a/src/api/page-template.ts +++ b/src/api/page-template.ts @@ -1,5 +1,6 @@ -import { BaseAPI } from './base'; -import { Headers } from './constants'; +import { BaseAPI } from './base.js'; +import { Headers } from './constants.js'; +import { unavailablePageTemplateDataError } from './unavailable-json-api.js'; export interface PageTemplate { id: number; @@ -18,7 +19,7 @@ export interface PageTemplateExtended extends PageTemplate { export class PageTemplateAPI extends BaseAPI { public async getAll(internal_name?: string, headers?: Headers) { - return ( + const data = ( await this.axios.get<{ page_templates: PageTemplate[] }>(`/api/page_template`, { withCredentials: true, headers: Object.assign({}, headers, { @@ -32,11 +33,17 @@ export class PageTemplateAPI extends BaseAPI { internal_name, }, }) - ).data.page_templates; + ).data; + + if (typeof data !== 'object' || data === null || !Array.isArray(data.page_templates)) { + throw unavailablePageTemplateDataError(); + } + + return data.page_templates; } public async get(id: number, headers?: Headers) { - return ( + const data = ( await this.axios.get<{ page_template: PageTemplateExtended }>(`/api/page_template/id/${id}`, { withCredentials: true, headers: Object.assign({}, headers, { @@ -44,6 +51,17 @@ export class PageTemplateAPI extends BaseAPI { 'content-type': 'application/json', }), }) - ).data.page_template; + ).data; + + if ( + typeof data !== 'object' || + data === null || + typeof data.page_template !== 'object' || + data.page_template === null + ) { + throw unavailablePageTemplateDataError(); + } + + return data.page_template; } } diff --git a/src/api/page.ts b/src/api/page.ts index 73d5df0..caddef5 100644 --- a/src/api/page.ts +++ b/src/api/page.ts @@ -1,5 +1,6 @@ import { Headers } from './constants.js'; import { BaseAPI } from './base.js'; +import { unavailablePageDataError } from './unavailable-json-api.js'; export interface Page { name: string; @@ -43,9 +44,7 @@ export class PageAPI extends BaseAPI { // Not a JSON response if (typeof data !== 'object' || data === null) { - throw new Error( - 'Something went wrong when fetching page data. The instance may be down or in maintenance mode. Please try again later.', - ); + throw unavailablePageDataError(); } return data.page; diff --git a/src/api/unavailable-json-api.ts b/src/api/unavailable-json-api.ts new file mode 100644 index 0000000..93c7883 --- /dev/null +++ b/src/api/unavailable-json-api.ts @@ -0,0 +1,26 @@ +/** Thrown when `/api/page/id/:id` returns non-JSON (e.g. HTML maintenance page). */ +export const PAGE_DATA_FETCH_FRAGMENT = 'Something went wrong when fetching page data'; + +/** Thrown when `/api/page_template/...` returns non-JSON (e.g. HTML maintenance page). */ +export const PAGE_TEMPLATE_FETCH_FRAGMENT = 'Something went wrong when fetching page template data'; + +const MAINTENANCE_SUFFIX = ' The instance may be down or in maintenance mode. Please try again later.'; + +export function unavailablePageDataError(): Error { + return new Error(`${PAGE_DATA_FETCH_FRAGMENT}.${MAINTENANCE_SUFFIX}`); +} + +export function unavailablePageTemplateDataError(): Error { + return new Error(`${PAGE_TEMPLATE_FETCH_FRAGMENT}.${MAINTENANCE_SUFFIX}`); +} + +/** + * Plain `Error` from PageAPI / PageTemplateAPI when axios returns 200 + HTML or invalid JSON shape. + */ +export function isUnavailableJsonApiError(err: unknown): boolean { + if (!(err instanceof Error)) { + return false; + } + + return err.message.includes(PAGE_DATA_FETCH_FRAGMENT) || err.message.includes(PAGE_TEMPLATE_FETCH_FRAGMENT); +} diff --git a/src/cli.ts b/src/cli.ts index d91ef70..5e425e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,27 +1,14 @@ import * as path from 'path'; import * as fs from 'fs'; +import { EventEmitter } from 'node:events'; import { performance } from 'node:perf_hooks'; import { watch } from 'chokidar'; import { cac } from 'cac'; -import { - ServerOptions, - BuildOptions, - LogLevel, - InlineConfig, - loadEnv, -} from 'vite'; +import { ServerOptions, BuildOptions, LogLevel, InlineConfig, loadEnv } from 'vite'; import { VERSION } from './constants.js'; import { bindShortcuts } from './shortcuts.js'; import { getViteConfig } from './index.js'; -import { - mergeConfig, - build, - optimizeDeps, - resolveConfig, - preview, - ViteDevServer, - loadConfigFromFile, -} from 'vite'; +import { mergeConfig, build, optimizeDeps, resolveConfig, preview, ViteDevServer, loadConfigFromFile } from 'vite'; import { parse } from 'url'; import { initRewriteResponse } from './lib/rewrite-response.middleware.js'; import { initPPRedirect } from './lib/pp-redirect.middleware.js'; @@ -90,11 +77,7 @@ function createConfigWatcher( let restartTimeout: NodeJS.Timeout | null = null; watcher.on('change', (filePath) => { - logger( - colors.blue( - `šŸ”§ Config file changed: ${path.relative(projectRoot, filePath)}`, - ), - ); + logger(colors.blue(`šŸ”§ Config file changed: ${path.relative(projectRoot, filePath)}`)); // Debounce restart to avoid multiple rapid restarts if (restartTimeout) { @@ -103,16 +86,10 @@ function createConfigWatcher( restartTimeout = setTimeout(async () => { try { - logger( - colors.yellow(`šŸ”„ Restarting dev server due to config change...`), - ); + logger(colors.yellow(`šŸ”„ Restarting dev server due to config change...`)); await restartCallback(); } catch (error: any) { - logger( - colors.red( - `āŒ Failed to restart dev server: ${error?.message}. Stack: ${error?.stack}`, - ), - ); + logger(colors.red(`āŒ Failed to restart dev server: ${error?.message}. Stack: ${error?.stack}`)); } }, 500); // 500ms debounce }); @@ -171,14 +148,10 @@ interface NextCommandCLIOptions extends GlobalCLIOptions { type NextBundlerChoice = { webpack?: boolean; turbopack?: boolean }; function parseNextBundlerCli(opts: NextCommandCLIOptions): NextBundlerChoice { - const envWebpack = - process.env.PP_DEV_NEXT_WEBPACK === '1' || - process.env.PP_DEV_NEXT_WEBPACK === 'true'; + const envWebpack = process.env.PP_DEV_NEXT_WEBPACK === '1' || process.env.PP_DEV_NEXT_WEBPACK === 'true'; if (envWebpack && (opts.webpack || opts.turbopack)) { - throw new Error( - 'Do not combine PP_DEV_NEXT_WEBPACK with --webpack or --turbopack', - ); + throw new Error('Do not combine PP_DEV_NEXT_WEBPACK with --webpack or --turbopack'); } if (envWebpack) { @@ -231,9 +204,7 @@ interface IconFontOptions { let profileSession = (global as any).__pp_dev_profile_session; let profileCount = 0; -export const stopProfiler = ( - log: (message: string) => void, -): void | Promise => { +export const stopProfiler = (log: (message: string) => void): void | Promise => { if (!profileSession) { return; } @@ -242,15 +213,9 @@ export const stopProfiler = ( profileSession!.post('Profiler.stop', (err: any, { profile }: any) => { // Write profile to disk, upload, etc. if (!err) { - const outPath = path.resolve( - `./pp-dev-profile-${profileCount++}.cpuprofile`, - ); + const outPath = path.resolve(`./pp-dev-profile-${profileCount++}.cpuprofile`); fs.writeFileSync(outPath, JSON.stringify(profile)); - log( - colors.yellow( - `CPU profile written to ${colors.white(colors.dim(outPath))}`, - ), - ); + log(colors.yellow(`CPU profile written to ${colors.white(colors.dim(outPath))}`)); profileSession = undefined; res(); } else { @@ -270,9 +235,7 @@ const filterDuplicateOptions = (options: T) => { /** * removing global flags before passing as command specific sub-configs */ -function cleanOptions( - options: Options, -): Omit { +function cleanOptions(options: Options): Omit { const ret = { ...options }; delete ret['--']; delete ret.c; @@ -311,10 +274,7 @@ cli .option('--open [path]', `[boolean | string] open browser on startup`) .option('--cors', `[boolean] enable CORS`) .option('--strictPort', `[boolean] exit if specified port is already in use`) - .option( - '--force', - `[boolean] force the optimizer to ignore the cache and re-bundle`, - ) + .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`) .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => { filterDuplicateOptions(options); @@ -322,9 +282,7 @@ cli let configWatcher: ConfigWatcher | null = null; let isRestarting = false; - const projectRoot = root - ? path.resolve(process.cwd(), root) - : process.cwd(); + const projectRoot = root ? path.resolve(process.cwd(), root) : process.cwd(); const logger = createLogger(options.logLevel); const startServer = async () => { @@ -356,11 +314,7 @@ cli let config = await getViteConfig(); - const envVars = loadEnv( - options.mode || 'development', - root ?? process.cwd(), - '', - ); + const envVars = loadEnv(options.mode || 'development', root ?? process.cwd(), ''); if (envVars) { Object.keys(envVars).forEach((key) => { @@ -406,18 +360,10 @@ cli const ppDevStartTime = (global as any).__pp_dev_start_time ?? false; const startupDurationString = ppDevStartTime - ? colors.dim( - `ready in ${colors.reset( - colors.bold(Math.ceil(performance.now() - ppDevStartTime)), - )} ms`, - ) + ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - ppDevStartTime)))} ms`) : ''; - logger.info( - `\n ${colors.green( - `${colors.bold('PP-DEV')} v${VERSION}`, - )} ${startupDurationString}\n`, - ); + logger.info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`); server.printUrls(); bindShortcuts(server, { @@ -432,9 +378,7 @@ cli if (profileSession) { await stopProfiler(logger.info); } else { - const inspector = await import('node:inspector').then( - (r) => (r as any).default, - ); + const inspector = await import('node:inspector').then((r) => (r as any).default); await new Promise((res) => { profileSession = new inspector.Session(); @@ -460,9 +404,7 @@ cli type: 'custom', event: 'redirect', data: { - url: `/auth/index/logout?proxyRedirect=${encodeURIComponent( - '/', - )}`, + url: `/auth/index/logout?proxyRedirect=${encodeURIComponent('/')}`, }, }); }, @@ -472,23 +414,16 @@ cli // Set up config watcher if (!configWatcher) { - configWatcher = createConfigWatcher( - projectRoot, - startServer, - logger.info, - ); + configWatcher = createConfigWatcher(projectRoot, startServer, logger.info); logger.info(colors.blue('šŸ”§ Config file watcher started')); } isRestarting = false; } catch (e: any) { isRestarting = false; - logger.error( - colors.red(`error when starting dev server:\n${e.stack}`), - { - error: e, - }, - ); + logger.error(colors.red(`error when starting dev server:\n${e.stack}`), { + error: e, + }); stopProfiler(logger.info); process.exit(1); } @@ -496,9 +431,7 @@ cli // Handle graceful shutdown const gracefulShutdown = async (signal: string) => { - logger.info( - colors.yellow(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`), - ); + logger.info(colors.yellow(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`)); try { // Clean up config watcher @@ -530,9 +463,7 @@ cli gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { - logger.error( - colors.red(`āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`), - ); + logger.error(colors.red(`āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`)); gracefulShutdown('unhandledRejection'); }); @@ -542,10 +473,7 @@ cli // Next.js development server cli - .command( - 'next [root]', - 'start Next.js development server with pp-dev integration', - ) + .command('next [root]', 'start Next.js development server with pp-dev integration') .alias('next-serve') .alias('next-dev') .option('--host [host]', `[string] specify hostname`) @@ -554,18 +482,9 @@ cli .option('--open [path]', `[boolean | string] open browser on startup`) .option('--cors', `[boolean] enable CORS`) .option('--strictPort', `[boolean] exit if specified port is already in use`) - .option( - '--force', - `[boolean] force the optimizer to ignore the cache and re-bundle`, - ) - .option( - '--webpack', - `[boolean] use Webpack for Next dev (use when Turbopack/native SWC is unavailable)`, - ) - .option( - '--turbopack', - `[boolean] use Turbopack for Next dev when native bindings work`, - ) + .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`) + .option('--webpack', `[boolean] use Webpack for Next dev (use when Turbopack/native SWC is unavailable)`) + .option('--turbopack', `[boolean] use Turbopack for Next dev when native bindings work`) .action(async (root: string, options: ServerOptions & NextCommandCLIOptions) => { filterDuplicateOptions(options); @@ -623,11 +542,7 @@ cli const opts = cleanOptions(options); // Load environment variables - const envVars = loadEnv( - options.mode || 'development', - root ?? process.cwd(), - '', - ); + const envVars = loadEnv(options.mode || 'development', root ?? process.cwd(), ''); if (envVars) { Object.keys(envVars).forEach((key) => { @@ -656,36 +571,23 @@ cli if (Object.keys(standaloneConfig).length > 0) { ppDevConfig = standaloneConfig; - logger.info( - colors.blue( - `šŸ”§ Loaded pp-dev config from standalone config file`, - ), - ); + logger.info(colors.blue(`šŸ”§ Loaded pp-dev config from standalone config file`)); } else { logger.info( - colors.yellow( - 'āš ļø No pp-dev config found in Next.js config or standalone file, using defaults', - ), + colors.yellow('āš ļø No pp-dev config found in Next.js config or standalone file, using defaults'), ); } } catch (error) { - logger.info( - colors.yellow( - 'āš ļø Failed to load standalone pp-dev config, using defaults', - ), - ); + logger.info(colors.yellow('āš ļø Failed to load standalone pp-dev config, using defaults')); console.debug('Error loading standalone config:', error); } } else { - logger.info( - colors.blue(`šŸ”§ Loaded pp-dev config from Next.js config`), - ); + logger.info(colors.blue(`šŸ”§ Loaded pp-dev config from Next.js config`)); } // Extract configuration values with defaults const { - backendBaseURL = process.env.MI_BACKEND_URL || - 'http://localhost:8080', + backendBaseURL = process.env.MI_BACKEND_URL || 'http://localhost:8080', appId: originalAppId, portalPageId, templateLess = true, @@ -700,12 +602,8 @@ cli const appId: number = originalAppId ?? portalPageId ?? - (process.env.MI_APP_ID - ? parseInt(process.env.MI_APP_ID, 10) || undefined - : undefined) ?? - (process.env.MI_PORTAL_PAGE_ID - ? parseInt(process.env.MI_PORTAL_PAGE_ID, 10) || undefined - : undefined) ?? + (process.env.MI_APP_ID ? parseInt(process.env.MI_APP_ID, 10) || undefined : undefined) ?? + (process.env.MI_PORTAL_PAGE_ID ? parseInt(process.env.MI_PORTAL_PAGE_ID, 10) || undefined : undefined) ?? 1; // Get template name from config, package.json, or fallback to project directory name @@ -733,11 +631,7 @@ cli if (configBasePath) { base = configBasePath; } else { - base = templateLess - ? pathPagePrefix - : v7Features - ? pathTemplateLocalPrefix - : '/pt'; + base = templateLess ? pathPagePrefix : v7Features ? pathTemplateLocalPrefix : '/pt'; base += `/${templateName}`; } @@ -786,11 +680,7 @@ cli if (!isNextTurbopackNativeBindingsError(e)) { throw e; } - logger.warn( - colors.yellow( - '⚠ Turbopack is unavailable (native bindings). Falling back to Webpack.', - ), - ); + logger.warn(colors.yellow('⚠ Turbopack is unavailable (native bindings). Falling back to Webpack.')); await createAndPrepareNext({ webpack: true }); } } else { @@ -801,9 +691,7 @@ cli throw e; } logger.warn( - colors.yellow( - '⚠ Turbopack cannot run (native Next.js bindings unavailable). Falling back to Webpack.', - ), + colors.yellow('⚠ Turbopack cannot run (native Next.js bindings unavailable). Falling back to Webpack.'), ); await createAndPrepareNext({ webpack: true }); } @@ -818,18 +706,12 @@ cli } if (base === '/') { - throw new Error( - 'basePath cannot be equal to "/" or equal to empty string', - ); + throw new Error('basePath cannot be equal to "/" or equal to empty string'); } // Log the configuration logger.info(colors.green('āœ… Next.js app prepared successfully')); - logger.info( - colors.blue( - `šŸ”§ pp-dev plugin configured for template: ${templateName}`, - ), - ); + logger.info(colors.blue(`šŸ”§ pp-dev plugin configured for template: ${templateName}`)); logger.info(colors.blue(`šŸ”§ Base path configured: ${base}`)); if (backendBaseURL) { @@ -842,8 +724,7 @@ cli // Start the server const port = typeof opts.port === 'number' ? opts.port : 3000; - const host = - typeof opts.host === 'string' ? opts.host || '0.0.0.0' : 'localhost'; + const host = typeof opts.host === 'string' ? opts.host || '0.0.0.0' : 'localhost'; // Track open sockets for proper cleanup const openSockets = new Set(); @@ -877,8 +758,7 @@ cli return; } - const middleware = - essentialMiddlewareChain[middlewareIndex]; + const middleware = essentialMiddlewareChain[middlewareIndex]; middlewareIndex++; middleware(req, res, runEssentialMiddleware); @@ -955,34 +835,30 @@ cli // Initialize pp-dev middlewares let mi: MiAPI | null = null; /** All pp-dev middlewares for non-internal routes (app pages). Includes redirect, proxy cache, proxy pass, load PP data, internal server, rewrite response. */ - let fullMiddlewareChain: Array< - (req: any, res: any, next: () => void) => void - > = []; + let fullMiddlewareChain: Array<(req: any, res: any, next: () => void) => void> = []; /** Essential middlewares for internal Next.js routes (e.g. /_next/, /favicon.ico). Only redirect and internal server; skips proxy/cache for faster static asset handling. */ - let essentialMiddlewareChain: Array< - (req: any, res: any, next: () => void) => void - > = []; + let essentialMiddlewareChain: Array<(req: any, res: any, next: () => void) => void> = []; if (backendBaseURL) { const baseUrlHost = new URL(backendBaseURL).host; + if (backendBaseURL.startsWith('https://')) { + EventEmitter.defaultMaxListeners = Math.max(EventEmitter.defaultMaxListeners, 20); + } + // Initialize MiAPI const miConfig: MiAPIOptions = { headers: { host: baseUrlHost, referer: backendBaseURL, - origin: backendBaseURL.replace( - /^(https?:\/\/)([^/]+)(\/.*)?$/i, - '$1$2', - ), + origin: backendBaseURL.replace(/^(https?:\/\/)([^/]+)(\/.*)?$/i, '$1$2'), }, portalPageId: appId, appId, templateLess, disableSSLValidation, v7Features, - personalAccessToken: - personalAccessToken ?? process.env.MI_ACCESS_TOKEN, + personalAccessToken: personalAccessToken ?? process.env.MI_ACCESS_TOKEN, }; mi = new MiAPI(backendBaseURL, miConfig); @@ -991,10 +867,7 @@ cli // Note: We need to adapt Express middlewares to work with raw HTTP requests // 1. PP Redirect middleware (essential for all routes) - const ppRedirectMiddleware = initPPRedirect( - base, - templateName ?? undefined, - ); + const ppRedirectMiddleware = initPPRedirect(base, templateName ?? undefined); const ppRedirectWrapper = (req: any, res: any, next: () => void) => { ppRedirectMiddleware(req, res, next); }; @@ -1022,27 +895,17 @@ cli }; const proxyCacheMiddleware = initProxyCache(cacheConfig); - const proxyCacheWrapper = ( - req: any, - res: any, - next: () => void, - ) => { + const proxyCacheWrapper = (req: any, res: any, next: () => void) => { proxyCacheMiddleware(req, res, next); }; fullMiddlewareChain.push(proxyCacheWrapper); - logger.info( - colors.blue(`šŸ”§ Proxy cache middleware added with TTL: ${ttl}ms`), - ); + logger.info(colors.blue(`šŸ”§ Proxy cache middleware added with TTL: ${ttl}ms`)); } // 3. Load PP Data middleware (only for non-internal routes; before proxy so v7 internal page name is available for `/data/page/` rewrites) const isIndexRegExp = new RegExp(`^((${escapeRegExp(base)})|/)$`); - const loadPPDataMiddleware = initLoadPPData( - isIndexRegExp, - mi, - Object.assign({ base }, miConfig), - ); + const loadPPDataMiddleware = initLoadPPData(isIndexRegExp, mi, Object.assign({ base }, miConfig)); const loadPPDataWrapper = (req: any, res: any, next: () => void) => { loadPPDataMiddleware(req, res, next); }; @@ -1050,9 +913,7 @@ cli fullMiddlewareChain.push(loadPPDataWrapper); // 4. Proxy Pass middleware (only for non-internal routes) - const baseWithoutTrailingSlash = base.endsWith('/') - ? base.substring(0, base.length - 1) - : base; + const baseWithoutTrailingSlash = base.endsWith('/') ? base.substring(0, base.length - 1) : base; // Create mock devServer object for proxy pass middleware const mockProxyDevServer = { @@ -1088,11 +949,7 @@ cli // 5. Internal Server middleware (API endpoints) - essential for all routes const internalServerMiddleware = internalServer; - const internalServerWrapper = ( - req: any, - res: any, - next: () => void, - ) => { + const internalServerWrapper = (req: any, res: any, next: () => void) => { // Check if this is an internal API request if (req.url?.startsWith('/@api/')) { const mockNext = () => {}; @@ -1115,61 +972,29 @@ cli return pathname.startsWith(base) && !pathname.includes('/_next/'); }, (response, req) => { - return Buffer.from( - urlReplacer( - baseUrlHost, - req.headers.host ?? '', - mi!.buildPage(response, miHudLess), - ), - ); + return Buffer.from(urlReplacer(baseUrlHost, req.headers.host ?? '', mi!.buildPage(response, miHudLess))); }, ); - const rewriteResponseWrapper = ( - req: any, - res: any, - next: () => void, - ) => { + const rewriteResponseWrapper = (req: any, res: any, next: () => void) => { rewriteResponseMiddleware(req, res, next); }; fullMiddlewareChain.push(rewriteResponseWrapper); - logger.info( - colors.blue( - `šŸ”§ ${fullMiddlewareChain.length} pp-dev middlewares initialized`, - ), - ); - logger.info( - colors.blue( - `šŸ”§ ${essentialMiddlewareChain.length} essential middlewares for internal routes`, - ), - ); - logger.info( - colors.blue(`šŸ”§ MiAPI initialized for backend: ${backendBaseURL}`), - ); + logger.info(colors.blue(`šŸ”§ ${fullMiddlewareChain.length} pp-dev middlewares initialized`)); + logger.info(colors.blue(`šŸ”§ ${essentialMiddlewareChain.length} essential middlewares for internal routes`)); + logger.info(colors.blue(`šŸ”§ MiAPI initialized for backend: ${backendBaseURL}`)); logger.info(colors.blue(`šŸ”§ Custom App ID: ${appId}`)); } httpServer.listen(port, host, () => { - logger.info( - colors.green( - `āœ… pp-dev Next.js server running at http://${host}:${port}`, - ), - ); - logger.info( - colors.blue( - `šŸ“± Next.js app accessible at http://${host}:${port}${base}`, - ), - ); + logger.info(colors.green(`āœ… pp-dev Next.js server running at http://${host}:${port}`)); + logger.info(colors.blue(`šŸ“± Next.js app accessible at http://${host}:${port}${base}`)); logger.info(colors.blue(`šŸ”§ Base path handling active`)); // Set up config watcher if (!configWatcher) { - configWatcher = createConfigWatcher( - projectRoot, - startNextServer, - logger.info, - ); + configWatcher = createConfigWatcher(projectRoot, startNextServer, logger.info); logger.info(colors.blue('šŸ”§ Config file watcher started')); } @@ -1181,17 +1006,11 @@ cli // Handle graceful shutdown const gracefulShutdown = async (signal: string) => { - logger.info( - colors.yellow( - `\nšŸ›‘ Received ${signal}, shutting down gracefully...`, - ), - ); + logger.info(colors.yellow(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`)); // Set a timeout to force exit if shutdown hangs const shutdownTimeout = setTimeout(() => { - logger.info( - colors.yellow('ā° Shutdown timeout reached, forcing exit'), - ); + logger.info(colors.yellow('ā° Shutdown timeout reached, forcing exit')); process.exit(0); }, 5000); @@ -1227,9 +1046,7 @@ cli process.exit(0); } catch (error) { clearTimeout(shutdownTimeout); - logger.error( - colors.red(`āŒ Error during graceful shutdown: ${error}`), - ); + logger.error(colors.red(`āŒ Error during graceful shutdown: ${error}`)); process.exit(1); } }; @@ -1239,16 +1056,11 @@ cli // If local process.on is not available, try global process if (typeof process.on !== 'function') { - const globalProcess = - (globalThis as any).process || (global as any).process; + const globalProcess = (globalThis as any).process || (global as any).process; if (globalProcess && typeof globalProcess.on === 'function') { processObj = globalProcess; - logger.info( - colors.green( - 'āœ… Using global process object for event handlers', - ), - ); + logger.info(colors.green('āœ… Using global process object for event handlers')); } } @@ -1264,54 +1076,29 @@ cli }); processObj.on('unhandledRejection', (reason, promise) => { - logger.error( - colors.red( - `āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`, - ), - ); + logger.error(colors.red(`āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`)); gracefulShutdown('unhandledRejection'); }); - logger.info( - colors.green( - 'āœ… Process event handlers registered successfully', - ), - ); + logger.info(colors.green('āœ… Process event handlers registered successfully')); } catch (error) { - logger.warn( - colors.yellow( - `āš ļø Failed to register process event handlers: ${error}`, - ), - ); + logger.warn(colors.yellow(`āš ļø Failed to register process event handlers: ${error}`)); } } else { logger.warn( - colors.yellow( - 'āš ļø process.on is not available, graceful shutdown handlers will not be registered', - ), - ); - logger.info( - colors.blue( - 'šŸ’” This might be due to bundling or environment constraints', - ), + colors.yellow('āš ļø process.on is not available, graceful shutdown handlers will not be registered'), ); + logger.info(colors.blue('šŸ’” This might be due to bundling or environment constraints')); } }); isRestarting = false; } catch (error: any) { isRestarting = false; - logger.error( - colors.red( - `āŒ Failed to start Next.js server: ${error?.message}. Stack: ${error?.stack}`, - ), - ); + logger.error(colors.red(`āŒ Failed to start Next.js server: ${error?.message}. Stack: ${error?.stack}`)); // Special handling for Next.js peer dependency errors - if ( - error instanceof Error && - error.message.includes('Next.js is required') - ) { + if (error instanceof Error && error.message.includes('Next.js is required')) { logger.error(colors.red('āŒ Next.js Peer Dependency Error:')); logger.error(colors.red(error.message)); logger.error(colors.yellow('\nšŸ’” To fix this issue:')); @@ -1322,9 +1109,7 @@ cli logger.error(colors.blue(' 3. Or use pnpm:')); logger.error(colors.white(' pnpm add next@^16')); logger.error(colors.yellow('\nšŸ“– For more information, see:')); - logger.error( - colors.blue(' https://nextjs.org/docs/getting-started'), - ); + logger.error(colors.blue(' https://nextjs.org/docs/getting-started')); } process.exit(1); @@ -1333,9 +1118,7 @@ cli // Handle graceful shutdown const gracefulShutdown = async (signal: string) => { - logger.info( - colors.yellow(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`), - ); + logger.info(colors.yellow(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`)); try { // Clean up config watcher @@ -1376,9 +1159,7 @@ cli gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { - logger.error( - colors.red(`āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`), - ); + logger.error(colors.red(`āŒ Unhandled Rejection at: ${promise}, reason: ${reason}`)); gracefulShutdown('unhandledRejection'); }); @@ -1391,343 +1172,248 @@ cli .command('build [root]', 'build for production') .option('--target ', `[string] transpile target (default: 'modules')`) .option('--outDir ', `[string] output directory (default: dist)`) - .option( - '--assetsDir ', - `[string] directory under outDir to place assets in (default: assets)`, - ) - .option( - '--assetsInlineLimit ', - `[number] static asset base64 inline threshold in bytes (default: 4096)`, - ) - .option( - '--ssr [entry]', - `[string] build specified entry for server-side rendering`, - ) - .option( - '--sourcemap [output]', - `[boolean | "inline" | "hidden"] output source maps for build (default: false)`, - ) + .option('--assetsDir ', `[string] directory under outDir to place assets in (default: assets)`) + .option('--assetsInlineLimit ', `[number] static asset base64 inline threshold in bytes (default: 4096)`) + .option('--ssr [entry]', `[string] build specified entry for server-side rendering`) + .option('--sourcemap [output]', `[boolean | "inline" | "hidden"] output source maps for build (default: false)`) .option( '--minify [minifier]', - `[boolean | "terser" | "esbuild"] enable/disable minification, ` + - `or specify minifier to use (default: esbuild)`, + `[boolean | "terser" | "esbuild"] enable/disable minification, ` + `or specify minifier to use (default: esbuild)`, ) .option('--manifest [name]', `[boolean | string] emit build manifest json`) .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`) - .option( - '--force', - `[boolean] force the optimizer to ignore the cache and re-bundle (experimental)`, - ) - .option( - '--emptyOutDir', - `[boolean] force empty outDir when it's outside of root`, - ) + .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle (experimental)`) + .option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`) .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) .option( '--changelog [assetsFile]', `[boolean | string] generate changelog between assetsFile and current build (default: false)`, ) - .action( - async (root: string, options: PPDevBuildOptions & GlobalCLIOptions) => { - filterDuplicateOptions(options); - const buildOptions: PPDevBuildOptions = cleanOptions(options); + .action(async (root: string, options: PPDevBuildOptions & GlobalCLIOptions) => { + filterDuplicateOptions(options); + const buildOptions: PPDevBuildOptions = cleanOptions(options); + + try { + const configFromFile = await loadConfigFromFile( + { mode: options.mode || 'production', command: 'build' }, + options.config, + root, + options.logLevel, + ); - try { - const configFromFile = await loadConfigFromFile( - { mode: options.mode || 'production', command: 'build' }, - options.config, - root, - options.logLevel, - ); + let config = await getViteConfig(); - let config = await getViteConfig(); + if (configFromFile) { + const { plugins, ...fileConfig } = configFromFile.config; - if (configFromFile) { - const { plugins, ...fileConfig } = configFromFile.config; + config = mergeConfig(config, fileConfig); + } - config = mergeConfig(config, fileConfig); - } + const buildConfig = mergeConfig( + config, + { + root, + base: options.base, + mode: options.mode, + configFile: options.config, + logLevel: options.logLevel, + clearScreen: options.clearScreen, + optimizeDeps: { force: options.force }, + build: buildOptions, + }, + true, + ) as InlineConfig; - const buildConfig = mergeConfig( - config, - { - root, - base: options.base, - mode: options.mode, - configFile: options.config, - logLevel: options.logLevel, - clearScreen: options.clearScreen, - optimizeDeps: { force: options.force }, - build: buildOptions, - }, - true, - ) as InlineConfig; + await build(buildConfig); - await build(buildConfig); + if (buildOptions.changelog) { + const executionRoot = root || process.cwd(); - if (buildOptions.changelog) { - const executionRoot = root || process.cwd(); + const outDir = buildConfig.build?.outDir || 'dist'; - const outDir = buildConfig.build?.outDir || 'dist'; + let oldAssetsPath = ''; - let oldAssetsPath = ''; + if (typeof buildOptions.changelog === 'string') { + oldAssetsPath = path.resolve(executionRoot, buildOptions.changelog); + } else { + const backupsDirPath = path.resolve(executionRoot, buildConfig.ppDevConfig?.syncBackupsDir || 'backups'); - if (typeof buildOptions.changelog === 'string') { - oldAssetsPath = path.resolve(executionRoot, buildOptions.changelog); - } else { - const backupsDirPath = path.resolve( - executionRoot, - buildConfig.ppDevConfig?.syncBackupsDir || 'backups', + if (!fs.existsSync(backupsDirPath)) { + createLogger(options.logLevel).warn( + colors.yellow(`backups directory not found, skipping changelog generation`), ); - if (!fs.existsSync(backupsDirPath)) { - createLogger(options.logLevel).warn( - colors.yellow( - `backups directory not found, skipping changelog generation`, - ), - ); - - return; - } - - const backups = fs.readdirSync(backupsDirPath, { - withFileTypes: true, - }); + return; + } - if (!backups.length) { - createLogger(options.logLevel).warn( - colors.yellow( - `no backups found, skipping changelog generation`, - ), - ); + const backups = fs.readdirSync(backupsDirPath, { + withFileTypes: true, + }); - return; - } + if (!backups.length) { + createLogger(options.logLevel).warn(colors.yellow(`no backups found, skipping changelog generation`)); - const latestBackup = backups - .filter((value) => { - return value.isFile() && value.name.endsWith('.zip'); - }) - .reduce((latest, current) => { - const latestTime = fs.statSync( - path.resolve(backupsDirPath, latest.name), - ).mtimeMs; - const currentTime = fs.statSync( - path.resolve(backupsDirPath, current.name), - ).mtimeMs; - - return latestTime > currentTime ? latest : current; - }, backups[0]).name; - - oldAssetsPath = path.resolve(backupsDirPath, latestBackup); + return; } - const currentAssetFilePath = path.resolve(executionRoot, outDir); + const latestBackup = backups + .filter((value) => { + return value.isFile() && value.name.endsWith('.zip'); + }) + .reduce((latest, current) => { + const latestTime = fs.statSync(path.resolve(backupsDirPath, latest.name)).mtimeMs; + const currentTime = fs.statSync(path.resolve(backupsDirPath, current.name)).mtimeMs; - let changelogDestination = 'dist-zip'; + return latestTime > currentTime ? latest : current; + }, backups[0]).name; - if (buildConfig.ppDevConfig) { - if (buildConfig.ppDevConfig.distZip === false) { - changelogDestination = - (buildConfig.build?.outDir as string) || 'dist'; - } else if ( - typeof buildConfig.ppDevConfig.distZip === 'object' && - typeof buildConfig.ppDevConfig.distZip.outDir === 'string' - ) { - changelogDestination = buildConfig.ppDevConfig.distZip.outDir; - } - } + oldAssetsPath = path.resolve(backupsDirPath, latestBackup); + } - const changelogGenerator = new ChangelogGenerator({ - oldAssetsPath, - newAssetsPath: currentAssetFilePath, - destinationPath: path.resolve(executionRoot, changelogDestination), - }); + const currentAssetFilePath = path.resolve(executionRoot, outDir); + + let changelogDestination = 'dist-zip'; - await changelogGenerator.generateChangelog(); + if (buildConfig.ppDevConfig) { + if (buildConfig.ppDevConfig.distZip === false) { + changelogDestination = (buildConfig.build?.outDir as string) || 'dist'; + } else if ( + typeof buildConfig.ppDevConfig.distZip === 'object' && + typeof buildConfig.ppDevConfig.distZip.outDir === 'string' + ) { + changelogDestination = buildConfig.ppDevConfig.distZip.outDir; + } } - } catch (e: any) { - createLogger(options.logLevel).error( - colors.red(`error during build:\n${e.stack}`), - { error: e }, - ); - process.exit(1); - } finally { - stopProfiler((message) => createLogger(options.logLevel).info(message)); + const changelogGenerator = new ChangelogGenerator({ + oldAssetsPath, + newAssetsPath: currentAssetFilePath, + destinationPath: path.resolve(executionRoot, changelogDestination), + }); + + await changelogGenerator.generateChangelog(); } - }, - ); + } catch (e: any) { + createLogger(options.logLevel).error(colors.red(`error during build:\n${e.stack}`), { error: e }); + + process.exit(1); + } finally { + stopProfiler((message) => createLogger(options.logLevel).info(message)); + } + }); // changelog cli - .command( - 'changelog [oldAssetPath] [newAssetPath]', - 'generate changelog between two assets files/folders', - ) - .option( - '--oldAssetsPath ', - `[string] path to the old assets zip file or folder`, - ) - .option( - '--newAssetsPath ', - `[string] path to the new assets zip file or folder`, - ) - .option( - '--destination ', - `[string] destination folder for the changelog (default: .)`, - ) - .option( - '--filename ', - `[string] filename for the changelog (default: CHANGELOG.html)`, - ) - .action( - async ( - oldAssetPath: string, - newAssetPath: string, - options: ChangelogOptions & GlobalCLIOptions, - ) => { - filterDuplicateOptions(options); + .command('changelog [oldAssetPath] [newAssetPath]', 'generate changelog between two assets files/folders') + .option('--oldAssetsPath ', `[string] path to the old assets zip file or folder`) + .option('--newAssetsPath ', `[string] path to the new assets zip file or folder`) + .option('--destination ', `[string] destination folder for the changelog (default: .)`) + .option('--filename ', `[string] filename for the changelog (default: CHANGELOG.html)`) + .action(async (oldAssetPath: string, newAssetPath: string, options: ChangelogOptions & GlobalCLIOptions) => { + filterDuplicateOptions(options); - const { - oldAssetsPath: oldPath = oldAssetPath, - newAssetsPath: newPath = newAssetPath, - destination = '.', - filename = 'CHANGELOG.html', - logLevel, - } = options; + const { + oldAssetsPath: oldPath = oldAssetPath, + newAssetsPath: newPath = newAssetPath, + destination = '.', + filename = 'CHANGELOG.html', + logLevel, + } = options; - const root = process.cwd(); + const root = process.cwd(); - if (!oldPath || !newPath) { - createLogger(logLevel).error( - colors.red( - `error during changelog generation: oldAssetPath and newAssetPath are required`, - ), - ); + if (!oldPath || !newPath) { + createLogger(logLevel).error( + colors.red(`error during changelog generation: oldAssetPath and newAssetPath are required`), + ); - process.exit(1); - } + process.exit(1); + } - const fullOldPath = path.resolve(root, oldPath); - const fullNewPath = path.resolve(root, newPath); - const fullDestination = path.resolve(root, destination); + const fullOldPath = path.resolve(root, oldPath); + const fullNewPath = path.resolve(root, newPath); + const fullDestination = path.resolve(root, destination); - const changelogGenerator = new ChangelogGenerator({ - oldAssetsPath: fullOldPath, - newAssetsPath: fullNewPath, - destinationPath: fullDestination, - changelogFilename: filename, - }); + const changelogGenerator = new ChangelogGenerator({ + oldAssetsPath: fullOldPath, + newAssetsPath: fullNewPath, + destinationPath: fullDestination, + changelogFilename: filename, + }); - await changelogGenerator.generateChangelog(); - }, - ); + await changelogGenerator.generateChangelog(); + }); cli - .command( - 'generate-icon-font [source] [destination]', - 'generate icon font from SVG files', - ) - .option( - '--source ', - `[string] path to the source directory with SVG files`, - ) - .option( - '--destination ', - `[string] path to the destination directory to save the generated font files`, - ) - .option( - '--font-name, -n ', - `[string] name of the font to generate (default: 'icon-font')`, - ) - .action( - async ( - source: string, - destination: string, - options: IconFontOptions & GlobalCLIOptions, - ) => { - filterDuplicateOptions(options); + .command('generate-icon-font [source] [destination]', 'generate icon font from SVG files') + .option('--source ', `[string] path to the source directory with SVG files`) + .option('--destination ', `[string] path to the destination directory to save the generated font files`) + .option('--font-name, -n ', `[string] name of the font to generate (default: 'icon-font')`) + .action(async (source: string, destination: string, options: IconFontOptions & GlobalCLIOptions) => { + filterDuplicateOptions(options); - const { - source: sourceDir = source, - destination: destDir = destination, - fontName = 'icon-font', - } = options; + const { source: sourceDir = source, destination: destDir = destination, fontName = 'icon-font' } = options; - const root = process.cwd(); + const root = process.cwd(); - const fullSourceDir = path.resolve(root, sourceDir); - const fullDestDir = path.resolve(root, destDir); + const fullSourceDir = path.resolve(root, sourceDir); + const fullDestDir = path.resolve(root, destDir); - const iconFontGenerator = new IconFontGenerator({ - sourceDir: fullSourceDir, - outputDir: fullDestDir, - fontName, - }); + const iconFontGenerator = new IconFontGenerator({ + sourceDir: fullSourceDir, + outputDir: fullDestDir, + fontName, + }); - const logger = createLogger(options.logLevel); + const logger = createLogger(options.logLevel); - logger.info( - `Generating icon font from SVG files in ${colors.dim(fullSourceDir)}`, - ); + logger.info(`Generating icon font from SVG files in ${colors.dim(fullSourceDir)}`); - await iconFontGenerator.generate(); + await iconFontGenerator.generate(); - logger.info( - `Icon font generated and saved to ${colors.dim(fullDestDir)}`, - ); - }, - ); + logger.info(`Icon font generated and saved to ${colors.dim(fullDestDir)}`); + }); // optimize cli .command('optimize [root]', 'pre-bundle dependencies') - .option( - '--force', - `[boolean] force the optimizer to ignore the cache and re-bundle`, - ) - .action( - async (root: string, options: { force?: boolean } & GlobalCLIOptions) => { - filterDuplicateOptions(options); - try { - const configFromFile = await loadConfigFromFile( - { mode: options.mode || 'production', command: 'build' }, - options.config, - root, - options.logLevel, - ); + .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`) + .action(async (root: string, options: { force?: boolean } & GlobalCLIOptions) => { + filterDuplicateOptions(options); + try { + const configFromFile = await loadConfigFromFile( + { mode: options.mode || 'production', command: 'build' }, + options.config, + root, + options.logLevel, + ); - let config = await getViteConfig(); + let config = await getViteConfig(); - if (configFromFile) { - const { plugins, ...fileConfig } = configFromFile.config; + if (configFromFile) { + const { plugins, ...fileConfig } = configFromFile.config; - config = mergeConfig(config, fileConfig); - } + config = mergeConfig(config, fileConfig); + } - const optimizeConfig = await resolveConfig( - mergeConfig(config, { - root, - base: options.base, - configFile: options.config, - logLevel: options.logLevel, - mode: options.mode, - }), - 'serve', - ); + const optimizeConfig = await resolveConfig( + mergeConfig(config, { + root, + base: options.base, + configFile: options.config, + logLevel: options.logLevel, + mode: options.mode, + }), + 'serve', + ); - await optimizeDeps(optimizeConfig, options.force, true); - } catch (e: any) { - createLogger(options.logLevel).error( - colors.red(`error when optimizing deps:\n${e.stack}`), - { error: e }, - ); + await optimizeDeps(optimizeConfig, options.force, true); + } catch (e: any) { + createLogger(options.logLevel).error(colors.red(`error when optimizing deps:\n${e.stack}`), { error: e }); - process.exit(1); - } - }, - ); + process.exit(1); + } + }); cli .command('preview [root]', 'locally preview production build') @@ -1789,12 +1475,9 @@ cli server.printUrls(); } catch (e: any) { - createLogger(options.logLevel).error( - colors.red(`error when starting preview server:\n${e.stack}`), - { - error: e, - }, - ); + createLogger(options.logLevel).error(colors.red(`error when starting preview server:\n${e.stack}`), { + error: e, + }); process.exit(1); } finally { diff --git a/src/client/assets/css/client.scss b/src/client/assets/css/client.scss index f3eaaaf..0b8a9c6 100644 --- a/src/client/assets/css/client.scss +++ b/src/client/assets/css/client.scss @@ -200,6 +200,80 @@ $pp-dev-height: 30px; } } + &.pp-dev-info__confirm-overlay, + .pp-dev-info__confirm-overlay { + position: fixed; + inset: 0; + z-index: 100003; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + } + + .pp-dev-info__confirm { + width: min(500px, calc(100vw - 32px)); + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.15); + background-color: var(--pp-dev-info-color-white); + box-shadow: + 0 18px 40px rgba(0, 0, 0, 0.35), + 0 2px 6px rgba(0, 0, 0, 0.2); + overflow: hidden; + + &-title { + padding: 14px 16px; + font-size: 15px; + font-weight: 600; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + color: var(--pp-dev-info-color-dark); + background-color: rgba(0, 0, 0, 0.03); + } + + &-content { + padding: 16px; + white-space: pre-wrap; + color: var(--pp-dev-info-color-dark); + font-size: 13px; + line-height: 1.5; + } + + &-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 0 16px 16px; + } + + &-btn { + min-width: 110px; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid transparent; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + + &--cancel { + color: var(--pp-dev-info-color-dark); + background-color: transparent; + border-color: rgba(0, 0, 0, 0.2); + } + + &--confirm { + color: var(--pp-dev-info-color-white); + background-color: var(--pp-dev-info-color-primary); + } + + &:hover { + opacity: 0.9; + } + } + } + &.pp-dev-info, .pp-dev-info { position: fixed; diff --git a/src/client/index.html b/src/client/index.html index 662da57..16258f8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -11,7 +11,11 @@
- + Package: {%= PACKAGE_NAME %} {%= VERSION %} @@ -43,7 +47,12 @@ {% } %} @@ -51,13 +60,7 @@ {% if (backendBaseURL && !Number.isNaN(+portalPageId)) { %}
`; } - function updateTokenUI( - type: TokenType, - tokenInput: HTMLInputElement, - tokenCaption: HTMLSpanElement, - ) { + function updateTokenUI(type: TokenType, tokenInput: HTMLInputElement, tokenCaption: HTMLSpanElement) { const info = TOKEN_TYPE_CONFIG[type]; if (info) { @@ -219,8 +215,7 @@ export const tokenLoginFunction = function () { tokenInput: HTMLInputElement, tokenCaption: HTMLSpanElement, ) { - const tokenTypeOptions = - tokenTypeSwitcher.querySelectorAll('.token-type-option'); + const tokenTypeOptions = tokenTypeSwitcher.querySelectorAll('.token-type-option'); const handleTokenTypeChange = (target: HTMLDivElement) => { const value = target.dataset.value as TokenType; @@ -279,11 +274,7 @@ export const tokenLoginFunction = function () { const url = response.url && new URL(response.url, window.location.origin); - if ( - url && - !url.pathname.startsWith('/login') && - !url.pathname.startsWith('/@api/') - ) { + if (url && !url.pathname.startsWith('/login') && !url.pathname.startsWith('/@api/')) { // Successful login - redirect window.location.href = response.url; } else { @@ -298,10 +289,7 @@ export const tokenLoginFunction = function () { } } } catch (networkError) { - showError( - 'Login failed: Network error. Please check your connection.', - errorElement, - ); + showError('Login failed: Network error. Please check your connection.', errorElement); } finally { // Remove loading state formWrapper.classList.remove('loading'); @@ -327,18 +315,10 @@ export const tokenLoginFunction = function () { } const tokenTypeSwitcher = document.getElementById('token-type-switcher'); - const activeOption = tokenTypeSwitcher?.querySelector( - '.token-type-option.active', - ) as HTMLDivElement; - const tokenType = (activeOption?.dataset.value || - 'personal') as TokenType; - - await handleFormSubmission( - tokenInput.value.trim(), - tokenType, - formWrapper, - errorElement, - ); + const activeOption = tokenTypeSwitcher?.querySelector('.token-type-option.active') as HTMLDivElement; + const tokenType = (activeOption?.dataset.value || 'personal') as TokenType; + + await handleFormSubmission(tokenInput.value.trim(), tokenType, formWrapper, errorElement); }); // Enter key handler for input @@ -363,9 +343,7 @@ export const tokenLoginFunction = function () { } // Create form elements - const rowTemplate = (contentWrapper.lastChild as HTMLDivElement)?.cloneNode( - true, - ) as HTMLDivElement; + const rowTemplate = (contentWrapper.lastChild as HTMLDivElement)?.cloneNode(true) as HTMLDivElement; if (!rowTemplate) { return; @@ -397,30 +375,13 @@ export const tokenLoginFunction = function () { // Get form elements const tokenTypeSwitcher = document.getElementById('token-type-switcher'); - const tokenInput = document.getElementById( - 'helper-token', - ) as HTMLInputElement; - const tokenCaption = document.getElementById( - 'token-caption', - ) as HTMLSpanElement; - const submitButton = document.getElementById( - 'helper-token-submit', - ) as HTMLButtonElement; - const errorElement = document.getElementById( - 'error-message', - ) as HTMLDivElement; - const formWrapper = formContent.querySelector( - '.helper-login-wrapper', - ) as HTMLElement; - - if ( - !tokenTypeSwitcher || - !tokenInput || - !tokenCaption || - !submitButton || - !errorElement || - !formWrapper - ) { + const tokenInput = document.getElementById('helper-token') as HTMLInputElement; + const tokenCaption = document.getElementById('token-caption') as HTMLSpanElement; + const submitButton = document.getElementById('helper-token-submit') as HTMLButtonElement; + const errorElement = document.getElementById('error-message') as HTMLDivElement; + const formWrapper = formContent.querySelector('.helper-login-wrapper') as HTMLElement; + + if (!tokenTypeSwitcher || !tokenInput || !tokenCaption || !submitButton || !errorElement || !formWrapper) { console.error('Failed to find required form elements'); return; @@ -434,17 +395,18 @@ export const tokenLoginFunction = function () { updateTokenUI('personal', tokenInput, tokenCaption); } + const formSelector = '#mi-react-root form:not([id]):has(input[name="login"]):has(input[name="password"])'; + // Main logic - use MutationObserver instead of setInterval for better performance const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { - const loginForm = document.querySelector( - '#mi-react-root form', - ); + const loginForm = document.querySelector(formSelector); if (loginForm) { observer.disconnect(); initializeLoginForm(loginForm); + break; } } @@ -458,9 +420,7 @@ export const tokenLoginFunction = function () { }); // Fallback: check if form already exists - const existingForm = document.querySelector( - '#mi-react-root form', - ); + const existingForm = document.querySelector(formSelector); if (existingForm) { observer.disconnect(); diff --git a/src/lib/helpers/server.ts b/src/lib/helpers/server.ts index c3c57c5..f6a7b22 100644 --- a/src/lib/helpers/server.ts +++ b/src/lib/helpers/server.ts @@ -33,10 +33,7 @@ export function createDevServer(logLevel = 'info') { let listener: ReturnType; - server.listen = function ( - this: express.Express, - ...args: Parameters - ) { + server.listen = function (this: express.Express, ...args: Parameters) { listener = originalListen.apply(this, args); } as typeof server.listen; @@ -45,10 +42,7 @@ export function createDevServer(logLevel = 'info') { throw new Error('Server is not listening'); } - const colorUrl = (url: string) => - colors.cyan( - url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`), - ); + const colorUrl = (url: string) => colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)); const address = listener.address(); @@ -56,18 +50,11 @@ export function createDevServer(logLevel = 'info') { if (address.address === '::') { const url = new URL(base || '', `http://localhost:${address.port}`); - logger.info( - ` ${colors.green('āžœ')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`, - ); + logger.info(` ${colors.green('āžœ')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`); } else { - const url = new URL( - base || '', - `http://[${address.address}]:${address.port}`, - ); + const url = new URL(base || '', `http://[${address.address}]:${address.port}`); - logger.info( - ` ${colors.green('āžœ')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`, - ); + logger.info(` ${colors.green('āžœ')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`); } } }; diff --git a/src/lib/helpers/token.helper.ts b/src/lib/helpers/token.helper.ts index 34768e3..56e2a01 100644 --- a/src/lib/helpers/token.helper.ts +++ b/src/lib/helpers/token.helper.ts @@ -20,17 +20,13 @@ export interface TokenErrorInfo { */ export function getTokenErrorInfo(error: any): TokenErrorInfo { const status = error.response?.status || 0; - const message = - error.response?.data?.message || error.message || 'Unknown error'; + const message = error.response?.data?.message || error.message || 'Unknown error'; switch (status) { case 412: const lowerMessage = message.toLowerCase(); - if ( - lowerMessage.includes('session expired') || - lowerMessage.includes('session has expired') - ) { + if (lowerMessage.includes('session expired') || lowerMessage.includes('session has expired')) { return { status, message, @@ -134,8 +130,6 @@ export function isTokenError(error: any): boolean { export function isSessionExpiredError(error: any): boolean { const message = error.response?.data?.message?.toLowerCase() ?? ''; return ( - error.response?.status === 412 && - (message.includes('session expired') || - message.includes('session has expired')) + error.response?.status === 412 && (message.includes('session expired') || message.includes('session has expired')) ); } diff --git a/src/lib/helpers/url.helper.ts b/src/lib/helpers/url.helper.ts index a279c3a..a11052a 100644 --- a/src/lib/helpers/url.helper.ts +++ b/src/lib/helpers/url.helper.ts @@ -6,9 +6,7 @@ export const stringIsAValidUrl = (s: string, protocols: string[]) => { const parsed = new URL(s); if (protocols) { - return parsed.protocol - ? protocols.map((x) => `${x.toLowerCase()}:`).includes(parsed.protocol) - : false; + return parsed.protocol ? protocols.map((x) => `${x.toLowerCase()}:`).includes(parsed.protocol) : false; } return true; @@ -17,15 +15,8 @@ export const stringIsAValidUrl = (s: string, protocols: string[]) => { } }; -export const urlReplacer = ( - originalHost: string, - destinationHost: string, - content: string, -) => { - const urlReplaceRegExp = new RegExp( - `(!!)?(https?(:(\\\\)?/(\\\\)?/)${originalHost})`, - 'gi', - ); +export const urlReplacer = (originalHost: string, destinationHost: string, content: string) => { + const urlReplaceRegExp = new RegExp(`(!!)?(https?(:(\\\\)?/(\\\\)?/)${originalHost})`, 'gi'); return content.replace(urlReplaceRegExp, (substring, ...args) => { if (substring.startsWith('!!')) { @@ -36,15 +27,8 @@ export const urlReplacer = ( }); }; -export const urlPathReplacer = ( - urlPath: string, - destinationPath: string, - content: string, -) => { - const urlReplaceRegExp = new RegExp( - `${urlPath.replace(/\\*\//gi, '\\\\/')}`, - 'gi', - ); +export const urlPathReplacer = (urlPath: string, destinationPath: string, content: string) => { + const urlReplaceRegExp = new RegExp(`${urlPath.replace(/\\*\//gi, '\\\\/')}`, 'gi'); const unescapedUrlReplaceRegExp = new RegExp(`${urlPath}`, 'gi'); const replacedContent = content.replace(urlReplaceRegExp, destinationPath); @@ -54,11 +38,7 @@ export const urlPathReplacer = ( : replacedContent; }; -export const redirect = ( - res: ServerResponse, - url: string, - statusCode?: number, -) => { +export const redirect = (res: ServerResponse, url: string, statusCode?: number) => { res.setHeader('location', url); res.statusCode = statusCode || 302; @@ -89,12 +69,7 @@ export function rewriteDataPagePathForV7Proxy( templateName: string | undefined, internalPageName: string | undefined, ): string { - if ( - !v7Features || - !templateName || - !internalPageName || - templateName === internalPageName - ) { + if (!v7Features || !templateName || !internalPageName || templateName === internalPageName) { return requestUrl; } @@ -119,8 +94,7 @@ export function rewriteDataPagePathForV7Proxy( } const tail = m[2] ?? ''; - const newPath = - `/data/page/${internalPageName}${tail}` + u.search + u.hash; + const newPath = `/data/page/${internalPageName}${tail}` + u.search + u.hash; return newPath; } catch { diff --git a/src/lib/internal.middleware.ts b/src/lib/internal.middleware.ts index 5fe9a6e..34cd0b7 100644 --- a/src/lib/internal.middleware.ts +++ b/src/lib/internal.middleware.ts @@ -10,7 +10,7 @@ export function createInternalServer(): express.Application { app.use(express.json()); app.use(express.urlencoded({ extended: true })); - + return app; } diff --git a/src/lib/load-pp-data.middleware.ts b/src/lib/load-pp-data.middleware.ts index e8dc0a1..a196d20 100644 --- a/src/lib/load-pp-data.middleware.ts +++ b/src/lib/load-pp-data.middleware.ts @@ -28,7 +28,7 @@ function getCachedResponse(key: string): any | null { if (Date.now() - cached.timestamp > CACHE_TTL) { apiResponseCache.delete(key); - + return null; } @@ -262,9 +262,7 @@ async function handleTemplateLoad( } const loadPageData = - !templateLess && typeof appId !== 'undefined' - ? mi.getPageVariables(appId, headers) - : mi.getPageTemplate(headers); + !templateLess && typeof appId !== 'undefined' ? mi.getPageVariables(appId, headers) : mi.getPageTemplate(headers); await loadPageData; diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 58a0f68..99adfc3 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -9,7 +9,7 @@ export const createLogger = (level: LogLevel = 'info', name = DEFAULT_LOGGER_KEY if (!storage || typeof storage.has !== 'function') { storage = new Map(); } - + if (storage.has(name)) { return storage.get(name) as unknown as Logger; } diff --git a/src/lib/next-import.ts b/src/lib/next-import.ts index 53fe6ee..dbd5c09 100644 --- a/src/lib/next-import.ts +++ b/src/lib/next-import.ts @@ -17,10 +17,7 @@ export interface NextJSImports { */ export async function safeNextImport(): Promise { try { - const [next, constants] = await Promise.all([ - import('next'), - import('next/constants.js'), - ]); + const [next, constants] = await Promise.all([import('next'), import('next/constants.js')]); return { next: next.default, diff --git a/src/lib/pp.middleware.ts b/src/lib/pp.middleware.ts index f68210c..d9a4e86 100644 --- a/src/lib/pp.middleware.ts +++ b/src/lib/pp.middleware.ts @@ -1,15 +1,12 @@ -import axios, { Axios } from "axios"; -import { JSDOM } from "jsdom"; -import { - AssetsAPI, - PageAPI, - AssetsV7API, - PageTemplateAPI, -} from "../api/index.js"; -import { Agent } from "https"; -import { createLogger } from "./logger.js"; -import { colors, getTokenErrorInfo, logTokenError } from "./helpers/index.js"; -import { Logger } from "vite"; +import axios, { Axios } from 'axios'; +import http from 'node:http'; +import https from 'node:https'; +import { JSDOM } from 'jsdom'; +import { AssetsAPI, PageAPI, AssetsV7API, PageTemplateAPI } from '../api/index.js'; +import { isUnavailableJsonApiError } from '../api/unavailable-json-api.js'; +import { createLogger } from './logger.js'; +import { colors, getTokenErrorInfo, logTokenError } from './helpers/index.js'; +import { Logger } from 'vite'; export type Headers = Record; @@ -23,7 +20,7 @@ export interface MiAPIOptions { personalAccessToken?: string; } -export const TEMPLATE_PAGE_NAME = "[DEV PAGE. DO NOT DELETE]"; +export const TEMPLATE_PAGE_NAME = '[DEV PAGE. DO NOT DELETE]'; // Performance optimization: Connection pool for axios instances const axiosInstanceCache = new Map(); @@ -72,7 +69,7 @@ export class MiAPI { this.#headers = headers; this.#pageVars = []; - this.#pageTitle = ""; + this.#pageTitle = ''; this.#v7Features = v7Features; this.#personalAccessToken = personalAccessToken; @@ -88,21 +85,31 @@ export class MiAPI { } else { if (disableSSLValidation) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - - axios.defaults.httpsAgent = new Agent({ rejectUnauthorized: false }); } + const httpsAgent = new https.Agent({ + // Axios http adapter registers `socket.on('error', …)` per request; concurrent + // requests on one keep-alive TLS socket stack listeners and trigger MaxListenersExceededWarning. + keepAlive: false, + ...(disableSSLValidation ? { rejectUnauthorized: false } : {}), + }); + this.#axios = axios.create({ baseURL, headers, - // Performance optimization: Add connection pooling - timeout: 30000, // 30 seconds timeout + timeout: 30000, maxRedirects: 5, - // Keep connections alive - httpAgent: new Agent({ keepAlive: true, maxSockets: 10 }), - httpsAgent: new Agent({ keepAlive: true, maxSockets: 10 }), + httpAgent: new http.Agent({ keepAlive: false }), + httpsAgent, }); + // Some code paths (redirects, deps, or bare `axios` in the same process) merge with + // `axios.defaults`; setting the same agent avoids a second pool and keeps TLS relaxed + // consistent with `disableSSLValidation` without re‑enabling keep-alive. + if (disableSSLValidation) { + axios.defaults.httpsAgent = httpsAgent; + } + axiosInstanceCache.set(cacheKey, this.#axios); } @@ -134,9 +141,7 @@ export class MiAPI { referer: undefined, }); - Object.keys(obj).forEach( - (key) => obj[key] === undefined && delete obj[key] - ); + Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key]); this.#headers = obj; @@ -165,17 +170,17 @@ export class MiAPI { %%PAGE TITLE%% - - - - - - - - - - - + + + + + + + + + + + @@ -206,7 +211,7 @@ export class MiAPI { return Promise.resolve(this.#pageTemplate); } - if (typeof this.templateLess === "undefined") { + if (typeof this.templateLess === 'undefined') { this.templateLess = false; } @@ -214,28 +219,31 @@ export class MiAPI { const page = await this.pageApi .get(this.portalPageId!, this.#clearHeaders(headers)) .then((response) => { - this.logger.info(colors.green("Page fetched")); + this.logger.info(colors.green('Page fetched')); return response; }) .catch(async (e) => { this.#templateLoadedResolve(false); + // Auth still valid but HTML/non-JSON body — do not replace with portalPageId/access error + if (isUnavailableJsonApiError(e)) { + throw e; + } + if (await this.pageApi.checkAuth(this.#clearHeaders(headers))) { - this.logger.error( - colors.red(`Error fetching page data: ${e.message}\n${e.stack}`) - ); + this.logger.error(colors.red(`Error fetching page data: ${e.message}\n${e.stack}`)); throw new Error( - "The current user does not have access to this page. " + - "Check your configuration to ensure the portalPageId is correct." + 'The current user does not have access to this page. ' + + 'Check your configuration to ensure the portalPageId is correct.', ); } else { throw e; } }); - if (typeof page.template_id !== "undefined") { + if (typeof page.template_id !== 'undefined') { this.#isV710OrHigher = true; } @@ -243,12 +251,9 @@ export class MiAPI { this.#pageTemplate = this.localTemplateHTML; - this.#pageTemplate = this.#pageTemplate.replace( - /%%PAGE TITLE%%/g, - page.name || "Local template" - ); + this.#pageTemplate = this.#pageTemplate.replace(/%%PAGE TITLE%%/g, page.name || 'Local template'); - this.logger.info(colors.green("Local page template fetched")); + this.logger.info(colors.green('Local page template fetched')); this.#templateLoadedResolve(true); @@ -258,7 +263,7 @@ export class MiAPI { const pageList = await this.pageApi .getAll(this.#clearHeaders(headers)) .then((response) => { - this.logger.info(colors.green("Page list fetched")); + this.logger.info(colors.green('Page list fetched')); return response; }) @@ -266,11 +271,9 @@ export class MiAPI { this.#templateLoadedResolve(false); if (await this.pageApi.checkAuth(this.#clearHeaders(headers))) { - this.logger.error( - colors.red(`Error fetching page list: ${e.message}\n${e.stack}`) - ); + this.logger.error(colors.red(`Error fetching page list: ${e.message}\n${e.stack}`)); - throw new Error("Current user does not have access to page list"); + throw new Error('Current user does not have access to page list'); } else { throw e; } @@ -279,33 +282,31 @@ export class MiAPI { let page = pageList.find((p) => p.name === TEMPLATE_PAGE_NAME); if (!page) { - this.logger.warn(colors.yellow("Creating dev page template...")); + this.logger.warn(colors.yellow('Creating dev page template...')); page = await this.pageApi .create( { - enabled: "Y", + enabled: 'Y', name: TEMPLATE_PAGE_NAME, - internal_name: "dev-page-template", - visible_in_homepage: "Y", + internal_name: 'dev-page-template', + visible_in_homepage: 'Y', }, - this.#clearHeaders(headers) + this.#clearHeaders(headers), ) .then((response) => { - this.logger.info(colors.green("Dev page created")); + this.logger.info(colors.green('Dev page created')); return response; }) .catch((e) => { - this.logger.error( - colors.red(`Error creating dev page: ${e.message}`) - ); + this.logger.error(colors.red(`Error creating dev page: ${e.message}`)); this.#templateLoadedResolve(false); throw new Error( `Error when creating dev page. - That can be caused by missing permissions or page with name "${TEMPLATE_PAGE_NAME}" already exists` + That can be caused by missing permissions or page with name "${TEMPLATE_PAGE_NAME}" already exists`, ); }); } @@ -320,18 +321,16 @@ export class MiAPI { .then((response) => { this.#templateLoadedResolve(true); - this.logger.info(colors.green("Page template fetched")); + this.logger.info(colors.green('Page template fetched')); return response; }) .catch((e) => { this.#templateLoadedResolve(false); - this.logger.error( - colors.red(`Error fetching page template: ${e.message}`) - ); + this.logger.error(colors.red(`Error fetching page template: ${e.message}`)); - throw new Error("Error fetching page template"); + throw new Error('Error fetching page template'); }); } @@ -352,7 +351,7 @@ export class MiAPI { return await this.pageApi .get(pageId, this.#clearHeaders(headers)) .then((response) => { - const { tags = "[]", name, template } = response; + const { tags = '[]', name, template } = response; if (template && tags) { const parsed = JSON.parse(tags) as { name: string; value: string }[]; @@ -369,21 +368,17 @@ export class MiAPI { return []; }) .catch((reason) => { - this.logger.error( - colors.red(`Error fetching page variables: ${reason.message}`) - ); + this.logger.error(colors.red(`Error fetching page variables: ${reason.message}`)); if (reason.response?.status === 404) { - throw new Error( - `Portal Page with id "${pageId}" not found on instance ${this.#axios.getUri()}` - ); + throw new Error(`Portal Page with id "${pageId}" not found on instance ${this.#axios.getUri()}`); } if (reason.response?.status === 401) { console.log(`Unauthorized error: ${reason}`); throw new Error( - `Current user does not have access to page with id "${pageId}" on instance ${this.#axios.getUri()}` + `Current user does not have access to page with id "${pageId}" on instance ${this.#axios.getUri()}`, ); } @@ -422,9 +417,9 @@ export class MiAPI { async #fetchV7Page(pageId: number, headers: Headers) { const page = await this.pageApi.get(pageId, headers); - this.logger.info(colors.green("Page fetched")); + this.logger.info(colors.green('Page fetched')); - if (typeof page.template_id !== "undefined") { + if (typeof page.template_id !== 'undefined') { this.#isV710OrHigher = true; } @@ -444,24 +439,24 @@ export class MiAPI { * Handle page fetch errors with appropriate error messages */ #handlePageFetchError(error: any, pageId: number) { - if (this.#v7Features && error.message?.includes("access")) { + if (isUnavailableJsonApiError(error)) { + throw error; + } + + if (this.#v7Features && error.message?.includes('access')) { throw new Error( - "The current user does not have access to this page. " + - "Check your configuration to ensure the portalPageId is correct." + 'The current user does not have access to this page. ' + + 'Check your configuration to ensure the portalPageId is correct.', ); } if (error.response?.status === 404) { - throw new Error( - `Portal Page with id "${pageId}" not found on instance ${this.#axios.getUri()}` - ); + throw new Error(`Portal Page with id "${pageId}" not found on instance ${this.#axios.getUri()}`); } if (error.response?.status === 401) { this.logger.error( - colors.red( - `Current user does not have access to page with id "${pageId}" on instance ${this.#axios.getUri()}` - ) + colors.red(`Current user does not have access to page with id "${pageId}" on instance ${this.#axios.getUri()}`), ); } @@ -476,25 +471,24 @@ export class MiAPI { * @param miHudLess */ buildPage(content: string | Buffer, miHudLess = false) { - let result = - typeof content === "string" ? content : content.toString("utf-8"); + let result = typeof content === 'string' ? content : content.toString('utf-8'); for (const v of this.#pageVars) { - result = result.replace(new RegExp(`\\[${v.name}\\]`, "g"), v.value); + result = result.replace(new RegExp(`\\[${v.name}\\]`, 'g'), v.value); } const dom = new JSDOM(miHudLess ? result : this.#pageTemplate!); - const placeholderText = "%%PLACEHOLDER%%"; + const placeholderText = '%%PLACEHOLDER%%'; if (!miHudLess) { - const placeholder = dom.window.document.createElement("div"); + const placeholder = dom.window.document.createElement('div'); placeholder.innerHTML = placeholderText; - const container = dom.window.document.querySelector(".main-side"); + const container = dom.window.document.querySelector('.main-side'); if (container) { - const scripts = container.querySelectorAll("script"); + const scripts = container.querySelectorAll('script'); if (scripts.length) { container.insertBefore(placeholder, scripts.item(scripts.length - 1)); @@ -502,60 +496,45 @@ export class MiAPI { container.append(placeholder); } } else { - const container = dom.window.document.createElement("div"); + const container = dom.window.document.createElement('div'); container.append(placeholder); dom.window.document.body.append(container); } - const head = dom.window.document.querySelector("head")!; - const title = head!.querySelector("title"); + const head = dom.window.document.querySelector('head')!; + const title = head!.querySelector('title'); if (title) { title.text = this.#pageTitle; } else { - head.innerHTML += `${this.#pageTitle}`; + const titleEl = dom.window.document.createElement('title'); + + titleEl.textContent = this.#pageTitle; + head.appendChild(titleEl); } } const serializedDom = dom.serialize(); - return serializedDom.replace( - new RegExp(`
\\s*${placeholderText}\\s*<\\/div>`, "i"), - result - ); + return serializedDom.replace(new RegExp(`
\\s*${placeholderText}\\s*<\\/div>`, 'i'), result); } async getAssets() { if (this.portalPageId) { if (this.templateLess) { - return await this.assetsApi.downloadPageAssets( - this.portalPageId, - this.#headers - ); + return await this.assetsApi.downloadPageAssets(this.portalPageId, this.#headers); } else { - const pageInfo = await this.pageApi.get( - this.portalPageId, - this.#headers - ); + const pageInfo = await this.pageApi.get(this.portalPageId, this.#headers); if (this.#isV710OrHigher) { - const templateInfo = await this.pageTemplateApi.get( - pageInfo.template_id!, - this.#headers - ); + const templateInfo = await this.pageTemplateApi.get(pageInfo.template_id!, this.#headers); - return await this.assetsApi.downloadTemplateAssets( - templateInfo.id, - this.#headers - ); + return await this.assetsApi.downloadTemplateAssets(templateInfo.id, this.#headers); } else { if (pageInfo.template) { - return await this.assetsApi.downloadTemplateAssets( - pageInfo.template, - this.#headers - ); + return await this.assetsApi.downloadTemplateAssets(pageInfo.template, this.#headers); } } } @@ -565,35 +544,17 @@ export class MiAPI { async updateAssets(assets: Buffer) { if (this.portalPageId) { if (this.templateLess) { - return await this.assetsApi.uploadPageAssets( - this.portalPageId, - assets, - this.#headers - ); + return await this.assetsApi.uploadPageAssets(this.portalPageId, assets, this.#headers); } else { - const pageInfo = await this.pageApi.get( - this.portalPageId, - this.#headers - ); + const pageInfo = await this.pageApi.get(this.portalPageId, this.#headers); if (this.#isV710OrHigher) { - const templateInfo = await this.pageTemplateApi.get( - pageInfo.template_id!, - this.#headers - ); + const templateInfo = await this.pageTemplateApi.get(pageInfo.template_id!, this.#headers); - return await this.assetsApi.uploadTemplateAssets( - templateInfo.id, - assets, - this.#headers - ); + return await this.assetsApi.uploadTemplateAssets(templateInfo.id, assets, this.#headers); } else { if (pageInfo.template) { - return await this.assetsApi.uploadTemplateAssets( - pageInfo.template, - assets, - this.#headers - ); + return await this.assetsApi.uploadTemplateAssets(pageInfo.template, assets, this.#headers); } } } @@ -605,14 +566,12 @@ export class MiAPI { * @param headers Optional headers to use for validation * @returns Promise - true if valid, false if invalid */ - async validateCredentials( - headers?: Headers - ): Promise<{ isValid: boolean; error?: string; code?: string }> { + async validateCredentials(headers?: Headers): Promise<{ isValid: boolean; error?: string; code?: string }> { try { const testHeaders = headers || this.#headers; // Try to make a simple API call to validate credentials - await this.get("/api/user", testHeaders, true); + await this.get('/api/user', testHeaders, true); return { isValid: true }; } catch (error: any) { @@ -626,14 +585,8 @@ export class MiAPI { } } - async get( - path: string, - headers?: Record, - cleanup = false - ) { - const normalizedHeaders = cleanup - ? headers - : Object.assign({}, this.#clearHeaders(this.#headers), headers); + async get(path: string, headers?: Record, cleanup = false) { + const normalizedHeaders = cleanup ? headers : Object.assign({}, this.#clearHeaders(this.#headers), headers); try { return await this.#axios.get(path, { @@ -641,14 +594,12 @@ export class MiAPI { }); } catch (error: any) { // Use the token helper for better error handling - if (getTokenErrorInfo(error).code !== "UNKNOWN_ERROR") { + if (getTokenErrorInfo(error).code !== 'UNKNOWN_ERROR') { const errorInfo = getTokenErrorInfo(error); - this.logger.error( - colors.red(`API request failed: ${errorInfo.userFriendlyMessage}`) - ); + this.logger.error(colors.red(`API request failed: ${errorInfo.userFriendlyMessage}`)); // Log detailed error information with suggestions - logTokenError(this.logger, error, "API Request"); + logTokenError(this.logger, error, 'API Request'); // Preserve the original error structure but enhance it with our error info if (error.response) { diff --git a/src/lib/proxy-cache.middleware.ts b/src/lib/proxy-cache.middleware.ts index ef1fcef..812e50c 100644 --- a/src/lib/proxy-cache.middleware.ts +++ b/src/lib/proxy-cache.middleware.ts @@ -153,10 +153,7 @@ function createResponseBuffer(chunks: Buffer[]): Buffer { return chunks[0]; } - const totalLength = chunks.reduce( - (sum, chunk) => Buffer.byteLength(chunk) + sum, - 0, - ); + const totalLength = chunks.reduce((sum, chunk) => Buffer.byteLength(chunk) + sum, 0); return Buffer.concat(chunks as unknown as Uint8Array[], totalLength); } @@ -164,10 +161,7 @@ function createResponseBuffer(chunks: Buffer[]): Buffer { /** * Safely sets response headers */ -function setResponseHeaders( - res: ServerResponse, - headers: Record, -): void { +function setResponseHeaders(res: ServerResponse, headers: Record): void { try { for (const [header, value] of Object.entries(headers)) { if (value !== undefined && value !== null) { @@ -181,12 +175,7 @@ function setResponseHeaders( } export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { - const { - devServer, - ttl = DEFAULT_TTL, - maxSize = DEFAULT_MAX_SIZE, - maxItems = DEFAULT_MAX_ITEMS, - } = opts; + const { devServer, ttl = DEFAULT_TTL, maxSize = DEFAULT_MAX_SIZE, maxItems = DEFAULT_MAX_ITEMS } = opts; const logger = createLogger(); @@ -212,9 +201,7 @@ export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { if (cacheItem) { logger.info( - `${colors.blue('Proxies request:')} ${colors.green( - req.method, - )} ${url} -> ${colors.blue('Cache')} ${cacheKey}`, + `${colors.blue('Proxies request:')} ${colors.green(req.method)} ${url} -> ${colors.blue('Cache')} ${cacheKey}`, ); setResponseHeaders(res, cacheItem.headers); @@ -237,12 +224,7 @@ export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { callback?: (error?: Error | null) => void, ): boolean { if (!res.hasHeader(PROXY_HEADER)) { - return originalWrite.call( - this, - data, - encoding as BufferEncoding, - callback, - ); + return originalWrite.call(this, data, encoding as BufferEncoding, callback); } // Convert data to Buffer and store @@ -259,18 +241,9 @@ export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { }; // Override end method to finalize response and cache - (res as any).end = function ( - chunk?: any, - encoding?: BufferEncoding, - callback?: () => void, - ): ServerResponse { + (res as any).end = function (chunk?: any, encoding?: BufferEncoding, callback?: () => void): ServerResponse { if (!res.hasHeader(PROXY_HEADER)) { - return originalEnd.call( - this, - chunk, - encoding as BufferEncoding, - callback, - ); + return originalEnd.call(this, chunk, encoding as BufferEncoding, callback); } // Add final chunk if provided @@ -305,11 +278,7 @@ export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { cache.put(cacheKey, cacheItem, ttl); - logger.info( - `${colors.blue('[Cached]')} ${cacheKey} (${( - responseSize / 1024 - ).toFixed(1)}KB)`, - ); + logger.info(`${colors.blue('[Cached]')} ${cacheKey} (${(responseSize / 1024).toFixed(1)}KB)`); } } catch (error) { logger.error(`Failed to cache response for ${cacheKey}: ${error}`); @@ -319,12 +288,7 @@ export function initProxyCache(opts: ProxyCacheOpts): NextHandleFunction { // Send response with proper encoding handling const finalEncoding = encoding || 'utf8'; - return originalEnd.call( - this, - finalBuffer, - finalEncoding as BufferEncoding, - callback, - ); + return originalEnd.call(this, finalBuffer, finalEncoding as BufferEncoding, callback); }; next(); diff --git a/src/lib/proxy-pass.middleware.ts b/src/lib/proxy-pass.middleware.ts index c44cb83..77f5977 100644 --- a/src/lib/proxy-pass.middleware.ts +++ b/src/lib/proxy-pass.middleware.ts @@ -1,12 +1,5 @@ -import { - createProxyMiddleware, - responseInterceptor, -} from 'http-proxy-middleware'; -import { - rewriteDataPagePathForV7Proxy, - urlReplacer, - urlPathReplacer, -} from './helpers/url.helper.js'; +import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; +import { rewriteDataPagePathForV7Proxy, urlReplacer, urlPathReplacer } from './helpers/url.helper.js'; import { ViteDevServer } from 'vite'; import { Express } from 'express'; import { createLogger } from './logger.js'; @@ -36,14 +29,8 @@ const hostOriginRegExp = /^(https?:\/\/)([^/]+)(\/.*)?$/i; export const PROXY_HEADER = 'X-PP-Proxy'; // TODO: Implement interceptor for streaming responses -function streamResponseInterceptor( - interceptor?: (data: Buffer, encoding: BufferEncoding) => Buffer, -) { - return async ( - proxyRes: T, - req: T, - res: ServerResponse, - ) => { +function streamResponseInterceptor(interceptor?: (data: Buffer, encoding: BufferEncoding) => Buffer) { + return async (proxyRes: T, req: T, res: ServerResponse) => { res.setHeader(PROXY_HEADER, 1); res.setHeaders(new Map(Object.entries(proxyRes.headers)) as any); @@ -138,17 +125,11 @@ export function initProxy(opts: ProxyOpts) { const referer = proxyReq.getHeader('referer'); if (host && referer && typeof referer === 'string') { - proxyReq.setHeader( - 'referer', - referer.replace(new RegExp(`https?://${host}`), baseURL), - ); + proxyReq.setHeader('referer', referer.replace(new RegExp(`https?://${host}`), baseURL)); } if (miAPI.personalAccessToken) { - proxyReq.setHeader( - 'Authorization', - `Bearer ${miAPI.personalAccessToken}`, - ); + proxyReq.setHeader('Authorization', `Bearer ${miAPI.personalAccessToken}`); } const originalUrl = req.url ?? '/'; @@ -175,7 +156,13 @@ export function initProxy(opts: ProxyOpts) { )} ${proxyReq.protocol}//${proxyReq.host}${proxyReq.path}`, ); - req.socket.on('close', () => { + // Keep-alive reuses `req.socket`; each proxied request adds one listener until the + // socket closes once. Use `socket.once('close')` — the socket emits `close` at most + // once, so this matches `on` without retaining duplicate handlers. setMaxListeners(0) + // avoids MaxListenersExceeded when many proxied requests share one connection. + // (Do not use `req.once('close')`: that can fire too early vs upstream response.) + req.socket.setMaxListeners(0); + req.socket.once('close', () => { setTimeout(() => { if (!proxyReq.destroyed) { proxyReq.destroy(); @@ -191,137 +178,97 @@ export function initProxy(opts: ProxyOpts) { (serverRes.headers['transfer-encoding']?.includes('chunked') && serverRes.headers['x-accel-buffering'] === 'no') ) { - logger.info( - `${colors.blue('Start streaming for request:')} ${colors.green(req.method)} ${req.url}`, - ); + logger.info(`${colors.blue('Start streaming for request:')} ${colors.green(req.method)} ${req.url}`); - const streamInterceptor = streamResponseInterceptor( - (data, encoding) => { - return Buffer.from( - urlReplacer( - host, - req.headers.host ?? '', - data.toString(encoding), - ), - encoding, - ); - }, - ); + const streamInterceptor = streamResponseInterceptor((data, encoding) => { + return Buffer.from(urlReplacer(host, req.headers.host ?? '', data.toString(encoding)), encoding); + }); return streamInterceptor(serverRes, req, res); } - const rewriteInterceptor = responseInterceptor( - async (responseBuffer, proxyRes, req, res) => { - res.setHeader(PROXY_HEADER, 1); - - const type = await ( - await fileType - ).fileTypeFromBuffer(responseBuffer as unknown as Uint8Array); - - if (type) { - return responseBuffer; - } else { - let response = responseBuffer.toString('utf8'); // convert buffer to string - - try { - const reqUrl = new URL(req.url ?? '', `http://${host}`); - - if ( - reqUrl.searchParams && - reqUrl.searchParams.has('proxyRedirect') - ) { - const redirectToFunction = function () { - const storageKey = 'pp-dev::redirectCount' as const; - const lastRedirectKey = 'pp-dev::lastRedirect' as const; - - let redirectCount = +( - localStorage.getItem(storageKey) ?? 0 - ); - let lastRedirect = localStorage.getItem(lastRedirectKey); - - if (Number.isNaN(redirectCount)) { - redirectCount = 0; - } + const rewriteInterceptor = responseInterceptor(async (responseBuffer, proxyRes, req, res) => { + res.setHeader(PROXY_HEADER, 1); + + const type = await (await fileType).fileTypeFromBuffer(responseBuffer as unknown as Uint8Array); + + if (type) { + return responseBuffer; + } else { + let response = responseBuffer.toString('utf8'); // convert buffer to string + + try { + const reqUrl = new URL(req.url ?? '', `http://${host}`); + + if (reqUrl.searchParams && reqUrl.searchParams.has('proxyRedirect')) { + const redirectToFunction = function () { + const storageKey = 'pp-dev::redirectCount' as const; + const lastRedirectKey = 'pp-dev::lastRedirect' as const; + + let redirectCount = +(localStorage.getItem(storageKey) ?? 0); + let lastRedirect = localStorage.getItem(lastRedirectKey); - let url = window.location.href; + if (Number.isNaN(redirectCount)) { + redirectCount = 0; + } - const func = function () { - const params = new URLSearchParams( - window.location.search, - ); + let url = window.location.href; - if (!params.has('proxyRedirect')) { - console.debug('No proxyRedirect param. Cleaning up.'); - localStorage.removeItem(storageKey); + const func = function () { + const params = new URLSearchParams(window.location.search); - return; - } + if (!params.has('proxyRedirect')) { + console.debug('No proxyRedirect param. Cleaning up.'); + localStorage.removeItem(storageKey); - if (url !== window.location.href) { - url = window.location.href; + return; + } - setTimeout(func, redirectCount < 3 ? 3000 : 5000); - } else { - window.location.href = params.get( - 'proxyRedirect', - ) as string; - } + if (url !== window.location.href) { + url = window.location.href; - localStorage.setItem(storageKey, `${++redirectCount}`); - localStorage.setItem( - lastRedirectKey, - new Date().toISOString(), - ); - }; + setTimeout(func, redirectCount < 3 ? 3000 : 5000); + } else { + window.location.href = params.get('proxyRedirect') as string; + } - setTimeout(func, 3000); + localStorage.setItem(storageKey, `${++redirectCount}`); + localStorage.setItem(lastRedirectKey, new Date().toISOString()); }; - response += - ''; - } - - if (reqUrl.pathname.startsWith('/login')) { - //disable cache - res.setHeader( - 'Cache-Control', - 'no-cache, no-store, must-revalidate', - ); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - } - - const slice = response.slice(0, 100); - - if ( - slice.includes('') - ) { - response += - ''; - } - } catch { - // + setTimeout(func, 3000); + }; + + response += ''; } - const reqHost = req.headers.host ?? ''; + if (reqUrl.pathname.startsWith('/login')) { + //disable cache + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } + + const slice = response.slice(0, 100); - // manipulate response and return the result - return urlPathReplacer( - '/auth/saml/login', - '/login', - urlReplacer(host, reqHost, response), - ); + if ( + slice.includes('') + ) { + response += ''; + } + } catch { + // } - }, - ); + + const reqHost = req.headers.host ?? ''; + + // manipulate response and return the result + return urlPathReplacer('/auth/saml/login', '/login', urlReplacer(host, reqHost, response)); + } + }); return rewriteInterceptor(serverRes, req, res); }, diff --git a/src/lib/rewrite-response.middleware.ts b/src/lib/rewrite-response.middleware.ts index 6134c6b..aaabfaf 100644 --- a/src/lib/rewrite-response.middleware.ts +++ b/src/lib/rewrite-response.middleware.ts @@ -10,11 +10,7 @@ import { createLogger } from './logger.js'; * @param rewrite Rewrite response content handler */ export function initRewriteResponse( - predicate: ( - url: string, - req: IncomingMessage, - res: ServerResponse, - ) => Promise | boolean, + predicate: (url: string, req: IncomingMessage, res: ServerResponse) => Promise | boolean, rewrite: (response: Buffer, req: IncomingMessage, res: ServerResponse) => Buffer, ): NextHandleFunction { return async (req, res, next) => { diff --git a/src/plugin.ts b/src/plugin.ts index a6361b7..34d20cc 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -12,8 +12,7 @@ import type { ViteImageOptimizer } from 'vite-plugin-image-optimizer'; import { createInternalServer } from './lib/internal.middleware.js'; import { getTokenErrorInfo } from './lib/helpers/index.js'; -type RequiredSelection = Required> & - Omit; +type RequiredSelection = Required> & Omit; interface DistZipOptions { /** @@ -203,14 +202,8 @@ export type NormalizedVitePPDevOptions = RequiredSelection< // Add more specific types for validation type ValidationResult = { isValid: true } | { isValid: false; error: string }; -function validateConfigValue( - value: T, - validator: (v: T) => boolean, - errorMessage: string, -): ValidationResult { - return validator(value) - ? { isValid: true } - : { isValid: false, error: errorMessage }; +function validateConfigValue(value: T, validator: (v: T) => boolean, errorMessage: string): ValidationResult { + return validator(value) ? { isValid: true } : { isValid: false, error: errorMessage }; } function validateConfig(config: VitePPDevOptions): ValidationResult { @@ -227,36 +220,29 @@ function validateConfig(config: VitePPDevOptions): ValidationResult { ), validateConfigValue( config.backendBaseURL, - (url): url is string | undefined => - url === undefined || (typeof url === 'string' && url.length > 0), + (url): url is string | undefined => url === undefined || (typeof url === 'string' && url.length > 0), 'VitePPDevOptions.backendBaseURL must be a non-empty string if provided', ), validateConfigValue( config.portalPageId, - (id): id is number | undefined => - id === undefined || (typeof id === 'number' && id > 0), + (id): id is number | undefined => id === undefined || (typeof id === 'number' && id > 0), 'VitePPDevOptions.portalPageId must be a positive number if provided', ), validateConfigValue( config.appId, - (id): id is number | undefined => - id === undefined || (typeof id === 'number' && id > 0), + (id): id is number | undefined => id === undefined || (typeof id === 'number' && id > 0), 'VitePPDevOptions.appId must be a positive number if provided', ), validateConfigValue( config.proxyCacheTTL, - (ttl): ttl is number | undefined => - ttl === undefined || (typeof ttl === 'number' && ttl > 0), + (ttl): ttl is number | undefined => ttl === undefined || (typeof ttl === 'number' && ttl > 0), 'VitePPDevOptions.proxyCacheTTL must be a positive number if provided', ), validateConfigValue( config.integrateMiTopBar, ( integrate, - ): integrate is - | boolean - | { addRootElement?: boolean; addSharedComponentsScripts?: boolean } - | undefined => { + ): integrate is boolean | { addRootElement?: boolean; addSharedComponentsScripts?: boolean } | undefined => { if (integrate === undefined) { return true; } @@ -275,10 +261,7 @@ function validateConfig(config: VitePPDevOptions): ValidationResult { } if (typeof integrate === 'object' && integrate !== null) { - if ( - integrate.addRootElement !== undefined && - typeof integrate.addRootElement !== 'boolean' - ) { + if (integrate.addRootElement !== undefined && typeof integrate.addRootElement !== 'boolean') { return false; } @@ -291,8 +274,7 @@ function validateConfig(config: VitePPDevOptions): ValidationResult { if ( config.miHudLess !== true && - (integrate.addRootElement === true || - integrate.addSharedComponentsScripts === true) + (integrate.addRootElement === true || integrate.addSharedComponentsScripts === true) ) { return false; } @@ -309,9 +291,7 @@ function validateConfig(config: VitePPDevOptions): ValidationResult { return invalidResult || { isValid: true }; } -export function normalizeVitePPDevConfig( - config: VitePPDevOptions, -): NormalizedVitePPDevOptions { +export function normalizeVitePPDevConfig(config: VitePPDevOptions): NormalizedVitePPDevOptions { const validationResult = validateConfig(config); if (!validationResult.isValid) { @@ -366,8 +346,7 @@ export function normalizeVitePPDevConfig( imageOptimizerConfig = false; } - const versionPluginDefaultTemplate = - 'VERSION-v{packageversion}-{currentDate}.json'; + const versionPluginDefaultTemplate = 'VERSION-v{packageversion}-{currentDate}.json'; let versionPluginConfig: false | VersionPluginOptions; if (versionPlugin === false) { @@ -379,8 +358,7 @@ export function normalizeVitePPDevConfig( }; } else { versionPluginConfig = { - versionFileTemplate: - versionPlugin.versionFileTemplate ?? versionPluginDefaultTemplate, + versionFileTemplate: versionPlugin.versionFileTemplate ?? versionPluginDefaultTemplate, enabled: versionPlugin.enabled ?? true, }; } @@ -404,21 +382,17 @@ export function normalizeVitePPDevConfig( } as NormalizedVitePPDevOptions; } -function validateServerConfig( - config: ServerOptions & { base: string }, -): ValidationResult { +function validateServerConfig(config: ServerOptions & { base: string }): ValidationResult { const validations: ValidationResult[] = [ validateConfigValue( config.base, - (base): base is string => - typeof base === 'string' && base.length > 0 && base !== '/', + (base): base is string => typeof base === 'string' && base.length > 0 && base !== '/', 'Server base path cannot be empty or "/"', ), validateConfigValue( config.port, (port): port is number | undefined => - port === undefined || - (typeof port === 'number' && port > 0 && port < 65536), + port === undefined || (typeof port === 'number' && port > 0 && port < 65536), 'Server port must be a valid port number (1-65535)', ), ]; @@ -455,20 +429,17 @@ function validateMiAPIConfig(config: MiAPIConfig): ValidationResult { ), validateConfigValue( config.portalPageId, - (id): id is number | undefined => - id === undefined || (typeof id === 'number' && id > 0), + (id): id is number | undefined => id === undefined || (typeof id === 'number' && id > 0), 'MiAPI portalPageId must be a positive number if provided', ), validateConfigValue( config.appId, - (id): id is number | undefined => - id === undefined || (typeof id === 'number' && id > 0), + (id): id is number | undefined => id === undefined || (typeof id === 'number' && id > 0), 'MiAPI appId must be a positive number if provided', ), validateConfigValue( config.personalAccessToken, - (token): token is string | undefined => - token === undefined || typeof token === 'string', + (token): token is string | undefined => token === undefined || typeof token === 'string', 'MiAPI personalAccessToken must be a string if provided', ), ]; @@ -509,6 +480,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { proxyCacheTTL, disableSSLValidation, distZip, + versionPlugin, syncBackupsDir, v7Features, personalAccessToken, @@ -585,18 +557,14 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { headers: { host: baseUrlHost, referer: backendBaseURL, - origin: backendBaseURL.replace( - /^(https?:\/\/)([^/]+)(\/.*)?$/i, - '$1$2', - ), + origin: backendBaseURL.replace(/^(https?:\/\/)([^/]+)(\/.*)?$/i, '$1$2'), }, portalPageId: normalizedAppId, appId: normalizedAppId, templateLess, disableSSLValidation, v7Features, - personalAccessToken: - personalAccessToken ?? process.env.MI_ACCESS_TOKEN, + personalAccessToken: personalAccessToken ?? process.env.MI_ACCESS_TOKEN, }; const validationResult = validateMiAPIConfig(miConfig); @@ -632,22 +600,21 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { // Get portal page variables from the backend (also, redirect magic) server.middlewares.use( - initLoadPPData(isIndexRegExp, mi, Object.assign({}, options, { - appId: normalizedAppId, - portalPageId: normalizedAppId, - })), + initLoadPPData( + isIndexRegExp, + mi, + Object.assign({}, options, { + appId: normalizedAppId, + portalPageId: normalizedAppId, + }), + ), ); server.middlewares.use( proxyPassMiddleware({ devServer: server, baseURL: backendBaseURL, - proxyIgnore: [ - '/@vite', - '/@metricinsights', - '/@', - baseWithoutTrailingSlash, - ], + proxyIgnore: ['/@vite', '/@metricinsights', '/@', baseWithoutTrailingSlash], disableSSLValidation, miAPI: mi, templateName, @@ -655,13 +622,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { ); // Helper function to send error responses consistently - const sendErrorResponse = ( - res: any, - status: number, - error: string, - details?: string, - code?: string, - ) => { + const sendErrorResponse = (res: any, status: number, error: string, details?: string, code?: string) => { const response: any = { error }; if (details) { @@ -676,16 +637,9 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { }; // Helper function to handle token validation errors - const handleTokenValidationError = ( - res: any, - error: any, - tokenType: string, - ) => { + const handleTokenValidationError = (res: any, error: any, tokenType: string) => { // Log the error for debugging - server.config.logger.error( - `${tokenType} token validation error:`, - error, - ); + server.config.logger.error(`${tokenType} token validation error:`, error); // Use the token helper for better error handling const errorInfo = getTokenErrorInfo(error); @@ -717,12 +671,8 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { switch (errorInfo.code) { case 'SESSION_EXPIRED': status = 412; - errorMessage = - tokenType === 'personal' - ? 'Personal access token expired' - : 'Session expired'; - errorCode = - tokenType === 'personal' ? 'PAT_EXPIRED' : 'SESSION_EXPIRED'; + errorMessage = tokenType === 'personal' ? 'Personal access token expired' : 'Session expired'; + errorCode = tokenType === 'personal' ? 'PAT_EXPIRED' : 'SESSION_EXPIRED'; break; case 'UNAUTHORIZED': status = 401; @@ -764,9 +714,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { } // Log token validation attempt - server.config.logger.info( - `Attempting to validate ${tokenType} token...`, - ); + server.config.logger.info(`Attempting to validate ${tokenType} token...`); if (tokenType === 'personal') { const testRequest = await mi @@ -793,9 +741,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { sendErrorResponse(res, 400, 'Token expired or invalid'); return null; }) - .catch((error: any) => - handleTokenValidationError(res, error, 'personal'), - ); + .catch((error: any) => handleTokenValidationError(res, error, 'personal')); if (!testRequest) { return; @@ -821,10 +767,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { `Regular token validated successfully for ${response.data.users.length} user(s)`, ); - res.setHeader( - 'set-cookie', - response.headers['set-cookie'] ?? '', - ); + res.setHeader('set-cookie', response.headers['set-cookie'] ?? ''); return response; } @@ -832,15 +775,21 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { sendErrorResponse(res, 400, 'Token expired or invalid'); return null; }) - .catch((error: any) => - handleTokenValidationError(res, error, 'regular'), - ); + .catch((error: any) => handleTokenValidationError(res, error, 'regular')); if (!testRequest) { return; } redirect(res, '/', 302); + } else { + sendErrorResponse( + res, + 400, + 'Invalid tokenType', + 'tokenType must be "personal" or "regular".', + 'INVALID_TOKEN_TYPE', + ); } }); @@ -858,6 +807,11 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { distZipFilename: distZip.outFileName, } : undefined, + typeof versionPlugin === 'object' + ? { + versionFileTemplate: versionPlugin.versionFileTemplate, + } + : undefined, ), ) : undefined; @@ -876,13 +830,7 @@ function vitePPDev(options: NormalizedVitePPDevOptions): Plugin { return url.split('?')[0].endsWith('index.html'); }, (response, req) => { - return Buffer.from( - urlReplacer( - baseUrlHost, - req.headers.host ?? '', - mi.buildPage(response, miHudLess), - ), - ); + return Buffer.from(urlReplacer(baseUrlHost, req.headers.host ?? '', mi.buildPage(response, miHudLess))); }, ), ); diff --git a/src/plugins/client-injection-plugin.ts b/src/plugins/client-injection-plugin.ts index 1058c16..c1fca5b 100644 --- a/src/plugins/client-injection-plugin.ts +++ b/src/plugins/client-injection-plugin.ts @@ -1,11 +1,6 @@ import { IndexHtmlTransformResult, normalizePath, Plugin } from 'vite'; import * as path from 'path'; -import { - PP_DEV_CLIENT_ENTRY, - PACKAGE_NAME, - VERSION, - PP_DEV_PACKAGE_DIR, -} from '../constants.js'; +import { PP_DEV_CLIENT_ENTRY, PACKAGE_NAME, VERSION, PP_DEV_PACKAGE_DIR } from '../constants.js'; import * as fs from 'fs'; import ejs from 'ejs'; import type { AsyncTemplateFunction } from 'ejs'; @@ -82,10 +77,7 @@ class LRUCache { // Cache for template compilation and file reading with LRU strategy // Initialize with explicit type to ensure CJS compatibility -const templateCache: LRUCache = new LRUCache< - string, - AsyncTemplateFunction ->(5); +const templateCache: LRUCache = new LRUCache(5); const fileCache: Map = new Map(); // Memoized path resolution @@ -96,11 +88,7 @@ try { if (typeof __filename !== 'undefined' && __filename) { // @ts-ignore - __filename is available in CJS DIRNAME = path.resolve(path.dirname(__filename), '..'); - } else if ( - typeof import.meta !== 'undefined' && - import.meta && - import.meta.url - ) { + } else if (typeof import.meta !== 'undefined' && import.meta && import.meta.url) { DIRNAME = path.resolve(fileURLToPath(new URL('.', import.meta.url)), '..'); } else { // Fallback to current working directory @@ -117,10 +105,7 @@ const CLIENT_PATH = `/${PACKAGE_IMPORT}`; const PACKAGE_REGEXP = new RegExp(`^\\/?${PACKAGE_NAME}\\/client\\/(.*)$`); // Memoized function to get template with caching -function getTemplate( - base: string, - enableCache: boolean = true, -): AsyncTemplateFunction { +function getTemplate(base: string, enableCache: boolean = true): AsyncTemplateFunction { const cacheKey = base; if (enableCache && templateCache.has(cacheKey)) { @@ -141,10 +126,7 @@ function getTemplate( // Compile template with optimized replacement and better regex escaping const escapedClientPath = CLIENT_PATH.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const compiledTemplate = ejs.compile( - templateContent.replace( - new RegExp(escapedClientPath, 'g'), - path.posix.join(base, PACKAGE_IMPORT), - ), + templateContent.replace(new RegExp(escapedClientPath, 'g'), path.posix.join(base, PACKAGE_IMPORT)), { openDelimiter: '{', closeDelimiter: '}', @@ -178,9 +160,7 @@ const performanceMetrics = { totalRequests: 0, }; -export function clientInjectionPlugin( - opts?: ClientInjectionPluginOpts, -): Plugin { +export function clientInjectionPlugin(opts?: ClientInjectionPluginOpts): Plugin { const enableCache = opts?.enableCache ?? true; const maxCacheSize = opts?.maxCacheSize ?? 5; @@ -206,13 +186,7 @@ export function clientInjectionPlugin( resolveId(source) { if (PACKAGE_REGEXP.test(source)) { return { - id: normalizePath( - path.join( - PP_DEV_PACKAGE_DIR, - 'dist/client', - source.replace(PACKAGE_REGEXP, '$1'), - ), - ), + id: normalizePath(path.join(PP_DEV_PACKAGE_DIR, 'dist/client', source.replace(PACKAGE_REGEXP, '$1'))), }; } }, @@ -293,9 +267,7 @@ export function clientInjectionPlugin( }, configureServer(server) { - const clientDir = normalizePath( - path.resolve(server.config.root, path.dirname(PP_DEV_CLIENT_ENTRY)), - ); + const clientDir = normalizePath(path.resolve(server.config.root, path.dirname(PP_DEV_CLIENT_ENTRY))); if (server.config.server?.fs?.allow) { server.config.server.fs.allow.push(clientDir); diff --git a/src/plugins/mi-topbar-plugin.ts b/src/plugins/mi-topbar-plugin.ts index f3fe4c7..ecaa08f 100644 --- a/src/plugins/mi-topbar-plugin.ts +++ b/src/plugins/mi-topbar-plugin.ts @@ -1,50 +1,44 @@ -import { - HtmlTagDescriptor, - Plugin, -} from "vite"; +import { HtmlTagDescriptor, Plugin } from 'vite'; export function miTopBarPlugin( - integrateMiTopBar?: - | boolean - | { addRootElement?: boolean; addSharedComponentsScripts?: boolean } + integrateMiTopBar?: boolean | { addRootElement?: boolean; addSharedComponentsScripts?: boolean }, ): Plugin { return { - name: "mi-topbar-plugin", + name: 'mi-topbar-plugin', transformIndexHtml() { const tags: HtmlTagDescriptor[] = []; if ( integrateMiTopBar === true || - (typeof integrateMiTopBar === "object" && integrateMiTopBar.addRootElement === true) + (typeof integrateMiTopBar === 'object' && integrateMiTopBar.addRootElement === true) ) { tags.push({ - tag: "div", - injectTo: "body-prepend", - attrs: { id: "mi-react-root" }, + tag: 'div', + injectTo: 'body-prepend', + attrs: { id: 'mi-react-root' }, }); } if ( integrateMiTopBar === true || - (typeof integrateMiTopBar === "object" && - integrateMiTopBar.addSharedComponentsScripts === true) + (typeof integrateMiTopBar === 'object' && integrateMiTopBar.addSharedComponentsScripts === true) ) { tags.push( { - tag: "script", - injectTo: "head-prepend", - attrs: { src: "/auth/info.js" }, + tag: 'script', + injectTo: 'head-prepend', + attrs: { src: '/auth/info.js' }, }, { - tag: "script", - injectTo: "head-prepend", - attrs: { src: "/js/main.js", defer: "defer" }, + tag: 'script', + injectTo: 'head-prepend', + attrs: { src: '/js/main.js', defer: 'defer' }, }, { - tag: "link", - injectTo: "head-prepend", - attrs: { href: "/css/main.css", rel: "stylesheet" }, - } + tag: 'link', + injectTo: 'head-prepend', + attrs: { href: '/css/main.css', rel: 'stylesheet' }, + }, ); } diff --git a/src/plugins/version-plugin.ts b/src/plugins/version-plugin.ts index 0d401d9..7e37e79 100644 --- a/src/plugins/version-plugin.ts +++ b/src/plugins/version-plugin.ts @@ -1,22 +1,48 @@ import { createHash } from 'crypto'; +import { execSync } from 'child_process'; import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import type { Plugin } from 'vite'; import type { VersionPluginOptions } from '../plugin.js'; interface VersionPluginConfig extends VersionPluginOptions { outDir: string; packageVersion: string; + packageRepositoryUrl?: string; + packageBranchName?: string; } interface VersionManifest { + schemaVersion: number; version: string; date: string; checksum: string; files: Record; + repositoryUrl?: string; + branchName?: string; + helperVersion: string; } -function walkDir(dir: string, baseDir: string, excludeFile?: string): string[] { +interface BuildManifest { + schemaVersion: number; + manifestType: 'pp-dev-build-manifest'; + generatedAt: string; + versionFile: string; + versionFileTemplate: string; + versionFileSchemaVersion: number; + buildFingerprint: string; + compat: { + versionFileRequired: true; + }; +} + +const VERSION_FILE_SCHEMA_VERSION = 1; +const BUILD_MANIFEST_SCHEMA_VERSION = 1; +const BUILD_MANIFEST_FILE_NAME = 'BUILD-MANIFEST.json'; +const DEFAULT_VERSION_FILE_TEMPLATE = 'VERSION-v{packageversion}-{currentDate}.json'; + +function walkDir(dir: string, baseDir: string, excludeFiles: ReadonlySet = new Set()): string[] { const entries = readdirSync(dir, { withFileTypes: true }); const files: string[] = []; @@ -25,8 +51,8 @@ function walkDir(dir: string, baseDir: string, excludeFile?: string): string[] { const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/'); if (entry.isDirectory()) { - files.push(...walkDir(fullPath, baseDir, excludeFile)); - } else if (entry.isFile() && relativePath !== excludeFile) { + files.push(...walkDir(fullPath, baseDir, excludeFiles)); + } else if (entry.isFile() && !excludeFiles.has(relativePath)) { files.push(fullPath); } } @@ -47,14 +73,121 @@ function computeChecksum(files: Record): string { return createHash('sha256').update(hashesConcatenated).digest('hex'); } -function resolveVersionFileName( - template: string, - packageVersion: string, - currentDate: string, -): string { - return template - .replace(/\{packageversion\}/g, packageVersion) - .replace(/\{currentDate\}/g, currentDate); +function computeBuildFingerprint(files: Record): string { + const normalizedEntries = Object.keys(files) + .sort() + .map((entry) => `${entry}:${files[entry]}`) + .join('\n'); + + return createHash('sha256').update(normalizedEntries, 'utf-8').digest('hex'); +} + +function resolveVersionFileName(template: string, packageVersion: string, currentDate: string): string { + return template.replace(/\{packageversion\}/g, packageVersion).replace(/\{currentDate\}/g, currentDate); +} + +function normalizeRepositoryUrl(url: string): string { + const normalizedUrl = url + .trim() + .replace(/^git\+/, '') + .replace(/\.git$/i, ''); + const scpLikeMatch = normalizedUrl.match(/^([^@]+)@([^:]+):(.+)$/); + + if (scpLikeMatch) { + const [, , host, repositoryPath] = scpLikeMatch; + + return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + } + + const sshProtocolMatch = normalizedUrl.match(/^ssh:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+)$/i); + + if (sshProtocolMatch) { + const [, host, repositoryPath] = sshProtocolMatch; + + return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + } + + const gitProtocolMatch = normalizedUrl.match(/^git:\/\/([^/]+)\/(.+)$/i); + + if (gitProtocolMatch) { + const [, host, repositoryPath] = gitProtocolMatch; + + return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + } + + return normalizedUrl; +} + +function resolveHelperVersion(): string { + const modulePath = fileURLToPath(import.meta.url); + let currentDir = path.dirname(modulePath); + + while (true) { + const packageJsonPath = path.join(currentDir, 'package.json'); + const packageStat = statSync(packageJsonPath, { throwIfNoEntry: false }); + + if (packageStat?.isFile()) { + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { version?: unknown }; + + if (typeof packageJson.version === 'string') { + return packageJson.version; + } + } catch { + // Continue searching parent directories. + } + } + + const parentDir = path.dirname(currentDir); + + if (parentDir === currentDir) { + return 'unknown'; + } + + currentDir = parentDir; + } +} + +function resolveBranchName(configBranchName?: string): string | undefined { + if (typeof configBranchName === 'string' && configBranchName.trim()) { + return configBranchName.trim(); + } + + const ciBranchEnvVars = [ + 'GITHUB_HEAD_REF', + 'GITHUB_REF_NAME', + 'CI_COMMIT_REF_NAME', + 'BITBUCKET_BRANCH', + 'BUILD_SOURCEBRANCHNAME', + 'BRANCH_NAME', + 'TRAVIS_BRANCH', + 'CIRCLE_BRANCH', + 'BUILDKITE_BRANCH', + 'VERCEL_GIT_COMMIT_REF', + ] as const; + + for (const variableName of ciBranchEnvVars) { + const branchName = process.env[variableName]; + + if (typeof branchName === 'string' && branchName.trim()) { + return branchName.trim(); + } + } + + try { + const currentBranchName = execSync('git rev-parse --abbrev-ref HEAD', { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + + if (currentBranchName && currentBranchName !== 'HEAD') { + return currentBranchName; + } + } catch { + // Ignore git failures (not a git repo, git missing, or detached state). + } + + return undefined; } export function versionPlugin(config: VersionPluginConfig): Plugin { @@ -78,33 +211,51 @@ export function versionPlugin(config: VersionPluginConfig): Plugin { const now = new Date(); const manifestDate = now.toISOString(); const currentDate = manifestDate.replace(/:/g, '-'); - const versionFileName = resolveVersionFileName( - versionFileTemplate ?? 'VERSION-v{packageversion}-{currentDate}.json', - packageVersion, - currentDate, - ); + const resolvedVersionFileTemplate = versionFileTemplate ?? DEFAULT_VERSION_FILE_TEMPLATE; + const versionFileName = resolveVersionFileName(resolvedVersionFileTemplate, packageVersion, currentDate); - const allPaths = walkDir(outDirResolved, outDirResolved, versionFileName); + const allPaths = walkDir(outDirResolved, outDirResolved, new Set([versionFileName, BUILD_MANIFEST_FILE_NAME])); const files: Record = {}; for (const filePath of allPaths) { const relativePath = path.relative(outDirResolved, filePath).replace(/\\/g, '/'); - + files[relativePath] = sha256OfContent(filePath); } const checksum = computeChecksum(files); + const buildFingerprint = computeBuildFingerprint(files); + const helperVersion = resolveHelperVersion(); + const branchName = resolveBranchName(config.packageBranchName); const manifest: VersionManifest = { + schemaVersion: VERSION_FILE_SCHEMA_VERSION, version: `v${packageVersion}`, date: manifestDate, checksum, files, + repositoryUrl: config.packageRepositoryUrl ? normalizeRepositoryUrl(config.packageRepositoryUrl) : undefined, + branchName, + helperVersion, }; const manifestPath = path.join(outDirResolved, versionFileName); + const buildManifestPath = path.join(outDirResolved, BUILD_MANIFEST_FILE_NAME); + const buildManifest: BuildManifest = { + schemaVersion: BUILD_MANIFEST_SCHEMA_VERSION, + manifestType: 'pp-dev-build-manifest', + generatedAt: manifestDate, + versionFile: versionFileName, + versionFileTemplate: resolvedVersionFileTemplate, + versionFileSchemaVersion: VERSION_FILE_SCHEMA_VERSION, + buildFingerprint, + compat: { + versionFileRequired: true, + }, + }; writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8'); + writeFileSync(buildManifestPath, JSON.stringify(buildManifest, null, 2), 'utf-8'); }, }; } diff --git a/src/shortcuts.ts b/src/shortcuts.ts index 2c4517b..1dfe0d1 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -22,10 +22,7 @@ export type CLIShortcut = { let cleanupActiveShortcutBinding: (() => void) | null = null; -export function bindShortcuts( - server: ViteDevServer, - opts: BindShortcutsOptions, -): void { +export function bindShortcuts(server: ViteDevServer, opts: BindShortcutsOptions): void { if (!server.httpServer || !process.stdin.isTTY || process.env.CI) { return; } @@ -35,16 +32,11 @@ export function bindShortcuts( if (opts.print) { logger.info( - colors.dim(colors.green(' āžœ')) + - colors.dim(' press ') + - colors.bold('h') + - colors.dim(' to show help'), + colors.dim(colors.green(' āžœ')) + colors.dim(' press ') + colors.bold('h') + colors.dim(' to show help'), ); } - const shortcuts = (opts.customShortcuts ?? []) - .filter(isDefined) - .concat(BASE_SHORTCUTS); + const shortcuts = (opts.customShortcuts ?? []).filter(isDefined).concat(BASE_SHORTCUTS); // Ensure previous server bindings are fully detached before rebinding. cleanupActiveShortcutBinding?.(); @@ -82,9 +74,7 @@ export function bindShortcuts( colors.bold(' Shortcuts'), ...shortcuts.map( (shortcut) => - colors.dim(' press ') + - colors.bold(shortcut.key) + - colors.dim(` to ${shortcut.description}`), + colors.dim(' press ') + colors.bold(shortcut.key) + colors.dim(` to ${shortcut.description}`), ), ].join('\n'), ); From b2be1bda33538fc00205c78d3a6ca752808aa11b Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Tue, 21 Apr 2026 19:15:21 +0300 Subject: [PATCH 09/12] test: align e2e, integration, unit suites, and sandbox fixtures --- e2e/config.spec.ts | 7 +- e2e/server.spec.ts | 12 +- e2e/tsconfig.json | 7 +- .../middleware/pp-redirect.spec.ts | 6 +- .../middleware/proxy-cache.spec.ts | 60 ++- .../middleware/rewrite-response.spec.ts | 4 +- tests/setup.ts | 2 +- tests/test-commonjs/index.html | 25 +- tests/test-commonjs/package-lock.json | 2 +- tests/test-commonjs/pp-dev.config.ts | 4 +- .../public/__template_variables.json | 407 +----------------- tests/test-commonjs/src/api/dataset-data.ts | 2 +- tests/test-commonjs/src/api/entity.ts | 2 +- tests/test-commonjs/src/api/user.ts | 6 +- tests/test-commonjs/src/constants.ts | 2 +- tests/test-commonjs/src/main.ts | 16 +- tests/test-nextjs-cjs/src/api/entity.ts | 2 +- tests/test-nextjs-cjs/src/api/user.ts | 8 +- tests/test-nextjs-cjs/src/constants.ts | 2 +- tests/test-nextjs-cjs/src/pages/index.tsx | 5 +- tests/test-nextjs-cjs/src/styles/global.scss | 7 +- tests/test-nextjs-cjs/src/styles/tailwind.css | 2 +- tests/test-nextjs-cjs/tsconfig.json | 15 +- tests/test-nextjs/custom-server.ts | 90 ++-- tests/test-nextjs/src/api/entity.ts | 2 +- tests/test-nextjs/src/api/user.ts | 8 +- tests/test-nextjs/src/constants.ts | 2 +- tests/test-nextjs/src/pages/_document.tsx | 4 +- tests/test-nextjs/src/pages/index.tsx | 7 +- tests/test-nextjs/src/styles/global.scss | 7 +- tests/test-nextjs/src/styles/tailwind.css | 2 +- tests/test-nextjs/tsconfig.json | 14 +- tests/tsconfig.json | 12 +- tests/unit/config/config.loader.spec.ts | 21 +- tests/unit/helpers/formatting.spec.ts | 4 +- .../unit/lib/content-encoding.helper.spec.ts | 5 +- tests/unit/lib/token.helper.spec.ts | 4 +- tests/unit/lib/url.helper.spec.ts | 69 +-- tests/unit/plugin/plugin.normalize.spec.ts | 78 ++-- tests/unit/plugin/version-plugin.spec.ts | 164 ++++--- 40 files changed, 351 insertions(+), 747 deletions(-) diff --git a/e2e/config.spec.ts b/e2e/config.spec.ts index 40c77cd..a471dc3 100644 --- a/e2e/config.spec.ts +++ b/e2e/config.spec.ts @@ -11,7 +11,7 @@ if (testType !== 'config') { command: string, args: string[] = [], options: Record = {}, - callback: ((error: Error | null, result: { exitCode: number }) => void) | null + callback: ((error: Error | null, result: { exitCode: number }) => void) | null, ) => { const child = spawn(command, args, { stdio: ['pipe', 'inherit', 'inherit'], @@ -34,7 +34,9 @@ if (testType !== 'config') { } else if (code === null) { callback?.(null, { exitCode: 0 }); } else { - callback(new Error(`Command failed with exit code ${code}`), { exitCode: code }); + callback(new Error(`Command failed with exit code ${code}`), { + exitCode: code, + }); } }); @@ -95,7 +97,6 @@ if (testType !== 'config') { (error: Error | null, _result: { exitCode: number }) => { if (error) { console.error('āŒ Server failed to start:', error); - } else { console.log('āœ… Server stopped successfully'); } diff --git a/e2e/server.spec.ts b/e2e/server.spec.ts index 18600a4..c0090cf 100644 --- a/e2e/server.spec.ts +++ b/e2e/server.spec.ts @@ -14,12 +14,12 @@ test('is redirected to the page', async ({ page, baseURL }) => { await page.goto(baseURL); console.log(`šŸ“ Navigated to: ${page.url()}`); }); - + // Step 2: Verify redirection occurred await test.step('Verify redirection occurred', async () => { const currentUrl = page.url(); console.log(`šŸ“ Current URL after navigation: ${currentUrl}`); - + // Verify we were redirected away from the base URL await expect(page).not.toHaveURL(baseURL); console.log(`āœ… Successfully redirected away from base URL`); @@ -29,13 +29,13 @@ test('is redirected to the page', async ({ page, baseURL }) => { await test.step('Verify final URL pattern', async () => { const urlRegex = new RegExp(`^${baseURL}/p[tl]?/`); const finalUrl = page.url(); - + console.log(`šŸ”— Testing URL pattern: ${urlRegex}`); console.log(`šŸ“ Final URL: ${finalUrl}`); - + // Verify the final URL matches the expected pattern expect(finalUrl).toMatch(urlRegex); - + console.log(`āœ… Final URL matches expected pattern: ${finalUrl}`); }); }); @@ -52,4 +52,4 @@ test('pp-dev toolbar is available', async ({ page, baseURL }) => { test.skip(testType?.includes('nextjs') ?? false, 'Skipping toolbar test for Next.js'); await expect(page.locator('.pp-dev-info')).toBeVisible(); -}); \ No newline at end of file +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 582a866..5366251 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -3,10 +3,5 @@ "compilerOptions": { "types": ["node", "@playwright/test"] }, - "include": [ - "**/*.ts", - "**/*.tsx", - "**/*.js", - "**/*.jsx" - ] + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] } diff --git a/tests/integration/middleware/pp-redirect.spec.ts b/tests/integration/middleware/pp-redirect.spec.ts index f5502b5..7f88399 100644 --- a/tests/integration/middleware/pp-redirect.spec.ts +++ b/tests/integration/middleware/pp-redirect.spec.ts @@ -15,8 +15,8 @@ function createTestServer(middleware: any): Promise<{ server: Server; port: numb server.listen(0, () => { const address = server.address(); - const port = typeof address === 'object' ? address?.port ?? 0 : 0; - + const port = typeof address === 'object' ? (address?.port ?? 0) : 0; + resolve({ server, port }); }); }); @@ -42,7 +42,7 @@ async function makeRequest(port: number, path: string): Promise<{ status: number body, }); }); - } + }, ); req.end(); }); diff --git a/tests/integration/middleware/proxy-cache.spec.ts b/tests/integration/middleware/proxy-cache.spec.ts index c678f5e..e9c8e9a 100644 --- a/tests/integration/middleware/proxy-cache.spec.ts +++ b/tests/integration/middleware/proxy-cache.spec.ts @@ -43,13 +43,19 @@ describe('Proxy Cache Middleware', () => { describe('Initialization', () => { it('should create middleware function', () => { - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); expect(typeof middleware).toBe('function'); }); it('should accept custom TTL', () => { - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 10000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 10000, + }); expect(typeof middleware).toBe('function'); }); @@ -57,7 +63,10 @@ describe('Proxy Cache Middleware', () => { describe('Request Handling', () => { it('should call next for GET requests', () => { - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -66,7 +75,10 @@ describe('Proxy Cache Middleware', () => { it('should call next for POST requests', () => { mockReq.method = 'POST'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -75,7 +87,10 @@ describe('Proxy Cache Middleware', () => { it('should handle requests with query parameters', () => { mockReq.url = '/api/test?param=value'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -86,7 +101,10 @@ describe('Proxy Cache Middleware', () => { describe('Cache Behavior', () => { it('should not cache POST requests', () => { mockReq.method = 'POST'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); // First request middleware(mockReq, mockRes, nextFn); @@ -100,7 +118,10 @@ describe('Proxy Cache Middleware', () => { it('should not cache PUT requests', () => { mockReq.method = 'PUT'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -109,7 +130,10 @@ describe('Proxy Cache Middleware', () => { it('should not cache DELETE requests', () => { mockReq.method = 'DELETE'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -120,7 +144,10 @@ describe('Proxy Cache Middleware', () => { describe('URL Patterns', () => { it('should handle root URL', () => { mockReq.url = '/'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -129,7 +156,10 @@ describe('Proxy Cache Middleware', () => { it('should handle nested paths', () => { mockReq.url = '/api/v1/users/123/profile'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -138,7 +168,10 @@ describe('Proxy Cache Middleware', () => { it('should handle URLs with special characters', () => { mockReq.url = '/api/search?q=hello%20world&filter=name'; - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 5000 }); + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 5000, + }); middleware(mockReq, mockRes, nextFn); @@ -154,7 +187,10 @@ describe('Proxy Cache Middleware', () => { }); it('should accept very long TTL', () => { - const middleware = initProxyCache({ devServer: mockDevServer, ttl: 3600000 }); // 1 hour + const middleware = initProxyCache({ + devServer: mockDevServer, + ttl: 3600000, + }); // 1 hour expect(typeof middleware).toBe('function'); }); diff --git a/tests/integration/middleware/rewrite-response.spec.ts b/tests/integration/middleware/rewrite-response.spec.ts index 45cd5a0..f068797 100644 --- a/tests/integration/middleware/rewrite-response.spec.ts +++ b/tests/integration/middleware/rewrite-response.spec.ts @@ -113,7 +113,7 @@ describe('Rewrite Response Middleware', () => { it('should be a function that accepts req, res, and next', () => { const middleware = initRewriteResponse( () => false, - (body) => Buffer.from(body) + (body) => Buffer.from(body), ); expect(typeof middleware).toBe('function'); @@ -123,7 +123,7 @@ describe('Rewrite Response Middleware', () => { it('should process requests without throwing', () => { const middleware = initRewriteResponse( (url) => url.includes('special'), - (body) => Buffer.from(body) + (body) => Buffer.from(body), ); mockReq.url = '/normal/path'; diff --git a/tests/setup.ts b/tests/setup.ts index aa949ff..41c610c 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -33,6 +33,6 @@ export const createTempDir = async (prefix: string = 'pp-dev-test') => { // Helper to clean up temporary directories export const cleanupTempDir = async (dir: string) => { const { rm } = await import('fs/promises'); - + await rm(dir, { recursive: true, force: true }); }; diff --git a/tests/test-commonjs/index.html b/tests/test-commonjs/index.html index 207accd..90b30fa 100644 --- a/tests/test-commonjs/index.html +++ b/tests/test-commonjs/index.html @@ -1,4 +1,4 @@ - + @@ -18,12 +18,14 @@

- Vite + pp-dev test app — edit src/main.ts and index.html + Vite + pp-dev test app — edit src/main.ts and + index.html

- Uses jQuery 3 for DOM updates and API calls ($.ajax with credentials). + Uses jQuery 3 for DOM updates and API calls ($.ajax + with credentials).

@@ -36,21 +38,11 @@

Vite ->

Dev server and build tool for this project.

- +

jQuery.ajax ->

How requests are made for auth and dataset endpoints.

- +

TypeScript ->

Typed entry (main.ts) compiled by Vite.

@@ -72,7 +64,8 @@

Metric Insights ->

pp-dev template helpers

Layout below is rendered from the html tagged template in - src/main.ts (@metricinsights/pp-dev/helpers). + src/main.ts + (@metricinsights/pp-dev/helpers).

diff --git a/tests/test-commonjs/package-lock.json b/tests/test-commonjs/package-lock.json index 21a7184..1661aec 100644 --- a/tests/test-commonjs/package-lock.json +++ b/tests/test-commonjs/package-lock.json @@ -1232,7 +1232,7 @@ "node_modules/@metricinsights/pp-dev": { "version": "0.18.0-beta.1", "resolved": "file:../../metricinsights-pp-dev-latest.tgz", - "integrity": "sha512-yTfu4UHFVFdm0s46d+Wx82eqzDzdJRZBxLa5dQonQ6xsbWMMvIV7i1sMouKKal3cMVh9ZX3zJ891IFGx7izRsA==", + "integrity": "sha512-uOetXn4bxcIQo7EymjJDLrL1px8sm39G8lIcHaMbCKOxEyLkX9n3AzUKNtWM7lCvb2z8nYj+tS+o5E9rBJGsiw==", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/tests/test-commonjs/pp-dev.config.ts b/tests/test-commonjs/pp-dev.config.ts index 2f724fb..9d591d0 100644 --- a/tests/test-commonjs/pp-dev.config.ts +++ b/tests/test-commonjs/pp-dev.config.ts @@ -2,11 +2,11 @@ import { PPDevConfig } from '@metricinsights/pp-dev'; const ppDevConfig: PPDevConfig = { backendBaseURL: 'https://stg7x.metricinsights.com', - appId: 733, + appId: 937, miHudLess: true, integrateMiTopBar: true, v7Features: true, - templateLess: true, + templateLess: false, disableSSLValidation: true, }; diff --git a/tests/test-commonjs/public/__template_variables.json b/tests/test-commonjs/public/__template_variables.json index 1807785..4d0f9f3 100644 --- a/tests/test-commonjs/public/__template_variables.json +++ b/tests/test-commonjs/public/__template_variables.json @@ -1,409 +1,26 @@ { "tags": [ { - "portal_page_template_tag_id": "24", - "portal_page_template_id": "2", - "name": "All Data Dataset", - "uid": "bde45dfb4cb170d1e8d6", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "0", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "137", - "portal_page_template_id": "2", - "name": "All Data Filter Dataset", - "uid": "98d3cc844f8f4aa63062", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "1", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "140", - "portal_page_template_id": "2", - "name": "Last Snapshot Dataset", - "uid": "23668c5ee37b4d043175", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "2", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "138", - "portal_page_template_id": "2", - "name": "Change Report Dataset", - "uid": "3f92ef21763a768ef55e", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "3", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "139", - "portal_page_template_id": "2", - "name": "Change Report Filter Dataset", - "uid": "3c645b34f92d0da69025", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "4", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "26", - "portal_page_template_id": "2", - "name": "Additional Data Dataset", - "uid": "df02094424052b92c708", - "default_value": "", - "additional_options": "", - "tag_type": "select", - "tag_source": "dataset", - "javascript_code": "", - "display_order": "5", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "145", - "portal_page_template_id": "2", - "name": "Export Records Limit", - "uid": "a23511e75d626391fd4a", - "default_value": "100000", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "6", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "Max count of records for the backend export.\n0 = unlimited.\nWarning: Export for 1M records can take ~2.5 minutes and have a size ~70Mb" - }, - { - "portal_page_template_tag_id": "143", - "portal_page_template_id": "2", - "name": "Plant Default Selection", - "uid": "842a82e9496614de18ee", - "default_value": "[]", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "7", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "Y", - "json_list_ind": "N", - "description": "The JSON-like array of values. Example: [1110,2220] or [\"1110\", \"2220\"].\nImportant: All values from this list must be available in the additional dataset" - }, - { - "portal_page_template_tag_id": "30", - "portal_page_template_id": "2", - "name": "Logo", - "uid": "b77eecc8fa68210128f9", - "default_value": "", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "8", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "31", - "portal_page_template_id": "2", - "name": "Measurement Time Column Key", - "uid": "de2c2ae026463967fb40", - "default_value": "measurement_time", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "9", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "32", - "portal_page_template_id": "2", - "name": "Service Master Column Key", - "uid": "01cab92a97adf2594dcd", - "default_value": "service_master", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "10", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "33", - "portal_page_template_id": "2", - "name": "Created On Column Key", - "uid": "33123fd3dfe3b68f45c8", - "default_value": "created_on", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "11", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "34", - "portal_page_template_id": "2", - "name": "Created By Column Key", - "uid": "06a85aec92046ae5872f", - "default_value": "created_by", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "12", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "35", - "portal_page_template_id": "2", - "name": "Changed On Column Key", - "uid": "5b15c7a99279efa8d452", - "default_value": "changed_on", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "13", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "36", - "portal_page_template_id": "2", - "name": "Changed By Column Key", - "uid": "7f833ec421fdb4aec6d5", - "default_value": "changed_by", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "14", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "37", - "portal_page_template_id": "2", - "name": "Chart Filter Service Master Column Key", - "uid": "87119eebb54c0c9ed1ab", - "default_value": "Service Master", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "15", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "38", - "portal_page_template_id": "2", - "name": "Chart Filter Plant Column Key", - "uid": "2cc104fdc039f01990ff", - "default_value": "Plant", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "16", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "39", - "portal_page_template_id": "2", - "name": "Chart Filter Engine Column Key", - "uid": "c7ae47ca851e522f3c68", - "default_value": "Engine Model", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "17", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "40", - "portal_page_template_id": "2", - "name": "Chart Filter Material Column Key", - "uid": "30e71faecc7d1cc30a06", - "default_value": "Material", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "18", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "41", - "portal_page_template_id": "2", - "name": "Chart Filter LID Column Key", - "uid": "3a9c98911e836e984a7b", - "default_value": "LID", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "19", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "42", - "portal_page_template_id": "2", - "name": "Chart Filter ATA Column Key", - "uid": "8381e6a479d88e12fa80", - "default_value": "ATA Number", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "20", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "43", - "portal_page_template_id": "2", - "name": "Chart Filter SM Status Column Key", - "uid": "27fc6802f9a7a1724dc3", - "default_value": "SM Status", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "21", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "161", - "portal_page_template_id": "2", - "name": "Chart Filter Material Description Column Key", - "uid": "40c27ea64289b5a8b011", - "default_value": "", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "22", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "162", - "portal_page_template_id": "2", - "name": "Chart Filter Service Master Description Column Key", - "uid": "9461446a52d3bcce6981", - "default_value": "", - "additional_options": "", - "tag_type": "text", - "tag_source": "static", - "javascript_code": "", - "display_order": "23", - "use_hmtl_editor_ind": "N", - "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" - }, - { - "portal_page_template_tag_id": "160", - "portal_page_template_id": "2", - "name": "Data Classification", - "uid": "3b3366df3bdb5806c32d", - "default_value": "", + "portal_page_template_tag_id": 2341, + "portal_page_template_id": 313, + "name": "variable-test", + "uid": "c8dfece5cc68249206e4690fc4737a8d", + "default_value": "String value for test", "additional_options": "", "tag_type": "text", "tag_source": "static", - "javascript_code": "", - "display_order": "24", + "javascript_code": null, + "display_order": 0, "use_hmtl_editor_ind": "N", "use_json_editor_ind": "N", - "json_list_ind": "N", - "description": "" + "description": "Value for test", + "use_js_code_ind": "N", + "use_raw_html_ind": "N" } ], "settings": { - "image_capture_timeout": "20", + "image_capture_timeout": 20, "image_capture_on_event": "none", - "image_capture_css_selector": "" + "image_capture_css_selector": "none" } } diff --git a/tests/test-commonjs/src/api/dataset-data.ts b/tests/test-commonjs/src/api/dataset-data.ts index dd18ca2..d9716f8 100644 --- a/tests/test-commonjs/src/api/dataset-data.ts +++ b/tests/test-commonjs/src/api/dataset-data.ts @@ -10,6 +10,6 @@ export async function getDatasetData(datasetId: number): Promise { xhrFields: { withCredentials: true }, timeout: 25000, }); - + return (data as { data?: unknown }).data; } diff --git a/tests/test-commonjs/src/api/entity.ts b/tests/test-commonjs/src/api/entity.ts index 8cb2f05..6b9cc2b 100644 --- a/tests/test-commonjs/src/api/entity.ts +++ b/tests/test-commonjs/src/api/entity.ts @@ -10,6 +10,6 @@ export async function getEntities(): Promise { xhrFields: { withCredentials: true }, timeout: 25000, }); - + return (data as { data?: unknown }).data; } diff --git a/tests/test-commonjs/src/api/user.ts b/tests/test-commonjs/src/api/user.ts index 6b744a0..61433a4 100644 --- a/tests/test-commonjs/src/api/user.ts +++ b/tests/test-commonjs/src/api/user.ts @@ -23,9 +23,7 @@ export async function getCurrentUser(): Promise { const jq = xhr as JQuery.jqXHR; const msg = jq.statusText || 'request failed'; - - throw new Error( - `auth/info ${jq.status || '?'} (${internalPageName}): ${msg}`, - ); + + throw new Error(`auth/info ${jq.status || '?'} (${internalPageName}): ${msg}`); } } diff --git a/tests/test-commonjs/src/constants.ts b/tests/test-commonjs/src/constants.ts index 3531ff5..ead95d5 100644 --- a/tests/test-commonjs/src/constants.ts +++ b/tests/test-commonjs/src/constants.ts @@ -1 +1 @@ -export const internalPageName = window.location.pathname.replace(/\/p[tl]?\/([^/]+)\/?.*/, '$1'); \ No newline at end of file +export const internalPageName = window.location.pathname.replace(/\/p[tl]?\/([^/]+)\/?.*/, '$1'); diff --git a/tests/test-commonjs/src/main.ts b/tests/test-commonjs/src/main.ts index e5e25ce..01ca393 100644 --- a/tests/test-commonjs/src/main.ts +++ b/tests/test-commonjs/src/main.ts @@ -23,9 +23,7 @@ function setUserLine(text: string, loading: boolean): void { } $(() => { - $('#public-env').text( - `VITE_PUBLIC_TEST: ${import.meta.env.VITE_PUBLIC_TEST ?? 'No test variable'}`, - ); + $('#public-env').text(`VITE_PUBLIC_TEST: ${import.meta.env.VITE_PUBLIC_TEST ?? 'No test variable'}`); $('#helpers-html-root').append(htmlPart); @@ -33,15 +31,9 @@ $(() => { void getCurrentUser() .then((user) => { - const name = [user?.first_name, user?.last_name] - .filter(Boolean) - .join(' ') - .trim(); - - setUserLine( - name ? `Current user: ${name}` : 'Current user: (signed out or empty)', - false, - ); + const name = [user?.first_name, user?.last_name].filter(Boolean).join(' ').trim(); + + setUserLine(name ? `Current user: ${name}` : 'Current user: (signed out or empty)', false); }) .catch((err: unknown) => { const detail = err instanceof Error ? err.message : String(err); diff --git a/tests/test-nextjs-cjs/src/api/entity.ts b/tests/test-nextjs-cjs/src/api/entity.ts index 649deb9..02ef53f 100644 --- a/tests/test-nextjs-cjs/src/api/entity.ts +++ b/tests/test-nextjs-cjs/src/api/entity.ts @@ -10,6 +10,6 @@ export async function getEntities(): Promise { }); const body = await res.json(); - + return (body as { data?: unknown }).data; } diff --git a/tests/test-nextjs-cjs/src/api/user.ts b/tests/test-nextjs-cjs/src/api/user.ts index 14b93de..6d771dd 100644 --- a/tests/test-nextjs-cjs/src/api/user.ts +++ b/tests/test-nextjs-cjs/src/api/user.ts @@ -1,5 +1,5 @@ export async function getCurrentUser() { - return await fetch('/data/page/index/auth/info', { headers: { accept: 'application/json' } }).then(async (res) => - (await res.json()).user, - ); -} \ No newline at end of file + return await fetch('/data/page/index/auth/info', { + headers: { accept: 'application/json' }, + }).then(async (res) => (await res.json()).user); +} diff --git a/tests/test-nextjs-cjs/src/constants.ts b/tests/test-nextjs-cjs/src/constants.ts index 2916b95..4f64bab 100644 --- a/tests/test-nextjs-cjs/src/constants.ts +++ b/tests/test-nextjs-cjs/src/constants.ts @@ -2,6 +2,6 @@ export function getInternalPageName(): string { if (typeof window === 'undefined') { return ''; } - + return window.location.pathname.replace(/\/p[tl]?\/([^/]+)\/?.*/, '$1'); } diff --git a/tests/test-nextjs-cjs/src/pages/index.tsx b/tests/test-nextjs-cjs/src/pages/index.tsx index 5e1f047..6087a87 100644 --- a/tests/test-nextjs-cjs/src/pages/index.tsx +++ b/tests/test-nextjs-cjs/src/pages/index.tsx @@ -8,7 +8,10 @@ import { getDatasetData } from '@/api/dataset-data'; import { getEntities } from '@/api/entity'; export default function Home() { - const [user, setUser] = useState<{ first_name: string; last_name: string } | null>(null); + const [user, setUser] = useState<{ + first_name: string; + last_name: string; + } | null>(null); const [datasetData, setDatasetData] = useState(null); useEffect(() => { diff --git a/tests/test-nextjs-cjs/src/styles/global.scss b/tests/test-nextjs-cjs/src/styles/global.scss index 2b602f6..89513ef 100644 --- a/tests/test-nextjs-cjs/src/styles/global.scss +++ b/tests/test-nextjs-cjs/src/styles/global.scss @@ -14,10 +14,5 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); } diff --git a/tests/test-nextjs-cjs/src/styles/tailwind.css b/tests/test-nextjs-cjs/src/styles/tailwind.css index e1c3572..5ac3fdc 100644 --- a/tests/test-nextjs-cjs/src/styles/tailwind.css +++ b/tests/test-nextjs-cjs/src/styles/tailwind.css @@ -1,2 +1,2 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @config "../../tailwind.config.ts"; diff --git a/tests/test-nextjs-cjs/tsconfig.json b/tests/test-nextjs-cjs/tsconfig.json index 6c342dd..7a565bf 100644 --- a/tests/test-nextjs-cjs/tsconfig.json +++ b/tests/test-nextjs-cjs/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": [ @@ -36,8 +30,5 @@ "dist/types/**/*.ts", "dist/dev/types/**/*.ts" ], - "exclude": [ - "node_modules", - "dist" - ] + "exclude": ["node_modules", "dist"] } diff --git a/tests/test-nextjs/custom-server.ts b/tests/test-nextjs/custom-server.ts index c4d059c..64f90b6 100644 --- a/tests/test-nextjs/custom-server.ts +++ b/tests/test-nextjs/custom-server.ts @@ -1,17 +1,17 @@ -import { createServer } from "http"; -import { parse } from "url"; -import next from "next"; -import type { Socket } from "net"; -import { dirname } from "path"; -import { fileURLToPath } from "url"; - -const dev = process.env.NODE_ENV !== "production"; -const hostname = "localhost"; +import { createServer } from 'http'; +import { parse } from 'url'; +import next from 'next'; +import type { Socket } from 'net'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const dev = process.env.NODE_ENV !== 'production'; +const hostname = 'localhost'; const port = 3000; // Simple configuration without pp-dev -const templateName = "test-nextjs"; -const basePath = "/p/test-nextjs"; // Simple base path +const templateName = 'test-nextjs'; +const basePath = '/p/test-nextjs'; // Simple base path const projectRoot = dirname(fileURLToPath(import.meta.url)); @@ -44,14 +44,14 @@ async function startServer() { // Get the request handler const handle = app.getRequestHandler(); - console.log("āœ… Next.js app prepared successfully"); + console.log('āœ… Next.js app prepared successfully'); console.log(`šŸ”§ Template: ${templateName}`); console.log(`šŸ”§ Base path: ${basePath}`); // Create HTTP server server = createServer(async (req, res) => { try { - const originalUrl = req.url || "/"; + const originalUrl = req.url || '/'; const originalPathname = originalUrl; let parsedUrl = parse(originalUrl, true); @@ -62,27 +62,23 @@ async function startServer() { // Strip the base path for Next.js const nextPath = originalPathname.substring(basePath.length); - req.url = nextPath || "/"; + req.url = nextPath || '/'; parsedUrl = parse(nextPath, true); - console.log( - `[DEBUG] Base path request - Original: ${originalPathname}, Stripped: ${req.url}` - ); - } else if (originalPathname === basePath.replace(/\/$/, "")) { + console.log(`[DEBUG] Base path request - Original: ${originalPathname}, Stripped: ${req.url}`); + } else if (originalPathname === basePath.replace(/\/$/, '')) { // Handle base path without trailing slash - req.url = "/"; - parsedUrl = parse("/", true); - console.log( - `[DEBUG] Base path without slash - Redirected to: ${req.url}` - ); + req.url = '/'; + parsedUrl = parse('/', true); + console.log(`[DEBUG] Base path without slash - Redirected to: ${req.url}`); } else if ( - originalPathname.startsWith("/_next/") || - originalPathname === "/favicon.ico" || - originalPathname.startsWith("/__nextjs_") + originalPathname.startsWith('/_next/') || + originalPathname === '/favicon.ico' || + originalPathname.startsWith('/__nextjs_') ) { // Next.js internal routes - pass through as-is console.log(`[DEBUG] Next.js internal route: ${originalPathname}`); - } else if (originalPathname === "/") { + } else if (originalPathname === '/') { // Root path - this should redirect to base path console.log(`[DEBUG] Root path - redirecting to base path`); res.writeHead(302, { Location: basePath }); @@ -99,21 +95,19 @@ async function startServer() { } catch (error) { console.error(`[DEBUG] Error:`, error); res.statusCode = 500; - res.end("Internal Server Error"); + res.end('Internal Server Error'); } }); // Track open sockets for proper cleanup - server.on("connection", (socket: Socket) => { + server.on('connection', (socket: Socket) => { openSockets.add(socket); - socket.on("close", () => openSockets.delete(socket)); + socket.on('close', () => openSockets.delete(socket)); }); // Start the server server.listen(port, hostname, () => { - console.log( - `āœ… Custom Next.js server running at http://${hostname}:${port}` - ); + console.log(`āœ… Custom Next.js server running at http://${hostname}:${port}`); console.log(`šŸ“± App accessible at http://${hostname}:${port}${basePath}`); console.log(`šŸ”§ Base path: ${basePath}`); }); @@ -124,7 +118,7 @@ async function startServer() { // Set a timeout to force exit if shutdown hangs const shutdownTimeout = setTimeout(() => { - console.log("ā° Shutdown timeout reached, forcing exit"); + console.log('ā° Shutdown timeout reached, forcing exit'); process.exit(0); }, 5000); @@ -138,43 +132,43 @@ async function startServer() { // Stop accepting new connections and wait for server to close await new Promise((resolve) => { server.close(() => { - console.log("šŸ›‘ HTTP server closed"); + console.log('šŸ›‘ HTTP server closed'); resolve(); }); }); // Close the Next.js app properly - if (app && typeof app.close === "function") { + if (app && typeof app.close === 'function') { await app.close(); - console.log("šŸ›‘ Next.js app closed"); + console.log('šŸ›‘ Next.js app closed'); } clearTimeout(shutdownTimeout); - console.log("āœ… Graceful shutdown completed"); + console.log('āœ… Graceful shutdown completed'); process.exit(0); } catch (error) { clearTimeout(shutdownTimeout); - console.error("āŒ Error during graceful shutdown:", error); + console.error('āŒ Error during graceful shutdown:', error); process.exit(1); } }; // Handle process signals - process.on("SIGINT", gracefulShutdown); - process.on("SIGTERM", gracefulShutdown); + process.on('SIGINT', gracefulShutdown); + process.on('SIGTERM', gracefulShutdown); // Handle uncaught exceptions - process.on("uncaughtException", (error) => { - console.error("āŒ Uncaught Exception:", error); - gracefulShutdown("uncaughtException"); + process.on('uncaughtException', (error) => { + console.error('āŒ Uncaught Exception:', error); + gracefulShutdown('uncaughtException'); }); - process.on("unhandledRejection", (reason, promise) => { - console.error("āŒ Unhandled Rejection at:", promise, "reason:", reason); - gracefulShutdown("unhandledRejection"); + process.on('unhandledRejection', (reason, promise) => { + console.error('āŒ Unhandled Rejection at:', promise, 'reason:', reason); + gracefulShutdown('unhandledRejection'); }); } catch (error) { - console.error("Failed to start server:", error); + console.error('Failed to start server:', error); process.exit(1); } } diff --git a/tests/test-nextjs/src/api/entity.ts b/tests/test-nextjs/src/api/entity.ts index 649deb9..02ef53f 100644 --- a/tests/test-nextjs/src/api/entity.ts +++ b/tests/test-nextjs/src/api/entity.ts @@ -10,6 +10,6 @@ export async function getEntities(): Promise { }); const body = await res.json(); - + return (body as { data?: unknown }).data; } diff --git a/tests/test-nextjs/src/api/user.ts b/tests/test-nextjs/src/api/user.ts index 14b93de..6d771dd 100644 --- a/tests/test-nextjs/src/api/user.ts +++ b/tests/test-nextjs/src/api/user.ts @@ -1,5 +1,5 @@ export async function getCurrentUser() { - return await fetch('/data/page/index/auth/info', { headers: { accept: 'application/json' } }).then(async (res) => - (await res.json()).user, - ); -} \ No newline at end of file + return await fetch('/data/page/index/auth/info', { + headers: { accept: 'application/json' }, + }).then(async (res) => (await res.json()).user); +} diff --git a/tests/test-nextjs/src/constants.ts b/tests/test-nextjs/src/constants.ts index 2916b95..4f64bab 100644 --- a/tests/test-nextjs/src/constants.ts +++ b/tests/test-nextjs/src/constants.ts @@ -2,6 +2,6 @@ export function getInternalPageName(): string { if (typeof window === 'undefined') { return ''; } - + return window.location.pathname.replace(/\/p[tl]?\/([^/]+)\/?.*/, '$1'); } diff --git a/tests/test-nextjs/src/pages/_document.tsx b/tests/test-nextjs/src/pages/_document.tsx index 2571f4c..fc490bb 100644 --- a/tests/test-nextjs/src/pages/_document.tsx +++ b/tests/test-nextjs/src/pages/_document.tsx @@ -23,9 +23,7 @@ export default function Document() { }`, }} > -
- {process.env.NEXT_PUBLIC_TEST ?? 'No test variable'} -
+
{process.env.NEXT_PUBLIC_TEST ?? 'No test variable'}
diff --git a/tests/test-nextjs/src/pages/index.tsx b/tests/test-nextjs/src/pages/index.tsx index e16c49f..6087a87 100644 --- a/tests/test-nextjs/src/pages/index.tsx +++ b/tests/test-nextjs/src/pages/index.tsx @@ -8,9 +8,12 @@ import { getDatasetData } from '@/api/dataset-data'; import { getEntities } from '@/api/entity'; export default function Home() { - const [user, setUser] = useState<{ first_name: string; last_name: string } | null>(null); + const [user, setUser] = useState<{ + first_name: string; + last_name: string; + } | null>(null); const [datasetData, setDatasetData] = useState(null); - + useEffect(() => { if (!user) { getCurrentUser().then((user) => { diff --git a/tests/test-nextjs/src/styles/global.scss b/tests/test-nextjs/src/styles/global.scss index 2b602f6..89513ef 100644 --- a/tests/test-nextjs/src/styles/global.scss +++ b/tests/test-nextjs/src/styles/global.scss @@ -14,10 +14,5 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); } diff --git a/tests/test-nextjs/src/styles/tailwind.css b/tests/test-nextjs/src/styles/tailwind.css index e1c3572..5ac3fdc 100644 --- a/tests/test-nextjs/src/styles/tailwind.css +++ b/tests/test-nextjs/src/styles/tailwind.css @@ -1,2 +1,2 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @config "../../tailwind.config.ts"; diff --git a/tests/test-nextjs/tsconfig.json b/tests/test-nextjs/tsconfig.json index c2ef0ab..88220b3 100644 --- a/tests/test-nextjs/tsconfig.json +++ b/tests/test-nextjs/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": [ @@ -37,7 +31,5 @@ "dist/dev/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 6bd107e..d86a79f 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -18,14 +18,6 @@ }, "types": ["node"] }, - "include": [ - "**/*.ts", - "**/*.spec.ts" - ], - "exclude": [ - "node_modules", - "test-commonjs", - "test-nextjs", - "test-nextjs-cjs" - ] + "include": ["**/*.ts", "**/*.spec.ts"], + "exclude": ["node_modules", "test-commonjs", "test-nextjs", "test-nextjs-cjs"] } diff --git a/tests/unit/config/config.loader.spec.ts b/tests/unit/config/config.loader.spec.ts index bf849ce..98c876b 100644 --- a/tests/unit/config/config.loader.spec.ts +++ b/tests/unit/config/config.loader.spec.ts @@ -54,7 +54,7 @@ describe('Config Loader', () => { backendBaseURL: 'https://pkg.example.com', portalPageId: 456, }, - }) + }), ); const { getConfig, clearConfigCache } = await import('../../../src/config.js'); @@ -73,7 +73,7 @@ describe('Config Loader', () => { JSON.stringify({ baseURL: 'https://watch.example.com', portalPageId: 789, - }) + }), ); const { getConfig, clearConfigCache } = await import('../../../src/config.js'); @@ -93,7 +93,7 @@ describe('Config Loader', () => { backendBaseURL: 'https://json.example.com', portalPageId: 111, templateLess: true, - }) + }), ); const { getConfig, clearConfigCache } = await import('../../../src/config.js'); @@ -115,14 +115,14 @@ describe('Config Loader', () => { JSON.stringify({ backendBaseURL: 'https://pp-dev.example.com', portalPageId: 100, - }) + }), ); writeFileSync( join(testDir, '.pp-watch.config.json'), JSON.stringify({ baseURL: 'https://pp-watch.example.com', portalPageId: 200, - }) + }), ); const { getConfig, clearConfigCache } = await import('../../../src/config.js'); @@ -144,14 +144,14 @@ describe('Config Loader', () => { backendBaseURL: 'https://pkg.example.com', portalPageId: 1, }, - }) + }), ); writeFileSync( join(testDir, 'pp-dev.config.json'), JSON.stringify({ backendBaseURL: 'https://file.example.com', portalPageId: 2, - }) + }), ); const { getConfig, clearConfigCache } = await import('../../../src/config.js'); @@ -173,7 +173,7 @@ describe('Config Loader', () => { name: 'test-package', version: '1.0.0', description: 'Test package', - }) + }), ); const { getPkg, clearConfigCache } = await import('../../../src/config.js'); @@ -199,10 +199,7 @@ describe('Config Loader', () => { describe('Config Caching', () => { it('should cache package.json reads', async () => { - writeFileSync( - join(testDir, 'package.json'), - JSON.stringify({ name: 'test', version: '1.0.0' }) - ); + writeFileSync(join(testDir, 'package.json'), JSON.stringify({ name: 'test', version: '1.0.0' })); const { getPkg, clearConfigCache, getConfigCacheStats } = await import('../../../src/config.js'); clearConfigCache(); diff --git a/tests/unit/helpers/formatting.spec.ts b/tests/unit/helpers/formatting.spec.ts index a22f84a..8d999f5 100644 --- a/tests/unit/helpers/formatting.spec.ts +++ b/tests/unit/helpers/formatting.spec.ts @@ -72,7 +72,9 @@ line2`; it('should work with CSS content', () => { const color = 'red'; - const result = css`.container { color: ${color}; }`; + const result = + // prettier-ignore + css`.container { color: ${color}; }`; expect(result).toBe('.container { color: red; }'); }); diff --git a/tests/unit/lib/content-encoding.helper.spec.ts b/tests/unit/lib/content-encoding.helper.spec.ts index 40b9d6c..c3a1b03 100644 --- a/tests/unit/lib/content-encoding.helper.spec.ts +++ b/tests/unit/lib/content-encoding.helper.spec.ts @@ -1,9 +1,6 @@ import { describe, it, expect } from 'vitest'; import { gzipSync, brotliCompressSync, deflateSync, inflateSync } from 'zlib'; -import { - decodeContent, - encodeContent, -} from '../../../src/lib/helpers/content-encoding.helper.js'; +import { decodeContent, encodeContent } from '../../../src/lib/helpers/content-encoding.helper.js'; describe('Content Encoding Helper', () => { const plainContent = Buffer.from('Hello, World!', 'utf-8'); diff --git a/tests/unit/lib/token.helper.spec.ts b/tests/unit/lib/token.helper.spec.ts index c31cdf2..fae2035 100644 --- a/tests/unit/lib/token.helper.spec.ts +++ b/tests/unit/lib/token.helper.spec.ts @@ -232,9 +232,7 @@ describe('Token Helper', () => { expect(mockLogger.error).toHaveBeenCalled(); const calls = mockLogger.error.mock.calls; - expect(calls.some((c) => c[0].includes('Unauthorized access'))).toBe( - true, - ); + expect(calls.some((c) => c[0].includes('Unauthorized access'))).toBe(true); }); it('should include context prefix when provided', () => { diff --git a/tests/unit/lib/url.helper.spec.ts b/tests/unit/lib/url.helper.spec.ts index 125c777..9645250 100644 --- a/tests/unit/lib/url.helper.spec.ts +++ b/tests/unit/lib/url.helper.spec.ts @@ -1,9 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { - redirect, - rewriteDataPagePathForV7Proxy, - urlReplacer, -} from '../../../src/lib/helpers/url.helper.js'; +import { redirect, rewriteDataPagePathForV7Proxy, urlReplacer } from '../../../src/lib/helpers/url.helper.js'; describe('URL Helper', () => { describe('urlReplacer', () => { @@ -31,31 +27,19 @@ describe('URL Helper', () => { }); it('should handle HTTP URLs', () => { - const result = urlReplacer( - 'backend.com', - 'localhost:5000', - 'Link' - ); + const result = urlReplacer('backend.com', 'localhost:5000', 'Link'); expect(result).toContain('localhost:5000/path'); }); it('should handle HTTPS URLs', () => { - const result = urlReplacer( - 'backend.com', - 'localhost:5000', - 'Link' - ); + const result = urlReplacer('backend.com', 'localhost:5000', 'Link'); expect(result).toContain('localhost:5000/path'); }); it('should preserve URLs from different hosts', () => { - const result = urlReplacer( - 'api.example.com', - 'localhost:3000', - 'Link' - ); + const result = urlReplacer('api.example.com', 'localhost:3000', 'Link'); expect(result).toContain('https://other.com/path'); }); @@ -87,57 +71,36 @@ describe('URL Helper', () => { describe('rewriteDataPagePathForV7Proxy', () => { it('should rewrite first /data/page/ segment when v7 and names differ', () => { - expect( - rewriteDataPagePathForV7Proxy( - '/data/page/npm-name/auth/info', - true, - 'npm-name', - 'real-internal', - ), - ).toBe('/data/page/real-internal/auth/info'); + expect(rewriteDataPagePathForV7Proxy('/data/page/npm-name/auth/info', true, 'npm-name', 'real-internal')).toBe( + '/data/page/real-internal/auth/info', + ); }); it('should preserve query string', () => { - expect( - rewriteDataPagePathForV7Proxy( - '/data/page/npm-name/x?a=1', - true, - 'npm-name', - 'real-internal', - ), - ).toBe('/data/page/real-internal/x?a=1'); + expect(rewriteDataPagePathForV7Proxy('/data/page/npm-name/x?a=1', true, 'npm-name', 'real-internal')).toBe( + '/data/page/real-internal/x?a=1', + ); }); it('should not rewrite when v7Features is false', () => { const url = '/data/page/npm-name/x'; - expect( - rewriteDataPagePathForV7Proxy(url, false, 'npm-name', 'real-internal'), - ).toBe(url); + expect(rewriteDataPagePathForV7Proxy(url, false, 'npm-name', 'real-internal')).toBe(url); }); it('should not rewrite when template and internal name match', () => { const url = '/data/page/same/x'; - expect(rewriteDataPagePathForV7Proxy(url, true, 'same', 'same')).toBe( - url, - ); + expect(rewriteDataPagePathForV7Proxy(url, true, 'same', 'same')).toBe(url); }); it('should not rewrite when path segment does not match templateName', () => { const url = '/data/page/index/auth/info'; - expect( - rewriteDataPagePathForV7Proxy(url, true, 'my-app', 'internal-slug'), - ).toBe(url); + expect(rewriteDataPagePathForV7Proxy(url, true, 'my-app', 'internal-slug')).toBe(url); }); it('should match URL-encoded segment to templateName', () => { - expect( - rewriteDataPagePathForV7Proxy( - '/data/page/foo%20bar/x', - true, - 'foo bar', - 'internal', - ), - ).toBe('/data/page/internal/x'); + expect(rewriteDataPagePathForV7Proxy('/data/page/foo%20bar/x', true, 'foo bar', 'internal')).toBe( + '/data/page/internal/x', + ); }); }); diff --git a/tests/unit/plugin/plugin.normalize.spec.ts b/tests/unit/plugin/plugin.normalize.spec.ts index b846e63..4874821 100644 --- a/tests/unit/plugin/plugin.normalize.spec.ts +++ b/tests/unit/plugin/plugin.normalize.spec.ts @@ -15,15 +15,11 @@ describe('normalizeVitePPDevConfig', () => { describe('Required Fields', () => { it('should throw error for missing templateName', () => { - expect(() => normalizeVitePPDevConfig({} as VitePPDevOptions)).toThrow( - 'templateName must be a non-empty string' - ); + expect(() => normalizeVitePPDevConfig({} as VitePPDevOptions)).toThrow('templateName must be a non-empty string'); }); it('should throw error for empty templateName', () => { - expect(() => normalizeVitePPDevConfig({ templateName: '' })).toThrow( - 'templateName must be a non-empty string' - ); + expect(() => normalizeVitePPDevConfig({ templateName: '' })).toThrow('templateName must be a non-empty string'); }); it('should accept valid templateName', () => { @@ -297,7 +293,7 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', backendBaseURL: '', - }) + }), ).toThrow('backendBaseURL must be a non-empty string if provided'); }); @@ -306,7 +302,7 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', portalPageId: -1, - }) + }), ).toThrow('portalPageId must be a positive number if provided'); }); @@ -315,7 +311,7 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', appId: 0, - }) + }), ).toThrow('appId must be a positive number if provided'); }); @@ -324,7 +320,7 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', proxyCacheTTL: -100, - }) + }), ).toThrow('proxyCacheTTL must be a positive number if provided'); }); @@ -334,8 +330,10 @@ describe('normalizeVitePPDevConfig', () => { templateName: 'test', integrateMiTopBar: true, miHudLess: false, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar object has addRootElement true but miHudLess is false', () => { @@ -344,8 +342,10 @@ describe('normalizeVitePPDevConfig', () => { templateName: 'test', integrateMiTopBar: { addRootElement: true }, miHudLess: false, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar object has addSharedComponentsScripts true but miHudLess is false', () => { @@ -354,8 +354,10 @@ describe('normalizeVitePPDevConfig', () => { templateName: 'test', integrateMiTopBar: { addSharedComponentsScripts: true }, miHudLess: false, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar object has both properties true but miHudLess is false', () => { @@ -367,8 +369,10 @@ describe('normalizeVitePPDevConfig', () => { addSharedComponentsScripts: true, }, miHudLess: false, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar is invalid type (string)', () => { @@ -376,8 +380,10 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', integrateMiTopBar: 'invalid' as any, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar is invalid type (number)', () => { @@ -385,8 +391,10 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', integrateMiTopBar: 123 as any, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar object has invalid addRootElement type', () => { @@ -394,8 +402,10 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', integrateMiTopBar: { addRootElement: 'invalid' as any }, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar object has invalid addSharedComponentsScripts type', () => { @@ -403,8 +413,10 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', integrateMiTopBar: { addSharedComponentsScripts: 123 as any }, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should throw error when integrateMiTopBar is null', () => { @@ -412,8 +424,10 @@ describe('normalizeVitePPDevConfig', () => { normalizeVitePPDevConfig({ templateName: 'test', integrateMiTopBar: null as any, - }) - ).toThrow('VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans'); + }), + ).toThrow( + 'VitePPDevOptions.integrateMiTopBar must be a boolean or an object with addRootElement and addSharedComponentsScripts booleans', + ); }); it('should allow integrateMiTopBar: true when miHudLess is true', () => { @@ -456,7 +470,9 @@ describe('normalizeVitePPDevConfig', () => { miHudLess: true, }); - expect(config.integrateMiTopBar).toEqual({ addSharedComponentsScripts: true }); + expect(config.integrateMiTopBar).toEqual({ + addSharedComponentsScripts: true, + }); expect(config.miHudLess).toBe(true); }); @@ -523,7 +539,9 @@ describe('normalizeVitePPDevConfig', () => { miHudLess: false, }); - expect(config.integrateMiTopBar).toEqual({ addSharedComponentsScripts: false }); + expect(config.integrateMiTopBar).toEqual({ + addSharedComponentsScripts: false, + }); expect(config.miHudLess).toBe(false); }); }); diff --git a/tests/unit/plugin/version-plugin.spec.ts b/tests/unit/plugin/version-plugin.spec.ts index 5cec0da..fd418be 100644 --- a/tests/unit/plugin/version-plugin.spec.ts +++ b/tests/unit/plugin/version-plugin.spec.ts @@ -1,12 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { - existsSync, - mkdtempSync, - mkdirSync, - readFileSync, - rmSync, - writeFileSync, -} from 'fs'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { createHash } from 'crypto'; @@ -64,25 +57,33 @@ describe('versionPlugin', () => { const versionFile = join(outDir, 'VERSION-v1.2.3-'); const files = require('fs').readdirSync(outDir); - const versionFilename = files.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const buildManifestFilename = files.find((f: string) => f === 'BUILD-MANIFEST.json'); expect(versionFilename).toBeDefined(); + expect(buildManifestFilename).toBeDefined(); - const manifest = JSON.parse( - readFileSync(join(outDir, versionFilename!), 'utf-8'), - ); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); + const buildManifest = JSON.parse(readFileSync(join(outDir, buildManifestFilename!), 'utf-8')); + expect(manifest.schemaVersion).toBe(1); expect(manifest.version).toBe('v1.2.3'); - expect(manifest.date).toMatch( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, - ); + expect(manifest.date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); expect(manifest.checksum).toMatch(/^[a-f0-9]{64}$/); expect(manifest.files).toHaveProperty('index.html'); expect(manifest.files).toHaveProperty('asset.js'); expect(manifest.files['index.html']).toMatch(/^[a-f0-9]{64}$/); expect(manifest.files['asset.js']).toMatch(/^[a-f0-9]{64}$/); + expect(manifest.helperVersion).toBeDefined(); + + expect(buildManifest.schemaVersion).toBe(1); + expect(buildManifest.manifestType).toBe('pp-dev-build-manifest'); + expect(buildManifest.generatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(buildManifest.versionFile).toBe(versionFilename); + expect(buildManifest.versionFileTemplate).toBe('VERSION-v{packageversion}-{currentDate}.json'); + expect(buildManifest.versionFileSchemaVersion).toBe(1); + expect(buildManifest.buildFingerprint).toMatch(/^[a-f0-9]{64}$/); + expect(buildManifest.compat).toEqual({ versionFileRequired: true }); }); it('should hash file content only (deterministic for same content)', () => { @@ -92,21 +93,15 @@ describe('versionPlugin', () => { writeFileSync(join(outDir, 'a.txt'), content, 'utf-8'); writeFileSync(join(outDir, 'b.txt'), content, 'utf-8'); - const expectedHash = createHash('sha256') - .update(content, 'utf-8') - .digest('hex'); + const expectedHash = createHash('sha256').update(content, 'utf-8').digest('hex'); const plugin = versionPlugin({ outDir, packageVersion: '1.0.0' }); invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const versionFilename = files.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); - const manifest = JSON.parse( - readFileSync(join(outDir, versionFilename!), 'utf-8'), - ); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); expect(manifest.files['a.txt']).toBe(expectedHash); expect(manifest.files['b.txt']).toBe(expectedHash); @@ -123,23 +118,15 @@ describe('versionPlugin', () => { invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const versionFilename = files.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); - const manifest1 = JSON.parse( - readFileSync(join(outDir, versionFilename!), 'utf-8'), - ); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest1 = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); rmSync(join(outDir, versionFilename!)); invokeCloseBundle(plugin); const filesAfter = require('fs').readdirSync(outDir); - const versionFilename2 = filesAfter.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); - const manifest2 = JSON.parse( - readFileSync(join(outDir, versionFilename2!), 'utf-8'), - ); + const versionFilename2 = filesAfter.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest2 = JSON.parse(readFileSync(join(outDir, versionFilename2!), 'utf-8')); expect(manifest1.checksum).toBe(manifest2.checksum); }); @@ -159,16 +146,77 @@ describe('versionPlugin', () => { invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const manifestFilename = files.find( - (f: string) => - f.startsWith('manifest-2.0.0-beta-') && f.endsWith('.json'), - ); + const manifestFilename = files.find((f: string) => f.startsWith('manifest-2.0.0-beta-') && f.endsWith('.json')); expect(manifestFilename).toBeDefined(); - const manifest = JSON.parse( - readFileSync(join(outDir, manifestFilename!), 'utf-8'), - ); + const manifest = JSON.parse(readFileSync(join(outDir, manifestFilename!), 'utf-8')); + const buildManifest = JSON.parse(readFileSync(join(outDir, 'BUILD-MANIFEST.json'), 'utf-8')); expect(manifest.version).toBe('v2.0.0-beta'); + expect(buildManifest.versionFile).toBe(manifestFilename); + expect(buildManifest.versionFileTemplate).toBe('manifest-{packageversion}-{currentDate}.json'); + }); + + it('should include normalized repository URL when provided', () => { + const outDir = join(tempDir, 'dist'); + + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, 'index.html'), 'test', 'utf-8'); + + const plugin = versionPlugin({ + outDir, + packageVersion: '3.0.0', + packageRepositoryUrl: 'git+https://github.com/example/template.git', + }); + + invokeCloseBundle(plugin); + + const files = require('fs').readdirSync(outDir); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); + + expect(manifest.repositoryUrl).toBe('https://github.com/example/template'); + }); + + it('should convert SSH repository URL to HTTPS when possible', () => { + const outDir = join(tempDir, 'dist'); + + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, 'index.html'), 'test', 'utf-8'); + + const plugin = versionPlugin({ + outDir, + packageVersion: '3.0.0', + packageRepositoryUrl: 'git@github.com:example/template.git', + }); + + invokeCloseBundle(plugin); + + const files = require('fs').readdirSync(outDir); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); + + expect(manifest.repositoryUrl).toBe('https://github.com/example/template'); + }); + + it('should include branch name when provided', () => { + const outDir = join(tempDir, 'dist'); + + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, 'index.html'), 'test', 'utf-8'); + + const plugin = versionPlugin({ + outDir, + packageVersion: '3.0.0', + packageBranchName: 'feature/version-metadata', + }); + + invokeCloseBundle(plugin); + + const files = require('fs').readdirSync(outDir); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); + + expect(manifest.branchName).toBe('feature/version-metadata'); }); it('should not run when enabled is false', () => { @@ -185,9 +233,7 @@ describe('versionPlugin', () => { invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const versionFile = files.find( - (f: string) => f.includes('VERSION') || f.includes('manifest'), - ); + const versionFile = files.find((f: string) => f.includes('VERSION') || f.includes('manifest')); expect(versionFile).toBeUndefined(); }); @@ -198,23 +244,15 @@ describe('versionPlugin', () => { mkdirSync(join(outDir, 'assets', 'nested'), { recursive: true }); writeFileSync(join(outDir, 'index.html'), 'x', 'utf-8'); writeFileSync(join(outDir, 'assets', 'style.css'), 'body{}', 'utf-8'); - writeFileSync( - join(outDir, 'assets', 'nested', 'script.js'), - '()=>{}', - 'utf-8', - ); + writeFileSync(join(outDir, 'assets', 'nested', 'script.js'), '()=>{}', 'utf-8'); const plugin = versionPlugin({ outDir, packageVersion: '1.0.0' }); invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const versionFilename = files.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); - const manifest = JSON.parse( - readFileSync(join(outDir, versionFilename!), 'utf-8'), - ); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); expect(manifest.files).toHaveProperty('index.html'); expect(manifest.files).toHaveProperty('assets/style.css'); @@ -232,12 +270,8 @@ describe('versionPlugin', () => { invokeCloseBundle(plugin); const files = require('fs').readdirSync(outDir); - const versionFilename = files.find( - (f: string) => f.startsWith('VERSION-') && f.endsWith('.json'), - ); - const manifest = JSON.parse( - readFileSync(join(outDir, versionFilename!), 'utf-8'), - ); + const versionFilename = files.find((f: string) => f.startsWith('VERSION-') && f.endsWith('.json')); + const manifest = JSON.parse(readFileSync(join(outDir, versionFilename!), 'utf-8')); expect(Object.keys(manifest.files)).not.toContain(versionFilename); expect(Object.keys(manifest.files)).toEqual(['only.txt']); From 01e73a7d6ba0556fa3a2ca00ebe5d34a377687a6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 21 Apr 2026 16:32:54 +0000 Subject: [PATCH 10/12] chore(release): 0.18.1-beta.1 [skip ci] ## [0.18.1-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.18.0...v0.18.1-beta.1) (2026-04-21) ### Bug Fixes * **cli:** harden shortcut cleanup and add dts trace logging ([4de05ed](https://github.com/mi-examples/pp-dev/commit/4de05ed2469933007aa654e370a84d90aecf3df3)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc643b..9ea293d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.18.1-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.18.0...v0.18.1-beta.1) (2026-04-21) + + +### Bug Fixes + +* **cli:** harden shortcut cleanup and add dts trace logging ([4de05ed](https://github.com/mi-examples/pp-dev/commit/4de05ed2469933007aa654e370a84d90aecf3df3)) + # [0.18.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.17.0...v0.18.0-beta.1) (2026-04-06) ### Features diff --git a/package-lock.json b/package-lock.json index 631155b..bec6afc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@metricinsights/pp-dev", - "version": "0.18.0-beta.1", + "version": "0.18.1-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@metricinsights/pp-dev", - "version": "0.18.0-beta.1", + "version": "0.18.1-beta.1", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 11f4def..1ea3a4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@metricinsights/pp-dev", "type": "module", - "version": "0.18.0-beta.1", + "version": "0.18.1-beta.1", "description": "Portal Page dev build tool", "bin": { "pp-dev": "bin/pp-dev.js" From 3aaaeb6a2a706f7f2ba6639c5ce87a320626eb32 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Wed, 22 Apr 2026 10:36:22 +0300 Subject: [PATCH 11/12] fix: harden sync prompt lifecycle and metadata safety Prevent unresolved sync actions and stale listeners while enforcing required VERSION manifest presence and stripping sensitive URL/replacement edge cases. --- src/cli.ts | 26 ++++++++------ src/client/index.ts | 56 +++++++++++++++++++++-------- src/lib/client.service.ts | 54 ++++++++++++++++++++++------ src/lib/dist.service.ts | 6 ++++ src/lib/pp.middleware.ts | 9 ++++- src/plugins/version-plugin.ts | 12 ++++--- src/shortcuts.ts | 12 ++++--- tests/test-nextjs/package-lock.json | 6 ++-- 8 files changed, 135 insertions(+), 46 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 5e425e5..2ca5590 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1256,16 +1256,22 @@ cli return; } - const latestBackup = backups - .filter((value) => { - return value.isFile() && value.name.endsWith('.zip'); - }) - .reduce((latest, current) => { - const latestTime = fs.statSync(path.resolve(backupsDirPath, latest.name)).mtimeMs; - const currentTime = fs.statSync(path.resolve(backupsDirPath, current.name)).mtimeMs; - - return latestTime > currentTime ? latest : current; - }, backups[0]).name; + const zipBackups = backups.filter((value) => { + return value.isFile() && value.name.endsWith('.zip'); + }); + + if (!zipBackups.length) { + createLogger(options.logLevel).warn(colors.yellow(`no ZIP backups found, skipping changelog generation`)); + + return; + } + + const latestBackup = zipBackups.reduce((latest, current) => { + const latestTime = fs.statSync(path.resolve(backupsDirPath, latest.name)).mtimeMs; + const currentTime = fs.statSync(path.resolve(backupsDirPath, current.name)).mtimeMs; + + return latestTime > currentTime ? latest : current; + }).name; oldAssetsPath = path.resolve(backupsDirPath, latestBackup); } diff --git a/src/client/index.ts b/src/client/index.ts index 7eb2b20..086c9ad 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -64,6 +64,25 @@ const POPUP_HEIGHT = 100; const ANIMATION_DURATION = 300; const CONFIRM_MODAL_OVERLAY_CLASS = 'pp-dev-info__confirm-overlay'; +const activeConfirmModals = new Map< + HTMLDivElement, + { resolve: (value: boolean) => void; onKeyDown: (event: KeyboardEvent) => void } +>(); + +function teardownConfirmModal(overlay: HTMLDivElement, result: boolean) { + const entry = activeConfirmModals.get(overlay); + + if (!entry) { + return; + } + + document.removeEventListener('keydown', entry.onKeyDown); + + activeConfirmModals.delete(overlay); + overlay.remove(); + entry.resolve(result); +} + function createPopupElement(opts: InfoPopupOptions): HTMLDivElement { const $popup = document.createElement('div'); @@ -220,13 +239,10 @@ function infoPopup(opts: InfoPopupOptions) { } } -function closeConfirmModalByResult(overlay: HTMLDivElement, resolve: (value: boolean) => void, value: boolean) { - overlay.remove(); - resolve(value); -} - function closeAllConfirmModals() { - document.querySelectorAll(`.${CONFIRM_MODAL_OVERLAY_CLASS}`).forEach((element) => element.remove()); + for (const overlay of [...activeConfirmModals.keys()]) { + teardownConfirmModal(overlay, false); + } } function confirmModal(opts: ConfirmModalOptions): Promise { @@ -234,50 +250,62 @@ function confirmModal(opts: ConfirmModalOptions): Promise { return new Promise((resolve) => { const $overlay = document.createElement('div'); + $overlay.classList.add('pp-dev-info-namespace', CONFIRM_MODAL_OVERLAY_CLASS); + const $confirm = document.createElement('div'); + $confirm.classList.add('pp-dev-info__confirm'); + const $title = document.createElement('div'); + $title.classList.add('pp-dev-info__confirm-title'); $title.textContent = opts.title; + const $content = document.createElement('div'); + $content.classList.add('pp-dev-info__confirm-content'); $content.textContent = opts.content; + const $actions = document.createElement('div'); + $actions.classList.add('pp-dev-info__confirm-actions'); + const $cancelButton = document.createElement('button'); + $cancelButton.type = 'button'; $cancelButton.classList.add('pp-dev-info__confirm-btn', 'pp-dev-info__confirm-btn--cancel'); $cancelButton.textContent = opts.cancelText; + const $confirmButton = document.createElement('button'); + $confirmButton.type = 'button'; $confirmButton.classList.add('pp-dev-info__confirm-btn', 'pp-dev-info__confirm-btn--confirm'); $confirmButton.textContent = opts.confirmText; + $actions.append($cancelButton, $confirmButton); $confirm.append($title, $content, $actions); $overlay.appendChild($confirm); const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - document.removeEventListener('keydown', onKeyDown); - closeConfirmModalByResult($overlay, resolve, false); + teardownConfirmModal($overlay, false); } }; + activeConfirmModals.set($overlay, { resolve, onKeyDown }); + $confirmButton.addEventListener('click', () => { - document.removeEventListener('keydown', onKeyDown); - closeConfirmModalByResult($overlay, resolve, true); + teardownConfirmModal($overlay, true); }); $cancelButton.addEventListener('click', () => { - document.removeEventListener('keydown', onKeyDown); - closeConfirmModalByResult($overlay, resolve, false); + teardownConfirmModal($overlay, false); }); $overlay.addEventListener('click', (event) => { if (event.target === $overlay) { - document.removeEventListener('keydown', onKeyDown); - closeConfirmModalByResult($overlay, resolve, false); + teardownConfirmModal($overlay, false); } }); diff --git a/src/lib/client.service.ts b/src/lib/client.service.ts index c1781ac..9e4cc6a 100644 --- a/src/lib/client.service.ts +++ b/src/lib/client.service.ts @@ -24,6 +24,12 @@ interface SyncActionResponsePayload { export interface ClientServiceOptions { distService?: DistService; miAPI?: MiAPI; + /** + * Max time to wait for the browser to respond to `template:sync:action-required`. + * After this, the pending promise resolves to `false` and the resolver is removed. + * @default 120_000 + */ + syncActionTimeoutMs?: number; } export class ClientService { @@ -32,7 +38,10 @@ export class ClientService { private readonly eventMap: Map void>; private logger: Logger; - private readonly syncActionResolvers: Map void> = new Map(); + private readonly syncActionResolvers = new Map< + string, + { resolve: (approved: boolean) => void; timeoutId: ReturnType } + >(); constructor(server: ViteDevServer, opts?: ClientServiceOptions) { this.server = server; @@ -55,6 +64,33 @@ export class ClientService { for (const [event, handler] of this.eventMap) { ws.on(event, handler); } + + ws.on('close', () => { + this.clearAllPendingSyncActions(false); + }); + + ws.on('error', () => { + this.clearAllPendingSyncActions(false); + }); + } + + private resolveSyncAction(requestId: string, approved: boolean) { + const entry = this.syncActionResolvers.get(requestId); + + if (!entry) { + return; + } + + clearTimeout(entry.timeoutId); + this.syncActionResolvers.delete(requestId); + entry.resolve(approved); + } + + /** Resolves every pending `requestSyncAction` promise and clears timeouts (e.g. WebSocket closed). */ + private clearAllPendingSyncActions(approved: boolean) { + for (const requestId of [...this.syncActionResolvers.keys()]) { + this.resolveSyncAction(requestId, approved); + } } onInfoDataRequest() { @@ -70,21 +106,19 @@ export class ClientService { return; } - const resolver = this.syncActionResolvers.get(payload.requestId); - - if (!resolver) { - return; - } - - this.syncActionResolvers.delete(payload.requestId); - resolver(payload.approved); + this.resolveSyncAction(payload.requestId, payload.approved); } async requestSyncAction(payload: Omit) { const requestId = randomUUID(); + const timeoutMs = this.opts.syncActionTimeoutMs ?? 120_000; return await new Promise((resolve) => { - this.syncActionResolvers.set(requestId, resolve); + const timeoutId = setTimeout(() => { + this.resolveSyncAction(requestId, false); + }, timeoutMs); + + this.syncActionResolvers.set(requestId, { resolve, timeoutId }); this.server.ws.send('template:sync:action-required', { ...payload, diff --git a/src/lib/dist.service.ts b/src/lib/dist.service.ts index dd3fdf6..2cb16ce 100644 --- a/src/lib/dist.service.ts +++ b/src/lib/dist.service.ts @@ -370,6 +370,12 @@ export class DistService { if (resolvedCandidates.length === 1) { versionManifestPath = resolvedCandidates[0]!; } + + if (buildManifest.compat?.versionFileRequired === true && versionManifestPath === null) { + throw new Error( + `Build manifest requires version file "${buildManifest.versionFile}", but it is missing in backup ZIP`, + ); + } } else { const versionFileMatcher = this.versionFileTemplateMatcher(); const versionManifestCandidates = relativePaths.filter((filePath) => diff --git a/src/lib/pp.middleware.ts b/src/lib/pp.middleware.ts index d9a4e86..009e759 100644 --- a/src/lib/pp.middleware.ts +++ b/src/lib/pp.middleware.ts @@ -152,6 +152,10 @@ export class MiAPI { return obj; } + #escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&'); + } + get personalAccessToken(): string | undefined { return this.#personalAccessToken; } @@ -474,7 +478,10 @@ export class MiAPI { let result = typeof content === 'string' ? content : content.toString('utf-8'); for (const v of this.#pageVars) { - result = result.replace(new RegExp(`\\[${v.name}\\]`, 'g'), v.value); + const escapedName = this.#escapeRegExp(v.name); + const pageVarRegex = new RegExp(`\\[${escapedName}\\]`, 'g'); + + result = result.replace(pageVarRegex, () => v.value); } const dom = new JSDOM(miHudLess ? result : this.#pageTemplate!); diff --git a/src/plugins/version-plugin.ts b/src/plugins/version-plugin.ts index 7e37e79..065e89e 100644 --- a/src/plugins/version-plugin.ts +++ b/src/plugins/version-plugin.ts @@ -86,6 +86,10 @@ function resolveVersionFileName(template: string, packageVersion: string, curren return template.replace(/\{packageversion\}/g, packageVersion).replace(/\{currentDate\}/g, currentDate); } +function stripUrlUserInfo(value: string): string { + return value.replace(/^([a-z][a-z\d+.-]*:\/\/)(?:[^/?#@]+(?::[^/?#@]*)?@)/i, '$1'); +} + function normalizeRepositoryUrl(url: string): string { const normalizedUrl = url .trim() @@ -96,7 +100,7 @@ function normalizeRepositoryUrl(url: string): string { if (scpLikeMatch) { const [, , host, repositoryPath] = scpLikeMatch; - return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + return stripUrlUserInfo(`https://${host}/${repositoryPath.replace(/^\/+/, '')}`); } const sshProtocolMatch = normalizedUrl.match(/^ssh:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+)$/i); @@ -104,7 +108,7 @@ function normalizeRepositoryUrl(url: string): string { if (sshProtocolMatch) { const [, host, repositoryPath] = sshProtocolMatch; - return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + return stripUrlUserInfo(`https://${host}/${repositoryPath.replace(/^\/+/, '')}`); } const gitProtocolMatch = normalizedUrl.match(/^git:\/\/([^/]+)\/(.+)$/i); @@ -112,10 +116,10 @@ function normalizeRepositoryUrl(url: string): string { if (gitProtocolMatch) { const [, host, repositoryPath] = gitProtocolMatch; - return `https://${host}/${repositoryPath.replace(/^\/+/, '')}`; + return stripUrlUserInfo(`https://${host}/${repositoryPath.replace(/^\/+/, '')}`); } - return normalizedUrl; + return stripUrlUserInfo(normalizedUrl); } function resolveHelperVersion(): string { diff --git a/src/shortcuts.ts b/src/shortcuts.ts index 1dfe0d1..3f80843 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -94,8 +94,14 @@ export function bindShortcuts(server: ViteDevServer, opts: BindShortcutsOptions) process.stdin.setRawMode(true); process.stdin.on('data', onInput).setEncoding('utf8').resume(); + const boundServer = server.httpServer; const cleanupBinding = () => { + if (cleanupActiveShortcutBinding !== cleanupBinding) { + return; + } + + boundServer.removeListener('close', cleanupBinding); process.stdin.off('data', onInput); if (!hadRawModeEnabled) { @@ -106,13 +112,11 @@ export function bindShortcuts(server: ViteDevServer, opts: BindShortcutsOptions) process.stdin.pause(); } - if (cleanupActiveShortcutBinding === cleanupBinding) { - cleanupActiveShortcutBinding = null; - } + cleanupActiveShortcutBinding = null; }; cleanupActiveShortcutBinding = cleanupBinding; - server.httpServer.on('close', cleanupBinding); + boundServer.on('close', cleanupBinding); } const BASE_SHORTCUTS: CLIShortcut[] = [ diff --git a/tests/test-nextjs/package-lock.json b/tests/test-nextjs/package-lock.json index 3ed9b58..84f7465 100644 --- a/tests/test-nextjs/package-lock.json +++ b/tests/test-nextjs/package-lock.json @@ -1461,9 +1461,9 @@ } }, "node_modules/@metricinsights/pp-dev": { - "version": "0.17.0-beta.1", + "version": "0.18.0-beta.1", "resolved": "file:../../metricinsights-pp-dev-latest.tgz", - "integrity": "sha512-nJbTMF46s3lLkg+nJ41kr3ShhV7Hh38fl4kOeAHbxBE2A3264OCNu50z+tri3toI4hbPkaTImYSsVQFkpbuiwQ==", + "integrity": "sha512-utJ8rWKnJcYcZuYO6RUQ74zUSrKN+rT6NndjdMan2B+GK3HeYtFmCmB1FNZq2/OTTOYA+6dXtjKtO44yQb0p5Q==", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -1491,7 +1491,7 @@ "svgo": "^4.0.1", "svgtofont": "^6.5.1", "typescript": "^6.0.2", - "vite": "^8.0.3", + "vite": "^8.0.5", "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-zip-pack": "^1.2.4", "winston": "^3.19.0" From 50c33599c3815b8f243f78004b25fb32bd67aa7d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 22 Apr 2026 07:47:26 +0000 Subject: [PATCH 12/12] chore(release): 0.18.1-beta.2 [skip ci] ## [0.18.1-beta.2](https://github.com/mi-examples/pp-dev/compare/v0.18.1-beta.1...v0.18.1-beta.2) (2026-04-22) ### Bug Fixes * harden sync prompt lifecycle and metadata safety ([3aaaeb6](https://github.com/mi-examples/pp-dev/commit/3aaaeb6a2a706f7f2ba6639c5ce87a320626eb32)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea293d..188d43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.18.1-beta.2](https://github.com/mi-examples/pp-dev/compare/v0.18.1-beta.1...v0.18.1-beta.2) (2026-04-22) + + +### Bug Fixes + +* harden sync prompt lifecycle and metadata safety ([3aaaeb6](https://github.com/mi-examples/pp-dev/commit/3aaaeb6a2a706f7f2ba6639c5ce87a320626eb32)) + ## [0.18.1-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.18.0...v0.18.1-beta.1) (2026-04-21) diff --git a/package-lock.json b/package-lock.json index bec6afc..90e45b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@metricinsights/pp-dev", - "version": "0.18.1-beta.1", + "version": "0.18.1-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@metricinsights/pp-dev", - "version": "0.18.1-beta.1", + "version": "0.18.1-beta.2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 1ea3a4e..ab2d5b5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@metricinsights/pp-dev", "type": "module", - "version": "0.18.1-beta.1", + "version": "0.18.1-beta.2", "description": "Portal Page dev build tool", "bin": { "pp-dev": "bin/pp-dev.js"