Error handling bugfix: pass #1
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"commander": "^14.0.2",
|
||||
"esbuild": "^0.27.4",
|
||||
"esbuild": "^0.28.2",
|
||||
"lightningcss-wasm": "^1.32.0",
|
||||
"lodash": "^4.17.23",
|
||||
"micromatch": "^4.0.8",
|
||||
@@ -45,57 +45,57 @@
|
||||
|
||||
"@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]7.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]8.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
|
||||
"@esbuild/android-arm": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/[email protected]7.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
|
||||
"@esbuild/android-x64": ["@esbuild/[email protected]8.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/[email protected]7.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/[email protected]8.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/[email protected]7.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/[email protected]8.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/[email protected]7.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/[email protected]8.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]7.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]8.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/[email protected]7.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/[email protected]8.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]7.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]8.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/[email protected]7.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/[email protected]8.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
|
||||
"@esbuild/win32-ia32": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/[email protected]7.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
|
||||
"@esbuild/win32-x64": ["@esbuild/[email protected]8.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
|
||||
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"esbuild": ["[email protected]7.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||
"esbuild": ["[email protected]8.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||
|
||||
"fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
|
||||
Vendored
+6
@@ -22,6 +22,12 @@ export default function () {
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||
await bunextInit({ build_only: true });
|
||||
// await pagesSSRBundler();
|
||||
// A failed bundle must fail the command: CI and scripts rely on
|
||||
// the exit code instead of grepping logs.
|
||||
if (global.BUNEXT_BUILD_FAILED) {
|
||||
log.error("Build failed with errors. Exiting.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
let bundler_ctx_json_str = JSON.stringify(global.BUNEXT_BUNDLER_CTX_MAP);
|
||||
writeFileSync(BUNDLER_CTX_MAP_JSON, bundler_ctx_json_str, "utf-8");
|
||||
|
||||
Vendored
+14
@@ -2,13 +2,23 @@ import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { rmSync } from "fs";
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
// The supervisor only restarts the dev server when this process actually
|
||||
// exits with a code. Registering a listener that merely logs (the previous
|
||||
// behavior) keeps Bun from exiting: the crash is swallowed, no error file is
|
||||
// written, and the supervisor never restarts the server. Log, record the real
|
||||
// error, and exit non-zero so recovery actually runs.
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error(`Uncaught exception: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
log.error(`Unhandled rejection: ${reason}`);
|
||||
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
log.info("Running development server ...");
|
||||
try {
|
||||
@@ -21,5 +31,9 @@ try {
|
||||
await startServer();
|
||||
}
|
||||
catch (error) {
|
||||
// Startup failures (e.g. port in use, config errors) must exit non-zero;
|
||||
// otherwise the process hangs alive with no server and no restart.
|
||||
log.error(`Failed to start development server: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Vendored
+9
-2
@@ -3,7 +3,7 @@ import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = grabDirNames();
|
||||
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
||||
let retries = 0;
|
||||
let timeout;
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -14,6 +14,10 @@ export default function () {
|
||||
try {
|
||||
rmSync(SSR_BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
rmSync(BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
// A stale error file from a previous session makes the bundler
|
||||
// run in "silent" mode and pages fail with misleading
|
||||
// "Application Error" messages until the first rebuild.
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
}
|
||||
catch (error) { }
|
||||
await dev();
|
||||
@@ -54,7 +58,10 @@ async function dev() {
|
||||
return await dev();
|
||||
}
|
||||
const exited = await dev_process.exited;
|
||||
if (exited) {
|
||||
// Restart on any numeric exit (0 included: the server should never exit
|
||||
// cleanly). A null exit means the child was killed by a signal (e.g.
|
||||
// Ctrl+C in the shared terminal), which we must not restart.
|
||||
if (exited != null) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
|
||||
Vendored
+9
-2
@@ -1,7 +1,9 @@
|
||||
import { Command } from "commander";
|
||||
import path from "path";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
let retries = 0;
|
||||
let timeout;
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -9,6 +11,9 @@ export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.action(async () => {
|
||||
// A stale error file from a previous session makes the bundler
|
||||
// run in "silent" mode and pages fail with misleading messages.
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
await start();
|
||||
});
|
||||
}
|
||||
@@ -47,7 +52,9 @@ async function start() {
|
||||
return await start();
|
||||
}
|
||||
const exited = await dev_process.exited;
|
||||
if (exited) {
|
||||
// Restart on any numeric exit (0 included). A null exit means the child
|
||||
// was killed by a signal, which we must not restart.
|
||||
if (exited != null) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
|
||||
Vendored
+21
@@ -1,6 +1,27 @@
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
// Same crash contract as the dev child: log the real error, record it in the
|
||||
// error file, and exit non-zero so the supervisor can restart and so the
|
||||
// bundler pipeline sees the failure.
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error(`Uncaught exception: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
log.error(`Unhandled rejection: ${reason}`);
|
||||
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
log.info("Starting production server ...");
|
||||
try {
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Failed to start production server: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+23
-7
@@ -7,11 +7,12 @@ import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import path from "path";
|
||||
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
||||
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||
import { existsSync } from "fs";
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
||||
import { logBuildErrorsOnce, } from "../../utils/build-error-log";
|
||||
import { log } from "../../utils/log";
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
export default async function allPagesESBuildContextBundler(params) {
|
||||
const _t0 = Date.now();
|
||||
try {
|
||||
const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
global.BUNEXT_PAGE_FILES = pages;
|
||||
const dev = isDevelopment();
|
||||
@@ -28,6 +29,9 @@ export default async function allPagesESBuildContextBundler(params) {
|
||||
entryToPage.set(entryFile, { ...page, tsx });
|
||||
}
|
||||
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
|
||||
// This is a brand-new context: wipe the previous context's failure
|
||||
// history so the "2 failed starts => dispose" guard starts fresh.
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
@@ -64,14 +68,26 @@ export default async function allPagesESBuildContextBundler(params) {
|
||||
"react/jsx-dev-runtime",
|
||||
...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
|
||||
],
|
||||
logLevel: did_process_exit_because_of_bundler_error
|
||||
? "silent"
|
||||
: undefined,
|
||||
// esbuild's own error logging prints per attempt; the
|
||||
// artifact-tracker plugin reports errors (deduped, once) itself.
|
||||
logLevel: "silent",
|
||||
});
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
catch (error) {
|
||||
console.log(`ESBUILD Error =>`, error);
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
// When the build fails with esbuild errors, the artifact-tracker's
|
||||
// onEnd already reported them (deduped). "Build failed with N errors"
|
||||
// is that same failure rethrown by esbuild's API — re-logging it here
|
||||
// would double every incident.
|
||||
if (String(error?.message || "").startsWith("Build failed with")) {
|
||||
return;
|
||||
}
|
||||
logBuildErrorsOnce([
|
||||
{
|
||||
text: `ESBUILD Error => ${error?.message || String(error)}`,
|
||||
},
|
||||
], (line) => log.error(line));
|
||||
}
|
||||
}
|
||||
function forceExternalReact() {
|
||||
|
||||
+9
-10
@@ -1,18 +1,17 @@
|
||||
export default async function buildOnstartErrorHandler(params) {
|
||||
// const error_msg = `Build Failed. Please check all your components and imports.`;
|
||||
// log.error(error_msg);
|
||||
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
return;
|
||||
}
|
||||
// console.log(`Killing Bundler ...`);
|
||||
// console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = true;
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
await Promise.all([
|
||||
global.BUNEXT_SSR_BUNDLER_CTX?.dispose(),
|
||||
global.BUNEXT_BUNDLER_CTX?.dispose(),
|
||||
]);
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||
// NOTE: must NOT dispose the currently-building context here. This
|
||||
// handler runs from the artifact-tracker plugin's onStart, i.e. while the
|
||||
// context's own build is in flight. esbuild's `dispose()` waits for the
|
||||
// in-flight build to finish, so disposing self = deadlock, which left the
|
||||
// dev server permanently stuck after a failed rebuild.
|
||||
//
|
||||
// Marking the context as disposed is enough: the next rebuild attempt
|
||||
// (watcher / fullRebuild) sees BUNEXT_BUNDLER_CTX_DISPOSED and recreates a
|
||||
// fresh context, whose cleanup disposes the old (now idle) one.
|
||||
}
|
||||
|
||||
+8
-1
@@ -10,6 +10,7 @@ import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||
import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { log } from "../../utils/log";
|
||||
import { logBuildErrorsOnce, } from "../../utils/build-error-log";
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
export default async function pagesSSRBundler(params) {
|
||||
const pages = grabAllPages({
|
||||
@@ -85,10 +86,16 @@ export default async function pagesSSRBundler(params) {
|
||||
...(config.ssr_compiler_excludes || []),
|
||||
],
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
logBuildErrorsOnce([
|
||||
{
|
||||
text: `SSR Bundler Error: ${error?.message || String(error)}`,
|
||||
},
|
||||
], (line) => log.error(line));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||
import _ from "lodash";
|
||||
import pagesSSRBundler from "../pages-ssr-bundler";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import { logBuildErrorsOnce, resetBuildErrorLog, } from "../../../utils/build-error-log";
|
||||
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import fullRebuild from "../../server/full-rebuild";
|
||||
import path from "path";
|
||||
@@ -28,12 +29,12 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
||||
});
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
log.error(`Build errors:`);
|
||||
for (const err of result.errors) {
|
||||
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||
}
|
||||
// Rebuild attempts repeat for every file edit / request
|
||||
// while the error persists; only log the error once.
|
||||
logBuildErrorsOnce(result.errors, (line) => log.error(` ${line}`));
|
||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||
try {
|
||||
@@ -62,6 +63,13 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
|
||||
}
|
||||
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
// A successful build clears the "last error" so a NEW incident
|
||||
// with the same signature is reported again.
|
||||
resetBuildErrorLog();
|
||||
if (result.warnings.length > 0) {
|
||||
logBuildErrorsOnce(result.warnings, (line) => log.warn(line));
|
||||
}
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
||||
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import { logBuildErrorsOnce, resetBuildErrorLog, } from "../../../utils/build-error-log";
|
||||
import { log } from "../../../utils/log";
|
||||
let build_start = 0;
|
||||
let build_starts = 0;
|
||||
const MAX_BUILD_STARTS = 2;
|
||||
@@ -23,6 +25,7 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
||||
});
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
try {
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||
@@ -30,9 +33,7 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
||||
catch { }
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
build_starts = 0;
|
||||
for (const err of result.errors) {
|
||||
console.error(`SSR Build error: ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||
}
|
||||
logBuildErrorsOnce(result.errors, (line) => log.error(`SSR Build error: ${line}`));
|
||||
return;
|
||||
}
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
@@ -63,6 +64,8 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
||||
}
|
||||
catch (error) { }
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
resetBuildErrorLog();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Vendored
+1
@@ -44,6 +44,7 @@ declare global {
|
||||
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||
var BUNEXT_REBUILD_RETRIES: number;
|
||||
var BUNEXT_IS_404_PAGE: boolean;
|
||||
var BUNEXT_BUILD_FAILED: boolean;
|
||||
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
||||
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -40,6 +40,7 @@ export default async function bunextInit(params) {
|
||||
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
||||
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
||||
global.BUNEXT_REBUILD_RETRIES = 0;
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
global.BUNEXT_PAGE_FILES = [];
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||
global.BUNEXT_DIR_NAMES = dirNames;
|
||||
|
||||
Vendored
+5
-1
@@ -14,6 +14,10 @@ export default async function grabConfig() {
|
||||
return config;
|
||||
}
|
||||
catch (error) {
|
||||
return undefined;
|
||||
// A config that fails to load (syntax error, bad export) must not be
|
||||
// silently replaced by defaults — the app would run with the wrong
|
||||
// configuration and nothing would explain why.
|
||||
const { CONFIG_FILE } = grabDirNames();
|
||||
exitWithError(`Failed to load config file \`${CONFIG_FILE}\`: ${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import handleFiles from "./handle-files";
|
||||
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import { log } from "../../utils/log";
|
||||
import fullRebuild from "./full-rebuild";
|
||||
const HMR_RETRY_COOLDOWN_MS = 5000;
|
||||
let lastHmrRetryTime = 0;
|
||||
@@ -67,12 +68,12 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Server Error: ${error?.message || error}\n${error?.stack || ""}`);
|
||||
if (is_dev) {
|
||||
return new Response(`Server Error: ${error.message}`, {
|
||||
return new Response(`Server Error: ${error?.message || error}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
console.error(`Server Error: ${error.message}`, error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
+26
-6
@@ -1,8 +1,31 @@
|
||||
import { log } from "../../utils/log";
|
||||
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
||||
import serverPostBuildFn from "./server-post-build-fn";
|
||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||
import { restartWatcher } from "./watcher-esbuild-ctx";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
// Single-flight: error recovery paths (grab-page-component retries, the
|
||||
// watcher, post-build HMR pushes) all call fullRebuild, and each rebuild
|
||||
// triggers serverPostBuildFn which can re-enter error handling. Without this
|
||||
// guard one broken page could fan out into cascading, overlapping rebuilds
|
||||
// that leave the dev server unresponsive. Concurrent callers await the same
|
||||
// in-flight rebuild instead of starting a new one.
|
||||
let rebuild_in_progress;
|
||||
export default async function fullRebuild(params) {
|
||||
if (rebuild_in_progress) {
|
||||
if (params?.msg) {
|
||||
log.watch(`Rebuild already in progress, skipping: ${params.msg}`);
|
||||
}
|
||||
return rebuild_in_progress;
|
||||
}
|
||||
const run = doFullRebuild(params).finally(() => {
|
||||
if (rebuild_in_progress === run) {
|
||||
rebuild_in_progress = undefined;
|
||||
}
|
||||
});
|
||||
rebuild_in_progress = run;
|
||||
return run;
|
||||
}
|
||||
async function doFullRebuild(params) {
|
||||
try {
|
||||
const { msg } = params || {};
|
||||
global.BUNEXT_RECOMPILING = true;
|
||||
@@ -24,14 +47,11 @@ export default async function fullRebuild(params) {
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
log.error(error);
|
||||
logBuildErrorsOnce([{ text: error?.message || String(error) }], (line) => log.error(line));
|
||||
}
|
||||
finally {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
}
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
restartWatcher();
|
||||
}
|
||||
|
||||
+46
-1
@@ -2,12 +2,14 @@ import grabRouteParams from "../../utils/grab-route-params";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import grabRouter from "../../utils/grab-router";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { log } from "../../utils/log";
|
||||
import _ from "lodash";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import grabRelativePath from "../../utils/grab-relative-path";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default async function ({ req }) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const is_dev = isDevelopment();
|
||||
const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = grabConstants();
|
||||
@@ -75,13 +77,42 @@ export default async function ({ req }) {
|
||||
},
|
||||
});
|
||||
}
|
||||
try {
|
||||
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Invalid JSON body for ${url.pathname}: ${error?.message}`);
|
||||
return Response.json({
|
||||
success: false,
|
||||
msg: "Invalid JSON body",
|
||||
}, {
|
||||
status: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const target_module = (module["default"] ||
|
||||
module["handler"]);
|
||||
const res = await target_module?.({
|
||||
let res;
|
||||
try {
|
||||
res = await target_module?.({
|
||||
...routeParams,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`);
|
||||
return Response.json({
|
||||
success: false,
|
||||
msg: `Route handler error: ${error?.message || "Internal Server Error"}`,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (res instanceof Response) {
|
||||
if (is_dev) {
|
||||
res.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
@@ -102,3 +133,17 @@ export default async function ({ req }) {
|
||||
}
|
||||
return Response.json({ err: `Route handler error` });
|
||||
}
|
||||
catch (error) {
|
||||
const url = new URL(req.url);
|
||||
log.error(`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`);
|
||||
return Response.json({
|
||||
success: false,
|
||||
msg: `Route handler error: ${error?.message || "Internal Server Error"}`,
|
||||
}, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ export default async function serverPostBuildFn(params) {
|
||||
req: mock_req,
|
||||
return_server_res_only: true,
|
||||
is_hydration: true,
|
||||
// This runs inside serverPostBuildFn (post-build of fullRebuild);
|
||||
// letting it trigger another rebuild/retry would cascade.
|
||||
no_retry: true,
|
||||
});
|
||||
if (page_component instanceof Response) {
|
||||
continue;
|
||||
|
||||
+7
-4
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import { log } from "../../utils/log";
|
||||
import serverParamsGen from "./server-params-gen";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||
import { restartWatcher, watcherRestartAvailable, } from "./watcher-esbuild-ctx";
|
||||
export default async function startServer() {
|
||||
const serverParams = await serverParamsGen();
|
||||
const server = Bun.serve(serverParams);
|
||||
@@ -10,10 +10,13 @@ export default async function startServer() {
|
||||
global.BUNEXT_SERVER = server;
|
||||
log.server(`http://${server.hostname}:${server.port}`);
|
||||
if (is_dev) {
|
||||
// Watchdog: periodically re-create the FS watcher in case it went
|
||||
// stale and stopped receiving events. Skipped while a recent restart
|
||||
// (e.g. by fullRebuild) is still fresh so the watcher isn't churned
|
||||
// twice in a row.
|
||||
setInterval(() => {
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
if (watcherRestartAvailable()) {
|
||||
restartWatcher();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
+11
@@ -1 +1,12 @@
|
||||
/**
|
||||
* Closes and re-creates the FS watcher. Deduped: callers that restart the
|
||||
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
|
||||
* trigger a single re-creation.
|
||||
*/
|
||||
export declare function restartWatcher(): void;
|
||||
/**
|
||||
* True when the watcher was last (re)created long enough ago that the
|
||||
* watchdog should consider re-creating it again.
|
||||
*/
|
||||
export declare function watcherRestartAvailable(): boolean;
|
||||
export default function watcherEsbuildCTX(): Promise<void>;
|
||||
|
||||
+40
-6
@@ -6,7 +6,33 @@ import { AppData } from "../../data/app-data";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
// Tracks the last watcher re-creation so the 5s watchdog in start-server
|
||||
// doesn't fight fullRebuild (which also restarts the watcher after a rebuild).
|
||||
let last_watcher_restart = 0;
|
||||
/**
|
||||
* Closes and re-creates the FS watcher. Deduped: callers that restart the
|
||||
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
|
||||
* trigger a single re-creation.
|
||||
*/
|
||||
export function restartWatcher() {
|
||||
if (!global.BUNEXT_PAGES_SRC_WATCHER)
|
||||
return;
|
||||
if (Date.now() - last_watcher_restart < 1000)
|
||||
return;
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
global.BUNEXT_PAGES_SRC_WATCHER = undefined;
|
||||
last_watcher_restart = Date.now();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
/**
|
||||
* True when the watcher was last (re)created long enough ago that the
|
||||
* watchdog should consider re-creating it again.
|
||||
*/
|
||||
export function watcherRestartAvailable() {
|
||||
return Date.now() - last_watcher_restart > 6000;
|
||||
}
|
||||
export default async function watcherEsbuildCTX() {
|
||||
const pages_src_watcher = watch(ROOT_DIR, {
|
||||
recursive: true,
|
||||
@@ -85,9 +111,16 @@ export default async function watcherEsbuildCTX() {
|
||||
if (filename.match(/.*\.server\.tsx?/)) {
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = true;
|
||||
}
|
||||
if (global.BUNEXT_BUNDLER_CTX) {
|
||||
if (global.BUNEXT_BUNDLER_CTX &&
|
||||
!global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
else if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
await fullRebuild({
|
||||
msg: `Restarting Bundler ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (filename.match(/(404|500)\.tsx?/)) {
|
||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||
@@ -129,7 +162,11 @@ export default async function watcherEsbuildCTX() {
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Watcher rebuild failed: ${error}`);
|
||||
logBuildErrorsOnce([
|
||||
{
|
||||
text: `Watcher rebuild failed: ${error?.message || String(error)}`,
|
||||
},
|
||||
], (line) => log.error(line));
|
||||
}
|
||||
finally {
|
||||
if (owns_recompile) {
|
||||
@@ -141,8 +178,5 @@ export default async function watcherEsbuildCTX() {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
|
||||
}
|
||||
function reloadWatcher() {
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
restartWatcher();
|
||||
}
|
||||
|
||||
+13
-1
@@ -79,11 +79,17 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
||||
console.info = () => { };
|
||||
console.debug = () => { };
|
||||
let htmlBody;
|
||||
let render_error;
|
||||
let has_render_error = false;
|
||||
try {
|
||||
const stream = await renderToReadableStream(final_component, {
|
||||
onError(error) {
|
||||
if (error.message.includes('unique "key" prop'))
|
||||
if (error?.message?.includes('unique "key" prop'))
|
||||
return;
|
||||
if (!has_render_error) {
|
||||
has_render_error = true;
|
||||
render_error = error;
|
||||
}
|
||||
originalConsole.error(error);
|
||||
},
|
||||
});
|
||||
@@ -92,6 +98,12 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
|
||||
finally {
|
||||
Object.assign(console, originalConsole);
|
||||
}
|
||||
// A component crash during SSR must surface as a proper error (handled
|
||||
// upstream as a 500 page). React only reports these to `onError`; without
|
||||
// rethrowing, the user gets a 200 with broken/blank markup.
|
||||
if (has_render_error) {
|
||||
throw render_error;
|
||||
}
|
||||
html += htmlBody;
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ type Params = {
|
||||
file_path?: string;
|
||||
debug?: boolean;
|
||||
retry?: boolean;
|
||||
/**
|
||||
* Skip the dev auto-rebuild/retry recovery path. Used by internal callers
|
||||
* (e.g. HMR post-build) where triggering yet another fullRebuild on top
|
||||
* of the rebuild that's already in progress would cascade.
|
||||
*/
|
||||
no_retry?: boolean;
|
||||
return_server_res_only?: boolean;
|
||||
skip_server_res?: boolean;
|
||||
is_hydration?: boolean;
|
||||
|
||||
+21
-8
@@ -10,6 +10,7 @@ import isDevelopment from "../../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, ROOT_DIR } = grabDirNames();
|
||||
class NotFoundError extends Error {
|
||||
status = 404;
|
||||
@@ -19,7 +20,7 @@ class NotFoundError extends Error {
|
||||
}
|
||||
}
|
||||
export default async function grabPageComponent(params) {
|
||||
const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, } = params;
|
||||
const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, no_retry, } = params;
|
||||
const url = req?.url ? new URL(req.url) : undefined;
|
||||
const router = global.BUNEXT_ROUTER;
|
||||
const is_dev = isDevelopment();
|
||||
@@ -59,14 +60,20 @@ export default async function grabPageComponent(params) {
|
||||
}
|
||||
let retries = 0;
|
||||
const MAX_RETRIES = 2;
|
||||
while (retries < MAX_RETRIES) {
|
||||
// Only retry transient failures: if the last rebuild failed
|
||||
// (BUNEXT_BUILD_FAILED), the file is broken and more rebuilds
|
||||
// will just repeat the same error.
|
||||
while (retries < MAX_RETRIES &&
|
||||
!global.BUNEXT_BUILD_FAILED) {
|
||||
await fullRebuild({
|
||||
msg: `Retrying Bundle map for file \`${relative_file_path}\``,
|
||||
});
|
||||
await Bun.sleep(1000);
|
||||
bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
||||
bundledMap =
|
||||
global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
||||
if (bundledMap?.path)
|
||||
break;
|
||||
retries++;
|
||||
}
|
||||
if (!bundledMap?.path) {
|
||||
const errMsg = `No Bundled File Path for this request path!`;
|
||||
@@ -116,8 +123,11 @@ export default async function grabPageComponent(params) {
|
||||
const is404 = error instanceof NotFoundError ||
|
||||
error?.name === "NotFoundError" ||
|
||||
error?.status === 404;
|
||||
if (!params.retry && is_dev) {
|
||||
while (global.BUNEXT_REBUILD_RETRIES < 2) {
|
||||
if (!params.retry && !params.no_retry && is_dev) {
|
||||
// Skip the retry cascade when the last rebuild failed: the error
|
||||
// is persistent and more rebuilds would repeat it endlessly.
|
||||
while (!global.BUNEXT_BUILD_FAILED &&
|
||||
global.BUNEXT_REBUILD_RETRIES < 2) {
|
||||
global.BUNEXT_REBUILD_RETRIES =
|
||||
global.BUNEXT_REBUILD_RETRIES + 1;
|
||||
await fullRebuild();
|
||||
@@ -139,9 +149,12 @@ export default async function grabPageComponent(params) {
|
||||
global.BUNEXT_IS_404_PAGE = true;
|
||||
}
|
||||
else {
|
||||
log.error(`Error Grabbing Page Component: ${error.message}`);
|
||||
log.error(`Page: ${passed_file_path || url?.pathname}`);
|
||||
if (is_dev) {
|
||||
logBuildErrorsOnce([
|
||||
{
|
||||
text: `Error Grabbing Page Component: ${error.message}; Page: ${passed_file_path || url?.pathname}`,
|
||||
},
|
||||
], (line) => log.error(line));
|
||||
if (is_dev && !params.no_retry && !global.BUNEXT_BUILD_FAILED) {
|
||||
fullRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import { log } from "../../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||
import getCache from "../../cache/get-cache";
|
||||
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
|
||||
import grabPageComponent from "./grab-page-component";
|
||||
@@ -32,7 +33,11 @@ export default async function handleWebPages({ req, }) {
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Error Handling Web Page: ${error.message}`, error);
|
||||
logBuildErrorsOnce([
|
||||
{
|
||||
text: `Error Handling Web Page: ${error?.message || String(error)}`,
|
||||
},
|
||||
], (line) => log.error(line));
|
||||
const componentRes = await grabPageErrorComponent({
|
||||
error,
|
||||
});
|
||||
|
||||
Vendored
+22
-4
@@ -1,4 +1,22 @@
|
||||
export default function writeErrorFile({ exitCode, error, }: {
|
||||
error?: Bun.ErrorLike;
|
||||
exitCode: number | null;
|
||||
}): void;
|
||||
type Params = {
|
||||
/**
|
||||
* Only include when the exiting process actually crashed with an error.
|
||||
* Bun's spawn `onExit` `error` is only set on spawn failures, so callers
|
||||
* that DO have the error (crash handlers) should pass `force: true`.
|
||||
*/
|
||||
force?: boolean;
|
||||
error?: unknown;
|
||||
exitCode?: number | null;
|
||||
};
|
||||
/**
|
||||
* Writes the `BUNDLER_EXIT.txt` flag that the bundler pipeline watches to
|
||||
* detect that the dev/prod child process previously crashed.
|
||||
*
|
||||
* Guardrails:
|
||||
* - Never writes for clean exits (exitCode === 0) or signal deaths where no
|
||||
* error is available (Ctrl+C must not leave a stale error file behind).
|
||||
* - The supervisor writes only when the file does not already exist, so the
|
||||
* crash handler's richer error/stack payload is not clobbered.
|
||||
*/
|
||||
export default function writeErrorFile({ force, error, exitCode, }: Params): void;
|
||||
export {};
|
||||
|
||||
Vendored
+34
-13
@@ -1,21 +1,42 @@
|
||||
import path from "path";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
export default function writeErrorFile({ exitCode, error, }) {
|
||||
/**
|
||||
* Writes the `BUNDLER_EXIT.txt` flag that the bundler pipeline watches to
|
||||
* detect that the dev/prod child process previously crashed.
|
||||
*
|
||||
* Guardrails:
|
||||
* - Never writes for clean exits (exitCode === 0) or signal deaths where no
|
||||
* error is available (Ctrl+C must not leave a stale error file behind).
|
||||
* - The supervisor writes only when the file does not already exist, so the
|
||||
* crash handler's richer error/stack payload is not clobbered.
|
||||
*/
|
||||
export default function writeErrorFile({ force, error, exitCode, }) {
|
||||
const has_error = error != null;
|
||||
const abnormal_exit = exitCode != null && exitCode !== 0;
|
||||
if (!has_error && !abnormal_exit)
|
||||
return;
|
||||
if (!force && existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE))
|
||||
return;
|
||||
const err = error instanceof Error
|
||||
? error
|
||||
: typeof error === "string"
|
||||
? new Error(error)
|
||||
: undefined;
|
||||
const message = err?.message || "Unknown error";
|
||||
const stack = err?.stack || "N/A";
|
||||
let txt = ``;
|
||||
txt += `Bunext Error\n`;
|
||||
txt += `============================================\n`;
|
||||
txt += `ERROR: ${error?.message}\n`;
|
||||
txt += `EXIT_CODE: ${exitCode || 0}\n`;
|
||||
txt += `CALL_STACK: ${error?.stack}\n`;
|
||||
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), { recursive: true });
|
||||
txt += `ERROR: ${message}\n`;
|
||||
txt += `EXIT_CODE: ${exitCode ?? "N/A"}\n`;
|
||||
txt += `CALL_STACK:\n${stack}\n`;
|
||||
try {
|
||||
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(BUNX_BUNDLER_ERROR_EXIT_FILE, txt);
|
||||
}
|
||||
// log.info("Running development server ...");
|
||||
// try {
|
||||
// rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
// } catch (error) {}
|
||||
// await bunextInit();
|
||||
// await startServer();
|
||||
catch { }
|
||||
}
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
export type BuildErrorLike = {
|
||||
text?: string;
|
||||
message?: string;
|
||||
id?: string;
|
||||
location?: {
|
||||
file?: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
} | null;
|
||||
};
|
||||
/**
|
||||
* Logs each error once — identical error sets are suppressed until the
|
||||
* signature changes or `resetBuildErrorLog()` is called (after a successful
|
||||
* build). Returns true when something was logged.
|
||||
*/
|
||||
export declare function logBuildErrorsOnce(errors: BuildErrorLike[], logLine?: (line: string) => void): boolean;
|
||||
/**
|
||||
* Clears the dedup state so a new incident with the same signature is logged
|
||||
* again. Call after a successful build.
|
||||
*/
|
||||
export declare function resetBuildErrorLog(): void;
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
// Logs build/render errors exactly once while the same error state persists.
|
||||
//
|
||||
// Dev recovery paths (watcher, request retries, post-build HMR) re-attempt a
|
||||
// rebuild for every file edit and request while a syntax error is still in the
|
||||
// file. Without deduplication that one error is printed again on every
|
||||
// attempt — an endless spiral of identical log lines. The signature is
|
||||
// cleared after a successful build, so the same error later (after a fix and
|
||||
// regression) is logged again as a new incident.
|
||||
let last_build_error_signature;
|
||||
function errorSignature(errors) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const text = e.text || e.message || e.id || "";
|
||||
const loc = e.location
|
||||
? `${e.location.file}:${e.location.line}:${e.location.column}`
|
||||
: "";
|
||||
return `${text}${loc}`;
|
||||
})
|
||||
.join("\n---\n");
|
||||
}
|
||||
/**
|
||||
* Logs each error once — identical error sets are suppressed until the
|
||||
* signature changes or `resetBuildErrorLog()` is called (after a successful
|
||||
* build). Returns true when something was logged.
|
||||
*/
|
||||
export function logBuildErrorsOnce(errors, logLine = (line) => console.error(line)) {
|
||||
if (!errors || errors.length === 0)
|
||||
return false;
|
||||
const signature = errorSignature(errors);
|
||||
if (signature === last_build_error_signature)
|
||||
return false;
|
||||
last_build_error_signature = signature;
|
||||
for (const e of errors) {
|
||||
const text = e.text || e.message || e.id || "Build Error";
|
||||
const loc = e.location
|
||||
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
||||
: "";
|
||||
logLine(`${text}${loc}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Clears the dedup state so a new incident with the same signature is logged
|
||||
* again. Call after a successful build.
|
||||
*/
|
||||
export function resetBuildErrorLog() {
|
||||
last_build_error_signature = undefined;
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"commander": "^14.0.2",
|
||||
"esbuild": "^0.27.4",
|
||||
"esbuild": "^0.28.2",
|
||||
"lightningcss-wasm": "^1.32.0",
|
||||
"lodash": "^4.17.23",
|
||||
"micromatch": "^4.0.8",
|
||||
|
||||
@@ -34,6 +34,13 @@ export default function () {
|
||||
await bunextInit({ build_only: true });
|
||||
// await pagesSSRBundler();
|
||||
|
||||
// A failed bundle must fail the command: CI and scripts rely on
|
||||
// the exit code instead of grepping logs.
|
||||
if (global.BUNEXT_BUILD_FAILED) {
|
||||
log.error("Build failed with errors. Exiting.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
let bundler_ctx_json_str = JSON.stringify(
|
||||
global.BUNEXT_BUNDLER_CTX_MAP,
|
||||
|
||||
@@ -2,16 +2,26 @@ import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { rmSync } from "fs";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
// The supervisor only restarts the dev server when this process actually
|
||||
// exits with a code. Registering a listener that merely logs (the previous
|
||||
// behavior) keeps Bun from exiting: the crash is swallowed, no error file is
|
||||
// written, and the supervisor never restarts the server. Log, record the real
|
||||
// error, and exit non-zero so recovery actually runs.
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error(`Uncaught exception: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
log.error(`Unhandled rejection: ${reason}`);
|
||||
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
log.info("Running development server ...");
|
||||
@@ -25,5 +35,9 @@ try {
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
} catch (error) {
|
||||
// Startup failures (e.g. port in use, config errors) must exit non-zero;
|
||||
// otherwise the process hangs alive with no server and no restart.
|
||||
log.error(`Failed to start development server: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
|
||||
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = grabDirNames();
|
||||
const {
|
||||
BUNDLER_CTX_MAP_JSON,
|
||||
SSR_BUNDLER_CTX_MAP_JSON,
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE,
|
||||
} = grabDirNames();
|
||||
|
||||
let retries = 0;
|
||||
let timeout: any;
|
||||
@@ -18,6 +22,10 @@ export default function () {
|
||||
try {
|
||||
rmSync(SSR_BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
rmSync(BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
// A stale error file from a previous session makes the bundler
|
||||
// run in "silent" mode and pages fail with misleading
|
||||
// "Application Error" messages until the first rebuild.
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
} catch (error) {}
|
||||
|
||||
await dev();
|
||||
@@ -65,7 +73,10 @@ async function dev() {
|
||||
|
||||
const exited = await dev_process.exited;
|
||||
|
||||
if (exited) {
|
||||
// Restart on any numeric exit (0 included: the server should never exit
|
||||
// cleanly). A null exit means the child was killed by a signal (e.g.
|
||||
// Ctrl+C in the shared terminal), which we must not restart.
|
||||
if (exited != null) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
|
||||
@@ -2,7 +2,10 @@ import { Command } from "commander";
|
||||
import path from "path";
|
||||
import type { BunSpawnOptions } from "../../types";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
let retries = 0;
|
||||
let timeout: any;
|
||||
@@ -12,6 +15,10 @@ export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.action(async () => {
|
||||
// A stale error file from a previous session makes the bundler
|
||||
// run in "silent" mode and pages fail with misleading messages.
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
|
||||
await start();
|
||||
});
|
||||
}
|
||||
@@ -59,7 +66,9 @@ async function start() {
|
||||
|
||||
const exited = await dev_process.exited;
|
||||
|
||||
if (exited) {
|
||||
// Restart on any numeric exit (0 included). A null exit means the child
|
||||
// was killed by a signal, which we must not restart.
|
||||
if (exited != null) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
|
||||
// Same crash contract as the dev child: log the real error, record it in the
|
||||
// error file, and exit non-zero so the supervisor can restart and so the
|
||||
// bundler pipeline sees the failure.
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error(`Uncaught exception: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
log.error(`Unhandled rejection: ${reason}`);
|
||||
writeErrorFile({ error: reason, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
log.info("Starting production server ...");
|
||||
|
||||
try {
|
||||
await bunextInit();
|
||||
|
||||
await startServer();
|
||||
} catch (error) {
|
||||
log.error(`Failed to start production server: ${error}`);
|
||||
writeErrorFile({ error, exitCode: 1, force: true });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import type { BundlerCTXMap, PageFiles } from "../../types";
|
||||
import path from "path";
|
||||
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
||||
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||
import { existsSync } from "fs";
|
||||
import {
|
||||
logBuildErrorsOnce,
|
||||
} from "../../utils/build-error-log";
|
||||
import { log } from "../../utils/log";
|
||||
|
||||
const {
|
||||
HYDRATION_DST_DIR,
|
||||
BUNX_HYDRATION_SRC_DIR,
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE,
|
||||
} = grabDirNames();
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
post_build_fn?: (params: {
|
||||
@@ -25,11 +24,8 @@ type Params = {
|
||||
};
|
||||
|
||||
export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
const _t0 = Date.now();
|
||||
try {
|
||||
const did_process_exit_because_of_bundler_error = existsSync(
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE,
|
||||
);
|
||||
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
|
||||
global.BUNEXT_PAGE_FILES = pages;
|
||||
@@ -60,6 +56,10 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
(e) => `hydration-virtual:${e}`,
|
||||
);
|
||||
|
||||
// This is a brand-new context: wipe the previous context's failure
|
||||
// history so the "2 failed starts => dispose" guard starts fresh.
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
|
||||
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
@@ -98,14 +98,33 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
"react/jsx-dev-runtime",
|
||||
...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
|
||||
],
|
||||
logLevel: did_process_exit_because_of_bundler_error
|
||||
? "silent"
|
||||
: undefined,
|
||||
// esbuild's own error logging prints per attempt; the
|
||||
// artifact-tracker plugin reports errors (deduped, once) itself.
|
||||
logLevel: "silent",
|
||||
});
|
||||
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
} catch (error) {
|
||||
console.log(`ESBUILD Error =>`, error);
|
||||
} catch (error: any) {
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
|
||||
// When the build fails with esbuild errors, the artifact-tracker's
|
||||
// onEnd already reported them (deduped). "Build failed with N errors"
|
||||
// is that same failure rethrown by esbuild's API — re-logging it here
|
||||
// would double every incident.
|
||||
if (String(error?.message || "").startsWith("Build failed with")) {
|
||||
return;
|
||||
}
|
||||
|
||||
logBuildErrorsOnce(
|
||||
[
|
||||
{
|
||||
text: `ESBUILD Error => ${
|
||||
error?.message || String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
type Params = {};
|
||||
|
||||
export default async function buildOnstartErrorHandler(params?: Params) {
|
||||
// const error_msg = `Build Failed. Please check all your components and imports.`;
|
||||
// log.error(error_msg);
|
||||
|
||||
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`Killing Bundler ...`);
|
||||
// console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
|
||||
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = true;
|
||||
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
|
||||
await Promise.all([
|
||||
global.BUNEXT_SSR_BUNDLER_CTX?.dispose(),
|
||||
global.BUNEXT_BUNDLER_CTX?.dispose(),
|
||||
]);
|
||||
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||
// NOTE: must NOT dispose the currently-building context here. This
|
||||
// handler runs from the artifact-tracker plugin's onStart, i.e. while the
|
||||
// context's own build is in flight. esbuild's `dispose()` waits for the
|
||||
// in-flight build to finish, so disposing self = deadlock, which left the
|
||||
// dev server permanently stuck after a failed rebuild.
|
||||
//
|
||||
// Marking the context as disposed is enough: the next rebuild attempt
|
||||
// (watcher / fullRebuild) sees BUNEXT_BUNDLER_CTX_DISPOSED and recreates a
|
||||
// fresh context, whose cleanup disposes the old (now idle) one.
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||
import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { log } from "../../utils/log";
|
||||
import {
|
||||
logBuildErrorsOnce,
|
||||
} from "../../utils/build-error-log";
|
||||
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
@@ -114,9 +117,20 @@ export default async function pagesSSRBundler(params?: Params) {
|
||||
...(config.ssr_compiler_excludes || []),
|
||||
],
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
logBuildErrorsOnce(
|
||||
[
|
||||
{
|
||||
text: `SSR Bundler Error: ${
|
||||
error?.message || String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||
import _ from "lodash";
|
||||
import pagesSSRBundler from "../pages-ssr-bundler";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import {
|
||||
logBuildErrorsOnce,
|
||||
resetBuildErrorLog,
|
||||
} from "../../../utils/build-error-log";
|
||||
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import fullRebuild from "../../server/full-rebuild";
|
||||
import path from "path";
|
||||
@@ -55,15 +59,15 @@ export default function esbuildCTXArtifactTracker({
|
||||
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
|
||||
log.error(`Build errors:`);
|
||||
for (const err of result.errors) {
|
||||
log.error(
|
||||
` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`,
|
||||
// Rebuild attempts repeat for every file edit / request
|
||||
// while the error persists; only log the error once.
|
||||
logBuildErrorsOnce(result.errors, (line) =>
|
||||
log.error(` ${line}`),
|
||||
);
|
||||
}
|
||||
|
||||
for (
|
||||
let i = global.BUNEXT_HMR_CONTROLLERS.length - 1;
|
||||
@@ -114,6 +118,17 @@ export default function esbuildCTXArtifactTracker({
|
||||
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
|
||||
// A successful build clears the "last error" so a NEW incident
|
||||
// with the same signature is reported again.
|
||||
resetBuildErrorLog();
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
logBuildErrorsOnce(result.warnings, (line) =>
|
||||
log.warn(line),
|
||||
);
|
||||
}
|
||||
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import {
|
||||
logBuildErrorsOnce,
|
||||
resetBuildErrorLog,
|
||||
} from "../../../utils/build-error-log";
|
||||
import { log } from "../../../utils/log";
|
||||
|
||||
let build_start = 0;
|
||||
let build_starts = 0;
|
||||
@@ -41,17 +46,16 @@ export default function ssrCTXArtifactTracker({
|
||||
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_BUILD_FAILED = true;
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
try {
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||
} catch {}
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
build_starts = 0;
|
||||
for (const err of result.errors) {
|
||||
console.error(
|
||||
`SSR Build error: ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`,
|
||||
logBuildErrorsOnce(result.errors, (line) =>
|
||||
log.error(`SSR Build error: ${line}`),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,6 +104,8 @@ export default function ssrCTXArtifactTracker({
|
||||
} catch (error) {}
|
||||
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
resetBuildErrorLog();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ declare global {
|
||||
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||
var BUNEXT_REBUILD_RETRIES: number;
|
||||
var BUNEXT_IS_404_PAGE: boolean;
|
||||
var BUNEXT_BUILD_FAILED: boolean;
|
||||
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
||||
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
||||
}
|
||||
@@ -101,6 +102,7 @@ export default async function bunextInit(params?: Params) {
|
||||
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
||||
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
||||
global.BUNEXT_REBUILD_RETRIES = 0;
|
||||
global.BUNEXT_BUILD_FAILED = false;
|
||||
global.BUNEXT_PAGE_FILES = [];
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set<string>();
|
||||
global.BUNEXT_DIR_NAMES = dirNames;
|
||||
|
||||
@@ -20,7 +20,15 @@ export default async function grabConfig(): Promise<BunextConfig | undefined> {
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
} catch (error: any) {
|
||||
// A config that fails to load (syntax error, bad export) must not be
|
||||
// silently replaced by defaults — the app would run with the wrong
|
||||
// configuration and nothing would explain why.
|
||||
const { CONFIG_FILE } = grabDirNames();
|
||||
exitWithError(
|
||||
`Failed to load config file \`${CONFIG_FILE}\`: ${
|
||||
error?.message || error
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import handleFiles from "./handle-files";
|
||||
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import { log } from "../../utils/log";
|
||||
import fullRebuild from "./full-rebuild";
|
||||
|
||||
const HMR_RETRY_COOLDOWN_MS = 5000;
|
||||
@@ -86,12 +87,15 @@ export default async function bunextRequestHandler({
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
log.error(
|
||||
`Server Error: ${error?.message || error}\n${error?.stack || ""}`,
|
||||
);
|
||||
|
||||
if (is_dev) {
|
||||
return new Response(`Server Error: ${error.message}`, {
|
||||
return new Response(`Server Error: ${error?.message || error}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
console.error(`Server Error: ${error.message}`, error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
import { log } from "../../utils/log";
|
||||
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
||||
import serverPostBuildFn from "./server-post-build-fn";
|
||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||
import { restartWatcher } from "./watcher-esbuild-ctx";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
|
||||
// Single-flight: error recovery paths (grab-page-component retries, the
|
||||
// watcher, post-build HMR pushes) all call fullRebuild, and each rebuild
|
||||
// triggers serverPostBuildFn which can re-enter error handling. Without this
|
||||
// guard one broken page could fan out into cascading, overlapping rebuilds
|
||||
// that leave the dev server unresponsive. Concurrent callers await the same
|
||||
// in-flight rebuild instead of starting a new one.
|
||||
let rebuild_in_progress: Promise<void> | undefined;
|
||||
|
||||
export default async function fullRebuild(params?: { msg?: string }) {
|
||||
if (rebuild_in_progress) {
|
||||
if (params?.msg) {
|
||||
log.watch(`Rebuild already in progress, skipping: ${params.msg}`);
|
||||
}
|
||||
return rebuild_in_progress;
|
||||
}
|
||||
|
||||
const run = doFullRebuild(params).finally(() => {
|
||||
if (rebuild_in_progress === run) {
|
||||
rebuild_in_progress = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
rebuild_in_progress = run;
|
||||
return run;
|
||||
}
|
||||
|
||||
async function doFullRebuild(params?: { msg?: string }) {
|
||||
try {
|
||||
const { msg } = params || {};
|
||||
|
||||
@@ -29,14 +56,14 @@ export default async function fullRebuild(params?: { msg?: string }) {
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
log.error(error);
|
||||
logBuildErrorsOnce(
|
||||
[{ text: error?.message || String(error) }],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
} finally {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
}
|
||||
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
restartWatcher();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import grabRouteParams from "../../utils/grab-route-params";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import grabRouter from "../../utils/grab-router";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { log } from "../../utils/log";
|
||||
import _ from "lodash";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
@@ -19,10 +20,12 @@ type Params = {
|
||||
};
|
||||
|
||||
export default async function ({ req }: Params): Promise<Response> {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const is_dev = isDevelopment();
|
||||
|
||||
const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = grabConstants();
|
||||
const { MBInBytes, ServerDefaultRequestBodyLimitBytes } =
|
||||
grabConstants();
|
||||
|
||||
const router = grabRouter();
|
||||
|
||||
@@ -114,15 +117,54 @@ export default async function ({ req }: Params): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
|
||||
try {
|
||||
routeParams.body = JSON.parse(
|
||||
new TextDecoder().decode(body) || "{}",
|
||||
);
|
||||
} catch (error: any) {
|
||||
log.error(
|
||||
`Invalid JSON body for ${url.pathname}: ${error?.message}`,
|
||||
);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
msg: "Invalid JSON body",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const target_module = (module["default"] ||
|
||||
module["handler"]) as BunextAPIRouteHandler;
|
||||
|
||||
const res = await target_module?.({
|
||||
let res: any;
|
||||
try {
|
||||
res = await target_module?.({
|
||||
...routeParams,
|
||||
});
|
||||
} catch (error: any) {
|
||||
log.error(
|
||||
`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`,
|
||||
);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
msg: `Route handler error: ${error?.message || "Internal Server Error"}`,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (res instanceof Response) {
|
||||
if (is_dev) {
|
||||
@@ -146,11 +188,31 @@ export default async function ({ req }: Params): Promise<Response> {
|
||||
);
|
||||
|
||||
if (res.bunext_api_route_res_transform_fn) {
|
||||
final_res = await res.bunext_api_route_res_transform_fn(final_res);
|
||||
final_res = await res.bunext_api_route_res_transform_fn(
|
||||
final_res,
|
||||
);
|
||||
}
|
||||
|
||||
return final_res;
|
||||
}
|
||||
|
||||
return Response.json({ err: `Route handler error` });
|
||||
} catch (error: any) {
|
||||
const url = new URL(req.url);
|
||||
log.error(
|
||||
`API Route Error (${url.pathname}): ${error?.message}\n${error?.stack || ""}`,
|
||||
);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
msg: `Route handler error: ${error?.message || "Internal Server Error"}`,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ export default async function serverPostBuildFn(params?: Params) {
|
||||
req: mock_req,
|
||||
return_server_res_only: true,
|
||||
is_hydration: true,
|
||||
// This runs inside serverPostBuildFn (post-build of fullRebuild);
|
||||
// letting it trigger another rebuild/retry would cascade.
|
||||
no_retry: true,
|
||||
});
|
||||
|
||||
if (page_component instanceof Response) {
|
||||
|
||||
@@ -2,7 +2,10 @@ import _ from "lodash";
|
||||
import { log } from "../../utils/log";
|
||||
import serverParamsGen from "./server-params-gen";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||
import {
|
||||
restartWatcher,
|
||||
watcherRestartAvailable,
|
||||
} from "./watcher-esbuild-ctx";
|
||||
|
||||
export default async function startServer() {
|
||||
const serverParams = await serverParamsGen();
|
||||
@@ -15,10 +18,13 @@ export default async function startServer() {
|
||||
log.server(`http://${server.hostname}:${server.port}`);
|
||||
|
||||
if (is_dev) {
|
||||
// Watchdog: periodically re-create the FS watcher in case it went
|
||||
// stale and stopped receiving events. Skipped while a recent restart
|
||||
// (e.g. by fullRebuild) is still fresh so the watcher isn't churned
|
||||
// twice in a row.
|
||||
setInterval(() => {
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
if (watcherRestartAvailable()) {
|
||||
restartWatcher();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,38 @@ import { AppData } from "../../data/app-data";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
|
||||
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
// Tracks the last watcher re-creation so the 5s watchdog in start-server
|
||||
// doesn't fight fullRebuild (which also restarts the watcher after a rebuild).
|
||||
let last_watcher_restart = 0;
|
||||
|
||||
/**
|
||||
* Closes and re-creates the FS watcher. Deduped: callers that restart the
|
||||
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
|
||||
* trigger a single re-creation.
|
||||
*/
|
||||
export function restartWatcher() {
|
||||
if (!global.BUNEXT_PAGES_SRC_WATCHER) return;
|
||||
|
||||
if (Date.now() - last_watcher_restart < 1000) return;
|
||||
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
global.BUNEXT_PAGES_SRC_WATCHER = undefined;
|
||||
last_watcher_restart = Date.now();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the watcher was last (re)created long enough ago that the
|
||||
* watchdog should consider re-creating it again.
|
||||
*/
|
||||
export function watcherRestartAvailable() {
|
||||
return Date.now() - last_watcher_restart > 6000;
|
||||
}
|
||||
|
||||
export default async function watcherEsbuildCTX() {
|
||||
const pages_src_watcher = watch(
|
||||
ROOT_DIR,
|
||||
@@ -114,8 +143,16 @@ export default async function watcherEsbuildCTX() {
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = true;
|
||||
}
|
||||
|
||||
if (global.BUNEXT_BUNDLER_CTX) {
|
||||
if (
|
||||
global.BUNEXT_BUNDLER_CTX &&
|
||||
!global.BUNEXT_BUNDLER_CTX_DISPOSED
|
||||
) {
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
} else if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
await fullRebuild({
|
||||
msg: `Restarting Bundler ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (filename.match(/(404|500)\.tsx?/)) {
|
||||
@@ -169,8 +206,17 @@ export default async function watcherEsbuildCTX() {
|
||||
await fullRebuild({
|
||||
msg: `${type} ${action}: ${filename}. Rebuilding ...`,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Watcher rebuild failed: ${error}`);
|
||||
} catch (error: any) {
|
||||
logBuildErrorsOnce(
|
||||
[
|
||||
{
|
||||
text: `Watcher rebuild failed: ${
|
||||
error?.message || String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
} finally {
|
||||
if (owns_recompile) {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
@@ -184,8 +230,5 @@ export default async function watcherEsbuildCTX() {
|
||||
}
|
||||
|
||||
function reloadWatcher() {
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
restartWatcher();
|
||||
}
|
||||
|
||||
@@ -185,10 +185,19 @@ export default async function genWebHTML({
|
||||
console.debug = () => {};
|
||||
|
||||
let htmlBody: string;
|
||||
let render_error: any;
|
||||
let has_render_error = false;
|
||||
|
||||
try {
|
||||
const stream = await renderToReadableStream(final_component, {
|
||||
onError(error: any) {
|
||||
if (error.message.includes('unique "key" prop')) return;
|
||||
if (error?.message?.includes('unique "key" prop')) return;
|
||||
|
||||
if (!has_render_error) {
|
||||
has_render_error = true;
|
||||
render_error = error;
|
||||
}
|
||||
|
||||
originalConsole.error(error);
|
||||
},
|
||||
});
|
||||
@@ -198,6 +207,13 @@ export default async function genWebHTML({
|
||||
Object.assign(console, originalConsole);
|
||||
}
|
||||
|
||||
// A component crash during SSR must surface as a proper error (handled
|
||||
// upstream as a 500 page). React only reports these to `onError`; without
|
||||
// rethrowing, the user gets a 200 with broken/blank markup.
|
||||
if (has_render_error) {
|
||||
throw render_error;
|
||||
}
|
||||
|
||||
html += htmlBody;
|
||||
|
||||
return html;
|
||||
|
||||
@@ -11,6 +11,7 @@ import isDevelopment from "../../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, ROOT_DIR } = grabDirNames();
|
||||
|
||||
@@ -28,6 +29,12 @@ type Params = {
|
||||
file_path?: string;
|
||||
debug?: boolean;
|
||||
retry?: boolean;
|
||||
/**
|
||||
* Skip the dev auto-rebuild/retry recovery path. Used by internal callers
|
||||
* (e.g. HMR post-build) where triggering yet another fullRebuild on top
|
||||
* of the rebuild that's already in progress would cascade.
|
||||
*/
|
||||
no_retry?: boolean;
|
||||
return_server_res_only?: boolean;
|
||||
skip_server_res?: boolean;
|
||||
is_hydration?: boolean;
|
||||
@@ -43,6 +50,7 @@ export default async function grabPageComponent(
|
||||
return_server_res_only,
|
||||
skip_server_res,
|
||||
is_hydration,
|
||||
no_retry,
|
||||
} = params;
|
||||
|
||||
const url = req?.url ? new URL(req.url) : undefined;
|
||||
@@ -104,13 +112,21 @@ export default async function grabPageComponent(
|
||||
let retries = 0;
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
while (retries < MAX_RETRIES) {
|
||||
// Only retry transient failures: if the last rebuild failed
|
||||
// (BUNEXT_BUILD_FAILED), the file is broken and more rebuilds
|
||||
// will just repeat the same error.
|
||||
while (
|
||||
retries < MAX_RETRIES &&
|
||||
!global.BUNEXT_BUILD_FAILED
|
||||
) {
|
||||
await fullRebuild({
|
||||
msg: `Retrying Bundle map for file \`${relative_file_path}\``,
|
||||
});
|
||||
await Bun.sleep(1000);
|
||||
bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
||||
bundledMap =
|
||||
global.BUNEXT_BUNDLER_CTX_MAP[relative_file_path];
|
||||
if (bundledMap?.path) break;
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (!bundledMap?.path) {
|
||||
@@ -170,8 +186,13 @@ export default async function grabPageComponent(
|
||||
error?.name === "NotFoundError" ||
|
||||
error?.status === 404;
|
||||
|
||||
if (!params.retry && is_dev) {
|
||||
while (global.BUNEXT_REBUILD_RETRIES < 2) {
|
||||
if (!params.retry && !params.no_retry && is_dev) {
|
||||
// Skip the retry cascade when the last rebuild failed: the error
|
||||
// is persistent and more rebuilds would repeat it endlessly.
|
||||
while (
|
||||
!global.BUNEXT_BUILD_FAILED &&
|
||||
global.BUNEXT_REBUILD_RETRIES < 2
|
||||
) {
|
||||
global.BUNEXT_REBUILD_RETRIES =
|
||||
global.BUNEXT_REBUILD_RETRIES + 1;
|
||||
|
||||
@@ -198,10 +219,18 @@ export default async function grabPageComponent(
|
||||
if (is404) {
|
||||
global.BUNEXT_IS_404_PAGE = true;
|
||||
} else {
|
||||
log.error(`Error Grabbing Page Component: ${error.message}`);
|
||||
log.error(`Page: ${passed_file_path || url?.pathname}`);
|
||||
logBuildErrorsOnce(
|
||||
[
|
||||
{
|
||||
text: `Error Grabbing Page Component: ${error.message}; Page: ${
|
||||
passed_file_path || url?.pathname
|
||||
}`,
|
||||
},
|
||||
],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
|
||||
if (is_dev) {
|
||||
if (is_dev && !params.no_retry && !global.BUNEXT_BUILD_FAILED) {
|
||||
fullRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import { log } from "../../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||
import getCache from "../../cache/get-cache";
|
||||
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
|
||||
import grabPageComponent from "./grab-page-component";
|
||||
@@ -45,7 +46,16 @@ export default async function handleWebPages({
|
||||
...componentRes,
|
||||
});
|
||||
} catch (error: any) {
|
||||
log.error(`Error Handling Web Page: ${error.message}`, error);
|
||||
logBuildErrorsOnce(
|
||||
[
|
||||
{
|
||||
text: `Error Handling Web Page: ${
|
||||
error?.message || String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
(line) => log.error(line),
|
||||
);
|
||||
|
||||
const componentRes = await grabPageErrorComponent({
|
||||
error,
|
||||
|
||||
@@ -1,34 +1,62 @@
|
||||
import path from "path";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Only include when the exiting process actually crashed with an error.
|
||||
* Bun's spawn `onExit` `error` is only set on spawn failures, so callers
|
||||
* that DO have the error (crash handlers) should pass `force: true`.
|
||||
*/
|
||||
force?: boolean;
|
||||
error?: unknown;
|
||||
exitCode?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes the `BUNDLER_EXIT.txt` flag that the bundler pipeline watches to
|
||||
* detect that the dev/prod child process previously crashed.
|
||||
*
|
||||
* Guardrails:
|
||||
* - Never writes for clean exits (exitCode === 0) or signal deaths where no
|
||||
* error is available (Ctrl+C must not leave a stale error file behind).
|
||||
* - The supervisor writes only when the file does not already exist, so the
|
||||
* crash handler's richer error/stack payload is not clobbered.
|
||||
*/
|
||||
export default function writeErrorFile({
|
||||
exitCode,
|
||||
force,
|
||||
error,
|
||||
}: {
|
||||
error?: Bun.ErrorLike;
|
||||
exitCode: number | null;
|
||||
}) {
|
||||
exitCode,
|
||||
}: Params) {
|
||||
const has_error = error != null;
|
||||
const abnormal_exit = exitCode != null && exitCode !== 0;
|
||||
|
||||
if (!has_error && !abnormal_exit) return;
|
||||
if (!force && existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) return;
|
||||
|
||||
const err =
|
||||
error instanceof Error
|
||||
? error
|
||||
: typeof error === "string"
|
||||
? new Error(error)
|
||||
: undefined;
|
||||
|
||||
const message = err?.message || "Unknown error";
|
||||
const stack = err?.stack || "N/A";
|
||||
|
||||
let txt = ``;
|
||||
txt += `Bunext Error\n`;
|
||||
txt += `============================================\n`;
|
||||
txt += `ERROR: ${error?.message}\n`;
|
||||
txt += `EXIT_CODE: ${exitCode || 0}\n`;
|
||||
txt += `CALL_STACK: ${error?.stack}\n`;
|
||||
txt += `ERROR: ${message}\n`;
|
||||
txt += `EXIT_CODE: ${exitCode ?? "N/A"}\n`;
|
||||
txt += `CALL_STACK:\n${stack}\n`;
|
||||
|
||||
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), { recursive: true });
|
||||
try {
|
||||
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(BUNX_BUNDLER_ERROR_EXIT_FILE, txt);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// log.info("Running development server ...");
|
||||
|
||||
// try {
|
||||
// rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
// } catch (error) {}
|
||||
|
||||
// await bunextInit();
|
||||
|
||||
// await startServer();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Logs build/render errors exactly once while the same error state persists.
|
||||
//
|
||||
// Dev recovery paths (watcher, request retries, post-build HMR) re-attempt a
|
||||
// rebuild for every file edit and request while a syntax error is still in the
|
||||
// file. Without deduplication that one error is printed again on every
|
||||
// attempt — an endless spiral of identical log lines. The signature is
|
||||
// cleared after a successful build, so the same error later (after a fix and
|
||||
// regression) is logged again as a new incident.
|
||||
|
||||
export type BuildErrorLike = {
|
||||
text?: string;
|
||||
message?: string;
|
||||
id?: string;
|
||||
location?: { file?: string; line?: number; column?: number } | null;
|
||||
};
|
||||
|
||||
let last_build_error_signature: string | undefined;
|
||||
|
||||
function errorSignature(errors: BuildErrorLike[]): string {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const text = e.text || e.message || e.id || "";
|
||||
const loc = e.location
|
||||
? `${e.location.file}:${e.location.line}:${e.location.column}`
|
||||
: "";
|
||||
return `${text}${loc}`;
|
||||
})
|
||||
.join("\n---\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs each error once — identical error sets are suppressed until the
|
||||
* signature changes or `resetBuildErrorLog()` is called (after a successful
|
||||
* build). Returns true when something was logged.
|
||||
*/
|
||||
export function logBuildErrorsOnce(
|
||||
errors: BuildErrorLike[],
|
||||
logLine: (line: string) => void = (line) => console.error(line),
|
||||
): boolean {
|
||||
if (!errors || errors.length === 0) return false;
|
||||
|
||||
const signature = errorSignature(errors);
|
||||
if (signature === last_build_error_signature) return false;
|
||||
|
||||
last_build_error_signature = signature;
|
||||
|
||||
for (const e of errors) {
|
||||
const text = e.text || e.message || e.id || "Build Error";
|
||||
const loc = e.location
|
||||
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
||||
: "";
|
||||
logLine(`${text}${loc}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the dedup state so a new incident with the same signature is logged
|
||||
* again. Call after a successful build.
|
||||
*/
|
||||
export function resetBuildErrorLog(): void {
|
||||
last_build_error_signature = undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user