@wular/pnext 0.0.4 → 0.0.6
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 +11 -11
- package/bin/pnext +1 -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 +1 -1
- package/reference/config.md +2 -2
- package/reference/css.md +13 -9
- package/reference/dev.md +1 -1
- package/reference/metadata.md +7 -7
- package/reference/navigation.md +35 -28
- package/reference/performance.md +70 -70
- package/reference/rendering.md +17 -17
- package/reference/routing.md +17 -17
- package/reference/typegen.md +12 -8
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +113 -109
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +49 -52
- 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 +71 -63
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +88 -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 +794 -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 +17 -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 +1988 -1992
- package/src/{api → client}/router/types.ts +93 -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 +69 -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 +2076 -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 +69 -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 +952 -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 +425 -404
- package/src/styles.d.ts +9 -0
- package/src/types.ts +320 -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/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
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
// Served from the request interceptor registered in ../register-image.ts.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
import path from 'node:path'
|
|
15
|
-
import type { ResolvedConfig } from '../../config'
|
|
16
|
-
import { getNextConfig } from '../next/config-loader'
|
|
17
|
-
import { getImagesConfig, type ResolvedImagesConfig } from '../next/image/config'
|
|
18
|
-
import { hasLocalMatch, hasRemoteMatch } from '../next/image/patterns'
|
|
19
|
-
import { cacheHandlerGet, cacheHandlerSet, hasCacheHandler } from '../cache/handler'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import type { ResolvedConfig } from '../../config'
|
|
16
|
+
import { getNextConfig } from '../next/config-loader'
|
|
17
|
+
import { getImagesConfig, type ResolvedImagesConfig } from '../next/image/config'
|
|
18
|
+
import { hasLocalMatch, hasRemoteMatch } from '../next/image/patterns'
|
|
19
|
+
import { cacheHandlerGet, cacheHandlerSet, hasCacheHandler } from '../cache/handler'
|
|
20
20
|
import {
|
|
21
21
|
ANIMATABLE_TYPES,
|
|
22
22
|
AVIF,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
extensionForType,
|
|
29
29
|
isAnimated,
|
|
30
30
|
typeForExtension,
|
|
31
|
-
} from './detect'
|
|
31
|
+
} from './detect'
|
|
32
32
|
import {
|
|
33
33
|
IMAGE_CACHE_VERSION,
|
|
34
34
|
getHash,
|
|
@@ -36,74 +36,74 @@ import {
|
|
|
36
36
|
readCacheEntry,
|
|
37
37
|
writeCacheEntry,
|
|
38
38
|
type ImageCacheEntry,
|
|
39
|
-
} from './cache'
|
|
39
|
+
} from './cache'
|
|
40
40
|
import {
|
|
41
41
|
ImageError,
|
|
42
42
|
fetchExternalImage,
|
|
43
43
|
fetchInternalImage,
|
|
44
44
|
readPublicFile,
|
|
45
45
|
type ImageUpstream,
|
|
46
|
-
} from './source'
|
|
46
|
+
} from './source'
|
|
47
47
|
|
|
48
|
-
type XCacheHeader = 'MISS' | 'HIT' | 'STALE'
|
|
48
|
+
type XCacheHeader = 'MISS' | 'HIT' | 'STALE'
|
|
49
49
|
|
|
50
50
|
interface ImageParams {
|
|
51
|
-
href: string
|
|
52
|
-
isAbsolute: boolean
|
|
53
|
-
isStatic: boolean
|
|
54
|
-
width: number
|
|
55
|
-
quality: number
|
|
56
|
-
mimeType: string
|
|
51
|
+
href: string
|
|
52
|
+
isAbsolute: boolean
|
|
53
|
+
isStatic: boolean
|
|
54
|
+
width: number
|
|
55
|
+
quality: number
|
|
56
|
+
mimeType: string
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
interface GeneratedImage {
|
|
60
|
-
buffer: Buffer
|
|
61
|
-
contentType: string
|
|
62
|
-
maxAge: number
|
|
63
|
-
etag: string
|
|
64
|
-
upstreamEtag: string
|
|
60
|
+
buffer: Buffer
|
|
61
|
+
contentType: string
|
|
62
|
+
maxAge: number
|
|
63
|
+
etag: string
|
|
64
|
+
upstreamEtag: string
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// -- sharp ------------------------------------------------------------------
|
|
68
68
|
|
|
69
69
|
interface SharpPipeline {
|
|
70
|
-
timeout(options: { seconds: number }): SharpPipeline
|
|
71
|
-
rotate(): SharpPipeline
|
|
70
|
+
timeout(options: { seconds: number }): SharpPipeline
|
|
71
|
+
rotate(): SharpPipeline
|
|
72
72
|
resize(
|
|
73
73
|
width: number | null,
|
|
74
74
|
height?: number | null,
|
|
75
75
|
options?: { withoutEnlargement?: boolean },
|
|
76
|
-
): SharpPipeline
|
|
77
|
-
webp(options: { quality: number }): SharpPipeline
|
|
78
|
-
avif(options: { quality: number; effort?: number }): SharpPipeline
|
|
79
|
-
png(options: { quality: number }): SharpPipeline
|
|
80
|
-
jpeg(options: { quality: number; mozjpeg?: boolean }): SharpPipeline
|
|
81
|
-
toBuffer(): Promise<Buffer
|
|
76
|
+
): SharpPipeline
|
|
77
|
+
webp(options: { quality: number }): SharpPipeline
|
|
78
|
+
avif(options: { quality: number; effort?: number }): SharpPipeline
|
|
79
|
+
png(options: { quality: number }): SharpPipeline
|
|
80
|
+
jpeg(options: { quality: number; mozjpeg?: boolean }): SharpPipeline
|
|
81
|
+
toBuffer(): Promise<Buffer>
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
type SharpModule = (input: Buffer | Uint8Array) => SharpPipeline
|
|
84
|
+
type SharpModule = (input: Buffer | Uint8Array) => SharpPipeline
|
|
85
85
|
|
|
86
|
-
let sharpPromise: Promise<SharpModule | undefined> | undefined
|
|
86
|
+
let sharpPromise: Promise<SharpModule | undefined> | undefined
|
|
87
87
|
function loadSharp(): Promise<SharpModule | undefined> {
|
|
88
88
|
if (!sharpPromise) {
|
|
89
89
|
sharpPromise = import('sharp')
|
|
90
90
|
.then(m => ((m as { default?: unknown }).default ?? m) as SharpModule)
|
|
91
|
-
.catch(() => undefined)
|
|
91
|
+
.catch(() => undefined)
|
|
92
92
|
}
|
|
93
|
-
return sharpPromise
|
|
93
|
+
return sharpPromise
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// -- helpers ----------------------------------------------------------------
|
|
97
97
|
|
|
98
|
-
const warnedOnce = new Set<string>()
|
|
98
|
+
const warnedOnce = new Set<string>()
|
|
99
99
|
function warnOnce(message: string): void {
|
|
100
|
-
if (warnedOnce.has(message)) return
|
|
101
|
-
warnedOnce.add(message)
|
|
102
|
-
console.warn(message)
|
|
100
|
+
if (warnedOnce.has(message)) return
|
|
101
|
+
warnedOnce.add(message)
|
|
102
|
+
console.warn(message)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function badRequest(message: string): Response {
|
|
106
|
-
return new Response(message, { status: 400, headers: { 'content-type': 'text/plain' } })
|
|
106
|
+
return new Response(message, { status: 400, headers: { 'content-type': 'text/plain' } })
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function errorResponse(error: unknown): Response {
|
|
@@ -111,47 +111,47 @@ function errorResponse(error: unknown): Response {
|
|
|
111
111
|
return new Response(error.message, {
|
|
112
112
|
status: error.statusCode,
|
|
113
113
|
headers: { 'content-type': 'text/plain' },
|
|
114
|
-
})
|
|
114
|
+
})
|
|
115
115
|
}
|
|
116
|
-
console.error('image optimizer failed', error)
|
|
117
|
-
return new Response('Internal Server Error', { status: 500 })
|
|
116
|
+
console.error('image optimizer failed', error)
|
|
117
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function getMaxAge(cacheControl: string | null | undefined): number {
|
|
121
|
-
if (!cacheControl) return 0
|
|
121
|
+
if (!cacheControl) return 0
|
|
122
122
|
for (const directive of cacheControl.split(',')) {
|
|
123
|
-
const [key = '', rawValue] = directive.trim().split('=', 2)
|
|
124
|
-
if (key.toLowerCase() !== 's-maxage' && key.toLowerCase() !== 'max-age') continue
|
|
125
|
-
let value = (rawValue ?? '').toLowerCase()
|
|
126
|
-
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1)
|
|
127
|
-
const n = parseInt(value, 10)
|
|
128
|
-
if (!isNaN(n)) return n
|
|
123
|
+
const [key = '', rawValue] = directive.trim().split('=', 2)
|
|
124
|
+
if (key.toLowerCase() !== 's-maxage' && key.toLowerCase() !== 'max-age') continue
|
|
125
|
+
let value = (rawValue ?? '').toLowerCase()
|
|
126
|
+
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1)
|
|
127
|
+
const n = parseInt(value, 10)
|
|
128
|
+
if (!isNaN(n)) return n
|
|
129
129
|
}
|
|
130
|
-
return 0
|
|
130
|
+
return 0
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/** First configured format the Accept header explicitly names, or '' (keep source format). */
|
|
134
134
|
function getSupportedMimeType(formats: string[], accept: string): string {
|
|
135
135
|
for (const format of formats) {
|
|
136
|
-
if (accept.includes(format)) return format
|
|
136
|
+
if (accept.includes(format)) return format
|
|
137
137
|
}
|
|
138
|
-
return ''
|
|
138
|
+
return ''
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
function getFileNameWithExtension(url: string, contentType: string | null): string {
|
|
142
|
-
const [urlWithoutQueryParams = url] = url.split('?', 1)
|
|
143
|
-
const fileNameWithExtension = urlWithoutQueryParams.split('/').pop()
|
|
144
|
-
if (!contentType || !fileNameWithExtension) return 'image.bin'
|
|
145
|
-
const [fileName = 'image'] = fileNameWithExtension.split('.', 1)
|
|
146
|
-
const extension = extensionForType(contentType) ?? 'bin'
|
|
147
|
-
return `${fileName}.${extension}
|
|
142
|
+
const [urlWithoutQueryParams = url] = url.split('?', 1)
|
|
143
|
+
const fileNameWithExtension = urlWithoutQueryParams.split('/').pop()
|
|
144
|
+
if (!contentType || !fileNameWithExtension) return 'image.bin'
|
|
145
|
+
const [fileName = 'image'] = fileNameWithExtension.split('.', 1)
|
|
146
|
+
const extension = extensionForType(contentType) ?? 'bin'
|
|
147
|
+
return `${fileName}.${extension}`
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function contentDispositionHeader(type: string, fileName: string): string {
|
|
151
151
|
if (/^[\x20-\x7e]*$/.test(fileName) && !fileName.includes('"')) {
|
|
152
|
-
return `${type}; filename="${fileName}"
|
|
152
|
+
return `${type}; filename="${fileName}"`
|
|
153
153
|
}
|
|
154
|
-
return `${type}; filename*=UTF-8''${encodeURIComponent(fileName)}
|
|
154
|
+
return `${type}; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// -- validation -------------------------------------------------------------
|
|
@@ -162,82 +162,82 @@ function validateParams(
|
|
|
162
162
|
config: ResolvedImagesConfig,
|
|
163
163
|
basePath: string,
|
|
164
164
|
): ImageParams | { errorMessage: string } {
|
|
165
|
-
const url = searchParams.get('url')
|
|
166
|
-
const w = searchParams.get('w')
|
|
167
|
-
const q = searchParams.get('q')
|
|
165
|
+
const url = searchParams.get('url')
|
|
166
|
+
const w = searchParams.get('w')
|
|
167
|
+
const q = searchParams.get('q')
|
|
168
168
|
|
|
169
|
-
if (!url) return { errorMessage: '"url" parameter is required' }
|
|
170
|
-
if (url.length > 3072) return { errorMessage: '"url" parameter is too long' }
|
|
169
|
+
if (!url) return { errorMessage: '"url" parameter is required' }
|
|
170
|
+
if (url.length > 3072) return { errorMessage: '"url" parameter is too long' }
|
|
171
171
|
if (url.startsWith('//')) {
|
|
172
|
-
return { errorMessage: '"url" parameter cannot be a protocol-relative URL (//)' }
|
|
172
|
+
return { errorMessage: '"url" parameter cannot be a protocol-relative URL (//)' }
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
let href: string
|
|
176
|
-
let isAbsolute: boolean
|
|
175
|
+
let href: string
|
|
176
|
+
let isAbsolute: boolean
|
|
177
177
|
if (url.startsWith('/')) {
|
|
178
|
-
href = url
|
|
179
|
-
isAbsolute = false
|
|
180
|
-
let pathname = url.split('?', 1)[0] ?? url
|
|
178
|
+
href = url
|
|
179
|
+
isAbsolute = false
|
|
180
|
+
let pathname = url.split('?', 1)[0] ?? url
|
|
181
181
|
try {
|
|
182
|
-
pathname = decodeURIComponent(pathname)
|
|
182
|
+
pathname = decodeURIComponent(pathname)
|
|
183
183
|
} catch {
|
|
184
184
|
// keep the raw pathname; the regex below still guards the raw form
|
|
185
185
|
}
|
|
186
186
|
if (/\/_next\/image($|\/)/.test(pathname)) {
|
|
187
|
-
return { errorMessage: '"url" parameter cannot be recursive' }
|
|
187
|
+
return { errorMessage: '"url" parameter cannot be recursive' }
|
|
188
188
|
}
|
|
189
189
|
if (!hasLocalMatch(config.localPatterns, new URL(url, 'http://n'))) {
|
|
190
|
-
return { errorMessage: '"url" parameter is not allowed' }
|
|
190
|
+
return { errorMessage: '"url" parameter is not allowed' }
|
|
191
191
|
}
|
|
192
192
|
} else {
|
|
193
|
-
let parsed: URL
|
|
193
|
+
let parsed: URL
|
|
194
194
|
try {
|
|
195
|
-
parsed = new URL(url)
|
|
195
|
+
parsed = new URL(url)
|
|
196
196
|
} catch {
|
|
197
|
-
return { errorMessage: '"url" parameter is invalid' }
|
|
197
|
+
return { errorMessage: '"url" parameter is invalid' }
|
|
198
198
|
}
|
|
199
|
-
href = parsed.toString()
|
|
200
|
-
isAbsolute = true
|
|
199
|
+
href = parsed.toString()
|
|
200
|
+
isAbsolute = true
|
|
201
201
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
202
|
-
return { errorMessage: '"url" parameter is invalid' }
|
|
202
|
+
return { errorMessage: '"url" parameter is invalid' }
|
|
203
203
|
}
|
|
204
204
|
if (!hasRemoteMatch(config.domains, config.remotePatterns, parsed)) {
|
|
205
|
-
return { errorMessage: '"url" parameter is not allowed' }
|
|
205
|
+
return { errorMessage: '"url" parameter is not allowed' }
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
if (!w) return { errorMessage: '"w" parameter (width) is required' }
|
|
209
|
+
if (!w) return { errorMessage: '"w" parameter (width) is required' }
|
|
210
210
|
if (!/^[0-9]+$/.test(w)) {
|
|
211
|
-
return { errorMessage: '"w" parameter (width) must be an integer greater than 0' }
|
|
211
|
+
return { errorMessage: '"w" parameter (width) must be an integer greater than 0' }
|
|
212
212
|
}
|
|
213
|
-
if (!q) return { errorMessage: '"q" parameter (quality) is required' }
|
|
213
|
+
if (!q) return { errorMessage: '"q" parameter (quality) is required' }
|
|
214
214
|
if (!/^[0-9]+$/.test(q)) {
|
|
215
|
-
return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' }
|
|
215
|
+
return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' }
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
const width = parseInt(w, 10)
|
|
218
|
+
const width = parseInt(w, 10)
|
|
219
219
|
if (width <= 0 || isNaN(width)) {
|
|
220
|
-
return { errorMessage: '"w" parameter (width) must be an integer greater than 0' }
|
|
220
|
+
return { errorMessage: '"w" parameter (width) must be an integer greater than 0' }
|
|
221
221
|
}
|
|
222
|
-
const sizes = [...config.deviceSizes, ...config.imageSizes]
|
|
222
|
+
const sizes = [...config.deviceSizes, ...config.imageSizes]
|
|
223
223
|
if (!sizes.includes(width)) {
|
|
224
|
-
return { errorMessage: `"w" parameter (width) of ${width} is not allowed` }
|
|
224
|
+
return { errorMessage: `"w" parameter (width) of ${width} is not allowed` }
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
const quality = parseInt(q, 10)
|
|
227
|
+
const quality = parseInt(q, 10)
|
|
228
228
|
if (isNaN(quality) || quality < 1 || quality > 100) {
|
|
229
|
-
return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' }
|
|
229
|
+
return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' }
|
|
230
230
|
}
|
|
231
231
|
if (config.qualities && !config.qualities.includes(quality)) {
|
|
232
|
-
return { errorMessage: `"q" parameter (quality) of ${q} is not allowed` }
|
|
232
|
+
return { errorMessage: `"q" parameter (quality) of ${q} is not allowed` }
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
const mimeType = getSupportedMimeType(config.formats, request.headers.get('accept') ?? '')
|
|
235
|
+
const mimeType = getSupportedMimeType(config.formats, request.headers.get('accept') ?? '')
|
|
236
236
|
const isStatic =
|
|
237
237
|
url.startsWith(`${basePath}/_next/static/media`) ||
|
|
238
|
-
url.startsWith(`${basePath}/_next/static/immutable/media`)
|
|
238
|
+
url.startsWith(`${basePath}/_next/static/immutable/media`)
|
|
239
239
|
|
|
240
|
-
return { href, isAbsolute, isStatic, width, quality, mimeType }
|
|
240
|
+
return { href, isAbsolute, isStatic, width, quality, mimeType }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// -- optimization -----------------------------------------------------------
|
|
@@ -248,22 +248,22 @@ async function optimizeImage(
|
|
|
248
248
|
width: number,
|
|
249
249
|
quality: number,
|
|
250
250
|
): Promise<Buffer> {
|
|
251
|
-
const sharp = await loadSharp()
|
|
252
|
-
if (!sharp) throw new Error('sharp is not available')
|
|
251
|
+
const sharp = await loadSharp()
|
|
252
|
+
if (!sharp) throw new Error('sharp is not available')
|
|
253
253
|
let transformer = sharp(buffer)
|
|
254
254
|
.timeout({ seconds: 7 })
|
|
255
255
|
.rotate()
|
|
256
|
-
.resize(width, undefined, { withoutEnlargement: true })
|
|
256
|
+
.resize(width, undefined, { withoutEnlargement: true })
|
|
257
257
|
if (contentType === AVIF) {
|
|
258
|
-
transformer = transformer.avif({ quality: Math.max(quality - 20, 1), effort: 3 })
|
|
258
|
+
transformer = transformer.avif({ quality: Math.max(quality - 20, 1), effort: 3 })
|
|
259
259
|
} else if (contentType === WEBP) {
|
|
260
|
-
transformer = transformer.webp({ quality })
|
|
260
|
+
transformer = transformer.webp({ quality })
|
|
261
261
|
} else if (contentType === PNG) {
|
|
262
|
-
transformer = transformer.png({ quality })
|
|
262
|
+
transformer = transformer.png({ quality })
|
|
263
263
|
} else if (contentType === JPEG) {
|
|
264
|
-
transformer = transformer.jpeg({ quality, mozjpeg: true })
|
|
264
|
+
transformer = transformer.jpeg({ quality, mozjpeg: true })
|
|
265
265
|
}
|
|
266
|
-
return transformer.toBuffer()
|
|
266
|
+
return transformer.toBuffer()
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
async function generateImage(
|
|
@@ -271,33 +271,33 @@ async function generateImage(
|
|
|
271
271
|
params: ImageParams,
|
|
272
272
|
config: ResolvedImagesConfig,
|
|
273
273
|
): Promise<GeneratedImage> {
|
|
274
|
-
const { href, width, quality, mimeType } = params
|
|
275
|
-
const upstreamBuffer = upstream.buffer
|
|
276
|
-
const upstreamEtag = upstream.etag
|
|
277
|
-
const maxAge = Math.max(config.minimumCacheTTL, getMaxAge(upstream.cacheControl))
|
|
274
|
+
const { href, width, quality, mimeType } = params
|
|
275
|
+
const upstreamBuffer = upstream.buffer
|
|
276
|
+
const upstreamEtag = upstream.etag
|
|
277
|
+
const maxAge = Math.max(config.minimumCacheTTL, getMaxAge(upstream.cacheControl))
|
|
278
278
|
|
|
279
|
-
const upstreamType = detectContentType(upstreamBuffer)
|
|
279
|
+
const upstreamType = detectContentType(upstreamBuffer)
|
|
280
280
|
if (!upstreamType || !upstreamType.startsWith('image/') || upstreamType.includes(',')) {
|
|
281
|
-
console.error("The requested resource isn't a valid image for", href, 'received', upstreamType)
|
|
282
|
-
throw new ImageError(400, "The requested resource isn't a valid image.")
|
|
281
|
+
console.error("The requested resource isn't a valid image for", href, 'received', upstreamType)
|
|
282
|
+
throw new ImageError(400, "The requested resource isn't a valid image.")
|
|
283
283
|
}
|
|
284
284
|
if (upstreamType.startsWith('image/svg') && !config.dangerouslyAllowSVG) {
|
|
285
285
|
console.error(
|
|
286
286
|
`The requested resource "${href}" has type "${upstreamType}" but dangerouslyAllowSVG is disabled. Consider adding the "unoptimized" property to the <Image>.`,
|
|
287
|
-
)
|
|
288
|
-
throw new ImageError(400, '"url" parameter is valid but image type is not allowed')
|
|
287
|
+
)
|
|
288
|
+
throw new ImageError(400, '"url" parameter is valid but image type is not allowed')
|
|
289
289
|
}
|
|
290
290
|
if (ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)) {
|
|
291
291
|
warnOnce(
|
|
292
292
|
`The requested resource "${href}" is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`,
|
|
293
|
-
)
|
|
293
|
+
)
|
|
294
294
|
return {
|
|
295
295
|
buffer: upstreamBuffer,
|
|
296
296
|
contentType: upstreamType,
|
|
297
297
|
maxAge,
|
|
298
298
|
etag: upstreamEtag,
|
|
299
299
|
upstreamEtag,
|
|
300
|
-
}
|
|
300
|
+
}
|
|
301
301
|
}
|
|
302
302
|
if (BYPASS_TYPES.includes(upstreamType)) {
|
|
303
303
|
return {
|
|
@@ -306,31 +306,27 @@ async function generateImage(
|
|
|
306
306
|
maxAge,
|
|
307
307
|
etag: upstreamEtag,
|
|
308
308
|
upstreamEtag,
|
|
309
|
-
}
|
|
309
|
+
}
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
let contentType: string
|
|
312
|
+
let contentType: string
|
|
313
313
|
if (mimeType) {
|
|
314
|
-
contentType = mimeType
|
|
315
|
-
} else if (
|
|
316
|
-
|
|
317
|
-
upstreamType !== WEBP &&
|
|
318
|
-
upstreamType !== AVIF
|
|
319
|
-
) {
|
|
320
|
-
contentType = upstreamType;
|
|
314
|
+
contentType = mimeType
|
|
315
|
+
} else if (extensionForType(upstreamType) && upstreamType !== WEBP && upstreamType !== AVIF) {
|
|
316
|
+
contentType = upstreamType
|
|
321
317
|
} else {
|
|
322
|
-
contentType = JPEG
|
|
318
|
+
contentType = JPEG
|
|
323
319
|
}
|
|
324
320
|
|
|
325
321
|
try {
|
|
326
|
-
const optimizedBuffer = await optimizeImage(upstreamBuffer, contentType, width, quality)
|
|
322
|
+
const optimizedBuffer = await optimizeImage(upstreamBuffer, contentType, width, quality)
|
|
327
323
|
return {
|
|
328
324
|
buffer: optimizedBuffer,
|
|
329
325
|
contentType,
|
|
330
326
|
maxAge,
|
|
331
327
|
etag: getHash([optimizedBuffer]),
|
|
332
328
|
upstreamEtag,
|
|
333
|
-
}
|
|
329
|
+
}
|
|
334
330
|
} catch {
|
|
335
331
|
// If optimization fails, fall back to the original image bytes.
|
|
336
332
|
return {
|
|
@@ -339,31 +335,31 @@ async function generateImage(
|
|
|
339
335
|
maxAge: config.minimumCacheTTL,
|
|
340
336
|
etag: upstreamEtag,
|
|
341
337
|
upstreamEtag,
|
|
342
|
-
}
|
|
338
|
+
}
|
|
343
339
|
}
|
|
344
340
|
}
|
|
345
341
|
|
|
346
342
|
// -- cache orchestration ----------------------------------------------------
|
|
347
343
|
|
|
348
344
|
interface OptimizerImagesConfig extends ResolvedImagesConfig {
|
|
349
|
-
maximumDiskCacheSize: number | undefined
|
|
345
|
+
maximumDiskCacheSize: number | undefined
|
|
350
346
|
}
|
|
351
347
|
|
|
352
348
|
function resolveOptimizerConfig(): OptimizerImagesConfig {
|
|
353
|
-
const base = getImagesConfig()
|
|
354
|
-
const raw = (getNextConfig().images ?? {}) as Record<string, unknown
|
|
349
|
+
const base = getImagesConfig()
|
|
350
|
+
const raw = (getNextConfig().images ?? {}) as Record<string, unknown>
|
|
355
351
|
return {
|
|
356
352
|
...base,
|
|
357
353
|
maximumDiskCacheSize:
|
|
358
354
|
typeof raw.maximumDiskCacheSize === 'number' ? raw.maximumDiskCacheSize : undefined,
|
|
359
|
-
}
|
|
355
|
+
}
|
|
360
356
|
}
|
|
361
357
|
|
|
362
358
|
function diskCacheEnabled(config: OptimizerImagesConfig): boolean {
|
|
363
|
-
if (config.customCacheHandler && hasCacheHandler()) return false
|
|
364
|
-
if (config.maximumDiskCacheSize === 0) return false
|
|
365
|
-
const experimental = (getNextConfig().experimental ?? {}) as Record<string, unknown
|
|
366
|
-
return experimental.isrFlushToDisk !== false
|
|
359
|
+
if (config.customCacheHandler && hasCacheHandler()) return false
|
|
360
|
+
if (config.maximumDiskCacheSize === 0) return false
|
|
361
|
+
const experimental = (getNextConfig().experimental ?? {}) as Record<string, unknown>
|
|
362
|
+
return experimental.isrFlushToDisk !== false
|
|
367
363
|
}
|
|
368
364
|
|
|
369
365
|
async function fetchUpstream(
|
|
@@ -378,7 +374,7 @@ async function fetchUpstream(
|
|
|
378
374
|
config.dangerouslyAllowLocalIP,
|
|
379
375
|
config.maximumResponseBody,
|
|
380
376
|
config.maximumRedirects,
|
|
381
|
-
)
|
|
377
|
+
)
|
|
382
378
|
}
|
|
383
379
|
const publicFile = await readPublicFile(
|
|
384
380
|
[
|
|
@@ -387,16 +383,16 @@ async function fetchUpstream(
|
|
|
387
383
|
path.join(coreConfig.root, 'public'),
|
|
388
384
|
],
|
|
389
385
|
params.href,
|
|
390
|
-
)
|
|
386
|
+
)
|
|
391
387
|
if (publicFile) {
|
|
392
388
|
return {
|
|
393
389
|
buffer: publicFile,
|
|
394
390
|
contentType: null,
|
|
395
391
|
cacheControl: null,
|
|
396
392
|
etag: getHash([publicFile]),
|
|
397
|
-
}
|
|
393
|
+
}
|
|
398
394
|
}
|
|
399
|
-
return fetchInternalImage(params.href, origin, config.maximumResponseBody)
|
|
395
|
+
return fetchInternalImage(params.href, origin, config.maximumResponseBody)
|
|
400
396
|
}
|
|
401
397
|
|
|
402
398
|
async function generateAndStore(
|
|
@@ -406,8 +402,8 @@ async function generateAndStore(
|
|
|
406
402
|
coreConfig: ResolvedConfig,
|
|
407
403
|
origin: string,
|
|
408
404
|
): Promise<ImageCacheEntry & { contentType: string }> {
|
|
409
|
-
const upstream = await fetchUpstream(params, config, coreConfig, origin)
|
|
410
|
-
const image = await generateImage(upstream, params, config)
|
|
405
|
+
const upstream = await fetchUpstream(params, config, coreConfig, origin)
|
|
406
|
+
const image = await generateImage(upstream, params, config)
|
|
411
407
|
const entry: ImageCacheEntry & { contentType: string } = {
|
|
412
408
|
buffer: image.buffer,
|
|
413
409
|
etag: image.etag,
|
|
@@ -416,9 +412,9 @@ async function generateAndStore(
|
|
|
416
412
|
maxAge: image.maxAge,
|
|
417
413
|
expireAt: Math.max(image.maxAge, config.minimumCacheTTL) * 1000 + Date.now(),
|
|
418
414
|
contentType: image.contentType,
|
|
419
|
-
}
|
|
415
|
+
}
|
|
420
416
|
if (config.customCacheHandler && hasCacheHandler()) {
|
|
421
|
-
const revalidate = Math.max(image.maxAge, config.minimumCacheTTL)
|
|
417
|
+
const revalidate = Math.max(image.maxAge, config.minimumCacheTTL)
|
|
422
418
|
await cacheHandlerSet(
|
|
423
419
|
cacheKey,
|
|
424
420
|
{
|
|
@@ -430,23 +426,23 @@ async function generateAndStore(
|
|
|
430
426
|
revalidate,
|
|
431
427
|
},
|
|
432
428
|
{ kind: 'IMAGE' },
|
|
433
|
-
)
|
|
429
|
+
)
|
|
434
430
|
} else if (diskCacheEnabled(config)) {
|
|
435
431
|
await writeCacheEntry(
|
|
436
432
|
imageCacheDir(coreConfig.root),
|
|
437
433
|
cacheKey,
|
|
438
434
|
entry,
|
|
439
435
|
config.maximumDiskCacheSize,
|
|
440
|
-
)
|
|
436
|
+
)
|
|
441
437
|
}
|
|
442
|
-
return entry
|
|
438
|
+
return entry
|
|
443
439
|
}
|
|
444
440
|
|
|
445
441
|
// Concurrent requests for the same (url, w, q, format) share one upstream
|
|
446
442
|
// fetch + optimization; every waiter reports MISS, matching Next.
|
|
447
|
-
const inFlight = new Map<string, Promise<ImageCacheEntry & { contentType: string }>>()
|
|
443
|
+
const inFlight = new Map<string, Promise<ImageCacheEntry & { contentType: string }>>()
|
|
448
444
|
// One background stale-refresh per key at a time.
|
|
449
|
-
const refreshing = new Set<string>()
|
|
445
|
+
const refreshing = new Set<string>()
|
|
450
446
|
|
|
451
447
|
async function readCache(
|
|
452
448
|
cacheKey: string,
|
|
@@ -455,14 +451,13 @@ async function readCache(
|
|
|
455
451
|
): Promise<(ImageCacheEntry & { isStale: boolean }) | undefined> {
|
|
456
452
|
if (config.customCacheHandler && hasCacheHandler()) {
|
|
457
453
|
const cached = (await cacheHandlerGet(cacheKey, { kind: 'IMAGE' })) as
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (value?.kind !== 'IMAGE' || typeof value.buffer !== 'string') return undefined;
|
|
454
|
+
{ value?: Record<string, unknown>; lastModified?: number } | undefined
|
|
455
|
+
const value = cached?.value
|
|
456
|
+
if (value?.kind !== 'IMAGE' || typeof value.buffer !== 'string') return undefined
|
|
462
457
|
const revalidate =
|
|
463
|
-
typeof value.revalidate === 'number' ? value.revalidate : config.minimumCacheTTL
|
|
464
|
-
const lastModified = cached?.lastModified ?? Date.now()
|
|
465
|
-
const expireAt = Math.max(revalidate, config.minimumCacheTTL) * 1000 + lastModified
|
|
458
|
+
typeof value.revalidate === 'number' ? value.revalidate : config.minimumCacheTTL
|
|
459
|
+
const lastModified = cached?.lastModified ?? Date.now()
|
|
460
|
+
const expireAt = Math.max(revalidate, config.minimumCacheTTL) * 1000 + lastModified
|
|
466
461
|
return {
|
|
467
462
|
buffer: Buffer.from(value.buffer, 'base64'),
|
|
468
463
|
etag: typeof value.etag === 'string' ? value.etag : getHash([value.buffer]),
|
|
@@ -471,12 +466,12 @@ async function readCache(
|
|
|
471
466
|
maxAge: revalidate,
|
|
472
467
|
expireAt,
|
|
473
468
|
isStale: expireAt < Date.now(),
|
|
474
|
-
}
|
|
469
|
+
}
|
|
475
470
|
}
|
|
476
|
-
if (!diskCacheEnabled(config)) return undefined
|
|
477
|
-
const entry = await readCacheEntry(imageCacheDir(coreConfig.root), cacheKey)
|
|
478
|
-
if (!entry) return undefined
|
|
479
|
-
return { ...entry, isStale: Date.now() > entry.expireAt }
|
|
471
|
+
if (!diskCacheEnabled(config)) return undefined
|
|
472
|
+
const entry = await readCacheEntry(imageCacheDir(coreConfig.root), cacheKey)
|
|
473
|
+
if (!entry) return undefined
|
|
474
|
+
return { ...entry, isStale: Date.now() > entry.expireAt }
|
|
480
475
|
}
|
|
481
476
|
|
|
482
477
|
// -- response ---------------------------------------------------------------
|
|
@@ -491,34 +486,34 @@ function buildResponse(
|
|
|
491
486
|
maxAge: number,
|
|
492
487
|
xCache: XCacheHeader,
|
|
493
488
|
): Response {
|
|
494
|
-
const headers = new Headers()
|
|
495
|
-
headers.set('Vary', 'Accept')
|
|
489
|
+
const headers = new Headers()
|
|
490
|
+
headers.set('Vary', 'Accept')
|
|
496
491
|
headers.set(
|
|
497
492
|
'Cache-Control',
|
|
498
493
|
params.isStatic
|
|
499
494
|
? 'public, max-age=315360000, immutable'
|
|
500
495
|
: `public, max-age=${maxAge}, must-revalidate`,
|
|
501
|
-
)
|
|
502
|
-
headers.set('ETag', etag)
|
|
496
|
+
)
|
|
497
|
+
headers.set('ETag', etag)
|
|
503
498
|
|
|
504
|
-
const ifNoneMatch = request.headers.get('if-none-match')
|
|
499
|
+
const ifNoneMatch = request.headers.get('if-none-match')
|
|
505
500
|
if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === `"${etag}"`)) {
|
|
506
|
-
headers.set('X-Nextjs-Cache', xCache)
|
|
507
|
-
return new Response(null, { status: 304, headers })
|
|
501
|
+
headers.set('X-Nextjs-Cache', xCache)
|
|
502
|
+
return new Response(null, { status: 304, headers })
|
|
508
503
|
}
|
|
509
504
|
|
|
510
|
-
headers.set('Content-Type', contentType)
|
|
511
|
-
const fileName = getFileNameWithExtension(params.href, contentType)
|
|
505
|
+
headers.set('Content-Type', contentType)
|
|
506
|
+
const fileName = getFileNameWithExtension(params.href, contentType)
|
|
512
507
|
headers.set(
|
|
513
508
|
'Content-Disposition',
|
|
514
509
|
contentDispositionHeader(config.contentDispositionType, fileName),
|
|
515
|
-
)
|
|
516
|
-
headers.set('Content-Security-Policy', config.contentSecurityPolicy)
|
|
517
|
-
headers.set('X-Nextjs-Cache', xCache)
|
|
518
|
-
headers.set('Content-Length', String(buffer.byteLength))
|
|
510
|
+
)
|
|
511
|
+
headers.set('Content-Security-Policy', config.contentSecurityPolicy)
|
|
512
|
+
headers.set('X-Nextjs-Cache', xCache)
|
|
513
|
+
headers.set('Content-Length', String(buffer.byteLength))
|
|
519
514
|
|
|
520
|
-
if (request.method === 'HEAD') return new Response(null, { status: 200, headers })
|
|
521
|
-
return new Response(new Uint8Array(buffer), { status: 200, headers })
|
|
515
|
+
if (request.method === 'HEAD') return new Response(null, { status: 200, headers })
|
|
516
|
+
return new Response(new Uint8Array(buffer), { status: 200, headers })
|
|
522
517
|
}
|
|
523
518
|
|
|
524
519
|
// -- entry point ------------------------------------------------------------
|
|
@@ -527,13 +522,13 @@ export async function serveImageOptimizer(
|
|
|
527
522
|
request: Request,
|
|
528
523
|
coreConfig: ResolvedConfig,
|
|
529
524
|
): Promise<Response> {
|
|
530
|
-
const config = resolveOptimizerConfig()
|
|
531
|
-
const url = new URL(request.url)
|
|
532
|
-
const basePath = ''
|
|
525
|
+
const config = resolveOptimizerConfig()
|
|
526
|
+
const url = new URL(request.url)
|
|
527
|
+
const basePath = ''
|
|
533
528
|
|
|
534
|
-
const paramsResult = validateParams(request, url.searchParams, config, basePath)
|
|
535
|
-
if ('errorMessage' in paramsResult) return badRequest(paramsResult.errorMessage)
|
|
536
|
-
const params = paramsResult
|
|
529
|
+
const paramsResult = validateParams(request, url.searchParams, config, basePath)
|
|
530
|
+
if ('errorMessage' in paramsResult) return badRequest(paramsResult.errorMessage)
|
|
531
|
+
const params = paramsResult
|
|
537
532
|
|
|
538
533
|
const cacheKey = getHash([
|
|
539
534
|
IMAGE_CACHE_VERSION,
|
|
@@ -541,11 +536,11 @@ export async function serveImageOptimizer(
|
|
|
541
536
|
params.width,
|
|
542
537
|
params.quality,
|
|
543
538
|
params.mimeType,
|
|
544
|
-
])
|
|
545
|
-
const origin = url.origin
|
|
539
|
+
])
|
|
540
|
+
const origin = url.origin
|
|
546
541
|
|
|
547
542
|
try {
|
|
548
|
-
const cached = await readCache(cacheKey, config, coreConfig)
|
|
543
|
+
const cached = await readCache(cacheKey, config, coreConfig)
|
|
549
544
|
if (cached && !cached.isStale) {
|
|
550
545
|
return buildResponse(
|
|
551
546
|
request,
|
|
@@ -556,15 +551,15 @@ export async function serveImageOptimizer(
|
|
|
556
551
|
cached.etag,
|
|
557
552
|
cached.maxAge,
|
|
558
553
|
'HIT',
|
|
559
|
-
)
|
|
554
|
+
)
|
|
560
555
|
}
|
|
561
556
|
if (cached) {
|
|
562
557
|
// Serve the stale bytes now, refresh in the background.
|
|
563
558
|
if (!refreshing.has(cacheKey)) {
|
|
564
|
-
refreshing.add(cacheKey)
|
|
559
|
+
refreshing.add(cacheKey)
|
|
565
560
|
void generateAndStore(cacheKey, params, config, coreConfig, origin)
|
|
566
561
|
.catch(() => undefined)
|
|
567
|
-
.finally(() => refreshing.delete(cacheKey))
|
|
562
|
+
.finally(() => refreshing.delete(cacheKey))
|
|
568
563
|
}
|
|
569
564
|
return buildResponse(
|
|
570
565
|
request,
|
|
@@ -575,16 +570,16 @@ export async function serveImageOptimizer(
|
|
|
575
570
|
cached.etag,
|
|
576
571
|
cached.maxAge,
|
|
577
572
|
'STALE',
|
|
578
|
-
)
|
|
573
|
+
)
|
|
579
574
|
}
|
|
580
575
|
|
|
581
|
-
let pending = inFlight.get(cacheKey)
|
|
576
|
+
let pending = inFlight.get(cacheKey)
|
|
582
577
|
if (!pending) {
|
|
583
|
-
pending = generateAndStore(cacheKey, params, config, coreConfig, origin)
|
|
584
|
-
inFlight.set(cacheKey, pending)
|
|
585
|
-
void pending.catch(() => undefined).finally(() => inFlight.delete(cacheKey))
|
|
578
|
+
pending = generateAndStore(cacheKey, params, config, coreConfig, origin)
|
|
579
|
+
inFlight.set(cacheKey, pending)
|
|
580
|
+
void pending.catch(() => undefined).finally(() => inFlight.delete(cacheKey))
|
|
586
581
|
}
|
|
587
|
-
const entry = await pending
|
|
582
|
+
const entry = await pending
|
|
588
583
|
return buildResponse(
|
|
589
584
|
request,
|
|
590
585
|
params,
|
|
@@ -594,8 +589,8 @@ export async function serveImageOptimizer(
|
|
|
594
589
|
entry.etag,
|
|
595
590
|
entry.maxAge,
|
|
596
591
|
'MISS',
|
|
597
|
-
)
|
|
592
|
+
)
|
|
598
593
|
} catch (error) {
|
|
599
|
-
return errorResponse(error)
|
|
594
|
+
return errorResponse(error)
|
|
600
595
|
}
|
|
601
596
|
}
|