@wular/pnext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1018 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type {
4
+ Metadata,
5
+ MetadataImage,
6
+ MetadataLink,
7
+ MetadataRouteEntry,
8
+ MetadataRouteKind,
9
+ RouteManifestEntry,
10
+ RouteParamValue,
11
+ StaticMetadataFileEntry,
12
+ } from '../types';
13
+ import { escapeHtml, stringifyHtmlValue } from '../utils/html';
14
+ import { toPosixPath } from '../utils/fs';
15
+
16
+ export type StaticMetadataKind =
17
+ | 'favicon'
18
+ | 'icon'
19
+ | 'apple-icon'
20
+ | 'opengraph-image'
21
+ | 'twitter-image'
22
+ | 'robots'
23
+ | 'sitemap'
24
+ | 'manifest';
25
+
26
+ export interface StaticMetadataFile extends StaticMetadataFileEntry {
27
+ kind: StaticMetadataKind;
28
+ }
29
+
30
+ export interface StaticMetadataForPath {
31
+ favicon?: StaticMetadataFile;
32
+ manifest?: StaticMetadataFile;
33
+ manifestLink?: MetadataLink;
34
+ icons: StaticMetadataFile[];
35
+ appleIcons: StaticMetadataFile[];
36
+ rootIcons: StaticMetadataFile[];
37
+ dynamicIcons?: MetadataLink[];
38
+ dynamicAppleIcons?: MetadataLink[];
39
+ openGraphImage?: MetadataImage;
40
+ dynamicOpenGraphImages?: MetadataImage[];
41
+ twitterImage?: MetadataImage;
42
+ dynamicTwitterImages?: MetadataImage[];
43
+ }
44
+
45
+ const imageExtensions = ['ico', 'jpg', 'jpeg', 'png', 'svg', 'gif', 'webp'] as const;
46
+ const imageExtensionPattern = imageExtensions.join('|');
47
+ const iconPattern = new RegExp(`^icon\\d*\\.(${imageExtensionPattern})$`, 'i');
48
+ const appleIconPattern = new RegExp(`^apple-icon\\d*\\.(${imageExtensionPattern})$`, 'i');
49
+ const openGraphPattern = new RegExp(`^opengraph-image\\d*\\.(${imageExtensionPattern})$`, 'i');
50
+ const twitterPattern = new RegExp(`^twitter-image\\d*\\.(${imageExtensionPattern})$`, 'i');
51
+
52
+ export const staticMetadataCacheControl = 'public, max-age=0, must-revalidate';
53
+
54
+ // Both discoveries walk ALL of app/ (recursive readdirSync) and every render asked for both. The
55
+ // answer changes only when files appear or disappear, so memoize per appPath; dev clears these on a
56
+ // structural save.
57
+ const staticMetadataFiles = new Map<string, StaticMetadataFile[]>();
58
+ const dynamicMetadataFiles = new Map<string, DynamicMetadataFile[]>();
59
+
60
+ /** Drop the memoized app/ metadata-file walks (dev structural invalidation). */
61
+ export function clearMetadataFileCaches() {
62
+ staticMetadataFiles.clear();
63
+ dynamicMetadataFiles.clear();
64
+ }
65
+
66
+ export function discoverStaticMetadataFiles(appPath: string): StaticMetadataFile[] {
67
+ const cached = staticMetadataFiles.get(appPath);
68
+ if (cached) return cached;
69
+ const files = existsSync(appPath)
70
+ ? listFilesSync(appPath)
71
+ .flatMap(file => staticMetadataFile(appPath, file) ?? [])
72
+ .sort((a, b) => a.outputPath.localeCompare(b.outputPath))
73
+ : [];
74
+ staticMetadataFiles.set(appPath, files);
75
+ return files;
76
+ }
77
+
78
+ export function staticMetadataForPath(appPath: string, pathname: string): StaticMetadataForPath {
79
+ const files = discoverStaticMetadataFiles(appPath).filter(file =>
80
+ metadataAppliesToPath(file, pathname),
81
+ );
82
+ return staticMetadataForFiles(files);
83
+ }
84
+
85
+ export function staticMetadataForPathFromFiles(
86
+ files: StaticMetadataFile[],
87
+ pathname: string,
88
+ ): StaticMetadataForPath {
89
+ return staticMetadataForFiles(files.filter(file => metadataAppliesToPath(file, pathname)));
90
+ }
91
+
92
+ export function staticMetadataForRoute(
93
+ appPath: string,
94
+ routeFile: string,
95
+ pathname: string,
96
+ ): StaticMetadataForPath {
97
+ const routeDir = toPosixPath(path.relative(appPath, path.dirname(routeFile)));
98
+ const files = discoverStaticMetadataFiles(appPath).filter(
99
+ file => metadataAppliesToPath(file, pathname) || metadataAppliesToRoute(file, routeDir),
100
+ );
101
+ return staticMetadataForFiles(files);
102
+ }
103
+
104
+ export function staticMetadataForRouteFromFiles(
105
+ files: StaticMetadataFile[],
106
+ appPath: string,
107
+ routeFile: string,
108
+ pathname: string,
109
+ ): StaticMetadataForPath {
110
+ const routeDir = toPosixPath(path.relative(appPath, path.dirname(routeFile)));
111
+ return staticMetadataForFiles(
112
+ files.filter(
113
+ file => metadataAppliesToPath(file, pathname) || metadataAppliesToRoute(file, routeDir),
114
+ ),
115
+ );
116
+ }
117
+
118
+ export function staticRouteMetadataKey(pathname: string) {
119
+ const path = pathname.length > 1 ? pathname.replace(/\/+$/, '') || '/' : pathname;
120
+ return path || '/';
121
+ }
122
+
123
+ export type MetadataModuleLoader = (file: string) => Promise<Record<string, unknown>>;
124
+
125
+ interface DynamicMetadataFile {
126
+ kind: MetadataRouteKind;
127
+ file: string;
128
+ relative: string;
129
+ dir: string;
130
+ routeSegments: string[];
131
+ base: string;
132
+ }
133
+
134
+ interface MatchedDynamicMetadataFile extends DynamicMetadataFile {
135
+ params: Record<string, RouteParamValue>;
136
+ routePath: string;
137
+ specificity: number;
138
+ }
139
+
140
+ export async function withDynamicMetadataRoutes(
141
+ metadata: StaticMetadataForPath,
142
+ appPath: string,
143
+ routeFile: string,
144
+ pathname: string,
145
+ loadModule: MetadataModuleLoader,
146
+ ): Promise<StaticMetadataForPath> {
147
+ const routeDir = toPosixPath(path.relative(appPath, path.dirname(routeFile)));
148
+ const files = discoverDynamicMetadataFiles(appPath)
149
+ .map(file => matchDynamicMetadataFile(file, pathname, routeDir))
150
+ .filter((file): file is MatchedDynamicMetadataFile => file !== undefined);
151
+
152
+ const manifest = bestDynamicFiles(files, 'manifest')[0];
153
+ const icons = await dynamicImageLinks(bestDynamicFiles(files, 'icon'), loadModule, 'icon');
154
+ const appleIcons = await dynamicImageLinks(
155
+ bestDynamicFiles(files, 'apple-icon'),
156
+ loadModule,
157
+ 'apple-touch-icon',
158
+ );
159
+ const openGraphImages = await dynamicMetadataImages(
160
+ bestDynamicFiles(files, 'opengraph-image'),
161
+ loadModule,
162
+ );
163
+ const twitterImages = await dynamicMetadataImages(
164
+ bestDynamicFiles(files, 'twitter-image'),
165
+ loadModule,
166
+ );
167
+
168
+ return {
169
+ ...metadata,
170
+ ...(manifest ? { manifestLink: { rel: 'manifest', url: manifest.routePath } } : {}),
171
+ ...(icons.length > 0 ? { dynamicIcons: icons } : {}),
172
+ ...(appleIcons.length > 0 ? { dynamicAppleIcons: appleIcons } : {}),
173
+ ...(openGraphImages.length > 0 ? { dynamicOpenGraphImages: openGraphImages } : {}),
174
+ ...(twitterImages.length > 0 ? { dynamicTwitterImages: twitterImages } : {}),
175
+ };
176
+ }
177
+
178
+ function staticMetadataForFiles(files: StaticMetadataFile[]): StaticMetadataForPath {
179
+ const favicon = files.find(file => file.kind === 'favicon' && file.routeSegments.length === 0);
180
+ const manifest = files.find(file => file.kind === 'manifest' && file.routeSegments.length === 0);
181
+ const icons = nearestFiles(files, 'icon').filter(file => file.routeSegments.length > 0);
182
+ const appleIcons = nearestFiles(files, 'apple-icon');
183
+ const rootIcons = nearestFiles(files, 'icon').filter(file => file.routeSegments.length === 0);
184
+ const openGraphFile = nearestFile(files, 'opengraph-image');
185
+ const twitterFile = nearestFile(files, 'twitter-image');
186
+ return {
187
+ favicon,
188
+ manifest,
189
+ icons,
190
+ appleIcons,
191
+ rootIcons,
192
+ openGraphImage: openGraphFile ? metadataImage(openGraphFile) : undefined,
193
+ twitterImage: twitterFile ? metadataImage(twitterFile) : undefined,
194
+ };
195
+ }
196
+
197
+ export function staticMetadataLink(file: StaticMetadataFile): MetadataLink {
198
+ const url = staticMetadataUrl(file);
199
+ if (file.kind === 'manifest') {
200
+ return { rel: 'manifest', url };
201
+ }
202
+ return {
203
+ rel: file.kind === 'apple-icon' ? 'apple-touch-icon' : 'icon',
204
+ url,
205
+ type: file.contentType,
206
+ ...(file.sizes ? { sizes: file.sizes } : {}),
207
+ };
208
+ }
209
+
210
+ export function staticMetadataOutputFile(outPath: string, file: StaticMetadataFile) {
211
+ return path.join(outPath, 'public', file.outputPath);
212
+ }
213
+
214
+ export function metadataHasOwnImages(metadata: Metadata, field: 'openGraph' | 'twitter') {
215
+ const value = metadata[field];
216
+ return Boolean(
217
+ value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'images'),
218
+ );
219
+ }
220
+
221
+ function staticMetadataFile(appPath: string, file: string): StaticMetadataFile | undefined {
222
+ const relative = toPosixPath(path.relative(appPath, file));
223
+ const name = path.basename(relative);
224
+ if (name.endsWith('.alt.txt')) return undefined;
225
+ const dir = path.dirname(relative) === '.' ? '' : path.dirname(relative);
226
+ const root = dir === '';
227
+ const routeSegments = routeSegmentsForDir(dir);
228
+ const outputDir = outputDirForDir(dir);
229
+ const kind = metadataKind(name, root);
230
+ if (!kind) return undefined;
231
+ const info = imageInfo(file);
232
+ if (kind === 'favicon' && !info) {
233
+ throw new Error(`Error: Process image "/${relative}" failed: unable to decode image data`);
234
+ }
235
+ const alt = metadataAltText(file);
236
+ const outputName = metadataOutputName(name, kind, dir);
237
+ return {
238
+ kind,
239
+ file,
240
+ relative,
241
+ outputPath: toPosixPath(path.join(outputDir, outputName)),
242
+ routeSegments,
243
+ contentType: contentTypeForFile(name),
244
+ ...(info?.width ? { width: info.width } : {}),
245
+ ...(info?.height ? { height: info.height } : {}),
246
+ ...(info?.sizes ? { sizes: info.sizes } : {}),
247
+ ...(alt ? { alt } : {}),
248
+ };
249
+ }
250
+
251
+ function metadataKind(name: string, root: boolean): StaticMetadataKind | undefined {
252
+ if (name === 'favicon.ico') return root ? 'favicon' : undefined;
253
+ if (iconPattern.test(name)) return 'icon';
254
+ if (appleIconPattern.test(name)) return 'apple-icon';
255
+ if (openGraphPattern.test(name)) return 'opengraph-image';
256
+ if (twitterPattern.test(name)) return 'twitter-image';
257
+ if (name === 'robots.txt') return root ? 'robots' : undefined;
258
+ if (name === 'sitemap.xml') return 'sitemap';
259
+ if (name === 'manifest.webmanifest') return root ? 'manifest' : undefined;
260
+ if (name === 'manifest.json') return root ? 'manifest' : undefined;
261
+ return undefined;
262
+ }
263
+
264
+ function metadataOutputName(name: string, kind: StaticMetadataKind, relativeDir: string) {
265
+ if (!metadataUsesSegmentSuffix(kind)) return name;
266
+ const suffix = metadataRouteSuffix(relativeDir);
267
+ if (!suffix) return name;
268
+ const extension = path.extname(name);
269
+ return `${name.slice(0, -extension.length)}-${suffix}${extension}`;
270
+ }
271
+
272
+ function metadataUsesSegmentSuffix(kind: StaticMetadataKind) {
273
+ return (
274
+ kind === 'icon' ||
275
+ kind === 'apple-icon' ||
276
+ kind === 'opengraph-image' ||
277
+ kind === 'twitter-image'
278
+ );
279
+ }
280
+
281
+ function metadataRouteSuffix(relativeDir: string) {
282
+ if (!relativeDir) return '';
283
+ const segments = relativeDir.split('/').filter(Boolean);
284
+ if (!segments.some(segment => isRouteGroup(segment) || isSlotSegment(segment))) return '';
285
+ return djb2Hash(`/${relativeDir}`).toString(36).slice(0, 6);
286
+ }
287
+
288
+ function metadataImage(file: StaticMetadataFile): MetadataImage {
289
+ return {
290
+ url: staticMetadataUrl(file),
291
+ ...(file.width ? { width: file.width } : {}),
292
+ ...(file.height ? { height: file.height } : {}),
293
+ ...(file.alt ? { alt: file.alt } : {}),
294
+ type: file.contentType,
295
+ };
296
+ }
297
+
298
+ function staticMetadataUrl(file: StaticMetadataFile) {
299
+ const suffix = file.kind === 'favicon' ? 'favicon' : 'metadata';
300
+ return `/${file.outputPath}?${suffix}`;
301
+ }
302
+
303
+ function nearestFile(files: StaticMetadataFile[], kind: StaticMetadataKind) {
304
+ return nearestFiles(files, kind)[0];
305
+ }
306
+
307
+ function nearestFiles(files: StaticMetadataFile[], kind: StaticMetadataKind) {
308
+ const matches = files.filter(file => file.kind === kind);
309
+ const maxDepth = Math.max(-1, ...matches.map(file => file.routeSegments.length));
310
+ return matches.filter(file => file.routeSegments.length === maxDepth);
311
+ }
312
+
313
+ function metadataAppliesToPath(file: StaticMetadataFile, pathname: string) {
314
+ if (file.kind === 'favicon') return file.routeSegments.length === 0;
315
+ const segments = pathname
316
+ .replace(/^\/+|\/+$/g, '')
317
+ .split('/')
318
+ .filter(Boolean);
319
+ if (file.routeSegments.length > segments.length) return false;
320
+ return file.routeSegments.every((segment, index) => {
321
+ if (segment === '*') return true;
322
+ if (segment.startsWith(':')) return Boolean(segments[index]);
323
+ return segment === segments[index];
324
+ });
325
+ }
326
+
327
+ function metadataAppliesToRoute(file: StaticMetadataFile, routeDir: string) {
328
+ const dir = path.dirname(file.relative) === '.' ? '' : path.dirname(file.relative);
329
+ if (!dir) return false;
330
+ return routeDir === dir || routeDir.startsWith(`${dir}/`);
331
+ }
332
+
333
+ function discoverDynamicMetadataFiles(appPath: string): DynamicMetadataFile[] {
334
+ const cached = dynamicMetadataFiles.get(appPath);
335
+ if (cached) return cached;
336
+ const files = existsSync(appPath)
337
+ ? listFilesSync(appPath)
338
+ .flatMap(file => dynamicMetadataFile(appPath, file) ?? [])
339
+ .sort((a, b) => a.relative.localeCompare(b.relative))
340
+ : [];
341
+ dynamicMetadataFiles.set(appPath, files);
342
+ return files;
343
+ }
344
+
345
+ function dynamicMetadataFile(appPath: string, file: string): DynamicMetadataFile | undefined {
346
+ const relative = toPosixPath(path.relative(appPath, file));
347
+ const name = path.basename(relative);
348
+ const extension = path.extname(name).slice(1);
349
+ if (!(metadataCodeExtensions as readonly string[]).includes(extension)) return undefined;
350
+ const dir = path.dirname(relative) === '.' ? '' : path.dirname(relative);
351
+ const root = dir === '';
352
+ const base = name.slice(0, -(extension.length + 1));
353
+ const kind = dynamicMetadataKind(base, root);
354
+ if (!kind) return undefined;
355
+ return {
356
+ kind,
357
+ file,
358
+ relative,
359
+ dir,
360
+ routeSegments: routeSegmentsForDir(dir),
361
+ base,
362
+ };
363
+ }
364
+
365
+ const metadataCodeExtensions = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const;
366
+
367
+ function dynamicMetadataKind(base: string, root: boolean): MetadataRouteKind | undefined {
368
+ if (base === 'robots') return root ? 'robots' : undefined;
369
+ if (base === 'sitemap') return 'sitemap';
370
+ if (base === 'manifest') return root ? 'manifest' : undefined;
371
+ if (/^icon\d*$/.test(base)) return 'icon';
372
+ if (/^apple-icon\d*$/.test(base)) return 'apple-icon';
373
+ if (/^opengraph-image\d*$/.test(base)) return 'opengraph-image';
374
+ if (/^twitter-image\d*$/.test(base)) return 'twitter-image';
375
+ return undefined;
376
+ }
377
+
378
+ function metadataImageKind(kind: MetadataRouteKind) {
379
+ return (
380
+ kind === 'icon' ||
381
+ kind === 'apple-icon' ||
382
+ kind === 'opengraph-image' ||
383
+ kind === 'twitter-image'
384
+ );
385
+ }
386
+
387
+ function matchDynamicMetadataFile(
388
+ file: DynamicMetadataFile,
389
+ pathname: string,
390
+ routeDir: string,
391
+ ): MatchedDynamicMetadataFile | undefined {
392
+ const match = matchDynamicMetadataDir(file.dir, pathname);
393
+ const routeMatch = dynamicMetadataAppliesToRoute(file.dir, routeDir);
394
+ if (!match) return undefined;
395
+ if (hasHiddenSegments(file.dir) && !routeMatch && file.routeSegments.length === 0) {
396
+ return undefined;
397
+ }
398
+ return {
399
+ ...file,
400
+ params: match.params,
401
+ routePath: dynamicMetadataRoutePath(file, match.params),
402
+ specificity: file.dir.split('/').filter(Boolean).length + (routeMatch ? 1000 : 0),
403
+ };
404
+ }
405
+
406
+ function matchDynamicMetadataDir(
407
+ relativeDir: string,
408
+ pathname: string,
409
+ ): { params: Record<string, RouteParamValue> } | undefined {
410
+ const routeSegments = relativeDir
411
+ .split('/')
412
+ .filter(segment => segment && !isRouteGroup(segment) && !isSlotSegment(segment));
413
+ const pathSegments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
414
+ if (routeSegments.length > pathSegments.length) return undefined;
415
+
416
+ const params: Record<string, RouteParamValue> = {};
417
+ for (let index = 0; index < routeSegments.length; index += 1) {
418
+ const routeSegment = routeSegments[index]!;
419
+ const pathSegment = decodeURIComponent(pathSegments[index] ?? '');
420
+ const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(routeSegment);
421
+ if (catchAll) {
422
+ params[catchAll[1]!] = pathSegments.slice(index).map(decodeURIComponent);
423
+ return { params };
424
+ }
425
+ const optionalCatchAll = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(routeSegment);
426
+ if (optionalCatchAll) {
427
+ const values = pathSegments.slice(index).map(decodeURIComponent);
428
+ if (values.length > 0) params[optionalCatchAll[1]!] = values;
429
+ return { params };
430
+ }
431
+ const dynamic = /^\[([^\]]+)\]$/.exec(routeSegment);
432
+ if (dynamic) {
433
+ if (!pathSegment) return undefined;
434
+ params[dynamic[1]!] = pathSegment;
435
+ continue;
436
+ }
437
+ if (routeSegment !== pathSegment) return undefined;
438
+ }
439
+ return { params };
440
+ }
441
+
442
+ function dynamicMetadataAppliesToRoute(relativeDir: string, routeDir: string) {
443
+ if (!relativeDir) return routeDir === '';
444
+ return routeDir === relativeDir || routeDir.startsWith(`${relativeDir}/`);
445
+ }
446
+
447
+ function hasHiddenSegments(relativeDir: string) {
448
+ return relativeDir.split('/').some(segment => isRouteGroup(segment) || isSlotSegment(segment));
449
+ }
450
+
451
+ function dynamicMetadataRoutePath(
452
+ file: DynamicMetadataFile,
453
+ params: Record<string, RouteParamValue>,
454
+ ) {
455
+ if (file.kind === 'robots') return '/robots.txt';
456
+ if (file.kind === 'manifest') return '/manifest.webmanifest';
457
+ if (file.kind === 'sitemap') return `/${joinUrlPath(dynamicMetadataOutputDir(file.dir, params), 'sitemap.xml').replace(/^\/+/, '')}`;
458
+
459
+ const suffix = metadataRouteSuffix(file.dir);
460
+ const base = suffix ? `${file.base}-${suffix}` : file.base;
461
+ return joinUrlPath(dynamicMetadataOutputDir(file.dir, params), base);
462
+ }
463
+
464
+ function dynamicMetadataOutputDir(relativeDir: string, params: Record<string, RouteParamValue>) {
465
+ return relativeDir
466
+ .split('/')
467
+ .filter(segment => segment && !isRouteGroup(segment) && !isSlotSegment(segment))
468
+ .flatMap(segment => {
469
+ const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(segment);
470
+ const optionalCatchAll = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(segment);
471
+ const dynamic = /^\[([^\]]+)\]$/.exec(segment);
472
+ const key = catchAll?.[1] ?? optionalCatchAll?.[1] ?? dynamic?.[1];
473
+ if (!key) return [segment];
474
+ const value = params[key];
475
+ if (value === undefined) return [];
476
+ return Array.isArray(value) ? value : [value];
477
+ })
478
+ .join('/');
479
+ }
480
+
481
+ function bestDynamicFiles(files: MatchedDynamicMetadataFile[], kind: MetadataRouteKind) {
482
+ const matches = files.filter(file => file.kind === kind);
483
+ const specificity = Math.max(-1, ...matches.map(file => file.specificity));
484
+ return matches.filter(file => file.specificity === specificity);
485
+ }
486
+
487
+ async function dynamicImageLinks(
488
+ files: MatchedDynamicMetadataFile[],
489
+ loadModule: MetadataModuleLoader,
490
+ rel: string,
491
+ ): Promise<MetadataLink[]> {
492
+ const links: MetadataLink[] = [];
493
+ for (const file of files) {
494
+ const module = (await loadModule(file.file)) as DynamicMetadataModule;
495
+ for (const image of await dynamicImageEntries(file, module)) {
496
+ links.push({
497
+ rel,
498
+ url: image.url,
499
+ type: image.type,
500
+ ...(image.sizes ? { sizes: image.sizes } : {}),
501
+ });
502
+ }
503
+ }
504
+ return links;
505
+ }
506
+
507
+ async function dynamicMetadataImages(
508
+ files: MatchedDynamicMetadataFile[],
509
+ loadModule: MetadataModuleLoader,
510
+ ): Promise<MetadataImage[]> {
511
+ const images: MetadataImage[] = [];
512
+ for (const file of files) {
513
+ const module = (await loadModule(file.file)) as DynamicMetadataModule;
514
+ for (const image of await dynamicImageEntries(file, module)) {
515
+ images.push({
516
+ url: image.url,
517
+ type: image.type,
518
+ ...(image.width ? { width: image.width } : {}),
519
+ ...(image.height ? { height: image.height } : {}),
520
+ ...(image.alt ? { alt: image.alt } : {}),
521
+ });
522
+ }
523
+ }
524
+ return images;
525
+ }
526
+
527
+ interface DynamicImageEntry {
528
+ url: string;
529
+ type: string;
530
+ width?: string;
531
+ height?: string;
532
+ sizes?: string;
533
+ alt?: string;
534
+ }
535
+
536
+ async function dynamicImageEntries(
537
+ file: MatchedDynamicMetadataFile,
538
+ module: DynamicMetadataModule,
539
+ ): Promise<DynamicImageEntry[]> {
540
+ const generated =
541
+ metadataImageKind(file.kind) && module.generateImageMetadata
542
+ ? await metadataGeneratedEntries(module, { kind: file.kind }, file.params)
543
+ : [];
544
+ const entries = generated.length > 0 ? generated : [undefined];
545
+ return entries.map(entry => {
546
+ const id = entry ? coerceString(entry.id) : undefined;
547
+ const size = dynamicImageSize(entry?.size) ?? dynamicImageSize(module.size);
548
+ const type =
549
+ (entry && typeof entry.contentType === 'string' ? entry.contentType : undefined) ??
550
+ module.contentType ??
551
+ 'image/png';
552
+ const routePath = id ? joinUrlPath(file.routePath, encodeURIComponent(id)) : file.routePath;
553
+ return {
554
+ url: `${routePath}?metadata`,
555
+ type,
556
+ ...(size?.width ? { width: size.width } : {}),
557
+ ...(size?.height ? { height: size.height } : {}),
558
+ ...(size?.sizes ? { sizes: size.sizes } : {}),
559
+ ...(typeof entry?.alt === 'string'
560
+ ? { alt: entry.alt }
561
+ : typeof module.alt === 'string'
562
+ ? { alt: module.alt }
563
+ : {}),
564
+ };
565
+ });
566
+ }
567
+
568
+ function dynamicImageSize(value: unknown) {
569
+ if (!isRecord(value)) return undefined;
570
+ const width = value.width === undefined ? undefined : coerceString(value.width);
571
+ const height = value.height === undefined ? undefined : coerceString(value.height);
572
+ return {
573
+ width,
574
+ height,
575
+ ...(width && height ? { sizes: `${width}x${height}` } : {}),
576
+ };
577
+ }
578
+
579
+ function routeSegmentsForDir(relativeDir: string) {
580
+ if (!relativeDir) return [];
581
+ return relativeDir
582
+ .split('/')
583
+ .filter(segment => segment && !isRouteGroup(segment) && !isSlotSegment(segment))
584
+ .map(segment => {
585
+ if (/^\[\[\.\.\.[^\]]+\]\]$/.test(segment)) return '*';
586
+ if (/^\[\.\.\.[^\]]+\]$/.test(segment)) return '*';
587
+ if (/^\[[^\]]+\]$/.test(segment)) return `:${segment.slice(1, -1)}`;
588
+ return segment;
589
+ });
590
+ }
591
+
592
+ function outputDirForDir(relativeDir: string) {
593
+ if (!relativeDir) return '';
594
+ return toPosixPath(
595
+ relativeDir
596
+ .split('/')
597
+ .filter(segment => segment && !isRouteGroup(segment) && !isSlotSegment(segment))
598
+ .map(segment => (segment.startsWith('[') && segment.endsWith(']') ? '-' : segment))
599
+ .join('/'),
600
+ );
601
+ }
602
+
603
+ function isRouteGroup(segment: string) {
604
+ return segment.startsWith('(') && segment.endsWith(')');
605
+ }
606
+
607
+ function isSlotSegment(segment: string) {
608
+ return segment.startsWith('@');
609
+ }
610
+
611
+ function metadataAltText(file: string) {
612
+ const extension = path.extname(file);
613
+ const altFile = `${file.slice(0, -extension.length)}.alt.txt`;
614
+ if (!existsSync(altFile)) return undefined;
615
+ return readFileSync(altFile, 'utf8').trim() || undefined;
616
+ }
617
+
618
+ function imageInfo(file: string): { width?: number; height?: number; sizes?: string } | undefined {
619
+ const extension = path.extname(file).toLowerCase();
620
+ if (extension === '.svg') return { sizes: 'any' };
621
+ const bytes = readFileSync(file);
622
+ const png = pngInfo(bytes);
623
+ if (png) return png;
624
+ if (extension === '.png') return pngInfo(bytes);
625
+ if (extension === '.ico') return icoInfo(bytes);
626
+ return undefined;
627
+ }
628
+
629
+ function pngInfo(bytes: Buffer): { width?: number; height?: number; sizes?: string } | undefined {
630
+ if (
631
+ bytes.length < 24 ||
632
+ bytes[0] !== 0x89 ||
633
+ bytes[1] !== 0x50 ||
634
+ bytes[2] !== 0x4e ||
635
+ bytes[3] !== 0x47
636
+ ) {
637
+ return undefined;
638
+ }
639
+ const width = bytes.readUInt32BE(16);
640
+ const height = bytes.readUInt32BE(20);
641
+ return { width, height, sizes: `${width}x${height}` };
642
+ }
643
+
644
+ function icoInfo(bytes: Buffer): { width?: number; height?: number; sizes?: string } | undefined {
645
+ if (bytes.length < 22) return undefined;
646
+ if (bytes.readUInt16LE(0) !== 0) return undefined;
647
+ const type = bytes.readUInt16LE(2);
648
+ if (type !== 1 && type !== 2) return undefined;
649
+ if (bytes.readUInt16LE(4) < 1) return undefined;
650
+ const width = bytes[6] === 0 ? 256 : bytes[6];
651
+ const height = bytes[7] === 0 ? 256 : bytes[7];
652
+ return { width, height, sizes: `${width}x${height}` };
653
+ }
654
+
655
+ function contentTypeForFile(file: string) {
656
+ const extension = path.extname(file).toLowerCase();
657
+ if (file === 'manifest.json') return 'application/manifest+json';
658
+ if (extension === '.ico') return 'image/x-icon';
659
+ if (extension === '.jpg' || extension === '.jpeg') return 'image/jpeg';
660
+ if (extension === '.png') return 'image/png';
661
+ if (extension === '.svg') return 'image/svg+xml';
662
+ if (extension === '.gif') return 'image/gif';
663
+ if (extension === '.webp') return 'image/webp';
664
+ if (extension === '.txt') return 'text/plain';
665
+ if (extension === '.xml') return 'application/xml';
666
+ if (extension === '.webmanifest') return 'application/manifest+json';
667
+ return 'application/octet-stream';
668
+ }
669
+
670
+ function listFilesSync(root: string): string[] {
671
+ const entries = readdirSync(root, { withFileTypes: true });
672
+ return entries.flatMap(entry => {
673
+ const file = path.join(root, entry.name);
674
+ if (entry.isDirectory()) return listFilesSync(file);
675
+ return entry.isFile() ? [file] : [];
676
+ });
677
+ }
678
+
679
+ function djb2Hash(value: string) {
680
+ let hash = 5381;
681
+ for (let i = 0; i < value.length; i++) {
682
+ hash = ((hash << 5) + hash + value.charCodeAt(i)) & 0xffffffff;
683
+ }
684
+ return hash >>> 0;
685
+ }
686
+
687
+ interface DynamicMetadataModule {
688
+ default?: (props?: MetadataRouteProps) => unknown;
689
+ generateImageMetadata?: (props: { params: MetadataRouteParams }) => unknown;
690
+ generateSitemaps?: (props: { params: MetadataRouteParams }) => unknown;
691
+ alt?: string;
692
+ size?: { width?: number; height?: number };
693
+ contentType?: string;
694
+ }
695
+
696
+ type MetadataRouteParams = Promise<Record<string, RouteParamValue>> &
697
+ Record<string, RouteParamValue>;
698
+
699
+ interface MetadataRouteProps {
700
+ params: MetadataRouteParams;
701
+ id?: Promise<string | number> & { valueOf(): string | number; toString(): string };
702
+ }
703
+
704
+ interface DynamicMetadataHandlerModule {
705
+ params?: () => Promise<Record<string, RouteParamValue>[]>;
706
+ GET: (
707
+ request: Request,
708
+ context: { params: Promise<Record<string, RouteParamValue>> },
709
+ ) => Promise<Response>;
710
+ }
711
+
712
+ export function metadataRouteHandlerModule(
713
+ module: DynamicMetadataModule,
714
+ route: RouteManifestEntry,
715
+ ): DynamicMetadataHandlerModule | undefined {
716
+ const metadata = route.metadataRoute;
717
+ if (!metadata) return undefined;
718
+
719
+ const handler: DynamicMetadataHandlerModule = {
720
+ GET: async (_request, context) => metadataRouteResponse(module, metadata, await context.params),
721
+ };
722
+ if (metadata.generatedParam) {
723
+ handler.params = () => metadataRouteStaticParams(module, route);
724
+ }
725
+ return handler;
726
+ }
727
+
728
+ async function metadataRouteStaticParams(
729
+ module: DynamicMetadataModule,
730
+ route: RouteManifestEntry,
731
+ ): Promise<Record<string, RouteParamValue>[]> {
732
+ const metadata = route.metadataRoute;
733
+ const generatedParam = metadata?.generatedParam;
734
+ if (!metadata || !generatedParam) return [{}];
735
+ if (route.params.some(param => param !== generatedParam)) return [];
736
+ const entries = await metadataGeneratedEntries(module, metadata, {});
737
+ return entries.map(entry => ({ [generatedParam]: String(entry.id) }));
738
+ }
739
+
740
+ async function metadataRouteResponse(
741
+ module: DynamicMetadataModule,
742
+ metadata: MetadataRouteEntry,
743
+ params: Record<string, RouteParamValue>,
744
+ ) {
745
+ const generatedParam = metadata.generatedParam;
746
+ const metadataId = generatedParam ? coerceRouteParam(params[generatedParam]) : undefined;
747
+ const routeParams = generatedParam ? omitKey(params, generatedParam) : params;
748
+ const generated = generatedParam
749
+ ? await metadataGeneratedEntries(module, metadata, routeParams)
750
+ : undefined;
751
+ const generatedEntry = metadataId
752
+ ? generated?.find(entry => String(entry.id) === metadataId)
753
+ : undefined;
754
+ if (generatedParam && !generatedEntry) return new Response('Not Found', { status: 404 });
755
+
756
+ if (typeof module.default !== 'function') return new Response('Not Found', { status: 404 });
757
+ const result = await module.default(
758
+ metadataRouteProps(routeParams, metadataRouteId(metadata, metadataId, generatedEntry)),
759
+ );
760
+ return metadataResultResponse(result, metadata, module, generatedEntry);
761
+ }
762
+
763
+ async function metadataGeneratedEntries(
764
+ module: DynamicMetadataModule,
765
+ metadata: MetadataRouteEntry,
766
+ params: Record<string, RouteParamValue>,
767
+ ): Promise<Record<string, unknown>[]> {
768
+ const result =
769
+ metadata.kind === 'sitemap'
770
+ ? await module.generateSitemaps?.({ params: paramsPromise(params) })
771
+ : await module.generateImageMetadata?.({ params: paramsPromise(params) });
772
+ return Array.isArray(result)
773
+ ? result.filter(
774
+ (entry): entry is Record<string, unknown> => isRecord(entry) && entry.id !== undefined,
775
+ )
776
+ : [];
777
+ }
778
+
779
+ function metadataRouteProps(
780
+ params: Record<string, RouteParamValue>,
781
+ id?: string | number,
782
+ ): MetadataRouteProps {
783
+ return {
784
+ params: paramsPromise(params),
785
+ ...(id !== undefined ? { id: idPromise(id) } : {}),
786
+ };
787
+ }
788
+
789
+ function metadataRouteId(
790
+ metadata: MetadataRouteEntry,
791
+ metadataId: string | undefined,
792
+ generatedEntry: Record<string, unknown> | undefined,
793
+ ) {
794
+ if (metadata.kind === 'sitemap') return metadataId;
795
+ const id = generatedEntry?.id;
796
+ return typeof id === 'string' || typeof id === 'number' ? id : metadataId;
797
+ }
798
+
799
+ function paramsPromise(params: Record<string, RouteParamValue>) {
800
+ return Object.assign(Promise.resolve(params), params);
801
+ }
802
+
803
+ function idPromise(id: string | number) {
804
+ return Object.assign(Promise.resolve(id), {
805
+ valueOf: () => id,
806
+ toString: () => String(id),
807
+ });
808
+ }
809
+
810
+ function metadataResultResponse(
811
+ result: unknown,
812
+ metadata: MetadataRouteEntry,
813
+ module: DynamicMetadataModule,
814
+ generatedEntry: Record<string, unknown> | undefined,
815
+ ) {
816
+ if (result instanceof Response) {
817
+ return withMetadataHeaders(
818
+ result,
819
+ metadataContentType(metadata, module, generatedEntry, result.headers.get('content-type')),
820
+ );
821
+ }
822
+ if (metadata.kind === 'robots') {
823
+ return metadataTextResponse(serializeRobots(result), 'text/plain');
824
+ }
825
+ if (metadata.kind === 'sitemap') {
826
+ return metadataTextResponse(serializeSitemap(result), 'application/xml');
827
+ }
828
+ if (metadata.kind === 'manifest') {
829
+ return metadataTextResponse(JSON.stringify(result), 'application/manifest+json');
830
+ }
831
+
832
+ const body = metadataBinaryBody(result);
833
+ return new Response(body, {
834
+ headers: metadataHeaders(metadataContentType(metadata, module, generatedEntry)),
835
+ });
836
+ }
837
+
838
+ function withMetadataHeaders(response: Response, contentType: string) {
839
+ const headers = metadataHeaders(contentType, response.headers);
840
+ return new Response(response.body, {
841
+ status: response.status,
842
+ statusText: response.statusText,
843
+ headers,
844
+ });
845
+ }
846
+
847
+ function metadataTextResponse(body: string, contentType: string) {
848
+ return new Response(body, { headers: metadataHeaders(contentType) });
849
+ }
850
+
851
+ function metadataHeaders(contentType: string, base?: Headers) {
852
+ const headers = new Headers(base);
853
+ if (!headers.has('content-type')) headers.set('content-type', contentType);
854
+ // Metadata routes always serve with the revalidating cache-control, even when
855
+ // the underlying result (e.g. an ImageResponse) defaults to an immutable
856
+ // long-lived value — Next overrides it for generated metadata routes.
857
+ headers.set('cache-control', staticMetadataCacheControl);
858
+ return headers;
859
+ }
860
+
861
+ function metadataContentType(
862
+ metadata: MetadataRouteEntry,
863
+ module: DynamicMetadataModule,
864
+ generatedEntry?: Record<string, unknown>,
865
+ responseType?: string | null,
866
+ ) {
867
+ if (responseType) return responseType;
868
+ if (metadata.kind === 'robots') return 'text/plain';
869
+ if (metadata.kind === 'sitemap') return 'application/xml';
870
+ if (metadata.kind === 'manifest') return 'application/manifest+json';
871
+ const generatedType = generatedEntry?.contentType;
872
+ if (typeof generatedType === 'string') return generatedType;
873
+ return module.contentType ?? 'image/png';
874
+ }
875
+
876
+ function metadataBinaryBody(result: unknown): BodyInit {
877
+ if (result instanceof Blob) return result;
878
+ if (result instanceof ArrayBuffer) return result;
879
+ if (ArrayBuffer.isView(result)) return result as BodyInit;
880
+ if (result instanceof ReadableStream) return result;
881
+ if (typeof result === 'string') return result;
882
+ if (result == null) return new Uint8Array();
883
+ return JSON.stringify(result);
884
+ }
885
+
886
+ function serializeRobots(value: unknown) {
887
+ if (!isRecord(value)) return '';
888
+ const blocks = arrayOf(value.rules).flatMap(rule => (isRecord(rule) ? [robotRule(rule)] : []));
889
+ const footer = [
890
+ ...list(value.host).map(host => `Host: ${host}`),
891
+ ...list(value.sitemap).map(sitemap => `Sitemap: ${sitemap}`),
892
+ ];
893
+ const body = [...blocks, footer.join('\n')].filter(Boolean).join('\n\n');
894
+ if (!body) return '';
895
+ return footer.length > 0 ? `${body}\n` : `${body}\n\n`;
896
+ }
897
+
898
+ function robotRule(rule: Record<string, unknown>) {
899
+ const agents = list(rule.userAgent ?? '*');
900
+ const directives: string[] = [];
901
+ for (const item of list(rule.allow)) directives.push(`Allow: ${item}`);
902
+ for (const item of list(rule.disallow)) directives.push(`Disallow: ${item}`);
903
+ if (rule.crawlDelay !== undefined)
904
+ directives.push(`Crawl-delay: ${coerceString(rule.crawlDelay)}`);
905
+ return [...agents.map(agent => `User-Agent: ${agent}`), ...directives].join('\n');
906
+ }
907
+
908
+ function serializeSitemap(value: unknown) {
909
+ const entries = Array.isArray(value) ? value : isRecord(value) ? [value] : [];
910
+ const usesAlternates = entries.some(entry => isRecord(entry) && isRecord(entry.alternates));
911
+ const usesImages = entries.some(entry => isRecord(entry) && arrayOf(entry.images).length > 0);
912
+ const usesVideos = entries.some(entry => isRecord(entry) && arrayOf(entry.videos).length > 0);
913
+ const attrs = [
914
+ 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
915
+ ...(usesAlternates ? ['xmlns:xhtml="http://www.w3.org/1999/xhtml"'] : []),
916
+ ...(usesImages ? ['xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"'] : []),
917
+ ...(usesVideos ? ['xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"'] : []),
918
+ ];
919
+ return `<?xml version="1.0" encoding="UTF-8"?>
920
+ <urlset ${attrs.join(' ')}>
921
+ ${entries.flatMap(entry => (isRecord(entry) ? [sitemapUrl(entry)] : [])).join('\n')}
922
+ </urlset>
923
+ `;
924
+ }
925
+
926
+ function sitemapUrl(entry: Record<string, unknown>) {
927
+ const lines = ['<url>', `<loc>${escapeXml(entry.url)}</loc>`];
928
+ if (entry.lastModified)
929
+ lines.push(`<lastmod>${escapeXml(sitemapDate(entry.lastModified))}</lastmod>`);
930
+ if (entry.changeFrequency)
931
+ lines.push(`<changefreq>${escapeXml(entry.changeFrequency)}</changefreq>`);
932
+ if (entry.priority !== undefined) lines.push(`<priority>${escapeXml(entry.priority)}</priority>`);
933
+ const languages = isRecord(entry.alternates)
934
+ ? entry.alternates.languages
935
+ : undefined;
936
+ if (isRecord(languages)) {
937
+ for (const [language, href] of Object.entries(languages)) {
938
+ lines.push(
939
+ `<xhtml:link rel="alternate" hreflang="${escapeXml(language)}" href="${escapeXml(href)}" />`,
940
+ );
941
+ }
942
+ }
943
+ for (const image of arrayOf(entry.images)) {
944
+ lines.push(sitemapImage(image));
945
+ }
946
+ for (const video of arrayOf(entry.videos)) {
947
+ if (isRecord(video)) lines.push(sitemapVideo(video));
948
+ }
949
+ lines.push('</url>');
950
+ return lines.join('\n');
951
+ }
952
+
953
+ function sitemapImage(image: unknown) {
954
+ const loc = isRecord(image) ? (image.url ?? image.loc ?? image.location) : image;
955
+ return `<image:image>\n<image:loc>${escapeXml(loc)}</image:loc>\n</image:image>`;
956
+ }
957
+
958
+ function sitemapVideo(video: Record<string, unknown>) {
959
+ const lines = ['<video:video>'];
960
+ for (const [key, value] of Object.entries(video)) {
961
+ if (isRecord(value) && 'content' in value) {
962
+ const attrs = Object.entries(value)
963
+ .filter(([name]) => name !== 'content')
964
+ .map(([name, attr]) => ` ${name}="${escapeXml(attr)}"`)
965
+ .join('');
966
+ lines.push(`<video:${key}${attrs}>${escapeXml(value.content)}</video:${key}>`);
967
+ } else {
968
+ lines.push(`<video:${key}>${escapeXml(value)}</video:${key}>`);
969
+ }
970
+ }
971
+ lines.push('</video:video>');
972
+ return lines.join('\n');
973
+ }
974
+
975
+ function sitemapDate(value: unknown) {
976
+ if (value instanceof Date) return value.toISOString();
977
+ return coerceString(value);
978
+ }
979
+
980
+ function escapeXml(value: unknown) {
981
+ return escapeHtml(coerceString(value));
982
+ }
983
+
984
+ function coerceString(value: unknown) {
985
+ return stringifyHtmlValue(value);
986
+ }
987
+
988
+ function list(value: unknown): string[] {
989
+ if (value === undefined || value === null) return [];
990
+ return (Array.isArray(value) ? value : [value]).map(coerceString).filter(Boolean);
991
+ }
992
+
993
+ function arrayOf(value: unknown): unknown[] {
994
+ if (value === undefined) return [];
995
+ return Array.isArray(value) ? value : [value];
996
+ }
997
+
998
+ function isRecord(value: unknown): value is Record<string, unknown> {
999
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1000
+ }
1001
+
1002
+ function joinUrlPath(...parts: string[]) {
1003
+ const path = parts.filter(Boolean).join('/').replace(/\/+/g, '/');
1004
+ return path.startsWith('/') ? path : `/${path}`;
1005
+ }
1006
+
1007
+ function coerceRouteParam(value: RouteParamValue | undefined) {
1008
+ if (value === undefined) return undefined;
1009
+ return Array.isArray(value) ? value.join('/') : String(value);
1010
+ }
1011
+
1012
+ function omitKey<T>(value: Record<string, T>, key: string) {
1013
+ const next: Record<string, T> = {};
1014
+ for (const [name, item] of Object.entries(value)) {
1015
+ if (name !== key) next[name] = item;
1016
+ }
1017
+ return next;
1018
+ }