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