From 19f8557fe7da3323ad2749eb2c2907b09f9dc94e Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Wed, 4 Mar 2026 12:55:45 -0500 Subject: [PATCH 1/8] chore: upgrade rollup --- packages/pages/package.json | 2 +- pnpm-lock.yaml | 244 +++++++++++++++++++++++------------- 2 files changed, 157 insertions(+), 89 deletions(-) diff --git a/packages/pages/package.json b/packages/pages/package.json index 6d173dfc..0302ba44 100644 --- a/packages/pages/package.json +++ b/packages/pages/package.json @@ -76,7 +76,7 @@ "postcss-nested": "^6.0.1", "pretty-ms": "^9.0.0", "prompts": "^2.4.2", - "rollup": "^4.27.2", + "rollup": "^4.59.0", "ts-morph": "^22.0.0", "vite-plugin-node-polyfills": "0.17.0", "vitepress": "1.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31007700..7e81beac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,8 +141,8 @@ importers: specifier: ^18.2.0 || ^19.2.3 version: 19.2.4(react@19.2.4) rollup: - specifier: ^4.27.2 - version: 4.27.2 + specifier: ^4.59.0 + version: 4.59.0 ts-morph: specifier: ^22.0.0 version: 22.0.0 @@ -151,7 +151,7 @@ importers: version: 5.4.11(@types/node@20.11.28) vite-plugin-node-polyfills: specifier: 0.17.0 - version: 0.17.0(rollup@4.27.2)(vite@5.4.11(@types/node@20.11.28)) + version: 0.17.0(rollup@4.59.0)(vite@5.4.11(@types/node@20.11.28)) vitepress: specifier: 1.5.0 version: 1.5.0(@algolia/client-search@5.15.0)(@types/node@20.11.28)(@types/react@18.3.28)(postcss@8.4.35)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)(typescript@5.4.2) @@ -1210,93 +1210,128 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.27.2': - resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.27.2': - resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.27.2': - resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.27.2': - resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.27.2': - resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.27.2': - resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': - resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.27.2': - resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.27.2': - resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.27.2': - resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': - resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.27.2': - resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.27.2': - resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.27.2': - resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.27.2': - resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.27.2': - resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.27.2': - resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.27.2': - resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] @@ -1402,6 +1437,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.17.43': resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} @@ -3586,8 +3624,8 @@ packages: ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} - rollup@4.27.2: - resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5139,74 +5177,95 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@rollup/plugin-inject@5.0.5(rollup@4.27.2)': + '@rollup/plugin-inject@5.0.5(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.27.2) + '@rollup/pluginutils': 5.1.0(rollup@4.59.0) estree-walker: 2.0.2 magic-string: 0.30.7 optionalDependencies: - rollup: 4.27.2 + rollup: 4.59.0 - '@rollup/pluginutils@5.1.0(rollup@4.27.2)': + '@rollup/pluginutils@5.1.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.27.2 + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true - '@rollup/rollup-android-arm-eabi@4.27.2': + '@rollup/rollup-android-arm64@4.59.0': optional: true - '@rollup/rollup-android-arm64@4.27.2': + '@rollup/rollup-darwin-arm64@4.59.0': optional: true - '@rollup/rollup-darwin-arm64@4.27.2': + '@rollup/rollup-darwin-x64@4.59.0': optional: true - '@rollup/rollup-darwin-x64@4.27.2': + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true - '@rollup/rollup-freebsd-arm64@4.27.2': + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rollup/rollup-freebsd-x64@4.27.2': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.2': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.2': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.2': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.2': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.2': + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.27.2': + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-x64-musl@4.27.2': + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.27.2': + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.27.2': + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.27.2': + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true '@rushstack/node-core-library@5.9.0(@types/node@20.11.28)': @@ -5356,6 +5415,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.17.43': dependencies: '@types/node': 20.11.28 @@ -7820,28 +7881,35 @@ snapshots: hash-base: 3.1.0 inherits: 2.0.4 - rollup@4.27.2: + rollup@4.59.0: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.27.2 - '@rollup/rollup-android-arm64': 4.27.2 - '@rollup/rollup-darwin-arm64': 4.27.2 - '@rollup/rollup-darwin-x64': 4.27.2 - '@rollup/rollup-freebsd-arm64': 4.27.2 - '@rollup/rollup-freebsd-x64': 4.27.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.27.2 - '@rollup/rollup-linux-arm-musleabihf': 4.27.2 - '@rollup/rollup-linux-arm64-gnu': 4.27.2 - '@rollup/rollup-linux-arm64-musl': 4.27.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.27.2 - '@rollup/rollup-linux-riscv64-gnu': 4.27.2 - '@rollup/rollup-linux-s390x-gnu': 4.27.2 - '@rollup/rollup-linux-x64-gnu': 4.27.2 - '@rollup/rollup-linux-x64-musl': 4.27.2 - '@rollup/rollup-win32-arm64-msvc': 4.27.2 - '@rollup/rollup-win32-ia32-msvc': 4.27.2 - '@rollup/rollup-win32-x64-msvc': 4.27.2 + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 rrweb-cssom@0.6.0: {} @@ -8382,9 +8450,9 @@ snapshots: - supports-color - terser - vite-plugin-node-polyfills@0.17.0(rollup@4.27.2)(vite@5.4.11(@types/node@20.11.28)): + vite-plugin-node-polyfills@0.17.0(rollup@4.59.0)(vite@5.4.11(@types/node@20.11.28)): dependencies: - '@rollup/plugin-inject': 5.0.5(rollup@4.27.2) + '@rollup/plugin-inject': 5.0.5(rollup@4.59.0) buffer-polyfill: buffer@6.0.3 node-stdlib-browser: 1.2.0 process: 0.11.10 @@ -8396,7 +8464,7 @@ snapshots: dependencies: esbuild: 0.19.12 postcss: 8.4.35 - rollup: 4.27.2 + rollup: 4.59.0 optionalDependencies: '@types/node': 20.11.28 fsevents: 2.3.3 @@ -8405,7 +8473,7 @@ snapshots: dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.27.2 + rollup: 4.59.0 optionalDependencies: '@types/node': 20.11.28 fsevents: 2.3.3 From a606b8ed81babf80513da99bd02f0d3cdba617c3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:57:44 +0000 Subject: [PATCH 2/8] Automated update to THIRD-PARTY-NOTICES from github action's 3rd party notices check --- THIRD-PARTY-NOTICES | 59 +++++++++++++++++------------- packages/pages/THIRD-PARTY-NOTICES | 54 +++++++++++++++------------ 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 7871fb95..6e2d851b 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -406,6 +406,7 @@ Apache License The following npm packages may be included in this product: - @types/estree@1.0.6 + - @types/estree@1.0.8 - @types/hast@3.0.4 - @types/linkify-it@5.0.0 - @types/markdown-it@14.1.2 @@ -1772,7 +1773,7 @@ software or this license, under any kind of legal claim.*** The following npm package may be included in this product: - - rollup@4.27.2 + - rollup@4.59.0 This package contains the following license: @@ -1796,12 +1797,10 @@ MIT, ISC, 0BSD # Bundled dependencies: ## @jridgewell/sourcemap-codec License: MIT -By: Rich Harris -Repository: git+https://github.com/jridgewell/sourcemap-codec.git +By: Justin Ridgewell +Repository: git+https://github.com/jridgewell/sourcemaps.git -> The MIT License -> -> Copyright (c) 2015 Rich Harris +> Copyright 2024 Justin Ridgewell > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal @@ -1818,8 +1817,8 @@ Repository: git+https://github.com/jridgewell/sourcemap-codec.git > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -> THE SOFTWARE. +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. --------------------------------------- @@ -1968,21 +1967,6 @@ Repository: git+https://github.com/paulmillr/chokidar.git --------------------------------------- -## colorette -License: MIT -By: Jorge Bucaran -Repository: jorgebucaran/colorette - -> Copyright © Jorge Bucaran <> -> -> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------------------------------- - ## date-time License: MIT By: Sindre Sorhus @@ -2190,7 +2174,7 @@ Repository: git+https://gitlab.com/Rich-Harris/locate-character.git ## magic-string License: MIT By: Rich Harris -Repository: https://github.com/rich-harris/magic-string +Repository: git+https://github.com/Rich-Harris/magic-string.git > Copyright 2018 Rich Harris > @@ -2248,6 +2232,29 @@ Repository: sindresorhus/parse-ms --------------------------------------- +## picocolors +License: ISC +By: Alexey Raspopov +Repository: alexeyraspopov/picocolors + +> ISC License +> +> Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov +> +> Permission to use, copy, modify, and/or distribute this software for any +> purpose with or without fee is hereby granted, provided that the above +> copyright notice and this permission notice appear in all copies. +> +> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +> OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--------------------------------------- + ## picomatch License: MIT By: Jon Schlinkert @@ -7126,8 +7133,8 @@ The following npm packages may be included in this product: - @pnpm/config.env-replace@1.1.0 - @pnpm/network.ca-file@1.0.2 - @rollup/plugin-inject@5.0.5 - - @rollup/rollup-linux-x64-gnu@4.27.2 - - @rollup/rollup-linux-x64-musl@4.27.2 + - @rollup/rollup-linux-x64-gnu@4.59.0 + - @rollup/rollup-linux-x64-musl@4.59.0 - brorand@1.1.0 - constants-browserify@1.0.0 - cookie-signature@1.0.6 diff --git a/packages/pages/THIRD-PARTY-NOTICES b/packages/pages/THIRD-PARTY-NOTICES index 8e859b49..1e9d07f4 100644 --- a/packages/pages/THIRD-PARTY-NOTICES +++ b/packages/pages/THIRD-PARTY-NOTICES @@ -156,7 +156,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following npm package may be included in this product: - - rollup@4.27.2 + - rollup@4.59.0 This package contains the following license: @@ -180,12 +180,10 @@ MIT, ISC, 0BSD # Bundled dependencies: ## @jridgewell/sourcemap-codec License: MIT -By: Rich Harris -Repository: git+https://github.com/jridgewell/sourcemap-codec.git +By: Justin Ridgewell +Repository: git+https://github.com/jridgewell/sourcemaps.git -> The MIT License -> -> Copyright (c) 2015 Rich Harris +> Copyright 2024 Justin Ridgewell > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal @@ -202,8 +200,8 @@ Repository: git+https://github.com/jridgewell/sourcemap-codec.git > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -> THE SOFTWARE. +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. --------------------------------------- @@ -352,21 +350,6 @@ Repository: git+https://github.com/paulmillr/chokidar.git --------------------------------------- -## colorette -License: MIT -By: Jorge Bucaran -Repository: jorgebucaran/colorette - -> Copyright © Jorge Bucaran <> -> -> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------------------------------- - ## date-time License: MIT By: Sindre Sorhus @@ -574,7 +557,7 @@ Repository: git+https://gitlab.com/Rich-Harris/locate-character.git ## magic-string License: MIT By: Rich Harris -Repository: https://github.com/rich-harris/magic-string +Repository: git+https://github.com/Rich-Harris/magic-string.git > Copyright 2018 Rich Harris > @@ -632,6 +615,29 @@ Repository: sindresorhus/parse-ms --------------------------------------- +## picocolors +License: ISC +By: Alexey Raspopov +Repository: alexeyraspopov/picocolors + +> ISC License +> +> Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov +> +> Permission to use, copy, modify, and/or distribute this software for any +> purpose with or without fee is hereby granted, provided that the above +> copyright notice and this permission notice appear in all copies. +> +> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +> OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--------------------------------------- + ## picomatch License: MIT By: Jon Schlinkert From fd36b2f1d1e8640bf22bce021836441a7f5cc536 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Wed, 4 Mar 2026 13:41:09 -0500 Subject: [PATCH 3/8] added sanitation --- .../src/assets/getAssetsFilepath.test.ts | 36 +++++++++++++++++++ .../common/src/assets/getAssetsFilepath.ts | 26 +++++++++++++- packages/pages/src/vite-plugin/build/build.ts | 34 +++++++++++++++--- .../pages/src/vite-plugin/modules/plugin.ts | 16 ++++++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts index 18da277c..3016f925 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts @@ -26,4 +26,40 @@ describe("getAssetsFilepath - determineAssetsFilepath", () => { expect(actual).toEqual("viteConfigAssetsDir"); }); + + it("sanitizes absolute assetsDir to a safe relative path", async () => { + const viteConfig = { + default: { + plugins: [], + build: { + assetsDir: "/my/subpath/assets", + }, + }, + }; + + const importSpy = vi.spyOn(importHelper, "import_"); + importSpy.mockImplementation(async () => viteConfig); + + const actual = await determineAssetsFilepath("assets", "does not matter since mocked"); + + expect(actual).toEqual("my/subpath/assets"); + }); + + it("falls back when assetsDir would escape the output directory", async () => { + const viteConfig = { + default: { + plugins: [], + build: { + assetsDir: "../escape", + }, + }, + }; + + const importSpy = vi.spyOn(importHelper, "import_"); + importSpy.mockImplementation(async () => viteConfig); + + const actual = await determineAssetsFilepath("assets", "does not matter since mocked"); + + expect(actual).toEqual("assets"); + }); }); diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.ts index af6ed5fb..6173499c 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.ts @@ -1,4 +1,5 @@ import { pathToFileURL } from "url"; +import path from "node:path"; import { UserConfig } from "vite"; import { import_ } from "./import.js"; @@ -19,5 +20,28 @@ export const determineAssetsFilepath = async ( const viteConfig = await import_(pathToFileURL(viteConfigPath).toString()); const userConfig = viteConfig.default as UserConfig; - return userConfig.build?.assetsDir ?? defaultAssetsDir; + return sanitizeAssetsDir(userConfig.build?.assetsDir ?? defaultAssetsDir, defaultAssetsDir); +}; + +/** + * Ensures the assets directory is a safe, relative subpath for Rollup output. + * Falls back when the value is empty, absolute, or would escape the output dir. + */ +const sanitizeAssetsDir = (assetsDir: string, fallback: string): string => { + const trimmed = assetsDir.trim(); + if (trimmed.length === 0) { + return fallback; + } + + const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); + const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); + const normalized = path.posix + .normalize(withoutLeading.replace(/\\/g, "/")) + .replace(/^(\.\/)+/, ""); + + if (normalized === "" || normalized === "." || normalized.startsWith("..")) { + return fallback; + } + + return normalized; }; diff --git a/packages/pages/src/vite-plugin/build/build.ts b/packages/pages/src/vite-plugin/build/build.ts index 159de299..1f1c622d 100644 --- a/packages/pages/src/vite-plugin/build/build.ts +++ b/packages/pages/src/vite-plugin/build/build.ts @@ -1,4 +1,5 @@ import { Plugin, UserConfig } from "vite"; +import path from "node:path"; import buildStart from "./buildStart/buildStart.js"; import closeBundle from "./closeBundle/closeBundle.js"; import { ProjectStructure } from "../../common/src/project/structure.js"; @@ -21,6 +22,8 @@ export const build = async ( pluginTotalFilesizeLimit: number ): Promise => { const { envVarConfig, subfolders } = projectStructure.config; + const safeAssetsSubfolder = sanitizeOutputSubpath(subfolders.assets, "assets"); + const safeStaticSubfolder = sanitizeOutputSubpath(subfolders.static, "static"); return { name: "vite-plugin:build", @@ -54,11 +57,11 @@ export const build = async ( preserveEntrySignatures: "strict", output: { intro, - assetFileNames: `${subfolders.assets}/${subfolders.static}/[name]-[hash][extname]`, - chunkFileNames: `${subfolders.assets}/${subfolders.static}/[name]-[hash].js`, + assetFileNames: `${safeAssetsSubfolder}/${safeStaticSubfolder}/[name]-[hash][extname]`, + chunkFileNames: `${safeAssetsSubfolder}/${safeStaticSubfolder}/[name]-[hash].js`, sanitizeFileName: false, entryFileNames: () => { - return `${subfolders.assets}/[name].[hash].js`; + return `${safeAssetsSubfolder}/[name].[hash].js`; }, manualChunks: (id) => { // Fixes an error where the output is prefixed like \x00commonjsHelpers-hash.js @@ -67,7 +70,7 @@ export const build = async ( }, }, reportCompressedSize: false, - assetsDir: subfolders.assets, + assetsDir: safeAssetsSubfolder, }, define: processEnvVariables(envVarConfig.envVarPrefix), }; @@ -76,3 +79,26 @@ export const build = async ( closeBundle: closeBundle(projectStructure, pluginFilesizeLimit, pluginTotalFilesizeLimit), }; }; + +/** + * Sanitizes subfolder values used in Rollup output paths so they remain + * relative and cannot escape the output directory. + */ +const sanitizeOutputSubpath = (value: string, fallback: string): string => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + return fallback; + } + + const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); + const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); + const normalized = path.posix + .normalize(withoutLeading.replace(/\\/g, "/")) + .replace(/^(\.\/)+/, ""); + + if (normalized === "" || normalized === "." || normalized.startsWith("..")) { + return fallback; + } + + return normalized; +}; diff --git a/packages/pages/src/vite-plugin/modules/plugin.ts b/packages/pages/src/vite-plugin/modules/plugin.ts index 6f63b467..40c2e5f0 100644 --- a/packages/pages/src/vite-plugin/modules/plugin.ts +++ b/packages/pages/src/vite-plugin/modules/plugin.ts @@ -65,6 +65,7 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const viteConfig = viteConfigModule ? (viteConfigModule.default as UserConfig) : undefined; for (const [moduleName, fileInfo] of Object.entries(filepaths)) { + const safeModuleEntryName = sanitizeModuleEntryName(moduleName); logger.info = (msg, options) => { if (msg.includes("building for production")) { loggerInfo(pc.green(`\nBuilding ${moduleName} module...`)); @@ -117,7 +118,7 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< input: fileInfo.path, output: { format: "umd", - entryFileNames: `${moduleName}.umd.js`, + entryFileNames: `${safeModuleEntryName}.umd.js`, }, }, reportCompressedSize: false, @@ -184,6 +185,19 @@ const shouldBundleModules = (projectStructure: ProjectStructure) => { return fs.existsSync(path.join(rootFolders.source, subfolders.modules)); }; +/** + * Sanitizes module names used for entryFileNames to avoid path traversal and + * keep output files within the Rollup output directory. + */ +const sanitizeModuleEntryName = (moduleName: string): string => { + const normalized = path.posix.normalize(moduleName.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); + const base = path.posix.basename(normalized); + if (base === "" || base === "." || base === "..") { + return "module"; + } + return base; +}; + /** * Adds custom code to module when bundled into umd.js. * From 73d4ecf0350a8a3fb8c02cc8f91998a07596f3ea Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Wed, 4 Mar 2026 14:06:41 -0500 Subject: [PATCH 4/8] ensured uniqueness and refactored --- .../common/src/assets/getAssetsFilepath.ts | 28 ++++++------------- .../src/common/src/assets/sanitizeSubpath.ts | 24 ++++++++++++++++ packages/pages/src/vite-plugin/build/build.ts | 19 ++----------- .../pages/src/vite-plugin/modules/plugin.ts | 16 +++++++++-- 4 files changed, 47 insertions(+), 40 deletions(-) create mode 100644 packages/pages/src/common/src/assets/sanitizeSubpath.ts diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.ts index 6173499c..d95b81ce 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.ts @@ -1,11 +1,13 @@ import { pathToFileURL } from "url"; -import path from "node:path"; import { UserConfig } from "vite"; import { import_ } from "./import.js"; +import { sanitizeSubpath } from "./sanitizeSubpath.js"; /** - * Determines the assets directory to use by checking - * vite.config.json's assetDir or default to "assets". + * Determines the assets directory to use by checking vite.config.json's + * assetDir or default to "assets". Empty values or paths that would escape + * the output directory fall back to the default; absolute paths are sanitized + * into a safe relative subpath for Rollup output. * @param defaultAssetsDir the default directory for assets * @param viteConfigPath the path to vite.config.js */ @@ -25,23 +27,9 @@ export const determineAssetsFilepath = async ( /** * Ensures the assets directory is a safe, relative subpath for Rollup output. - * Falls back when the value is empty, absolute, or would escape the output dir. + * Falls back when the value is empty or would escape the output dir; absolute + * paths are sanitized into safe relative subpaths. */ const sanitizeAssetsDir = (assetsDir: string, fallback: string): string => { - const trimmed = assetsDir.trim(); - if (trimmed.length === 0) { - return fallback; - } - - const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); - const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); - const normalized = path.posix - .normalize(withoutLeading.replace(/\\/g, "/")) - .replace(/^(\.\/)+/, ""); - - if (normalized === "" || normalized === "." || normalized.startsWith("..")) { - return fallback; - } - - return normalized; + return sanitizeSubpath(assetsDir, fallback); }; diff --git a/packages/pages/src/common/src/assets/sanitizeSubpath.ts b/packages/pages/src/common/src/assets/sanitizeSubpath.ts new file mode 100644 index 00000000..698a4935 --- /dev/null +++ b/packages/pages/src/common/src/assets/sanitizeSubpath.ts @@ -0,0 +1,24 @@ +import path from "node:path"; + +/** + * Normalizes a subpath used for output file naming so it stays relative + * and cannot escape the output directory. Falls back for empty or unsafe inputs. + */ +export const sanitizeSubpath = (value: string, fallback: string): string => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + return fallback; + } + + const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); + const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); + const normalized = path.posix + .normalize(withoutLeading.replace(/\\/g, "/")) + .replace(/^(\.\/)+/, ""); + + if (normalized === "" || normalized === "." || normalized.startsWith("..")) { + return fallback; + } + + return normalized; +}; diff --git a/packages/pages/src/vite-plugin/build/build.ts b/packages/pages/src/vite-plugin/build/build.ts index 1f1c622d..94f5f1a2 100644 --- a/packages/pages/src/vite-plugin/build/build.ts +++ b/packages/pages/src/vite-plugin/build/build.ts @@ -1,11 +1,11 @@ import { Plugin, UserConfig } from "vite"; -import path from "node:path"; import buildStart from "./buildStart/buildStart.js"; import closeBundle from "./closeBundle/closeBundle.js"; import { ProjectStructure } from "../../common/src/project/structure.js"; import { processEnvVariables } from "../../util/processEnvVariables.js"; import { buildServerlessFunctions } from "../serverless-functions/plugin.js"; import { buildModules } from "../modules/plugin.js"; +import { sanitizeSubpath } from "../../common/src/assets/sanitizeSubpath.js"; const intro = ` var global = globalThis; @@ -85,20 +85,5 @@ export const build = async ( * relative and cannot escape the output directory. */ const sanitizeOutputSubpath = (value: string, fallback: string): string => { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return fallback; - } - - const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); - const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); - const normalized = path.posix - .normalize(withoutLeading.replace(/\\/g, "/")) - .replace(/^(\.\/)+/, ""); - - if (normalized === "" || normalized === "." || normalized.startsWith("..")) { - return fallback; - } - - return normalized; + return sanitizeSubpath(value, fallback); }; diff --git a/packages/pages/src/vite-plugin/modules/plugin.ts b/packages/pages/src/vite-plugin/modules/plugin.ts index 40c2e5f0..6433b944 100644 --- a/packages/pages/src/vite-plugin/modules/plugin.ts +++ b/packages/pages/src/vite-plugin/modules/plugin.ts @@ -187,15 +187,25 @@ const shouldBundleModules = (projectStructure: ProjectStructure) => { /** * Sanitizes module names used for entryFileNames to avoid path traversal and - * keep output files within the Rollup output directory. + * keep output files within the Rollup output directory. Appends a short hash + * so distinct module names do not collide after sanitization. */ const sanitizeModuleEntryName = (moduleName: string): string => { const normalized = path.posix.normalize(moduleName.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); const base = path.posix.basename(normalized); if (base === "" || base === "." || base === "..") { - return "module"; + return `module-${shortHash(moduleName)}`; } - return base; + return `${base}-${shortHash(moduleName)}`; +}; + +const shortHash = (value: string): string => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash << 5) - hash + value.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash).toString(36).padStart(6, "0").slice(0, 6); }; /** From c45e7e1673e49fba81450c885d86f584f4b0f8b2 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Wed, 4 Mar 2026 15:05:34 -0500 Subject: [PATCH 5/8] simplified and added tests --- .../common/src/assets/getAssetsFilepath.ts | 15 +- .../common/src/assets/sanitizeSubpath.test.ts | 38 +++++ .../src/common/src/assets/sanitizeSubpath.ts | 26 ++-- packages/pages/src/vite-plugin/build/build.ts | 12 +- .../src/vite-plugin/modules/plugin.test.ts | 137 ++++++++++++++++++ .../pages/src/vite-plugin/modules/plugin.ts | 66 ++++++--- 6 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 packages/pages/src/common/src/assets/sanitizeSubpath.test.ts create mode 100644 packages/pages/src/vite-plugin/modules/plugin.test.ts diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.ts index d95b81ce..92228539 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.ts @@ -6,8 +6,8 @@ import { sanitizeSubpath } from "./sanitizeSubpath.js"; /** * Determines the assets directory to use by checking vite.config.json's * assetDir or default to "assets". Empty values or paths that would escape - * the output directory fall back to the default; absolute paths are sanitized - * into a safe relative subpath for Rollup output. + * the output directory fall back to the default, while absolute paths are + * sanitized into safe relative subpaths for Rollup output. * @param defaultAssetsDir the default directory for assets * @param viteConfigPath the path to vite.config.js */ @@ -22,14 +22,5 @@ export const determineAssetsFilepath = async ( const viteConfig = await import_(pathToFileURL(viteConfigPath).toString()); const userConfig = viteConfig.default as UserConfig; - return sanitizeAssetsDir(userConfig.build?.assetsDir ?? defaultAssetsDir, defaultAssetsDir); -}; - -/** - * Ensures the assets directory is a safe, relative subpath for Rollup output. - * Falls back when the value is empty or would escape the output dir; absolute - * paths are sanitized into safe relative subpaths. - */ -const sanitizeAssetsDir = (assetsDir: string, fallback: string): string => { - return sanitizeSubpath(assetsDir, fallback); + return sanitizeSubpath(userConfig.build?.assetsDir ?? "", defaultAssetsDir); }; diff --git a/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts b/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts new file mode 100644 index 00000000..86d0838d --- /dev/null +++ b/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { sanitizeSubpath } from "./sanitizeSubpath.js"; + +describe("sanitizeSubpath", () => { + it("returns normalized relative path when already safe", () => { + expect(sanitizeSubpath("assets/static", "assets")).toEqual("assets/static"); + }); + + it("sanitizes absolute unix paths to relative subpaths", () => { + expect(sanitizeSubpath("/my/subpath/assets", "assets")).toEqual("my/subpath/assets"); + }); + + it("sanitizes absolute windows paths to relative subpaths", () => { + expect(sanitizeSubpath("C:\\my\\subpath\\assets", "assets")).toEqual("my/subpath/assets"); + }); + + it("falls back when value is empty", () => { + expect(sanitizeSubpath(" ", "assets")).toEqual("assets"); + }); + + it("falls back when value escapes output directory", () => { + expect(sanitizeSubpath("../escape", "assets")).toEqual("assets"); + }); + + it("falls back when normalized value resolves to current directory", () => { + expect(sanitizeSubpath("./", "assets")).toEqual("assets"); + }); + + it("normalizes nested relative segments", () => { + expect(sanitizeSubpath("assets/../assets/static", "assets")).toEqual("assets/static"); + }); + + it("throws when fallback is unsafe", () => { + expect(() => sanitizeSubpath("assets", "../unsafe")).toThrowError( + "Invalid sanitizeSubpath fallback" + ); + }); +}); diff --git a/packages/pages/src/common/src/assets/sanitizeSubpath.ts b/packages/pages/src/common/src/assets/sanitizeSubpath.ts index 698a4935..d0a714c1 100644 --- a/packages/pages/src/common/src/assets/sanitizeSubpath.ts +++ b/packages/pages/src/common/src/assets/sanitizeSubpath.ts @@ -5,19 +5,25 @@ import path from "node:path"; * and cannot escape the output directory. Falls back for empty or unsafe inputs. */ export const sanitizeSubpath = (value: string, fallback: string): string => { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return fallback; - } + const normalizeCandidate = (input: string): string => { + const trimmed = input.trim(); + const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); + const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); + return path.posix.normalize(withoutLeading.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); + }; - const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); - const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); - const normalized = path.posix - .normalize(withoutLeading.replace(/\\/g, "/")) - .replace(/^(\.\/)+/, ""); + const normalizedFallback = normalizeCandidate(fallback); + if ( + normalizedFallback === "" || + normalizedFallback === "." || + normalizedFallback.startsWith("..") + ) { + throw new Error(`Invalid sanitizeSubpath fallback: ${fallback}`); + } + const normalized = normalizeCandidate(value); if (normalized === "" || normalized === "." || normalized.startsWith("..")) { - return fallback; + return normalizedFallback; } return normalized; diff --git a/packages/pages/src/vite-plugin/build/build.ts b/packages/pages/src/vite-plugin/build/build.ts index 94f5f1a2..ea3c7c01 100644 --- a/packages/pages/src/vite-plugin/build/build.ts +++ b/packages/pages/src/vite-plugin/build/build.ts @@ -22,8 +22,8 @@ export const build = async ( pluginTotalFilesizeLimit: number ): Promise => { const { envVarConfig, subfolders } = projectStructure.config; - const safeAssetsSubfolder = sanitizeOutputSubpath(subfolders.assets, "assets"); - const safeStaticSubfolder = sanitizeOutputSubpath(subfolders.static, "static"); + const safeAssetsSubfolder = sanitizeSubpath(subfolders.assets, "assets"); + const safeStaticSubfolder = sanitizeSubpath(subfolders.static, "static"); return { name: "vite-plugin:build", @@ -79,11 +79,3 @@ export const build = async ( closeBundle: closeBundle(projectStructure, pluginFilesizeLimit, pluginTotalFilesizeLimit), }; }; - -/** - * Sanitizes subfolder values used in Rollup output paths so they remain - * relative and cannot escape the output directory. - */ -const sanitizeOutputSubpath = (value: string, fallback: string): string => { - return sanitizeSubpath(value, fallback); -}; diff --git a/packages/pages/src/vite-plugin/modules/plugin.test.ts b/packages/pages/src/vite-plugin/modules/plugin.test.ts new file mode 100644 index 00000000..4e1704bf --- /dev/null +++ b/packages/pages/src/vite-plugin/modules/plugin.test.ts @@ -0,0 +1,137 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ProjectStructure } from "../../common/src/project/structure.js"; + +const buildMock = vi.hoisted(() => vi.fn()); +const mergeConfigMock = vi.hoisted(() => vi.fn((a, b) => ({ ...a, ...b }))); +const globSyncMock = vi.hoisted(() => vi.fn()); +const getModuleNameMock = vi.hoisted(() => vi.fn()); +const scopedViteConfigPathMock = vi.hoisted(() => vi.fn(() => undefined)); +const logWarningMock = vi.hoisted(() => vi.fn()); +const existsSyncMock = vi.hoisted(() => vi.fn(() => true)); + +vi.mock("vite", () => ({ + build: buildMock, + mergeConfig: mergeConfigMock, +})); + +vi.mock("glob", () => ({ + glob: { + sync: globSyncMock, + }, +})); + +vi.mock("../../common/src/module/internal/getModuleConfig.js", () => ({ + getModuleName: getModuleNameMock, +})); + +vi.mock("../../util/viteConfig.js", () => ({ + scopedViteConfigPath: scopedViteConfigPathMock, +})); + +vi.mock("../../util/processEnvVariables.js", () => ({ + processEnvVariables: () => ({}), +})); + +vi.mock("../../common/src/module/internal/logger.js", () => ({ + createModuleLogger: () => ({ info: vi.fn() }), +})); + +vi.mock("vite-plugin-node-polyfills", () => ({ + nodePolyfills: () => ({ name: "mock-polyfill-plugin" }), +})); + +vi.mock("../../util/logError.js", () => ({ + logWarning: logWarningMock, + logErrorAndExit: (error: string) => { + throw new Error(error); + }, +})); + +vi.mock("node:fs", () => ({ + default: { + existsSync: existsSyncMock, + readFileSync: vi.fn(() => ""), + }, +})); + +describe("buildModules duplicate and collision handling", () => { + beforeEach(() => { + buildMock.mockReset(); + buildMock.mockResolvedValue(undefined); + mergeConfigMock.mockClear(); + globSyncMock.mockReset(); + getModuleNameMock.mockReset(); + scopedViteConfigPathMock.mockReset(); + scopedViteConfigPathMock.mockReturnValue(undefined); + logWarningMock.mockReset(); + existsSyncMock.mockReset(); + existsSyncMock.mockReturnValue(true); + }); + + it("keeps scoped module when scoped and unscoped names are the same", async () => { + const scopedModule = "/repo/src/modules/brand/widget/index.tsx"; + const unscopedModule = "/repo/src/modules/widget/index.tsx"; + + globSyncMock.mockImplementation((pattern: string) => { + if (pattern.includes("/brand/*/*.{jsx,tsx}")) { + return [scopedModule]; + } + if (pattern.includes("/modules/*/*.{jsx,tsx}")) { + return [unscopedModule]; + } + return []; + }); + + getModuleNameMock.mockReturnValue("SharedWidget"); + + const { buildModules } = await import("./plugin.js"); + const projectStructure = new ProjectStructure({ scope: "brand" }); + await buildModules(projectStructure); + + expect(buildMock).toHaveBeenCalledTimes(1); + const buildArg = buildMock.mock.calls[0][0]; + expect(buildArg.build.rollupOptions.input).toEqual(scopedModule); + expect(buildArg.build.rollupOptions.output.entryFileNames).toEqual("SharedWidget.umd.js"); + }); + + it("throws when duplicate module names exist in the same scope", async () => { + globSyncMock.mockImplementation((pattern: string) => { + if (pattern.includes("/brand/*/*.{jsx,tsx}")) { + return ["/repo/src/modules/brand/a/first.tsx", "/repo/src/modules/brand/b/second.tsx"]; + } + if (pattern.includes("/modules/*/*.{jsx,tsx}")) { + return []; + } + return []; + }); + + getModuleNameMock.mockReturnValue("DuplicateName"); + + const { buildModules } = await import("./plugin.js"); + const projectStructure = new ProjectStructure({ scope: "brand" }); + + await expect(buildModules(projectStructure)).rejects.toThrowError( + 'Duplicate module name "DuplicateName" found in:' + ); + expect(buildMock).not.toHaveBeenCalled(); + }); + + it("throws when distinct module names sanitize to the same output filename", async () => { + globSyncMock.mockImplementation((pattern: string) => { + if (pattern.includes("/modules/*/*.{jsx,tsx}")) { + return ["/repo/src/modules/a/first.tsx", "/repo/src/modules/b/second.tsx"]; + } + return []; + }); + + getModuleNameMock.mockReturnValueOnce("foo/bar").mockReturnValueOnce("foo-bar"); + + const { buildModules } = await import("./plugin.js"); + const projectStructure = new ProjectStructure(); + + await expect(buildModules(projectStructure)).rejects.toThrowError( + 'resolve to the same output filename "foo-bar.umd.js"' + ); + expect(buildMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/pages/src/vite-plugin/modules/plugin.ts b/packages/pages/src/vite-plugin/modules/plugin.ts index 6433b944..cb442aa0 100644 --- a/packages/pages/src/vite-plugin/modules/plugin.ts +++ b/packages/pages/src/vite-plugin/modules/plugin.ts @@ -20,6 +20,7 @@ import { scopedViteConfigPath } from "../../util/viteConfig.js"; type FileInfo = { path: string; name: string; + isScoped: boolean; }; export const buildModules = async (projectStructure: ProjectStructure): Promise => { @@ -31,8 +32,13 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const outdir = path.join(rootFolders.dist, subfolders.modules); const filepaths: { [s: string]: FileInfo } = {}; + const scopedModulesRoot = projectStructure.config.scope + ? path.resolve(rootFolders.source, subfolders.modules, projectStructure.config.scope) + : undefined; const modulePaths = projectStructure.getModulePaths(); modulePaths.forEach((modulePath) => { + const isScopedPath = + scopedModulesRoot !== undefined && path.resolve(modulePath.path) === scopedModulesRoot; glob .sync(convertToPosixPath(path.join(modulePath.path, "*/*.{jsx,tsx}")), { nodir: true, @@ -41,15 +47,45 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const filepath = path.resolve(f); const moduleName = getModuleName(filepath); const { name } = path.parse(filepath); - // If that name exists already, don't overwrite the filepaths + const resolvedModuleName = moduleName ?? name; + const existing = filepaths[resolvedModuleName]; + // If that name exists already, don't overwrite the filepaths. // Example, if scope is declared, the scoped module's info should stay // in filepaths and not be overwritten by a non-scoped module. - if (!((moduleName ?? name) in filepaths)) { - filepaths[moduleName ?? name] = { path: filepath, name: name }; + if (existing) { + if (existing.isScoped && !isScopedPath) { + return; + } + if (!existing.isScoped && isScopedPath) { + filepaths[resolvedModuleName] = { path: filepath, name: name, isScoped: true }; + return; + } + logErrorAndExit( + `Duplicate module name "${resolvedModuleName}" found in:\n` + + `- ${existing.path}\n` + + `- ${filepath}\n` + + `Module names must be unique.` + ); } + filepaths[resolvedModuleName] = { path: filepath, name: name, isScoped: isScopedPath }; }); }); + const moduleEntryNames = new Map(); + const moduleByEntryName = new Map(); + for (const moduleName of Object.keys(filepaths)) { + const entryName = sanitizeModuleEntryName(moduleName); + const existingModule = moduleByEntryName.get(entryName); + if (existingModule) { + logErrorAndExit( + `Module names "${existingModule}" and "${moduleName}" resolve to the same ` + + `output filename "${entryName}.umd.js". Rename modules to be unique.` + ); + } + moduleByEntryName.set(entryName, moduleName); + moduleEntryNames.set(moduleName, entryName); + } + const logger = createModuleLogger(); const loggerInfo = logger.info; @@ -65,7 +101,8 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const viteConfig = viteConfigModule ? (viteConfigModule.default as UserConfig) : undefined; for (const [moduleName, fileInfo] of Object.entries(filepaths)) { - const safeModuleEntryName = sanitizeModuleEntryName(moduleName); + const safeModuleEntryName = + moduleEntryNames.get(moduleName) ?? sanitizeModuleEntryName(moduleName); logger.info = (msg, options) => { if (msg.includes("building for production")) { loggerInfo(pc.green(`\nBuilding ${moduleName} module...`)); @@ -187,25 +224,16 @@ const shouldBundleModules = (projectStructure: ProjectStructure) => { /** * Sanitizes module names used for entryFileNames to avoid path traversal and - * keep output files within the Rollup output directory. Appends a short hash - * so distinct module names do not collide after sanitization. + * keep output files within the Rollup output directory. */ const sanitizeModuleEntryName = (moduleName: string): string => { const normalized = path.posix.normalize(moduleName.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); - const base = path.posix.basename(normalized); - if (base === "" || base === "." || base === "..") { - return `module-${shortHash(moduleName)}`; - } - return `${base}-${shortHash(moduleName)}`; -}; + const safeName = normalized + .split("/") + .filter((segment) => segment !== "" && segment !== "." && segment !== "..") + .join("-"); -const shortHash = (value: string): string => { - let hash = 0; - for (let i = 0; i < value.length; i += 1) { - hash = (hash << 5) - hash + value.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36).padStart(6, "0").slice(0, 6); + return safeName.length > 0 ? safeName : "module"; }; /** From 3284103154a1335121772fc20f826183206085f0 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Thu, 5 Mar 2026 11:23:58 -0500 Subject: [PATCH 6/8] removed unnecessary sanitation --- .../src/assets/getAssetsFilepath.test.ts | 36 ----- .../common/src/assets/getAssetsFilepath.ts | 7 +- .../common/src/assets/sanitizeSubpath.test.ts | 38 ----- .../src/common/src/assets/sanitizeSubpath.ts | 30 ---- packages/pages/src/vite-plugin/build/build.ts | 11 +- .../src/vite-plugin/modules/plugin.test.ts | 137 ------------------ .../pages/src/vite-plugin/modules/plugin.ts | 67 +++------ 7 files changed, 24 insertions(+), 302 deletions(-) delete mode 100644 packages/pages/src/common/src/assets/sanitizeSubpath.test.ts delete mode 100644 packages/pages/src/common/src/assets/sanitizeSubpath.ts delete mode 100644 packages/pages/src/vite-plugin/modules/plugin.test.ts diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts index 3016f925..18da277c 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.test.ts @@ -26,40 +26,4 @@ describe("getAssetsFilepath - determineAssetsFilepath", () => { expect(actual).toEqual("viteConfigAssetsDir"); }); - - it("sanitizes absolute assetsDir to a safe relative path", async () => { - const viteConfig = { - default: { - plugins: [], - build: { - assetsDir: "/my/subpath/assets", - }, - }, - }; - - const importSpy = vi.spyOn(importHelper, "import_"); - importSpy.mockImplementation(async () => viteConfig); - - const actual = await determineAssetsFilepath("assets", "does not matter since mocked"); - - expect(actual).toEqual("my/subpath/assets"); - }); - - it("falls back when assetsDir would escape the output directory", async () => { - const viteConfig = { - default: { - plugins: [], - build: { - assetsDir: "../escape", - }, - }, - }; - - const importSpy = vi.spyOn(importHelper, "import_"); - importSpy.mockImplementation(async () => viteConfig); - - const actual = await determineAssetsFilepath("assets", "does not matter since mocked"); - - expect(actual).toEqual("assets"); - }); }); diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.ts index 92228539..afdaa825 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.ts @@ -1,13 +1,10 @@ import { pathToFileURL } from "url"; import { UserConfig } from "vite"; import { import_ } from "./import.js"; -import { sanitizeSubpath } from "./sanitizeSubpath.js"; /** * Determines the assets directory to use by checking vite.config.json's - * assetDir or default to "assets". Empty values or paths that would escape - * the output directory fall back to the default, while absolute paths are - * sanitized into safe relative subpaths for Rollup output. + * assetDir or default to "assets". * @param defaultAssetsDir the default directory for assets * @param viteConfigPath the path to vite.config.js */ @@ -22,5 +19,5 @@ export const determineAssetsFilepath = async ( const viteConfig = await import_(pathToFileURL(viteConfigPath).toString()); const userConfig = viteConfig.default as UserConfig; - return sanitizeSubpath(userConfig.build?.assetsDir ?? "", defaultAssetsDir); + return userConfig.build?.assetsDir ?? defaultAssetsDir; }; diff --git a/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts b/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts deleted file mode 100644 index 86d0838d..00000000 --- a/packages/pages/src/common/src/assets/sanitizeSubpath.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { sanitizeSubpath } from "./sanitizeSubpath.js"; - -describe("sanitizeSubpath", () => { - it("returns normalized relative path when already safe", () => { - expect(sanitizeSubpath("assets/static", "assets")).toEqual("assets/static"); - }); - - it("sanitizes absolute unix paths to relative subpaths", () => { - expect(sanitizeSubpath("/my/subpath/assets", "assets")).toEqual("my/subpath/assets"); - }); - - it("sanitizes absolute windows paths to relative subpaths", () => { - expect(sanitizeSubpath("C:\\my\\subpath\\assets", "assets")).toEqual("my/subpath/assets"); - }); - - it("falls back when value is empty", () => { - expect(sanitizeSubpath(" ", "assets")).toEqual("assets"); - }); - - it("falls back when value escapes output directory", () => { - expect(sanitizeSubpath("../escape", "assets")).toEqual("assets"); - }); - - it("falls back when normalized value resolves to current directory", () => { - expect(sanitizeSubpath("./", "assets")).toEqual("assets"); - }); - - it("normalizes nested relative segments", () => { - expect(sanitizeSubpath("assets/../assets/static", "assets")).toEqual("assets/static"); - }); - - it("throws when fallback is unsafe", () => { - expect(() => sanitizeSubpath("assets", "../unsafe")).toThrowError( - "Invalid sanitizeSubpath fallback" - ); - }); -}); diff --git a/packages/pages/src/common/src/assets/sanitizeSubpath.ts b/packages/pages/src/common/src/assets/sanitizeSubpath.ts deleted file mode 100644 index d0a714c1..00000000 --- a/packages/pages/src/common/src/assets/sanitizeSubpath.ts +++ /dev/null @@ -1,30 +0,0 @@ -import path from "node:path"; - -/** - * Normalizes a subpath used for output file naming so it stays relative - * and cannot escape the output directory. Falls back for empty or unsafe inputs. - */ -export const sanitizeSubpath = (value: string, fallback: string): string => { - const normalizeCandidate = (input: string): string => { - const trimmed = input.trim(); - const withoutDrive = trimmed.replace(/^[a-zA-Z]:/, ""); - const withoutLeading = withoutDrive.replace(/^[/\\]+/, ""); - return path.posix.normalize(withoutLeading.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); - }; - - const normalizedFallback = normalizeCandidate(fallback); - if ( - normalizedFallback === "" || - normalizedFallback === "." || - normalizedFallback.startsWith("..") - ) { - throw new Error(`Invalid sanitizeSubpath fallback: ${fallback}`); - } - - const normalized = normalizeCandidate(value); - if (normalized === "" || normalized === "." || normalized.startsWith("..")) { - return normalizedFallback; - } - - return normalized; -}; diff --git a/packages/pages/src/vite-plugin/build/build.ts b/packages/pages/src/vite-plugin/build/build.ts index ea3c7c01..159de299 100644 --- a/packages/pages/src/vite-plugin/build/build.ts +++ b/packages/pages/src/vite-plugin/build/build.ts @@ -5,7 +5,6 @@ import { ProjectStructure } from "../../common/src/project/structure.js"; import { processEnvVariables } from "../../util/processEnvVariables.js"; import { buildServerlessFunctions } from "../serverless-functions/plugin.js"; import { buildModules } from "../modules/plugin.js"; -import { sanitizeSubpath } from "../../common/src/assets/sanitizeSubpath.js"; const intro = ` var global = globalThis; @@ -22,8 +21,6 @@ export const build = async ( pluginTotalFilesizeLimit: number ): Promise => { const { envVarConfig, subfolders } = projectStructure.config; - const safeAssetsSubfolder = sanitizeSubpath(subfolders.assets, "assets"); - const safeStaticSubfolder = sanitizeSubpath(subfolders.static, "static"); return { name: "vite-plugin:build", @@ -57,11 +54,11 @@ export const build = async ( preserveEntrySignatures: "strict", output: { intro, - assetFileNames: `${safeAssetsSubfolder}/${safeStaticSubfolder}/[name]-[hash][extname]`, - chunkFileNames: `${safeAssetsSubfolder}/${safeStaticSubfolder}/[name]-[hash].js`, + assetFileNames: `${subfolders.assets}/${subfolders.static}/[name]-[hash][extname]`, + chunkFileNames: `${subfolders.assets}/${subfolders.static}/[name]-[hash].js`, sanitizeFileName: false, entryFileNames: () => { - return `${safeAssetsSubfolder}/[name].[hash].js`; + return `${subfolders.assets}/[name].[hash].js`; }, manualChunks: (id) => { // Fixes an error where the output is prefixed like \x00commonjsHelpers-hash.js @@ -70,7 +67,7 @@ export const build = async ( }, }, reportCompressedSize: false, - assetsDir: safeAssetsSubfolder, + assetsDir: subfolders.assets, }, define: processEnvVariables(envVarConfig.envVarPrefix), }; diff --git a/packages/pages/src/vite-plugin/modules/plugin.test.ts b/packages/pages/src/vite-plugin/modules/plugin.test.ts deleted file mode 100644 index 4e1704bf..00000000 --- a/packages/pages/src/vite-plugin/modules/plugin.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ProjectStructure } from "../../common/src/project/structure.js"; - -const buildMock = vi.hoisted(() => vi.fn()); -const mergeConfigMock = vi.hoisted(() => vi.fn((a, b) => ({ ...a, ...b }))); -const globSyncMock = vi.hoisted(() => vi.fn()); -const getModuleNameMock = vi.hoisted(() => vi.fn()); -const scopedViteConfigPathMock = vi.hoisted(() => vi.fn(() => undefined)); -const logWarningMock = vi.hoisted(() => vi.fn()); -const existsSyncMock = vi.hoisted(() => vi.fn(() => true)); - -vi.mock("vite", () => ({ - build: buildMock, - mergeConfig: mergeConfigMock, -})); - -vi.mock("glob", () => ({ - glob: { - sync: globSyncMock, - }, -})); - -vi.mock("../../common/src/module/internal/getModuleConfig.js", () => ({ - getModuleName: getModuleNameMock, -})); - -vi.mock("../../util/viteConfig.js", () => ({ - scopedViteConfigPath: scopedViteConfigPathMock, -})); - -vi.mock("../../util/processEnvVariables.js", () => ({ - processEnvVariables: () => ({}), -})); - -vi.mock("../../common/src/module/internal/logger.js", () => ({ - createModuleLogger: () => ({ info: vi.fn() }), -})); - -vi.mock("vite-plugin-node-polyfills", () => ({ - nodePolyfills: () => ({ name: "mock-polyfill-plugin" }), -})); - -vi.mock("../../util/logError.js", () => ({ - logWarning: logWarningMock, - logErrorAndExit: (error: string) => { - throw new Error(error); - }, -})); - -vi.mock("node:fs", () => ({ - default: { - existsSync: existsSyncMock, - readFileSync: vi.fn(() => ""), - }, -})); - -describe("buildModules duplicate and collision handling", () => { - beforeEach(() => { - buildMock.mockReset(); - buildMock.mockResolvedValue(undefined); - mergeConfigMock.mockClear(); - globSyncMock.mockReset(); - getModuleNameMock.mockReset(); - scopedViteConfigPathMock.mockReset(); - scopedViteConfigPathMock.mockReturnValue(undefined); - logWarningMock.mockReset(); - existsSyncMock.mockReset(); - existsSyncMock.mockReturnValue(true); - }); - - it("keeps scoped module when scoped and unscoped names are the same", async () => { - const scopedModule = "/repo/src/modules/brand/widget/index.tsx"; - const unscopedModule = "/repo/src/modules/widget/index.tsx"; - - globSyncMock.mockImplementation((pattern: string) => { - if (pattern.includes("/brand/*/*.{jsx,tsx}")) { - return [scopedModule]; - } - if (pattern.includes("/modules/*/*.{jsx,tsx}")) { - return [unscopedModule]; - } - return []; - }); - - getModuleNameMock.mockReturnValue("SharedWidget"); - - const { buildModules } = await import("./plugin.js"); - const projectStructure = new ProjectStructure({ scope: "brand" }); - await buildModules(projectStructure); - - expect(buildMock).toHaveBeenCalledTimes(1); - const buildArg = buildMock.mock.calls[0][0]; - expect(buildArg.build.rollupOptions.input).toEqual(scopedModule); - expect(buildArg.build.rollupOptions.output.entryFileNames).toEqual("SharedWidget.umd.js"); - }); - - it("throws when duplicate module names exist in the same scope", async () => { - globSyncMock.mockImplementation((pattern: string) => { - if (pattern.includes("/brand/*/*.{jsx,tsx}")) { - return ["/repo/src/modules/brand/a/first.tsx", "/repo/src/modules/brand/b/second.tsx"]; - } - if (pattern.includes("/modules/*/*.{jsx,tsx}")) { - return []; - } - return []; - }); - - getModuleNameMock.mockReturnValue("DuplicateName"); - - const { buildModules } = await import("./plugin.js"); - const projectStructure = new ProjectStructure({ scope: "brand" }); - - await expect(buildModules(projectStructure)).rejects.toThrowError( - 'Duplicate module name "DuplicateName" found in:' - ); - expect(buildMock).not.toHaveBeenCalled(); - }); - - it("throws when distinct module names sanitize to the same output filename", async () => { - globSyncMock.mockImplementation((pattern: string) => { - if (pattern.includes("/modules/*/*.{jsx,tsx}")) { - return ["/repo/src/modules/a/first.tsx", "/repo/src/modules/b/second.tsx"]; - } - return []; - }); - - getModuleNameMock.mockReturnValueOnce("foo/bar").mockReturnValueOnce("foo-bar"); - - const { buildModules } = await import("./plugin.js"); - const projectStructure = new ProjectStructure(); - - await expect(buildModules(projectStructure)).rejects.toThrowError( - 'resolve to the same output filename "foo-bar.umd.js"' - ); - expect(buildMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/pages/src/vite-plugin/modules/plugin.ts b/packages/pages/src/vite-plugin/modules/plugin.ts index cb442aa0..154fdf66 100644 --- a/packages/pages/src/vite-plugin/modules/plugin.ts +++ b/packages/pages/src/vite-plugin/modules/plugin.ts @@ -20,7 +20,6 @@ import { scopedViteConfigPath } from "../../util/viteConfig.js"; type FileInfo = { path: string; name: string; - isScoped: boolean; }; export const buildModules = async (projectStructure: ProjectStructure): Promise => { @@ -32,13 +31,8 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const outdir = path.join(rootFolders.dist, subfolders.modules); const filepaths: { [s: string]: FileInfo } = {}; - const scopedModulesRoot = projectStructure.config.scope - ? path.resolve(rootFolders.source, subfolders.modules, projectStructure.config.scope) - : undefined; const modulePaths = projectStructure.getModulePaths(); modulePaths.forEach((modulePath) => { - const isScopedPath = - scopedModulesRoot !== undefined && path.resolve(modulePath.path) === scopedModulesRoot; glob .sync(convertToPosixPath(path.join(modulePath.path, "*/*.{jsx,tsx}")), { nodir: true, @@ -48,44 +42,16 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const moduleName = getModuleName(filepath); const { name } = path.parse(filepath); const resolvedModuleName = moduleName ?? name; - const existing = filepaths[resolvedModuleName]; + validateModuleNameForEntryFile(resolvedModuleName, filepath); // If that name exists already, don't overwrite the filepaths. // Example, if scope is declared, the scoped module's info should stay // in filepaths and not be overwritten by a non-scoped module. - if (existing) { - if (existing.isScoped && !isScopedPath) { - return; - } - if (!existing.isScoped && isScopedPath) { - filepaths[resolvedModuleName] = { path: filepath, name: name, isScoped: true }; - return; - } - logErrorAndExit( - `Duplicate module name "${resolvedModuleName}" found in:\n` + - `- ${existing.path}\n` + - `- ${filepath}\n` + - `Module names must be unique.` - ); + if (!(resolvedModuleName in filepaths)) { + filepaths[resolvedModuleName] = { path: filepath, name: name }; } - filepaths[resolvedModuleName] = { path: filepath, name: name, isScoped: isScopedPath }; }); }); - const moduleEntryNames = new Map(); - const moduleByEntryName = new Map(); - for (const moduleName of Object.keys(filepaths)) { - const entryName = sanitizeModuleEntryName(moduleName); - const existingModule = moduleByEntryName.get(entryName); - if (existingModule) { - logErrorAndExit( - `Module names "${existingModule}" and "${moduleName}" resolve to the same ` + - `output filename "${entryName}.umd.js". Rename modules to be unique.` - ); - } - moduleByEntryName.set(entryName, moduleName); - moduleEntryNames.set(moduleName, entryName); - } - const logger = createModuleLogger(); const loggerInfo = logger.info; @@ -101,8 +67,6 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const viteConfig = viteConfigModule ? (viteConfigModule.default as UserConfig) : undefined; for (const [moduleName, fileInfo] of Object.entries(filepaths)) { - const safeModuleEntryName = - moduleEntryNames.get(moduleName) ?? sanitizeModuleEntryName(moduleName); logger.info = (msg, options) => { if (msg.includes("building for production")) { loggerInfo(pc.green(`\nBuilding ${moduleName} module...`)); @@ -155,7 +119,7 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< input: fileInfo.path, output: { format: "umd", - entryFileNames: `${safeModuleEntryName}.umd.js`, + entryFileNames: `${moduleName}.umd.js`, }, }, reportCompressedSize: false, @@ -223,17 +187,22 @@ const shouldBundleModules = (projectStructure: ProjectStructure) => { }; /** - * Sanitizes module names used for entryFileNames to avoid path traversal and - * keep output files within the Rollup output directory. + * Guards Rollup output naming from path traversal by requiring module names + * to be a single filename segment (no separators, absolute, or parent paths). */ -const sanitizeModuleEntryName = (moduleName: string): string => { - const normalized = path.posix.normalize(moduleName.replace(/\\/g, "/")).replace(/^(\.\/)+/, ""); - const safeName = normalized - .split("/") - .filter((segment) => segment !== "" && segment !== "." && segment !== "..") - .join("-"); +const validateModuleNameForEntryFile = (moduleName: string, filepath: string): void => { + const normalized = path.posix.normalize(moduleName.replace(/\\/g, "/")); + const hasPathSeparator = moduleName.includes("/") || moduleName.includes("\\"); + const isAbsolute = normalized.startsWith("/") || /^[a-zA-Z]:/.test(moduleName); + const isDotOrParentPath = + normalized === "." || normalized === ".." || normalized.startsWith("../"); - return safeName.length > 0 ? safeName : "module"; + if (hasPathSeparator || isAbsolute || isDotOrParentPath) { + logErrorAndExit( + `Invalid module name "${moduleName}" in ${filepath}. ` + + `Module names must not contain path separators or relative path segments.` + ); + } }; /** From b89458517872a38f7f6af9c4f29b68e413187814 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Thu, 5 Mar 2026 11:25:21 -0500 Subject: [PATCH 7/8] restored formatting --- packages/pages/src/common/src/assets/getAssetsFilepath.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pages/src/common/src/assets/getAssetsFilepath.ts b/packages/pages/src/common/src/assets/getAssetsFilepath.ts index afdaa825..af6ed5fb 100644 --- a/packages/pages/src/common/src/assets/getAssetsFilepath.ts +++ b/packages/pages/src/common/src/assets/getAssetsFilepath.ts @@ -3,8 +3,8 @@ import { UserConfig } from "vite"; import { import_ } from "./import.js"; /** - * Determines the assets directory to use by checking vite.config.json's - * assetDir or default to "assets". + * Determines the assets directory to use by checking + * vite.config.json's assetDir or default to "assets". * @param defaultAssetsDir the default directory for assets * @param viteConfigPath the path to vite.config.js */ From 6daa5a09c1da01a1289e319076e0f3fc4d2abd48 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Thu, 5 Mar 2026 15:10:08 -0500 Subject: [PATCH 8/8] address comment --- packages/pages/src/vite-plugin/modules/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pages/src/vite-plugin/modules/plugin.ts b/packages/pages/src/vite-plugin/modules/plugin.ts index 154fdf66..30e1d0e6 100644 --- a/packages/pages/src/vite-plugin/modules/plugin.ts +++ b/packages/pages/src/vite-plugin/modules/plugin.ts @@ -30,7 +30,7 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< const { rootFolders, subfolders, envVarConfig } = projectStructure.config; const outdir = path.join(rootFolders.dist, subfolders.modules); - const filepaths: { [s: string]: FileInfo } = {}; + const filepaths: { [s: string]: FileInfo } = Object.create(null); const modulePaths = projectStructure.getModulePaths(); modulePaths.forEach((modulePath) => { glob @@ -46,7 +46,7 @@ export const buildModules = async (projectStructure: ProjectStructure): Promise< // If that name exists already, don't overwrite the filepaths. // Example, if scope is declared, the scoped module's info should stay // in filepaths and not be overwritten by a non-scoped module. - if (!(resolvedModuleName in filepaths)) { + if (!Object.prototype.hasOwnProperty.call(filepaths, resolvedModuleName)) { filepaths[resolvedModuleName] = { path: filepath, name: name }; } });