@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
package/src/client/build.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
-
import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises'
|
|
4
|
-
import path from 'node:path'
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
+
import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
build,
|
|
7
7
|
type BuildOptions,
|
|
@@ -12,23 +12,27 @@ import {
|
|
|
12
12
|
type Plugin,
|
|
13
13
|
type PluginBuild,
|
|
14
14
|
type ResolveResult,
|
|
15
|
-
} from 'esbuild'
|
|
16
|
-
import { foldInitialChunks } from './chunk-fold'
|
|
17
|
-
import { clientEntryName } from './
|
|
18
|
-
import { clientProfile } from './profile'
|
|
19
|
-
import { publicEnvDefines, type ResolvedConfig } from '../config'
|
|
20
|
-
import { cssModuleClientPlugin } from '../css'
|
|
21
|
-
import { withAssetPrefix } from '../css/build'
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
} from 'esbuild'
|
|
16
|
+
import { foldInitialChunks } from './chunk-fold'
|
|
17
|
+
import { clientEntryName } from './chunk-name'
|
|
18
|
+
import { clientProfile } from './profile'
|
|
19
|
+
import { frameworkRuntimeAliasEntries, publicEnvDefines, type ResolvedConfig } from '../config'
|
|
20
|
+
import { cssModuleClientPlugin } from '../css/build'
|
|
21
|
+
import { withAssetPrefix } from '../css/build'
|
|
22
|
+
import {
|
|
23
|
+
devDynamicSplitEnabled,
|
|
24
|
+
rewriteDeferredDynamicImports,
|
|
25
|
+
rewriteLiteralDynamicCalls,
|
|
26
|
+
} from '../resolve/dynamic'
|
|
27
|
+
import { scanFacts } from '../resolve/scan-facts'
|
|
28
|
+
import { readSourceText } from '../resolve/source-text'
|
|
29
|
+
import { ensureDir, listFiles, readText, writeText } from '../utils/fs'
|
|
30
|
+
import { escapeRegex } from '../utils/code'
|
|
27
31
|
import {
|
|
28
32
|
getExternalPackagePolicy,
|
|
29
33
|
resolveImport,
|
|
30
34
|
resolveLinkedPackageSpecifier,
|
|
31
|
-
} from '../resolve/imports'
|
|
35
|
+
} from '../resolve/imports'
|
|
32
36
|
import {
|
|
33
37
|
CLIENT_RUNTIME_MODULE,
|
|
34
38
|
DYN_SHARED_GLOBAL,
|
|
@@ -37,8 +41,8 @@ import {
|
|
|
37
41
|
clientRuntimeFacts,
|
|
38
42
|
clientRuntimeSource,
|
|
39
43
|
type ClientRuntimeFacts,
|
|
40
|
-
} from './entry'
|
|
41
|
-
import { clientSuspenseFree } from './
|
|
44
|
+
} from './entry'
|
|
45
|
+
import { clientSuspenseFree } from './react-tier'
|
|
42
46
|
import {
|
|
43
47
|
ensurePrebuiltRuntime,
|
|
44
48
|
prebuiltAssets,
|
|
@@ -48,13 +52,13 @@ import {
|
|
|
48
52
|
prebuiltSpecifierFilter,
|
|
49
53
|
settlePrebuiltRuntime,
|
|
50
54
|
type PrebuiltRuntime,
|
|
51
|
-
} from './prebuilt'
|
|
52
|
-
import { findLayouts } from '../routing/routes'
|
|
53
|
-
import { clientReferenceId, ssrClientReference, type ClientReference } from './reference'
|
|
54
|
-
import { shouldReactCompile, transformReactCompiler } from './react-compiler'
|
|
55
|
-
import { createVerboseLogger } from '../utils/verbose'
|
|
56
|
-
import { dim as dimText } from '../utils/ansi'
|
|
57
|
-
import type { RouteManifestEntry } from '../types'
|
|
55
|
+
} from './prebuilt'
|
|
56
|
+
import { findLayouts } from '../routing/routes'
|
|
57
|
+
import { clientReferenceId, ssrClientReference, type ClientReference } from './reference'
|
|
58
|
+
import { shouldReactCompile, transformReactCompiler } from './react-compiler'
|
|
59
|
+
import { createVerboseLogger, formatDuration } from '../utils/verbose'
|
|
60
|
+
import { dim as dimText } from '../utils/ansi'
|
|
61
|
+
import type { RouteManifestEntry } from '../types'
|
|
58
62
|
import {
|
|
59
63
|
applyClientSourceAsyncPreTransforms,
|
|
60
64
|
applyClientSourceTransforms,
|
|
@@ -64,7 +68,7 @@ import {
|
|
|
64
68
|
getImportAliasExtensions,
|
|
65
69
|
hasClientSourceAsyncPreTransforms,
|
|
66
70
|
type CompatModeExtensions,
|
|
67
|
-
} from '../extensions'
|
|
71
|
+
} from '../extensions'
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* Write the standalone static chunks a bundler extension supplies (compat: the
|
|
@@ -73,44 +77,44 @@ import {
|
|
|
73
77
|
* build. No-op for pure-core apps (the seam returns none).
|
|
74
78
|
*/
|
|
75
79
|
export async function emitStaticClientChunks(config: ResolvedConfig, outDir: string) {
|
|
76
|
-
const chunks = getBundlerExtensions().staticClientChunks(config)
|
|
77
|
-
if (chunks.length === 0) return
|
|
78
|
-
const chunksDir = path.join(outDir, 'chunks')
|
|
79
|
-
await ensureDir(chunksDir)
|
|
80
|
+
const chunks = getBundlerExtensions().staticClientChunks(config)
|
|
81
|
+
if (chunks.length === 0) return
|
|
82
|
+
const chunksDir = path.join(outDir, 'chunks')
|
|
83
|
+
await ensureDir(chunksDir)
|
|
80
84
|
for (const chunk of chunks) {
|
|
81
|
-
await writeText(path.join(chunksDir, chunk.name), chunk.contents)
|
|
85
|
+
await writeText(path.join(chunksDir, chunk.name), chunk.contents)
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
interface ClientBuildOptions {
|
|
86
|
-
config: ResolvedConfig
|
|
87
|
-
route: RouteManifestEntry
|
|
88
|
-
outDir: string
|
|
89
|
-
dev?: boolean
|
|
90
|
+
config: ResolvedConfig
|
|
91
|
+
route: RouteManifestEntry
|
|
92
|
+
outDir: string
|
|
93
|
+
dev?: boolean
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
interface ClientBatchBuildOptions {
|
|
93
|
-
config: ResolvedConfig
|
|
94
|
-
routes: RouteManifestEntry[]
|
|
95
|
-
outDir: string
|
|
96
|
-
verbose?: boolean
|
|
97
|
+
config: ResolvedConfig
|
|
98
|
+
routes: RouteManifestEntry[]
|
|
99
|
+
outDir: string
|
|
100
|
+
verbose?: boolean
|
|
97
101
|
/**
|
|
98
102
|
* The app defines at least one server action (the build's action manifest). Unset means "unknown" -
|
|
99
103
|
* dev, where the manifest is discovered per request - and the action runtime is emitted, since only
|
|
100
104
|
* prod bundles are budgeted.
|
|
101
105
|
*/
|
|
102
|
-
hasServerActions?: boolean
|
|
106
|
+
hasServerActions?: boolean
|
|
103
107
|
/**
|
|
104
108
|
* A pipeline already warmed by the caller (the build starts one under the
|
|
105
109
|
* route-fact scan). Omitted — dev, tests — the stage opens its own.
|
|
106
110
|
*/
|
|
107
|
-
pipeline?: ClientSourcePipeline
|
|
111
|
+
pipeline?: ClientSourcePipeline
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
const virtualEntryNamespace = 'pnext-client-entry'
|
|
114
|
+
const virtualEntryNamespace = 'pnext-client-entry'
|
|
111
115
|
|
|
112
116
|
function nextCompatEnabled(config: ResolvedConfig) {
|
|
113
|
-
return getCompatModeExtensions().nextEnabled(config)
|
|
117
|
+
return getCompatModeExtensions().nextEnabled(config)
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
/**
|
|
@@ -119,13 +123,13 @@ function nextCompatEnabled(config: ResolvedConfig) {
|
|
|
119
123
|
* directories once per route. Memoized for the duration of one build and cleared at each entry
|
|
120
124
|
* point, so a dev rebuild re-stats after a file is added or removed.
|
|
121
125
|
*/
|
|
122
|
-
const conventionCache = new Map<string, unknown>()
|
|
126
|
+
const conventionCache = new Map<string, unknown>()
|
|
123
127
|
|
|
124
128
|
function memoConvention<T>(key: string, compute: () => T): T {
|
|
125
|
-
if (conventionCache.has(key)) return conventionCache.get(key) as T
|
|
126
|
-
const value = compute()
|
|
127
|
-
conventionCache.set(key, value)
|
|
128
|
-
return value
|
|
129
|
+
if (conventionCache.has(key)) return conventionCache.get(key) as T
|
|
130
|
+
const value = compute()
|
|
131
|
+
conventionCache.set(key, value)
|
|
132
|
+
return value
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/**
|
|
@@ -136,10 +140,10 @@ function memoConvention<T>(key: string, compute: () => T): T {
|
|
|
136
140
|
* drops it.
|
|
137
141
|
*/
|
|
138
142
|
function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean) {
|
|
139
|
-
if (hasServerActions !== false) return true
|
|
143
|
+
if (hasServerActions !== false) return true
|
|
140
144
|
return routes.some(route =>
|
|
141
145
|
route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form'),
|
|
142
|
-
)
|
|
146
|
+
)
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
@@ -148,10 +152,10 @@ function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean
|
|
|
148
152
|
* actions elsewhere.
|
|
149
153
|
*/
|
|
150
154
|
function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
|
|
151
|
-
if (!appActions) return false
|
|
155
|
+
if (!appActions) return false
|
|
152
156
|
return (
|
|
153
157
|
route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form') === true
|
|
154
|
-
)
|
|
158
|
+
)
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
/**
|
|
@@ -160,15 +164,15 @@ function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
|
|
|
160
164
|
* file - has to be in the tree to catch it.
|
|
161
165
|
*/
|
|
162
166
|
function routeThrowsClientControlFlow(route: RouteManifestEntry): boolean {
|
|
163
|
-
return route.clientEntryReasons?.includes('control-flow') === true
|
|
167
|
+
return route.clientEntryReasons?.includes('control-flow') === true
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
function routeErrorFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
|
|
167
|
-
return routeConventionFile(config, route, 'error')
|
|
171
|
+
return routeConventionFile(config, route, 'error')
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
function routeNotFoundFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
|
|
171
|
-
return routeConventionFile(config, route, 'not-found')
|
|
175
|
+
return routeConventionFile(config, route, 'not-found')
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
// Nearest segment convention file for a route: walked from the page's own
|
|
@@ -178,26 +182,26 @@ function routeConventionFile(
|
|
|
178
182
|
route: RouteManifestEntry,
|
|
179
183
|
name: string,
|
|
180
184
|
): string | undefined {
|
|
181
|
-
if (!nextCompatEnabled(config)) return undefined
|
|
182
|
-
const appPath = path.resolve(config.appPath)
|
|
183
|
-
const start = path.dirname(path.resolve(route.file))
|
|
184
|
-
const relative = path.relative(appPath, start)
|
|
185
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined
|
|
185
|
+
if (!nextCompatEnabled(config)) return undefined
|
|
186
|
+
const appPath = path.resolve(config.appPath)
|
|
187
|
+
const start = path.dirname(path.resolve(route.file))
|
|
188
|
+
const relative = path.relative(appPath, start)
|
|
189
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined
|
|
186
190
|
// Memoized per directory, not per route: sibling routes walk the same chain,
|
|
187
191
|
// and a parent's answer is the tail of every child's walk.
|
|
188
192
|
return memoConvention(`${name}\0${start}`, () => {
|
|
189
|
-
let dir = start
|
|
193
|
+
let dir = start
|
|
190
194
|
for (;;) {
|
|
191
195
|
for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
|
|
192
|
-
const candidate = path.join(dir, `${name}.${ext}`)
|
|
193
|
-
if (existsSync(candidate)) return candidate
|
|
196
|
+
const candidate = path.join(dir, `${name}.${ext}`)
|
|
197
|
+
if (existsSync(candidate)) return candidate
|
|
194
198
|
}
|
|
195
|
-
if (dir === appPath) return undefined
|
|
196
|
-
const parent = path.dirname(dir)
|
|
197
|
-
if (parent === dir) return undefined
|
|
198
|
-
dir = parent
|
|
199
|
+
if (dir === appPath) return undefined
|
|
200
|
+
const parent = path.dirname(dir)
|
|
201
|
+
if (parent === dir) return undefined
|
|
202
|
+
dir = parent
|
|
199
203
|
}
|
|
200
|
-
})
|
|
204
|
+
})
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
// The app-root global-error.* convention file (single per app, unlike error.*
|
|
@@ -205,15 +209,15 @@ function routeConventionFile(
|
|
|
205
209
|
// can mount a user global-error component for client throws that escape every
|
|
206
210
|
// route error boundary. Absent → the built-in fallback document is used.
|
|
207
211
|
function globalErrorFile(config: ResolvedConfig): string | undefined {
|
|
208
|
-
if (!nextCompatEnabled(config)) return undefined
|
|
209
|
-
const appPath = path.resolve(config.appPath)
|
|
212
|
+
if (!nextCompatEnabled(config)) return undefined
|
|
213
|
+
const appPath = path.resolve(config.appPath)
|
|
210
214
|
return memoConvention(`global-error\0${appPath}`, () => {
|
|
211
215
|
for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
|
|
212
|
-
const candidate = path.join(appPath, `global-error.${ext}`)
|
|
213
|
-
if (existsSync(candidate)) return candidate
|
|
216
|
+
const candidate = path.join(appPath, `global-error.${ext}`)
|
|
217
|
+
if (existsSync(candidate)) return candidate
|
|
214
218
|
}
|
|
215
|
-
return undefined
|
|
216
|
-
})
|
|
219
|
+
return undefined
|
|
220
|
+
})
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
/**
|
|
@@ -225,10 +229,10 @@ function globalErrorFile(config: ResolvedConfig): string | undefined {
|
|
|
225
229
|
* this is true, and the shell path depends on those markers.
|
|
226
230
|
*/
|
|
227
231
|
function hasClientRootLayout(config: ResolvedConfig, route: RouteManifestEntry): boolean {
|
|
228
|
-
if (route.client) return false
|
|
229
|
-
const rootLayout = shellLayoutOrder(config, route)[0]
|
|
230
|
-
if (!rootLayout) return false
|
|
231
|
-
return route.clientReferences.some(reference => reference.file === rootLayout)
|
|
232
|
+
if (route.client) return false
|
|
233
|
+
const rootLayout = shellLayoutOrder(config, route)[0]
|
|
234
|
+
if (!rootLayout) return false
|
|
235
|
+
return route.clientReferences.some(reference => reference.file === rootLayout)
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
/**
|
|
@@ -240,7 +244,7 @@ function shellLayoutOrder(config: ResolvedConfig, route: RouteManifestEntry): st
|
|
|
240
244
|
// Asked three times per route (shell order, client-root check, runtime facts).
|
|
241
245
|
return memoConvention(`layouts\0${config.appPath}\0${route.file}`, () =>
|
|
242
246
|
findLayouts(config.appPath, route.file),
|
|
243
|
-
)
|
|
247
|
+
)
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
/**
|
|
@@ -256,24 +260,22 @@ function prebuiltLocation(config: ResolvedConfig, outDir: string, key: string, d
|
|
|
256
260
|
dir: prebuiltRuntimeDir(config, key),
|
|
257
261
|
publicPath: `/__pnext/runtime/${key}`,
|
|
258
262
|
assetPath: `__pnext/runtime/${key}`,
|
|
259
|
-
}
|
|
263
|
+
}
|
|
260
264
|
}
|
|
261
265
|
// The SERVED url, not a relative one: chunks sit a directory below the
|
|
262
266
|
// entries, so no single relative path is correct from both — and esbuild
|
|
263
267
|
// writes an external path through verbatim, the same string everywhere.
|
|
264
|
-
const served = nextCompatEnabled(config)
|
|
265
|
-
? `/_next/static/rt/${key}`
|
|
266
|
-
: `/assets/rt/${key}`;
|
|
268
|
+
const served = nextCompatEnabled(config) ? `/_next/static/rt/${key}` : `/assets/rt/${key}`
|
|
267
269
|
return {
|
|
268
270
|
dir: path.join(outDir, 'rt', key),
|
|
269
271
|
publicPath: withAssetPrefix(config, served),
|
|
270
272
|
assetPath: `assets/rt/${key}`,
|
|
271
|
-
}
|
|
273
|
+
}
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
/** Where the dev server reads the artifact back from. */
|
|
275
277
|
export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
|
|
276
|
-
return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key)
|
|
278
|
+
return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key)
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
/**
|
|
@@ -285,7 +287,7 @@ export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
|
|
|
285
287
|
*/
|
|
286
288
|
function prebuiltEnabled() {
|
|
287
289
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
288
|
-
return process.env.PNEXT_PREBUILT_RUNTIME === '1'
|
|
290
|
+
return process.env.PNEXT_PREBUILT_RUNTIME === '1'
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
/**
|
|
@@ -296,7 +298,7 @@ function prebuiltEnabled() {
|
|
|
296
298
|
* the same framework names.
|
|
297
299
|
*/
|
|
298
300
|
function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, actions: boolean) {
|
|
299
|
-
const nextCompat = nextCompatEnabled(config)
|
|
301
|
+
const nextCompat = nextCompatEnabled(config)
|
|
300
302
|
const facts = clientRuntimeFacts(
|
|
301
303
|
[
|
|
302
304
|
{
|
|
@@ -306,7 +308,7 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
|
|
|
306
308
|
],
|
|
307
309
|
nextCompat,
|
|
308
310
|
!routeSuspenseFree(config, route),
|
|
309
|
-
)
|
|
311
|
+
)
|
|
310
312
|
return JSON.stringify([
|
|
311
313
|
nextCompat,
|
|
312
314
|
actions,
|
|
@@ -316,14 +318,14 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
|
|
|
316
318
|
routeThrowsClientControlFlow(route) === true,
|
|
317
319
|
Boolean(route.needsRouterEntry),
|
|
318
320
|
facts,
|
|
319
|
-
])
|
|
321
|
+
])
|
|
320
322
|
}
|
|
321
323
|
|
|
322
324
|
interface PreparedPrebuilt {
|
|
323
|
-
runtime: PrebuiltRuntime
|
|
324
|
-
plugin: Plugin
|
|
325
|
+
runtime: PrebuiltRuntime
|
|
326
|
+
plugin: Plugin
|
|
325
327
|
/** Fold what the build demanded into the artifact. */
|
|
326
|
-
settle(): Promise<void
|
|
328
|
+
settle(): Promise<void>
|
|
327
329
|
}
|
|
328
330
|
|
|
329
331
|
/**
|
|
@@ -339,10 +341,10 @@ async function preparePrebuilt(
|
|
|
339
341
|
signature = '',
|
|
340
342
|
reactLite = false,
|
|
341
343
|
): Promise<PreparedPrebuilt | undefined> {
|
|
342
|
-
if (!prebuiltEnabled()) return undefined
|
|
343
|
-
const buildOptions = baseClientBuildOptions(config, dev)
|
|
344
|
-
const key = prebuiltRuntimeKey(config, buildOptions, signature)
|
|
345
|
-
const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev)
|
|
344
|
+
if (!prebuiltEnabled()) return undefined
|
|
345
|
+
const buildOptions = baseClientBuildOptions(config, dev)
|
|
346
|
+
const key = prebuiltRuntimeKey(config, buildOptions, signature)
|
|
347
|
+
const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev)
|
|
346
348
|
// The artifact build gets the same plugin chain — and therefore the same
|
|
347
349
|
// aliases, transforms and asset seam — as the route build it stands in for.
|
|
348
350
|
// Not the prebuilt plugin itself: inside the artifact, framework imports are
|
|
@@ -355,51 +357,51 @@ async function preparePrebuilt(
|
|
|
355
357
|
assetPath,
|
|
356
358
|
buildOptions,
|
|
357
359
|
plugins: () => clientBuildPlugins(config, createClientSourcePipeline(config), [], reactLite),
|
|
358
|
-
}
|
|
359
|
-
const runtime = await ensurePrebuiltRuntime(options)
|
|
360
|
-
const unprobed = new Set<string>()
|
|
360
|
+
}
|
|
361
|
+
const runtime = await ensurePrebuiltRuntime(options)
|
|
362
|
+
const unprobed = new Set<string>()
|
|
361
363
|
return {
|
|
362
364
|
runtime,
|
|
363
365
|
plugin: prebuiltExternalPlugin(runtime, prebuiltSpecifierFilter(config), unprobed, sourceOf),
|
|
364
366
|
async settle() {
|
|
365
|
-
await settlePrebuiltRuntime(runtime, options, outDir, unprobed)
|
|
367
|
+
await settlePrebuiltRuntime(runtime, options, outDir, unprobed)
|
|
366
368
|
},
|
|
367
|
-
}
|
|
369
|
+
}
|
|
368
370
|
}
|
|
369
371
|
|
|
370
372
|
/** The served URL of a deferred dynamic reference's on-demand chunk (dev split). */
|
|
371
373
|
export function deferredDynamicChunkHref(reference: Pick<ClientReference, 'id'>) {
|
|
372
|
-
return `/__pnext/client-dyn/${reference.id}.js
|
|
374
|
+
return `/__pnext/client-dyn/${reference.id}.js`
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
export interface DeferredDynamicRef {
|
|
376
|
-
file: string
|
|
377
|
-
exportName: string
|
|
378
|
+
file: string
|
|
379
|
+
exportName: string
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
// Process-level registry the dev chunk endpoint resolves ids through. Entries
|
|
381
383
|
// come from the pipeline rewrite below and from each out-dir's sidecar (a
|
|
382
384
|
// restart serving a cached entry never re-ran the rewrite).
|
|
383
|
-
const deferredDynamicRefs = new Map<string, DeferredDynamicRef>()
|
|
385
|
+
const deferredDynamicRefs = new Map<string, DeferredDynamicRef>()
|
|
384
386
|
|
|
385
387
|
export function registerDeferredDynamicRef(id: string, ref: DeferredDynamicRef) {
|
|
386
|
-
deferredDynamicRefs.set(id, ref)
|
|
388
|
+
deferredDynamicRefs.set(id, ref)
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
export function deferredDynamicRefById(id: string) {
|
|
390
|
-
return deferredDynamicRefs.get(id)
|
|
392
|
+
return deferredDynamicRefs.get(id)
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
/** Sidecar name persisted next to a dev entry naming its deferred dynamic refs. */
|
|
394
|
-
export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json'
|
|
396
|
+
export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json'
|
|
395
397
|
|
|
396
398
|
/** Dev-only: deferred dynamic references load from the on-demand chunk endpoint. */
|
|
397
399
|
function devDeferredDynamicHref(dev: boolean | undefined) {
|
|
398
|
-
if (!dev || !devDynamicSplitEnabled()) return undefined
|
|
400
|
+
if (!dev || !devDynamicSplitEnabled()) return undefined
|
|
399
401
|
return (reference: ClientReference) =>
|
|
400
402
|
reference.dynamic && !ssrClientReference(reference)
|
|
401
403
|
? deferredDynamicChunkHref(reference)
|
|
402
|
-
: undefined
|
|
404
|
+
: undefined
|
|
403
405
|
}
|
|
404
406
|
|
|
405
407
|
/** Deferred chunk URLs resolve at runtime, never inside the entry build. */
|
|
@@ -410,15 +412,15 @@ function deferredDynamicExternalPlugin(): Plugin {
|
|
|
410
412
|
build.onResolve({ filter: /^\/__pnext\/client-dyn\// }, args => ({
|
|
411
413
|
path: args.path,
|
|
412
414
|
external: true,
|
|
413
|
-
}))
|
|
415
|
+
}))
|
|
414
416
|
},
|
|
415
|
-
}
|
|
417
|
+
}
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
export async function buildClientEntry({ config, route, outDir, dev }: ClientBuildOptions) {
|
|
419
|
-
conventionCache.clear()
|
|
420
|
-
await ensureDir(outDir)
|
|
421
|
-
const suspense = routeSuspenseFree(config, route) === false
|
|
421
|
+
conventionCache.clear()
|
|
422
|
+
await ensureDir(outDir)
|
|
423
|
+
const suspense = routeSuspenseFree(config, route) === false
|
|
422
424
|
const source = clientEntrySource({
|
|
423
425
|
deferredDynamicHref: devDeferredDynamicHref(dev),
|
|
424
426
|
pageFile: route.client ? route.file : undefined,
|
|
@@ -434,11 +436,11 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
|
|
|
434
436
|
router: Boolean(route.needsRouterEntry),
|
|
435
437
|
clientRootLayout: hasClientRootLayout(config, route),
|
|
436
438
|
shellLayoutOrder: shellLayoutOrder(config, route),
|
|
437
|
-
})
|
|
438
|
-
const entryName = clientEntryName(route)
|
|
439
|
-
const outfile = path.join(outDir, `${entryName}.js`)
|
|
440
|
-
const pipeline = createClientSourcePipeline(config, dev === true)
|
|
441
|
-
pipeline.warmRoutes([route])
|
|
439
|
+
})
|
|
440
|
+
const entryName = clientEntryName(route)
|
|
441
|
+
const outfile = path.join(outDir, `${entryName}.js`)
|
|
442
|
+
const pipeline = createClientSourcePipeline(config, dev === true)
|
|
443
|
+
pipeline.warmRoutes([route])
|
|
442
444
|
const runtimeFacts = clientRuntimeFacts(
|
|
443
445
|
[
|
|
444
446
|
{
|
|
@@ -448,65 +450,76 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
|
|
|
448
450
|
],
|
|
449
451
|
nextCompatEnabled(config),
|
|
450
452
|
suspense,
|
|
451
|
-
)
|
|
452
|
-
const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
importer
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
453
|
+
)
|
|
454
|
+
const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
|
|
455
|
+
preparePrebuilt(
|
|
456
|
+
config,
|
|
457
|
+
outDir,
|
|
458
|
+
dev === true,
|
|
459
|
+
importer =>
|
|
460
|
+
importer === CLIENT_RUNTIME_MODULE
|
|
461
|
+
? clientRuntimeSource(runtimeFacts)
|
|
462
|
+
: (pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)),
|
|
463
|
+
// One route is its own surface group, so the key is the same one the batch
|
|
464
|
+
// build would give this route's group — dev and prod derive it identically.
|
|
465
|
+
surfaceSignature(config, route, true),
|
|
466
|
+
!suspense,
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
let metafile: Metafile | undefined
|
|
467
471
|
try {
|
|
468
472
|
const result = await profileClientBuild(route, dev, () =>
|
|
469
|
-
clientProfile.timeAsync('esbuild', () =>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
473
|
+
clientProfile.timeAsync('esbuild', () =>
|
|
474
|
+
build({
|
|
475
|
+
...baseClientBuildOptions(config, dev),
|
|
476
|
+
stdin: {
|
|
477
|
+
contents: source,
|
|
478
|
+
loader: 'ts',
|
|
479
|
+
resolveDir: process.cwd(),
|
|
480
|
+
sourcefile: `${route.id}.ts`,
|
|
481
|
+
},
|
|
482
|
+
outdir: outDir,
|
|
483
|
+
entryNames: entryName,
|
|
484
|
+
metafile: true,
|
|
485
|
+
plugins: clientBuildPlugins(
|
|
486
|
+
config,
|
|
487
|
+
pipeline,
|
|
488
|
+
[
|
|
489
|
+
...(prebuilt ? [prebuilt.plugin] : []),
|
|
490
|
+
...(devDeferredDynamicHref(dev) ? [deferredDynamicExternalPlugin()] : []),
|
|
491
|
+
clientRuntimePlugin(runtimeFacts),
|
|
492
|
+
],
|
|
493
|
+
!suspense,
|
|
494
|
+
),
|
|
495
|
+
}),
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
metafile = result.metafile
|
|
488
499
|
} catch (error) {
|
|
489
|
-
throw withClientImportTrace(error, config, route, source)
|
|
500
|
+
throw withClientImportTrace(error, config, route, source)
|
|
490
501
|
}
|
|
491
|
-
await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
|
|
502
|
+
await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
|
|
492
503
|
|
|
493
504
|
if (metafile) {
|
|
494
|
-
metafile = await clientProfile.timeAsync('fold', () =>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
505
|
+
metafile = await clientProfile.timeAsync('fold', () =>
|
|
506
|
+
foldInitialChunks({
|
|
507
|
+
outDir,
|
|
508
|
+
metafile: metafile!,
|
|
509
|
+
buildOptions: baseClientBuildOptions(config, dev),
|
|
510
|
+
}),
|
|
511
|
+
)
|
|
499
512
|
}
|
|
500
|
-
await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile))
|
|
513
|
+
await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile))
|
|
501
514
|
if (dev && devDynamicSplitEnabled() && pipeline.deferredRefs().size > 0) {
|
|
502
515
|
await writeText(
|
|
503
516
|
path.join(outDir, DEFERRED_DYNAMIC_SIDECAR),
|
|
504
517
|
JSON.stringify(Object.fromEntries(pipeline.deferredRefs())),
|
|
505
|
-
)
|
|
518
|
+
)
|
|
506
519
|
}
|
|
507
|
-
clientProfile.report(`client entry ${route.id}`)
|
|
520
|
+
clientProfile.report(`client entry ${route.id}`)
|
|
508
521
|
|
|
509
|
-
return outfile
|
|
522
|
+
return outfile
|
|
510
523
|
}
|
|
511
524
|
|
|
512
525
|
/**
|
|
@@ -522,71 +535,73 @@ export async function buildClientDynamicChunk({
|
|
|
522
535
|
reference,
|
|
523
536
|
outDir,
|
|
524
537
|
}: {
|
|
525
|
-
config: ResolvedConfig
|
|
526
|
-
route?: RouteManifestEntry
|
|
527
|
-
reference: DeferredDynamicRef & { id: string }
|
|
528
|
-
outDir: string
|
|
538
|
+
config: ResolvedConfig
|
|
539
|
+
route?: RouteManifestEntry
|
|
540
|
+
reference: DeferredDynamicRef & { id: string }
|
|
541
|
+
outDir: string
|
|
529
542
|
}) {
|
|
530
|
-
await ensureDir(outDir)
|
|
531
|
-
const pipeline = createClientSourcePipeline(config, true)
|
|
543
|
+
await ensureDir(outDir)
|
|
544
|
+
const pipeline = createClientSourcePipeline(config, true)
|
|
532
545
|
const prebuilt = await preparePrebuilt(
|
|
533
546
|
config,
|
|
534
547
|
outDir,
|
|
535
548
|
true,
|
|
536
549
|
importer => pipeline.sourceOf(importer) ?? readTextSyncSafe(importer),
|
|
537
550
|
route ? surfaceSignature(config, route, true) : '',
|
|
538
|
-
)
|
|
551
|
+
)
|
|
539
552
|
const source = [
|
|
540
553
|
reference.exportName === 'default'
|
|
541
554
|
? `export { default } from ${JSON.stringify(reference.file)};`
|
|
542
555
|
: '',
|
|
543
556
|
`export * from ${JSON.stringify(reference.file)};`,
|
|
544
|
-
].join('\n')
|
|
545
|
-
const outfile = path.join(outDir, `${reference.id}.js`)
|
|
546
|
-
await clientProfile.timeAsync('dynChunk', () =>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
557
|
+
].join('\n')
|
|
558
|
+
const outfile = path.join(outDir, `${reference.id}.js`)
|
|
559
|
+
await clientProfile.timeAsync('dynChunk', () =>
|
|
560
|
+
build({
|
|
561
|
+
...baseClientBuildOptions(config, true),
|
|
562
|
+
stdin: {
|
|
563
|
+
contents: source,
|
|
564
|
+
loader: 'ts',
|
|
565
|
+
resolveDir: process.cwd(),
|
|
566
|
+
sourcefile: `${reference.id}.dyn.ts`,
|
|
567
|
+
},
|
|
568
|
+
outdir: outDir,
|
|
569
|
+
entryNames: reference.id,
|
|
570
|
+
plugins: clientBuildPlugins(config, pipeline, [
|
|
571
|
+
...(prebuilt ? [prebuilt.plugin] : []),
|
|
572
|
+
dynSharedVendorPlugin(nextCompatEnabled(config)),
|
|
573
|
+
deferredDynamicExternalPlugin(),
|
|
574
|
+
]),
|
|
575
|
+
}),
|
|
576
|
+
)
|
|
577
|
+
await prebuilt?.settle()
|
|
578
|
+
clientProfile.report(`client dynamic chunk ${reference.id}`)
|
|
579
|
+
return outfile
|
|
565
580
|
}
|
|
566
581
|
|
|
567
582
|
/** Shared-vendor shims: preact resolves to the entry's published namespace. */
|
|
568
583
|
function dynSharedVendorPlugin(nextCompat: boolean): Plugin {
|
|
569
|
-
const namespace = 'pnext-dyn-shared'
|
|
570
|
-
const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`)
|
|
584
|
+
const namespace = 'pnext-dyn-shared'
|
|
585
|
+
const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`)
|
|
571
586
|
return {
|
|
572
587
|
name: 'pnext-dyn-shared-vendor',
|
|
573
588
|
setup(build) {
|
|
574
589
|
build.onResolve({ filter }, args =>
|
|
575
590
|
args.namespace === namespace ? undefined : { path: args.path, namespace },
|
|
576
|
-
)
|
|
591
|
+
)
|
|
577
592
|
build.onLoad({ filter: /.*/, namespace }, async args => {
|
|
578
593
|
const names = Object.keys((await import(args.path)) as Record<string, unknown>).filter(
|
|
579
594
|
name => /^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default',
|
|
580
|
-
)
|
|
595
|
+
)
|
|
581
596
|
const lines = [
|
|
582
597
|
`const m = window.${DYN_SHARED_GLOBAL}[${JSON.stringify(args.path)}];`,
|
|
583
598
|
'export default (m && m.default);',
|
|
584
599
|
...names.map(name => `export const ${name} = m.${name};`),
|
|
585
|
-
]
|
|
586
|
-
return { contents: lines.join('\n'), loader: 'js' }
|
|
587
|
-
})
|
|
600
|
+
]
|
|
601
|
+
return { contents: lines.join('\n'), loader: 'js' }
|
|
602
|
+
})
|
|
588
603
|
},
|
|
589
|
-
}
|
|
604
|
+
}
|
|
590
605
|
}
|
|
591
606
|
|
|
592
607
|
/**
|
|
@@ -603,84 +618,86 @@ export async function buildClientEntries({
|
|
|
603
618
|
hasServerActions,
|
|
604
619
|
pipeline: warmed,
|
|
605
620
|
}: ClientBatchBuildOptions) {
|
|
606
|
-
if (!warmed) conventionCache.clear()
|
|
607
|
-
const actions = appUsesActions(routes, hasServerActions)
|
|
608
|
-
const entries = clientProfile.time('entrySources', () =>
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
route
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
621
|
+
if (!warmed) conventionCache.clear()
|
|
622
|
+
const actions = appUsesActions(routes, hasServerActions)
|
|
623
|
+
const entries = clientProfile.time('entrySources', () =>
|
|
624
|
+
routes
|
|
625
|
+
.filter(route => route.client || route.clientReferences.length > 0 || route.needsRouterEntry)
|
|
626
|
+
.map(route => ({
|
|
627
|
+
route,
|
|
628
|
+
entryName: clientEntryName(route),
|
|
629
|
+
source: clientEntrySource({
|
|
630
|
+
pageFile: route.client ? route.file : undefined,
|
|
631
|
+
clientReferences: route.clientReferences,
|
|
632
|
+
nextCompat: nextCompatEnabled(config),
|
|
633
|
+
suspense: !routeSuspenseFree(config, route),
|
|
634
|
+
actions: routeUsesActions(route, actions),
|
|
635
|
+
errorFile: routeErrorFile(config, route),
|
|
636
|
+
globalErrorFile: globalErrorFile(config),
|
|
637
|
+
notFoundFile: routeNotFoundFile(config, route),
|
|
638
|
+
controlFlow: routeThrowsClientControlFlow(route),
|
|
639
|
+
router: Boolean(route.needsRouterEntry),
|
|
640
|
+
clientRootLayout: hasClientRootLayout(config, route),
|
|
641
|
+
shellLayoutOrder: shellLayoutOrder(config, route),
|
|
642
|
+
}),
|
|
643
|
+
})),
|
|
644
|
+
)
|
|
645
|
+
if (entries.length === 0) return
|
|
629
646
|
// Every route's client sources start their transform and React Compiler pass here, in one batch on
|
|
630
647
|
// oxc's threadpool, while esbuild is still booting and walking the graph. A caller-warmed pipeline
|
|
631
648
|
// has them in flight already; warming twice is a Map hit, so the same call covers both.
|
|
632
|
-
const pipeline = warmed ?? createClientSourcePipeline(config)
|
|
633
|
-
pipeline.warmRoutes(entries.map(entry => entry.route))
|
|
634
|
-
await ensureDir(outDir)
|
|
635
|
-
const groups = surfaceGroups(config, entries, actions)
|
|
636
|
-
clientProfile.count('group#', groups.length)
|
|
637
|
-
const built: BuiltEntryGroup[] = []
|
|
649
|
+
const pipeline = warmed ?? createClientSourcePipeline(config)
|
|
650
|
+
pipeline.warmRoutes(entries.map(entry => entry.route))
|
|
651
|
+
await ensureDir(outDir)
|
|
652
|
+
const groups = surfaceGroups(config, entries, actions)
|
|
653
|
+
clientProfile.count('group#', groups.length)
|
|
654
|
+
const built: BuiltEntryGroup[] = []
|
|
638
655
|
for (const group of groups) {
|
|
639
|
-
built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }))
|
|
656
|
+
built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }))
|
|
640
657
|
}
|
|
641
658
|
|
|
642
659
|
// Fold and rename once over the finished output, never per group. Chunk names
|
|
643
660
|
// are content hashes, so two groups emit the same file for the same chunk —
|
|
644
661
|
// and the fold DELETES the members it merges, so a per-group fold would
|
|
645
662
|
// unlink a chunk another group's manifest still points at.
|
|
646
|
-
let merged: Metafile = { inputs: {}, outputs: {} }
|
|
663
|
+
let merged: Metafile = { inputs: {}, outputs: {} }
|
|
647
664
|
for (const item of built) {
|
|
648
|
-
if (!item.metafile) continue
|
|
649
|
-
Object.assign(merged.inputs, item.metafile.inputs)
|
|
650
|
-
Object.assign(merged.outputs, item.metafile.outputs)
|
|
665
|
+
if (!item.metafile) continue
|
|
666
|
+
Object.assign(merged.inputs, item.metafile.inputs)
|
|
667
|
+
Object.assign(merged.outputs, item.metafile.outputs)
|
|
651
668
|
}
|
|
652
669
|
merged = await clientProfile.timeAsync('fold', () =>
|
|
653
670
|
foldInitialChunks({ outDir, metafile: merged, buildOptions: baseClientBuildOptions(config) }),
|
|
654
|
-
)
|
|
671
|
+
)
|
|
655
672
|
const renames = await clientProfile.timeAsync('write', () =>
|
|
656
673
|
nameSharedClientChunks(outDir, merged),
|
|
657
|
-
)
|
|
674
|
+
)
|
|
658
675
|
clientProfile.time('entryImports', () => {
|
|
659
676
|
for (const item of built) {
|
|
660
677
|
if (item.metafile) {
|
|
661
|
-
assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt)
|
|
678
|
+
assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt)
|
|
662
679
|
}
|
|
663
680
|
}
|
|
664
|
-
})
|
|
665
|
-
clientProfile.report('client stage')
|
|
681
|
+
})
|
|
682
|
+
clientProfile.report('client stage')
|
|
666
683
|
}
|
|
667
684
|
|
|
668
685
|
interface ClientEntry {
|
|
669
|
-
route: RouteManifestEntry
|
|
670
|
-
entryName: string
|
|
671
|
-
source: string
|
|
686
|
+
route: RouteManifestEntry
|
|
687
|
+
entryName: string
|
|
688
|
+
source: string
|
|
672
689
|
}
|
|
673
690
|
|
|
674
691
|
interface SurfaceGroup {
|
|
675
|
-
signature: string
|
|
676
|
-
facts: ClientRuntimeFacts
|
|
677
|
-
entries: ClientEntry[]
|
|
692
|
+
signature: string
|
|
693
|
+
facts: ClientRuntimeFacts
|
|
694
|
+
entries: ClientEntry[]
|
|
678
695
|
}
|
|
679
696
|
|
|
680
697
|
interface BuiltEntryGroup {
|
|
681
|
-
group: SurfaceGroup
|
|
682
|
-
metafile: Metafile | undefined
|
|
683
|
-
prebuilt: PreparedPrebuilt | undefined
|
|
698
|
+
group: SurfaceGroup
|
|
699
|
+
metafile: Metafile | undefined
|
|
700
|
+
prebuilt: PreparedPrebuilt | undefined
|
|
684
701
|
}
|
|
685
702
|
|
|
686
703
|
/**
|
|
@@ -704,16 +721,16 @@ function surfaceGroups(
|
|
|
704
721
|
})),
|
|
705
722
|
nextCompatEnabled(config),
|
|
706
723
|
!members.every(entry => routeSuspenseFree(config, entry.route)),
|
|
707
|
-
)
|
|
724
|
+
)
|
|
708
725
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
709
726
|
if (!prebuiltEnabled() || process.env.PNEXT_PREBUILT_GROUPS === '0')
|
|
710
|
-
return [{ signature: '', facts: facts(entries), entries }]
|
|
711
|
-
const bySignature = new Map<string, ClientEntry[]>()
|
|
727
|
+
return [{ signature: '', facts: facts(entries), entries }]
|
|
728
|
+
const bySignature = new Map<string, ClientEntry[]>()
|
|
712
729
|
for (const entry of entries) {
|
|
713
|
-
const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions))
|
|
714
|
-
const members = bySignature.get(signature) ?? []
|
|
715
|
-
bySignature.set(signature, members)
|
|
716
|
-
members.push(entry)
|
|
730
|
+
const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions))
|
|
731
|
+
const members = bySignature.get(signature) ?? []
|
|
732
|
+
bySignature.set(signature, members)
|
|
733
|
+
members.push(entry)
|
|
717
734
|
}
|
|
718
735
|
return [...bySignature].map(([signature, members]) => ({
|
|
719
736
|
signature,
|
|
@@ -722,7 +739,7 @@ function surfaceGroups(
|
|
|
722
739
|
// no route in this group has.
|
|
723
740
|
facts: facts(members),
|
|
724
741
|
entries: members,
|
|
725
|
-
}))
|
|
742
|
+
}))
|
|
726
743
|
}
|
|
727
744
|
|
|
728
745
|
async function buildEntryGroup({
|
|
@@ -733,62 +750,69 @@ async function buildEntryGroup({
|
|
|
733
750
|
pipeline,
|
|
734
751
|
verbose,
|
|
735
752
|
}: {
|
|
736
|
-
config: ResolvedConfig
|
|
737
|
-
outDir: string
|
|
753
|
+
config: ResolvedConfig
|
|
754
|
+
outDir: string
|
|
738
755
|
/** Every entry in the app: the batch fallback and `sourceOf` span groups. */
|
|
739
|
-
entries: ClientEntry[]
|
|
740
|
-
group: SurfaceGroup
|
|
741
|
-
pipeline: ClientSourcePipeline
|
|
742
|
-
verbose?: boolean
|
|
756
|
+
entries: ClientEntry[]
|
|
757
|
+
group: SurfaceGroup
|
|
758
|
+
pipeline: ClientSourcePipeline
|
|
759
|
+
verbose?: boolean
|
|
743
760
|
}): Promise<BuiltEntryGroup> {
|
|
744
|
-
const reactLite = group.facts.suspense === false
|
|
761
|
+
const reactLite = group.facts.suspense === false
|
|
745
762
|
const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
|
|
746
763
|
preparePrebuilt(
|
|
747
764
|
config,
|
|
748
765
|
outDir,
|
|
749
766
|
false,
|
|
750
767
|
importer => {
|
|
751
|
-
if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts)
|
|
752
|
-
const entry = entries.find(item => item.route.id === importer)
|
|
753
|
-
return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)
|
|
768
|
+
if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts)
|
|
769
|
+
const entry = entries.find(item => item.route.id === importer)
|
|
770
|
+
return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)
|
|
754
771
|
},
|
|
755
772
|
group.signature,
|
|
756
773
|
reactLite,
|
|
757
774
|
),
|
|
758
|
-
)
|
|
775
|
+
)
|
|
759
776
|
|
|
760
|
-
let metafile: Metafile | undefined
|
|
777
|
+
let metafile: Metafile | undefined
|
|
761
778
|
try {
|
|
762
|
-
const result = await clientProfile.timeAsync('esbuild', () =>
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
779
|
+
const result = await clientProfile.timeAsync('esbuild', () =>
|
|
780
|
+
build({
|
|
781
|
+
...baseClientBuildOptions(config),
|
|
782
|
+
entryPoints: group.entries.map(entry => ({
|
|
783
|
+
in: virtualEntryPath(entry.route.id),
|
|
784
|
+
out: entry.entryName,
|
|
785
|
+
})),
|
|
786
|
+
outdir: outDir,
|
|
787
|
+
metafile: true,
|
|
788
|
+
plugins: clientBuildPlugins(
|
|
789
|
+
config,
|
|
790
|
+
pipeline,
|
|
791
|
+
[
|
|
792
|
+
...(prebuilt ? [prebuilt.plugin] : []),
|
|
793
|
+
virtualEntryPlugin(group.entries),
|
|
794
|
+
clientRuntimePlugin(group.facts),
|
|
795
|
+
],
|
|
796
|
+
reactLite,
|
|
797
|
+
),
|
|
798
|
+
}),
|
|
799
|
+
)
|
|
800
|
+
metafile = result.metafile
|
|
777
801
|
} catch (error) {
|
|
778
802
|
// A batched build cannot attribute a "Could not resolve" error to a single route, so fall back to
|
|
779
803
|
// per-route builds - the offending route then throws with its precise client import trace. Other
|
|
780
804
|
// diagnostics already carry a clear message from the batch build; re-throw rather than let a
|
|
781
805
|
// per-route build surface a more confusing downstream error.
|
|
782
|
-
if (!hasUnresolvedImportError(error)) throw error
|
|
806
|
+
if (!hasUnresolvedImportError(error)) throw error
|
|
783
807
|
for (const { route } of group.entries) {
|
|
784
|
-
await buildClientEntry({ config, route, outDir })
|
|
808
|
+
await buildClientEntry({ config, route, outDir })
|
|
785
809
|
}
|
|
786
|
-
throw error
|
|
810
|
+
throw error
|
|
787
811
|
}
|
|
788
|
-
await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
|
|
812
|
+
await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
|
|
789
813
|
|
|
790
|
-
if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir)
|
|
791
|
-
return { group, metafile, prebuilt }
|
|
814
|
+
if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir)
|
|
815
|
+
return { group, metafile, prebuilt }
|
|
792
816
|
}
|
|
793
817
|
|
|
794
818
|
/**
|
|
@@ -802,11 +826,11 @@ async function buildEntryGroup({
|
|
|
802
826
|
* Memoized per build; a group of routes qualifies iff every member does.
|
|
803
827
|
*/
|
|
804
828
|
function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): boolean {
|
|
805
|
-
if (!nextCompatEnabled(config)) return false
|
|
829
|
+
if (!nextCompatEnabled(config)) return false
|
|
806
830
|
return memoConvention(`suspenseFree:${route.id}`, () => {
|
|
807
|
-
const seeds: string[] = []
|
|
808
|
-
if (route.client) seeds.push(route.file)
|
|
809
|
-
for (const reference of route.clientReferences) seeds.push(reference.file)
|
|
831
|
+
const seeds: string[] = []
|
|
832
|
+
if (route.client) seeds.push(route.file)
|
|
833
|
+
for (const reference of route.clientReferences) seeds.push(reference.file)
|
|
810
834
|
for (const file of [
|
|
811
835
|
routeErrorFile(config, route),
|
|
812
836
|
globalErrorFile(config),
|
|
@@ -814,19 +838,19 @@ function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): b
|
|
|
814
838
|
// Shell layouts hydrate client-side only when the root layout is 'use client'.
|
|
815
839
|
...(hasClientRootLayout(config, route) ? shellLayoutOrder(config, route) : []),
|
|
816
840
|
]) {
|
|
817
|
-
if (file) seeds.push(file)
|
|
841
|
+
if (file) seeds.push(file)
|
|
818
842
|
}
|
|
819
|
-
return clientSuspenseFree(seeds)
|
|
820
|
-
})
|
|
843
|
+
return clientSuspenseFree(seeds)
|
|
844
|
+
})
|
|
821
845
|
}
|
|
822
846
|
|
|
823
847
|
function routeClientSources(routes: RouteManifestEntry[]) {
|
|
824
|
-
const files = new Set<string>()
|
|
848
|
+
const files = new Set<string>()
|
|
825
849
|
for (const route of routes) {
|
|
826
|
-
if (route.client) files.add(route.file)
|
|
827
|
-
for (const reference of route.clientReferences) files.add(reference.file)
|
|
850
|
+
if (route.client) files.add(route.file)
|
|
851
|
+
for (const reference of route.clientReferences) files.add(reference.file)
|
|
828
852
|
}
|
|
829
|
-
return files
|
|
853
|
+
return files
|
|
830
854
|
}
|
|
831
855
|
|
|
832
856
|
// Record each entry's static chunk closure on the route so rendered pages can
|
|
@@ -839,58 +863,58 @@ function assignClientEntryImports(
|
|
|
839
863
|
renames: Map<string, string>,
|
|
840
864
|
prebuilt: PreparedPrebuilt | undefined,
|
|
841
865
|
) {
|
|
842
|
-
const outputs = metafile.outputs
|
|
866
|
+
const outputs = metafile.outputs
|
|
843
867
|
// One basename index for the whole entry set: the linear scan this replaces
|
|
844
868
|
// was O(entries × outputs), which on a route-per-page app is the entire
|
|
845
869
|
// output list walked once per route.
|
|
846
|
-
const byBasename = new Map<string, string>()
|
|
870
|
+
const byBasename = new Map<string, string>()
|
|
847
871
|
for (const item of Object.keys(outputs)) {
|
|
848
872
|
// First match wins, as the linear scan did.
|
|
849
|
-
const basename = path.basename(item)
|
|
850
|
-
if (!byBasename.has(basename)) byBasename.set(basename, item)
|
|
873
|
+
const basename = path.basename(item)
|
|
874
|
+
if (!byBasename.has(basename)) byBasename.set(basename, item)
|
|
851
875
|
}
|
|
852
876
|
for (const { route, entryName } of entries) {
|
|
853
|
-
const entryOutput = byBasename.get(`${entryName}.js`)
|
|
854
|
-
if (!entryOutput) continue
|
|
855
|
-
const staticImports = new Set<string>()
|
|
856
|
-
const dynamicImports = new Set<string>()
|
|
857
|
-
const visited = new Set([`${entryOutput}:static`])
|
|
858
|
-
const queue = [{ output: entryOutput, dynamic: false }]
|
|
877
|
+
const entryOutput = byBasename.get(`${entryName}.js`)
|
|
878
|
+
if (!entryOutput) continue
|
|
879
|
+
const staticImports = new Set<string>()
|
|
880
|
+
const dynamicImports = new Set<string>()
|
|
881
|
+
const visited = new Set([`${entryOutput}:static`])
|
|
882
|
+
const queue = [{ output: entryOutput, dynamic: false }]
|
|
859
883
|
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
860
884
|
for (let head = 0; head < queue.length; head += 1) {
|
|
861
|
-
const current = queue[head]
|
|
862
|
-
if (!current) continue
|
|
885
|
+
const current = queue[head]
|
|
886
|
+
if (!current) continue
|
|
863
887
|
for (const imported of outputs[current.output]?.imports ?? []) {
|
|
864
|
-
if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue
|
|
865
|
-
const dynamic = current.dynamic || imported.kind === 'dynamic-import'
|
|
866
|
-
const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}
|
|
867
|
-
if (visited.has(visitKey)) continue
|
|
868
|
-
visited.add(visitKey)
|
|
888
|
+
if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue
|
|
889
|
+
const dynamic = current.dynamic || imported.kind === 'dynamic-import'
|
|
890
|
+
const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`
|
|
891
|
+
if (visited.has(visitKey)) continue
|
|
892
|
+
visited.add(visitKey)
|
|
869
893
|
// Prebuilt-runtime shims are external: already a url relative to the
|
|
870
894
|
// assets root, and with no output in THIS metafile to walk into. Their
|
|
871
895
|
// own chunk closure comes from the artifact's manifest instead, so the
|
|
872
896
|
// page preloads the whole set rather than discovering chunks one
|
|
873
897
|
// round trip after the shim lands.
|
|
874
898
|
if (imported.external) {
|
|
875
|
-
if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue
|
|
876
|
-
const assets = prebuiltAssets(prebuilt.runtime)
|
|
877
|
-
for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset)
|
|
878
|
-
for (const asset of assets.dynamic) dynamicImports.add(asset)
|
|
879
|
-
continue
|
|
899
|
+
if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue
|
|
900
|
+
const assets = prebuiltAssets(prebuilt.runtime)
|
|
901
|
+
for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset)
|
|
902
|
+
for (const asset of assets.dynamic) dynamicImports.add(asset)
|
|
903
|
+
continue
|
|
880
904
|
}
|
|
881
|
-
queue.push({ output: imported.path, dynamic })
|
|
882
|
-
const basename = path.basename(imported.path)
|
|
883
|
-
const relative = path.relative(outDir, path.resolve(imported.path))
|
|
905
|
+
queue.push({ output: imported.path, dynamic })
|
|
906
|
+
const basename = path.basename(imported.path)
|
|
907
|
+
const relative = path.relative(outDir, path.resolve(imported.path))
|
|
884
908
|
const publicRelative = path
|
|
885
909
|
.join('assets', path.dirname(relative), renames.get(basename) ?? basename)
|
|
886
910
|
.split(path.sep)
|
|
887
|
-
.join('/')
|
|
888
|
-
(dynamic ? dynamicImports : staticImports).add(publicRelative)
|
|
911
|
+
.join('/')
|
|
912
|
+
;(dynamic ? dynamicImports : staticImports).add(publicRelative)
|
|
889
913
|
}
|
|
890
914
|
}
|
|
891
|
-
if (staticImports.size > 0) route.clientEntryImports = [...staticImports]
|
|
892
|
-
for (const asset of staticImports) dynamicImports.delete(asset)
|
|
893
|
-
if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports]
|
|
915
|
+
if (staticImports.size > 0) route.clientEntryImports = [...staticImports]
|
|
916
|
+
for (const asset of staticImports) dynamicImports.delete(asset)
|
|
917
|
+
if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports]
|
|
894
918
|
}
|
|
895
919
|
}
|
|
896
920
|
|
|
@@ -905,62 +929,62 @@ function reportClientBundleSizes(
|
|
|
905
929
|
metafile: Metafile,
|
|
906
930
|
outDir: string,
|
|
907
931
|
) {
|
|
908
|
-
const log = createVerboseLogger(true, 'client')
|
|
909
|
-
const outputs = metafile.outputs
|
|
932
|
+
const log = createVerboseLogger(true, 'client')
|
|
933
|
+
const outputs = metafile.outputs
|
|
910
934
|
const rows = entries
|
|
911
935
|
.map(({ route, entryName }) => {
|
|
912
|
-
const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}
|
|
936
|
+
const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}`
|
|
913
937
|
const output = outputs[key]
|
|
914
938
|
? key
|
|
915
|
-
: Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`)
|
|
916
|
-
const bytes = output ? reachableOutputBytes(output, outputs) : 0
|
|
917
|
-
return { route: route.route, bytes }
|
|
939
|
+
: Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`)
|
|
940
|
+
const bytes = output ? reachableOutputBytes(output, outputs) : 0
|
|
941
|
+
return { route: route.route, bytes }
|
|
918
942
|
})
|
|
919
|
-
.sort((a, b) => b.bytes - a.bytes)
|
|
943
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
920
944
|
|
|
921
945
|
for (const row of rows) {
|
|
922
|
-
log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`)
|
|
946
|
+
log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`)
|
|
923
947
|
}
|
|
924
948
|
}
|
|
925
949
|
|
|
926
950
|
function reachableOutputBytes(entry: string, outputs: Metafile['outputs']) {
|
|
927
|
-
const visited = new Set<string>()
|
|
928
|
-
const queue = [entry]
|
|
929
|
-
let bytes = 0
|
|
951
|
+
const visited = new Set<string>()
|
|
952
|
+
const queue = [entry]
|
|
953
|
+
let bytes = 0
|
|
930
954
|
|
|
931
955
|
while (queue.length > 0) {
|
|
932
|
-
const current = queue.shift()
|
|
933
|
-
if (!current || visited.has(current)) continue
|
|
934
|
-
visited.add(current)
|
|
935
|
-
const output = outputs[current]
|
|
936
|
-
if (!output) continue
|
|
937
|
-
bytes += output.bytes
|
|
956
|
+
const current = queue.shift()
|
|
957
|
+
if (!current || visited.has(current)) continue
|
|
958
|
+
visited.add(current)
|
|
959
|
+
const output = outputs[current]
|
|
960
|
+
if (!output) continue
|
|
961
|
+
bytes += output.bytes
|
|
938
962
|
for (const imported of output.imports) {
|
|
939
963
|
if (imported.kind === 'import-statement' || imported.kind === 'dynamic-import') {
|
|
940
|
-
queue.push(imported.path)
|
|
964
|
+
queue.push(imported.path)
|
|
941
965
|
}
|
|
942
966
|
}
|
|
943
967
|
}
|
|
944
968
|
|
|
945
|
-
return bytes
|
|
969
|
+
return bytes
|
|
946
970
|
}
|
|
947
971
|
|
|
948
972
|
function formatBytes(bytes: number) {
|
|
949
|
-
if (bytes < 1024) return `${bytes} B
|
|
950
|
-
const units = ['KB', 'MB'] as const
|
|
951
|
-
let value = bytes / 1024
|
|
952
|
-
let unitIndex = 0
|
|
973
|
+
if (bytes < 1024) return `${bytes} B`
|
|
974
|
+
const units = ['KB', 'MB'] as const
|
|
975
|
+
let value = bytes / 1024
|
|
976
|
+
let unitIndex = 0
|
|
953
977
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
954
|
-
value /= 1024
|
|
955
|
-
unitIndex += 1
|
|
978
|
+
value /= 1024
|
|
979
|
+
unitIndex += 1
|
|
956
980
|
}
|
|
957
|
-
return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}
|
|
981
|
+
return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}`
|
|
958
982
|
}
|
|
959
983
|
|
|
960
984
|
function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptions {
|
|
961
|
-
const extensions = getBundlerExtensions()
|
|
962
|
-
const pure = extensions.clientPureFunctions(config)
|
|
963
|
-
const inject = extensions.clientInjects()
|
|
985
|
+
const extensions = getBundlerExtensions()
|
|
986
|
+
const pure = extensions.clientPureFunctions(config)
|
|
987
|
+
const inject = extensions.clientInjects()
|
|
964
988
|
return {
|
|
965
989
|
...(inject.length > 0 ? { inject } : {}),
|
|
966
990
|
chunkNames: 'chunks/[name]-[hash]',
|
|
@@ -1003,15 +1027,15 @@ function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptio
|
|
|
1003
1027
|
'.otf': 'file',
|
|
1004
1028
|
'.eot': 'file',
|
|
1005
1029
|
},
|
|
1006
|
-
}
|
|
1030
|
+
}
|
|
1007
1031
|
}
|
|
1008
1032
|
|
|
1009
1033
|
function deploymentIdDefine(): Record<string, string> {
|
|
1010
1034
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1011
|
-
const deploymentId = process.env.NEXT_DEPLOYMENT_ID
|
|
1035
|
+
const deploymentId = process.env.NEXT_DEPLOYMENT_ID
|
|
1012
1036
|
return deploymentId === undefined
|
|
1013
1037
|
? {}
|
|
1014
|
-
: { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) }
|
|
1038
|
+
: { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) }
|
|
1015
1039
|
}
|
|
1016
1040
|
|
|
1017
1041
|
/**
|
|
@@ -1021,28 +1045,29 @@ function deploymentIdDefine(): Record<string, string> {
|
|
|
1021
1045
|
* slow" from "we are answering 40k questions". Off unless PNEXT_CLIENT_PROFILE.
|
|
1022
1046
|
*/
|
|
1023
1047
|
function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
|
|
1024
|
-
if (!clientProfile.enabled) return plugins
|
|
1048
|
+
if (!clientProfile.enabled) return plugins
|
|
1025
1049
|
return plugins.map(plugin => ({
|
|
1026
1050
|
name: plugin.name,
|
|
1027
1051
|
setup(build) {
|
|
1028
|
-
const wrap =
|
|
1052
|
+
const wrap =
|
|
1053
|
+
(kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
|
|
1029
1054
|
(options: any, callback: any) =>
|
|
1030
1055
|
register(options, async (args: any) => {
|
|
1031
|
-
clientProfile.count(`${plugin.name}:${kind}#`)
|
|
1032
|
-
const start = performance.now()
|
|
1056
|
+
clientProfile.count(`${plugin.name}:${kind}#`)
|
|
1057
|
+
const start = performance.now()
|
|
1033
1058
|
try {
|
|
1034
|
-
return await (callback as (a: unknown) => unknown)(args)
|
|
1059
|
+
return await (callback as (a: unknown) => unknown)(args)
|
|
1035
1060
|
} finally {
|
|
1036
|
-
clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start)
|
|
1061
|
+
clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start)
|
|
1037
1062
|
}
|
|
1038
|
-
})
|
|
1063
|
+
})
|
|
1039
1064
|
return plugin.setup({
|
|
1040
1065
|
...build,
|
|
1041
1066
|
onResolve: wrap('resolve', build.onResolve.bind(build)),
|
|
1042
1067
|
onLoad: wrap('load', build.onLoad.bind(build)),
|
|
1043
|
-
})
|
|
1068
|
+
})
|
|
1044
1069
|
},
|
|
1045
|
-
}))
|
|
1070
|
+
}))
|
|
1046
1071
|
}
|
|
1047
1072
|
|
|
1048
1073
|
/**
|
|
@@ -1057,49 +1082,52 @@ function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
|
|
|
1057
1082
|
*/
|
|
1058
1083
|
export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[] {
|
|
1059
1084
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1060
|
-
if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins
|
|
1085
|
+
if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins
|
|
1061
1086
|
interface Registration {
|
|
1062
|
-
plugin: string
|
|
1063
|
-
filter: RegExp
|
|
1064
|
-
namespace?: string
|
|
1065
|
-
callback: (args: OnResolveArgs) => unknown
|
|
1087
|
+
plugin: string
|
|
1088
|
+
filter: RegExp
|
|
1089
|
+
namespace?: string
|
|
1090
|
+
callback: (args: OnResolveArgs) => unknown
|
|
1066
1091
|
}
|
|
1067
1092
|
return [
|
|
1068
1093
|
{
|
|
1069
1094
|
name,
|
|
1070
1095
|
async setup(build) {
|
|
1071
|
-
const registered: Registration[] = []
|
|
1096
|
+
const registered: Registration[] = []
|
|
1072
1097
|
// Registrations made after setup (rare, and esbuild allows it) keep
|
|
1073
1098
|
// their own hook: the chain's order is already fixed by then.
|
|
1074
|
-
let sealed = false
|
|
1099
|
+
let sealed = false
|
|
1075
1100
|
/** The chain, minus one plugin's own hooks — see `resolve` below. */
|
|
1076
1101
|
const dispatch = async (args: OnResolveArgs, skip?: string) => {
|
|
1077
1102
|
for (const entry of registered) {
|
|
1078
|
-
if (entry.plugin === skip) continue
|
|
1079
|
-
if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue
|
|
1080
|
-
if (!entry.filter.test(args.path)) continue
|
|
1081
|
-
const result = (await entry.callback(args)) as OnResolveResult | null | undefined
|
|
1082
|
-
if (result == null) continue
|
|
1103
|
+
if (entry.plugin === skip) continue
|
|
1104
|
+
if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue
|
|
1105
|
+
if (!entry.filter.test(args.path)) continue
|
|
1106
|
+
const result = (await entry.callback(args)) as OnResolveResult | null | undefined
|
|
1107
|
+
if (result == null) continue
|
|
1083
1108
|
for (const message of [...(result.errors ?? []), ...(result.warnings ?? [])]) {
|
|
1084
|
-
message.pluginName ??= entry.plugin
|
|
1109
|
+
message.pluginName ??= entry.plugin
|
|
1085
1110
|
}
|
|
1086
|
-
return result
|
|
1111
|
+
return result
|
|
1087
1112
|
}
|
|
1088
|
-
return undefined
|
|
1089
|
-
}
|
|
1113
|
+
return undefined
|
|
1114
|
+
}
|
|
1090
1115
|
for (const plugin of plugins) {
|
|
1091
1116
|
const proxy: PluginBuild = {
|
|
1092
1117
|
...build,
|
|
1093
1118
|
onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) => unknown) {
|
|
1094
|
-
if (sealed) return build.onResolve(options, callback as never)
|
|
1119
|
+
if (sealed) return build.onResolve(options, callback as never)
|
|
1095
1120
|
registered.push({
|
|
1096
1121
|
plugin: plugin.name,
|
|
1097
1122
|
// Drop /g and /y: a sticky or global regex carries lastIndex
|
|
1098
1123
|
// between tests, which would make dispatch order-dependent.
|
|
1099
|
-
filter: new RegExp(
|
|
1124
|
+
filter: new RegExp(
|
|
1125
|
+
options.filter.source,
|
|
1126
|
+
options.filter.flags.replace(/[gy]/g, ''),
|
|
1127
|
+
),
|
|
1100
1128
|
namespace: options.namespace,
|
|
1101
1129
|
callback,
|
|
1102
|
-
})
|
|
1130
|
+
})
|
|
1103
1131
|
},
|
|
1104
1132
|
// esbuild's own `resolve` re-runs every plugin EXCEPT the caller's (its recursion guard,
|
|
1105
1133
|
// keyed on plugin name). Sharing one name would make it skip the whole chain and fall
|
|
@@ -1122,16 +1150,16 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
|
|
|
1122
1150
|
? resolvedFromHook(specifier, result)
|
|
1123
1151
|
: build.resolve(specifier, { ...options, pluginName: name }),
|
|
1124
1152
|
),
|
|
1125
|
-
}
|
|
1126
|
-
await plugin.setup(proxy)
|
|
1153
|
+
}
|
|
1154
|
+
await plugin.setup(proxy)
|
|
1127
1155
|
}
|
|
1128
|
-
sealed = true
|
|
1129
|
-
if (registered.length === 0) return
|
|
1130
|
-
const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'))
|
|
1131
|
-
build.onResolve({ filter }, args => dispatch(args))
|
|
1156
|
+
sealed = true
|
|
1157
|
+
if (registered.length === 0) return
|
|
1158
|
+
const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'))
|
|
1159
|
+
build.onResolve({ filter }, args => dispatch(args))
|
|
1132
1160
|
},
|
|
1133
1161
|
},
|
|
1134
|
-
]
|
|
1162
|
+
]
|
|
1135
1163
|
}
|
|
1136
1164
|
|
|
1137
1165
|
/**
|
|
@@ -1139,7 +1167,7 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
|
|
|
1139
1167
|
* this specifier, do not bundle it" - esbuild's own rule, verified against the binary.
|
|
1140
1168
|
*/
|
|
1141
1169
|
function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveResult {
|
|
1142
|
-
const resolved = result.path ?? (result.external ? specifier : '')
|
|
1170
|
+
const resolved = result.path ?? (result.external ? specifier : '')
|
|
1143
1171
|
return {
|
|
1144
1172
|
errors: (result.errors ?? []) as ResolveResult['errors'],
|
|
1145
1173
|
warnings: (result.warnings ?? []) as ResolveResult['warnings'],
|
|
@@ -1149,7 +1177,7 @@ function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveRe
|
|
|
1149
1177
|
namespace: result.namespace ?? (result.path ? 'file' : ''),
|
|
1150
1178
|
suffix: result.suffix ?? '',
|
|
1151
1179
|
pluginData: result.pluginData as unknown,
|
|
1152
|
-
}
|
|
1180
|
+
}
|
|
1153
1181
|
}
|
|
1154
1182
|
|
|
1155
1183
|
/**
|
|
@@ -1163,7 +1191,7 @@ function resolveHookSettled(result: OnResolveResult) {
|
|
|
1163
1191
|
result.external !== undefined ||
|
|
1164
1192
|
(result.errors?.length ?? 0) > 0 ||
|
|
1165
1193
|
(result.warnings?.length ?? 0) > 0
|
|
1166
|
-
)
|
|
1194
|
+
)
|
|
1167
1195
|
}
|
|
1168
1196
|
|
|
1169
1197
|
function clientBuildPlugins(
|
|
@@ -1172,55 +1200,66 @@ function clientBuildPlugins(
|
|
|
1172
1200
|
extra: Plugin[] = [],
|
|
1173
1201
|
reactLite = false,
|
|
1174
1202
|
): Plugin[] {
|
|
1175
|
-
return coalesceResolveHooks(
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1203
|
+
return coalesceResolveHooks(
|
|
1204
|
+
instrumentedPlugins([
|
|
1205
|
+
serverOnlyClientImportPlugin(),
|
|
1206
|
+
// The virtual-module plugins (`extra`: generated entries + the shared route
|
|
1207
|
+
// runtime) go first so they claim their own synthetic specifiers before the
|
|
1208
|
+
// resolve-chain plugins are asked about them — one entry point per route is
|
|
1209
|
+
// otherwise offered to every plugin in the chain that only ever declines it.
|
|
1210
|
+
...extra,
|
|
1211
|
+
...getBundlerExtensions().clientEsbuildPlugins(config),
|
|
1212
|
+
linkedPackageClientResolvePlugin(config),
|
|
1213
|
+
clientStaticAssetPlugin(config),
|
|
1214
|
+
pipeline.plugin(),
|
|
1215
|
+
importAliasPlugin(config, reactLite),
|
|
1216
|
+
cssModuleClientPlugin(),
|
|
1217
|
+
]),
|
|
1218
|
+
'pnext-client-resolve-chain',
|
|
1219
|
+
)
|
|
1189
1220
|
}
|
|
1190
1221
|
|
|
1191
1222
|
function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
|
|
1192
|
-
|
|
1223
|
+
// preact core/hooks/jsx-runtime are single-instance, compat or not: an app with its
|
|
1224
|
+
// own `preact` in node_modules otherwise bundles a second physical copy next to the
|
|
1225
|
+
// framework's, and hooks called from framework components (Link) read a null current
|
|
1226
|
+
// component off options the app's copy never installed. The server runtime pins the
|
|
1227
|
+
// same set unconditionally (loader's coreAliases); the client build must match.
|
|
1228
|
+
const aliases: Record<string, string> = {
|
|
1229
|
+
...frameworkRuntimeAliasEntries(),
|
|
1230
|
+
...getImportAliasExtensions().aliases(config, 'client'),
|
|
1231
|
+
}
|
|
1193
1232
|
// Suspense-free tier: the app's `react` imports resolve to the compat-free lite shim, so the
|
|
1194
1233
|
// bundle ships preact core + hooks without preact/compat (see clientSuspenseFree).
|
|
1195
1234
|
if (reactLite && aliases.react) {
|
|
1196
|
-
aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
|
|
1235
|
+
aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
|
|
1197
1236
|
}
|
|
1198
|
-
const specifiers = Object.keys(aliases)
|
|
1237
|
+
const specifiers = Object.keys(aliases)
|
|
1199
1238
|
return {
|
|
1200
1239
|
name: 'pnext-import-alias',
|
|
1201
1240
|
setup(build) {
|
|
1202
1241
|
build.onResolve({ filter: /^(?:react(?:-dom)?|next)(?:\/|$)/ }, args => {
|
|
1203
|
-
if (aliases[args.path]) return undefined
|
|
1204
|
-
const message = getImportAliasExtensions().missingImportError(config, args.path)
|
|
1205
|
-
return message ? { errors: [{ text: message }] } : undefined
|
|
1206
|
-
})
|
|
1207
|
-
if (specifiers.length === 0) return
|
|
1242
|
+
if (aliases[args.path]) return undefined
|
|
1243
|
+
const message = getImportAliasExtensions().missingImportError(config, args.path)
|
|
1244
|
+
return message ? { errors: [{ text: message }] } : undefined
|
|
1245
|
+
})
|
|
1246
|
+
if (specifiers.length === 0) return
|
|
1208
1247
|
build.onResolve(
|
|
1209
1248
|
{ filter: new RegExp(`^(${specifiers.map(escapeRegex).join('|')})$`) },
|
|
1210
1249
|
async args => {
|
|
1211
|
-
const target = aliases[args.path]
|
|
1212
|
-
if (!target) return undefined
|
|
1213
|
-
if (path.isAbsolute(target)) return { path: target }
|
|
1250
|
+
const target = aliases[args.path]
|
|
1251
|
+
if (!target) return undefined
|
|
1252
|
+
if (path.isAbsolute(target)) return { path: target }
|
|
1214
1253
|
return build.resolve(target, {
|
|
1215
1254
|
kind: args.kind,
|
|
1216
1255
|
importer: args.importer,
|
|
1217
1256
|
namespace: args.namespace,
|
|
1218
1257
|
resolveDir: args.resolveDir,
|
|
1219
|
-
})
|
|
1258
|
+
})
|
|
1220
1259
|
},
|
|
1221
|
-
)
|
|
1260
|
+
)
|
|
1222
1261
|
},
|
|
1223
|
-
}
|
|
1262
|
+
}
|
|
1224
1263
|
}
|
|
1225
1264
|
|
|
1226
1265
|
// preact core + preact/compat + react/react-dom/next are single-instance: they must ALWAYS resolve
|
|
@@ -1230,11 +1269,11 @@ function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
|
|
|
1230
1269
|
// `preact` to the linked copy while pnext-internal compat code keeps resolving to the alias -
|
|
1231
1270
|
// shipping TWO physical preact cores. That breaks single-instance option hooks, context identity and
|
|
1232
1271
|
// error interception. Never let the linked-package resolver shadow these; the alias owns them.
|
|
1233
|
-
const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)
|
|
1272
|
+
const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/
|
|
1234
1273
|
|
|
1235
1274
|
function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
|
|
1236
|
-
const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts')
|
|
1237
|
-
const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts')
|
|
1275
|
+
const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts')
|
|
1276
|
+
const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts')
|
|
1238
1277
|
return {
|
|
1239
1278
|
name: 'pnext-linked-package-client-resolve',
|
|
1240
1279
|
setup(build) {
|
|
@@ -1243,36 +1282,36 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
|
|
|
1243
1282
|
// outputs under node_modules paths, e.g. @next/third-parties client
|
|
1244
1283
|
// components) where no sibling ./preact exists; only a real sibling
|
|
1245
1284
|
// file (an app's own ./preact.*) opts out.
|
|
1246
|
-
if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
|
|
1285
|
+
if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
|
|
1247
1286
|
const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
|
|
1248
1287
|
existsSync(path.join(args.resolveDir, `preact${ext}`)),
|
|
1249
|
-
)
|
|
1250
|
-
if (hasRealSibling) return undefined
|
|
1251
|
-
return { path: compatPreact }
|
|
1252
|
-
})
|
|
1288
|
+
)
|
|
1289
|
+
if (hasRealSibling) return undefined
|
|
1290
|
+
return { path: compatPreact }
|
|
1291
|
+
})
|
|
1253
1292
|
// One build's worth of memo, keyed on exactly what esbuild hands us: the resolver walks
|
|
1254
1293
|
// node_modules and probes package targets per call, and a directory of components importing
|
|
1255
1294
|
// the same few packages asks the same question repeatedly. Per-BUILD, so a linked package's
|
|
1256
1295
|
// layout changing between builds is still seen - esbuild already assumes it is fixed within one.
|
|
1257
|
-
const linked = new Map<string, string | undefined>()
|
|
1296
|
+
const linked = new Map<string, string | undefined>()
|
|
1258
1297
|
build.onResolve({ filter: /^[^./#][^:]*$/ }, args => {
|
|
1259
|
-
if (frameworkAliasedSpecifier.test(args.path)) return undefined
|
|
1260
|
-
const dir = args.resolveDir || config.root
|
|
1261
|
-
const key = `${dir}\0${args.path}
|
|
1262
|
-
let resolved = linked.get(key)
|
|
1298
|
+
if (frameworkAliasedSpecifier.test(args.path)) return undefined
|
|
1299
|
+
const dir = args.resolveDir || config.root
|
|
1300
|
+
const key = `${dir}\0${args.path}`
|
|
1301
|
+
let resolved = linked.get(key)
|
|
1263
1302
|
if (resolved === undefined && !linked.has(key)) {
|
|
1264
1303
|
resolved = resolveLinkedPackageSpecifier(
|
|
1265
1304
|
config.root,
|
|
1266
1305
|
path.join(dir, 'pnext-resolve.ts'),
|
|
1267
1306
|
args.path,
|
|
1268
1307
|
['browser', 'style', 'import', 'default'],
|
|
1269
|
-
)
|
|
1270
|
-
linked.set(key, resolved)
|
|
1308
|
+
)
|
|
1309
|
+
linked.set(key, resolved)
|
|
1271
1310
|
}
|
|
1272
|
-
return resolved ? { path: resolved } : undefined
|
|
1273
|
-
})
|
|
1311
|
+
return resolved ? { path: resolved } : undefined
|
|
1312
|
+
})
|
|
1274
1313
|
},
|
|
1275
|
-
}
|
|
1314
|
+
}
|
|
1276
1315
|
}
|
|
1277
1316
|
|
|
1278
1317
|
// Mirror of the server build's static-asset seam for the CLIENT bundle. A static image import from a
|
|
@@ -1280,7 +1319,7 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
|
|
|
1280
1319
|
// object - with the asset emitted under `/_next/static/media/...` - rather than a bare `file` loader
|
|
1281
1320
|
// URL string. Uses core's `staticAssetModule` extension seam so compat (next/image) supplies the
|
|
1282
1321
|
// descriptor and pure-core apps fall back to the generic URL module.
|
|
1283
|
-
const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])
|
|
1322
|
+
const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/
|
|
1284
1323
|
|
|
1285
1324
|
function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
|
|
1286
1325
|
return {
|
|
@@ -1291,66 +1330,66 @@ function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
|
|
|
1291
1330
|
// `*.svg`) preempts the generic static-image resolver: defer so the
|
|
1292
1331
|
// compat loader-rule plugin runs the chain instead.
|
|
1293
1332
|
if (getAssetExtensions().hasLoaderRuleFor(args.path)) {
|
|
1294
|
-
return undefined
|
|
1333
|
+
return undefined
|
|
1295
1334
|
}
|
|
1296
1335
|
return {
|
|
1297
1336
|
path: resolveAssetPath(args.path, args.resolveDir),
|
|
1298
1337
|
namespace: 'pnext-client-static-asset',
|
|
1299
|
-
}
|
|
1300
|
-
})
|
|
1338
|
+
}
|
|
1339
|
+
})
|
|
1301
1340
|
build.onLoad({ filter: /.*/, namespace: 'pnext-client-static-asset' }, async args => ({
|
|
1302
1341
|
contents: await staticImageModuleSource(config, args.path),
|
|
1303
1342
|
loader: 'js',
|
|
1304
|
-
}))
|
|
1343
|
+
}))
|
|
1305
1344
|
},
|
|
1306
|
-
}
|
|
1345
|
+
}
|
|
1307
1346
|
}
|
|
1308
1347
|
|
|
1309
1348
|
function resolveAssetPath(specifier: string, resolveDir: string) {
|
|
1310
|
-
const { sourcePath, hash } = splitAssetHash(specifier)
|
|
1311
|
-
const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)
|
|
1312
|
-
return `${resolved}${hash}
|
|
1349
|
+
const { sourcePath, hash } = splitAssetHash(specifier)
|
|
1350
|
+
const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)
|
|
1351
|
+
return `${resolved}${hash}`
|
|
1313
1352
|
}
|
|
1314
1353
|
|
|
1315
1354
|
function splitAssetHash(specifier: string) {
|
|
1316
|
-
const index = specifier.search(/[?#]/)
|
|
1355
|
+
const index = specifier.search(/[?#]/)
|
|
1317
1356
|
return index === -1
|
|
1318
1357
|
? { sourcePath: specifier, hash: '' }
|
|
1319
|
-
: { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
|
|
1358
|
+
: { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
|
|
1320
1359
|
}
|
|
1321
1360
|
|
|
1322
1361
|
async function staticImageModuleSource(config: ResolvedConfig, file: string) {
|
|
1323
|
-
const { sourcePath } = splitAssetHash(file)
|
|
1324
|
-
const bytes = new Uint8Array(await readFile(sourcePath))
|
|
1325
|
-
const emitted: string[] = []
|
|
1362
|
+
const { sourcePath } = splitAssetHash(file)
|
|
1363
|
+
const bytes = new Uint8Array(await readFile(sourcePath))
|
|
1364
|
+
const emitted: string[] = []
|
|
1326
1365
|
const emit = (relative: string) => {
|
|
1327
|
-
emitted.push(relative)
|
|
1328
|
-
return `/${relative}
|
|
1329
|
-
}
|
|
1330
|
-
const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
|
|
1331
|
-
const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
|
|
1366
|
+
emitted.push(relative)
|
|
1367
|
+
return `/${relative}`
|
|
1368
|
+
}
|
|
1369
|
+
const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
|
|
1370
|
+
const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
|
|
1332
1371
|
for (const relative of emitted) {
|
|
1333
|
-
const target = path.join(config.outPath, 'public', ...relative.split('/'))
|
|
1334
|
-
await mkdir(path.dirname(target), { recursive: true })
|
|
1335
|
-
if (!existsSync(target)) await copyFile(sourcePath, target)
|
|
1372
|
+
const target = path.join(config.outPath, 'public', ...relative.split('/'))
|
|
1373
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
1374
|
+
if (!existsSync(target)) await copyFile(sourcePath, target)
|
|
1336
1375
|
}
|
|
1337
|
-
return source
|
|
1376
|
+
return source
|
|
1338
1377
|
}
|
|
1339
1378
|
|
|
1340
1379
|
// Core's generic static-asset module (no compat override): emit under a hashed
|
|
1341
1380
|
// `/_next/static/media` URL and export the URL string as default. Mirrors the
|
|
1342
|
-
// identically named helper in runtime/
|
|
1381
|
+
// identically named helper in runtime/loader.ts + runtime/modules.ts.
|
|
1343
1382
|
function coreStaticAssetModule(
|
|
1344
1383
|
sourcePath: string,
|
|
1345
1384
|
bytes: Uint8Array,
|
|
1346
1385
|
emit: (relative: string) => string,
|
|
1347
1386
|
): string {
|
|
1348
|
-
const ext = path.extname(sourcePath).toLowerCase() || '.bin'
|
|
1349
|
-
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8)
|
|
1350
|
-
const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
1351
|
-
const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`)
|
|
1352
|
-
const src = emit(relative)
|
|
1353
|
-
return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n
|
|
1387
|
+
const ext = path.extname(sourcePath).toLowerCase() || '.bin'
|
|
1388
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8)
|
|
1389
|
+
const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
1390
|
+
const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`)
|
|
1391
|
+
const src = emit(relative)
|
|
1392
|
+
return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`
|
|
1354
1393
|
}
|
|
1355
1394
|
|
|
1356
1395
|
/**
|
|
@@ -1366,91 +1405,93 @@ function coreStaticAssetModule(
|
|
|
1366
1405
|
* loader; scoped to the source roots, so third-party node_modules .js keeps esbuild's default loader.
|
|
1367
1406
|
*/
|
|
1368
1407
|
function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
1369
|
-
const sourceRewrite = nextCompatEnabled(config)
|
|
1370
|
-
const compiler = getCompatModeExtensions().reactCompilerOptions(config)
|
|
1371
|
-
const asyncPre = hasClientSourceAsyncPreTransforms()
|
|
1408
|
+
const sourceRewrite = nextCompatEnabled(config)
|
|
1409
|
+
const compiler = getCompatModeExtensions().reactCompilerOptions(config)
|
|
1410
|
+
const asyncPre = hasClientSourceAsyncPreTransforms()
|
|
1372
1411
|
// Under a source root, and under a root only first-party source plus transpiled packages - said in
|
|
1373
1412
|
// the FILTER, not the callback: a dependency's files are the bulk of a real graph, so a callback
|
|
1374
1413
|
// would decline most of them one at a time.
|
|
1375
|
-
const roots = clientSourceRoots(config)
|
|
1376
|
-
|
|
1414
|
+
const roots = clientSourceRoots(config)
|
|
1415
|
+
.map(root => escapeRegex(root))
|
|
1416
|
+
.join('|')
|
|
1417
|
+
const sep = escapeRegex(path.sep)
|
|
1377
1418
|
const filter = new RegExp(
|
|
1378
1419
|
`^(?:${roots})(?:${sep}${nonSegmentPattern('node_modules', sep)})*${sep}[^${sep}]*\\.(?:[cm]?[jt]sx?)$`,
|
|
1379
|
-
)
|
|
1380
|
-
const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
|
|
1420
|
+
)
|
|
1421
|
+
const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
|
|
1381
1422
|
// One in-flight pipeline per file for the whole build: `warm()` and the
|
|
1382
1423
|
// onLoad hook share it, so a warmed file is already done (or in flight on
|
|
1383
1424
|
// oxc's threadpool) by the time esbuild asks for it, and a component shared
|
|
1384
1425
|
// by twenty routes is prepared once.
|
|
1385
|
-
const prepared = new Map<string, Promise<OnLoadOutput>>()
|
|
1426
|
+
const prepared = new Map<string, Promise<OnLoadOutput>>()
|
|
1386
1427
|
// Deferred dynamic refs THIS pipeline rewrote — persisted as the out-dir sidecar.
|
|
1387
|
-
const deferredRefs = new Map<string, DeferredDynamicRef>()
|
|
1428
|
+
const deferredRefs = new Map<string, DeferredDynamicRef>()
|
|
1388
1429
|
// The text esbuild actually parsed, kept so the prebuilt seam can read a
|
|
1389
1430
|
// file's framework imports off the POST-transform source — a compat rewrite
|
|
1390
1431
|
// can add or rename one, and the seam must offer exactly what is there.
|
|
1391
|
-
const loaded = new Map<string, string>()
|
|
1432
|
+
const loaded = new Map<string, string>()
|
|
1392
1433
|
|
|
1393
1434
|
function prepare(resolved: string): Promise<OnLoadOutput> {
|
|
1394
|
-
const existing = prepared.get(resolved)
|
|
1395
|
-
if (existing) return existing
|
|
1396
|
-
const pending = runPipeline(resolved)
|
|
1397
|
-
prepared.set(resolved, pending)
|
|
1398
|
-
return pending
|
|
1435
|
+
const existing = prepared.get(resolved)
|
|
1436
|
+
if (existing) return existing
|
|
1437
|
+
const pending = runPipeline(resolved)
|
|
1438
|
+
prepared.set(resolved, pending)
|
|
1439
|
+
return pending
|
|
1399
1440
|
}
|
|
1400
1441
|
|
|
1401
1442
|
async function runPipeline(resolved: string): Promise<OnLoadOutput> {
|
|
1402
|
-
const inNodeModules = resolved.split(path.sep).includes('node_modules')
|
|
1443
|
+
const inNodeModules = resolved.split(path.sep).includes('node_modules')
|
|
1403
1444
|
// A nested node_modules under a source root is a dependency, not app
|
|
1404
1445
|
// source: only an explicitly transpiled package opts into the rewrites,
|
|
1405
1446
|
// and none of them are ever React-Compiled.
|
|
1406
|
-
if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
|
|
1407
|
-
clientProfile.count('loadFiles')
|
|
1447
|
+
if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
|
|
1448
|
+
clientProfile.count('loadFiles')
|
|
1408
1449
|
// Build-scoped cache: the route-fact walk has already read most of these,
|
|
1409
1450
|
// so this is a map lookup rather than a second read of the same file.
|
|
1410
|
-
const original = await clientProfile.timeAsync('read', () => readSourceText(resolved))
|
|
1411
|
-
let contents = original
|
|
1451
|
+
const original = await clientProfile.timeAsync('read', () => readSourceText(resolved))
|
|
1452
|
+
let contents = original
|
|
1412
1453
|
// Worker bundling runs first and awaits: it consumes `import.meta.url`,
|
|
1413
1454
|
// which the sync chain inlines.
|
|
1414
1455
|
if (asyncPre) {
|
|
1415
|
-
contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
|
|
1456
|
+
contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
|
|
1416
1457
|
}
|
|
1417
1458
|
contents = clientProfile.time('chain', () =>
|
|
1418
1459
|
applyClientSourceTransforms(contents, resolved, config.root),
|
|
1419
|
-
)
|
|
1420
|
-
contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
|
|
1460
|
+
)
|
|
1461
|
+
contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
|
|
1421
1462
|
if (dev && devDynamicSplitEnabled()) {
|
|
1422
1463
|
contents = rewriteDeferredDynamicImports(
|
|
1423
1464
|
contents,
|
|
1424
1465
|
resolved,
|
|
1425
1466
|
specifier => resolveImport(rootFromFile(resolved), resolved, specifier),
|
|
1426
1467
|
target => {
|
|
1427
|
-
const id = clientReferenceId(target.file, target.exportName)
|
|
1428
|
-
registerDeferredDynamicRef(id, target)
|
|
1429
|
-
deferredRefs.set(id, target)
|
|
1430
|
-
return deferredDynamicChunkHref({ id })
|
|
1468
|
+
const id = clientReferenceId(target.file, target.exportName)
|
|
1469
|
+
registerDeferredDynamicRef(id, target)
|
|
1470
|
+
deferredRefs.set(id, target)
|
|
1471
|
+
return deferredDynamicChunkHref({ id })
|
|
1431
1472
|
},
|
|
1432
|
-
)
|
|
1473
|
+
)
|
|
1433
1474
|
}
|
|
1434
1475
|
if (compiler && !inNodeModules && shouldReactCompile(contents, resolved)) {
|
|
1435
1476
|
contents = await clientProfile.timeAsync('reactCompiler', () =>
|
|
1436
1477
|
reactCompiled(resolved, contents, compiler),
|
|
1437
|
-
)
|
|
1478
|
+
)
|
|
1438
1479
|
}
|
|
1439
1480
|
// Unchanged sources go back to esbuild's native loader; only .js/.mjs
|
|
1440
1481
|
// must be claimed to force the jsx parse.
|
|
1441
|
-
loaded.set(resolved, contents)
|
|
1442
|
-
if (contents === original && !/\.m?js$/.test(resolved)) return undefined
|
|
1443
|
-
return { contents, loader: clientSourceLoader(resolved) }
|
|
1482
|
+
loaded.set(resolved, contents)
|
|
1483
|
+
if (contents === original && !/\.m?js$/.test(resolved)) return undefined
|
|
1484
|
+
return { contents, loader: clientSourceLoader(resolved) }
|
|
1444
1485
|
}
|
|
1445
1486
|
|
|
1446
1487
|
return {
|
|
1447
1488
|
/** The parsed text of a file this build has loaded, if it has. */
|
|
1448
1489
|
sourceOf(file: string) {
|
|
1449
|
-
return loaded.get(file)
|
|
1490
|
+
return loaded.get(file)
|
|
1450
1491
|
},
|
|
1451
1492
|
/** Deferred dynamic refs rewritten during this build (dev split). */
|
|
1452
1493
|
deferredRefs() {
|
|
1453
|
-
return deferredRefs
|
|
1494
|
+
return deferredRefs
|
|
1454
1495
|
},
|
|
1455
1496
|
/**
|
|
1456
1497
|
* Start the pipeline for sources the route table already names, before esbuild runs. Without it
|
|
@@ -1459,15 +1500,15 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
|
1459
1500
|
*/
|
|
1460
1501
|
/** `warm` for whole routes - every client source the route table names. */
|
|
1461
1502
|
warmRoutes(routes: RouteManifestEntry[]) {
|
|
1462
|
-
this.warm(routeClientSources(routes))
|
|
1503
|
+
this.warm(routeClientSources(routes))
|
|
1463
1504
|
},
|
|
1464
1505
|
warm(files: Iterable<string>) {
|
|
1465
1506
|
for (const file of files) {
|
|
1466
|
-
const resolved = path.resolve(file)
|
|
1467
|
-
if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
|
|
1507
|
+
const resolved = path.resolve(file)
|
|
1508
|
+
if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
|
|
1468
1509
|
// Errors surface at the onLoad await, where esbuild attributes them to
|
|
1469
1510
|
// the importing module; nothing must reject on the warm path.
|
|
1470
|
-
prepare(resolved).catch(() => undefined)
|
|
1511
|
+
prepare(resolved).catch(() => undefined)
|
|
1471
1512
|
}
|
|
1472
1513
|
},
|
|
1473
1514
|
/**
|
|
@@ -1483,24 +1524,24 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
|
1483
1524
|
// namespace: 'file' — an onLoad without a namespace matches EVERY
|
|
1484
1525
|
// namespace, which would hijack virtual modules (e.g. the server-action
|
|
1485
1526
|
// client stub) whose paths end in .js and feed back the original source.
|
|
1486
|
-
build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)))
|
|
1527
|
+
build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)))
|
|
1487
1528
|
// Second hook, not a wider first one: `transpilePackages` opts a
|
|
1488
1529
|
// dependency's own files into the same pipeline, and they are the only
|
|
1489
1530
|
// node_modules paths that ever produce contents.
|
|
1490
1531
|
if (transpiled) {
|
|
1491
1532
|
build.onLoad({ filter: transpiled, namespace: 'file' }, args =>
|
|
1492
1533
|
prepare(path.resolve(args.path)),
|
|
1493
|
-
)
|
|
1534
|
+
)
|
|
1494
1535
|
}
|
|
1495
1536
|
},
|
|
1496
|
-
}
|
|
1537
|
+
}
|
|
1497
1538
|
},
|
|
1498
|
-
}
|
|
1539
|
+
}
|
|
1499
1540
|
}
|
|
1500
1541
|
|
|
1501
|
-
type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
|
|
1542
|
+
type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
|
|
1502
1543
|
|
|
1503
|
-
export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline
|
|
1544
|
+
export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
|
|
1504
1545
|
|
|
1505
1546
|
/**
|
|
1506
1547
|
* Open the client-source pipeline early so the build can feed it routes as the fact scan resolves
|
|
@@ -1509,64 +1550,64 @@ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
|
|
|
1509
1550
|
* otherwise create for itself.
|
|
1510
1551
|
*/
|
|
1511
1552
|
export function startClientSources(config: ResolvedConfig): ClientSourcePipeline {
|
|
1512
|
-
conventionCache.clear()
|
|
1513
|
-
return createClientSourcePipeline(config)
|
|
1553
|
+
conventionCache.clear()
|
|
1554
|
+
return createClientSourcePipeline(config)
|
|
1514
1555
|
}
|
|
1515
1556
|
|
|
1516
1557
|
// The compiled-source cache is keyed on the file and a hash of its post-rewrite
|
|
1517
1558
|
// input, so a rebuild of an unchanged file skips the oxc pass. It holds the
|
|
1518
1559
|
// PROMISE, so two concurrent loads of the same source (two routes sharing a
|
|
1519
1560
|
// component) share one compile instead of racing two.
|
|
1520
|
-
const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
|
|
1561
|
+
const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
|
|
1521
1562
|
|
|
1522
1563
|
function reactCompiled(
|
|
1523
1564
|
file: string,
|
|
1524
1565
|
source: string,
|
|
1525
1566
|
options: NonNullable<ReturnType<CompatModeExtensions['reactCompilerOptions']>>,
|
|
1526
1567
|
) {
|
|
1527
|
-
const hash = Bun.hash.wyhash(`${options.target}\0${source}`)
|
|
1528
|
-
const cached = reactCompiledCache.get(file)
|
|
1529
|
-
if (cached?.hash === hash) return cached.code
|
|
1530
|
-
const code = transformReactCompiler(source, file, options)
|
|
1531
|
-
reactCompiledCache.set(file, { hash, code })
|
|
1532
|
-
return code
|
|
1568
|
+
const hash = Bun.hash.wyhash(`${options.target}\0${source}`)
|
|
1569
|
+
const cached = reactCompiledCache.get(file)
|
|
1570
|
+
if (cached?.hash === hash) return cached.code
|
|
1571
|
+
const code = transformReactCompiler(source, file, options)
|
|
1572
|
+
reactCompiledCache.set(file, { hash, code })
|
|
1573
|
+
return code
|
|
1533
1574
|
}
|
|
1534
1575
|
|
|
1535
1576
|
// config.root can sit outside the workspace root (and vice versa), so both
|
|
1536
1577
|
// anchor the filter — as does each one's realpath, since esbuild reports
|
|
1537
1578
|
// resolved paths through the OS realpath.
|
|
1538
1579
|
function clientSourceRoots(config: ResolvedConfig) {
|
|
1539
|
-
const roots = new Set<string>()
|
|
1580
|
+
const roots = new Set<string>()
|
|
1540
1581
|
for (const root of [config.root, config.workspaceRoot]) {
|
|
1541
|
-
const resolved = path.resolve(root)
|
|
1542
|
-
roots.add(resolved)
|
|
1582
|
+
const resolved = path.resolve(root)
|
|
1583
|
+
roots.add(resolved)
|
|
1543
1584
|
try {
|
|
1544
|
-
roots.add(realpathSync.native(resolved))
|
|
1585
|
+
roots.add(realpathSync.native(resolved))
|
|
1545
1586
|
} catch {
|
|
1546
1587
|
// A root that cannot be realpath'd is already covered by its literal form.
|
|
1547
1588
|
}
|
|
1548
1589
|
}
|
|
1549
|
-
return [...roots]
|
|
1590
|
+
return [...roots]
|
|
1550
1591
|
}
|
|
1551
1592
|
|
|
1552
1593
|
// `.cjs` keeps the plain js loader: CommonJS never carries JSX, and parsing a
|
|
1553
1594
|
// `a < b` comparison as JSX would fail the build.
|
|
1554
1595
|
function clientSourceLoader(file: string): 'js' | 'ts' | 'tsx' | 'jsx' {
|
|
1555
|
-
if (file.endsWith('.tsx')) return 'tsx'
|
|
1556
|
-
if (/\.[cm]?ts$/.test(file)) return 'ts'
|
|
1557
|
-
return file.endsWith('.cjs') ? 'js' : 'jsx'
|
|
1596
|
+
if (file.endsWith('.tsx')) return 'tsx'
|
|
1597
|
+
if (/\.[cm]?ts$/.test(file)) return 'ts'
|
|
1598
|
+
return file.endsWith('.cjs') ? 'js' : 'jsx'
|
|
1558
1599
|
}
|
|
1559
1600
|
|
|
1560
1601
|
// A path segment that is NOT `word`, spelled positively: esbuild's Go regexp has
|
|
1561
1602
|
// no lookahead, so the negation enumerates "differs at some position / ends
|
|
1562
1603
|
// early / runs longer".
|
|
1563
1604
|
function nonSegmentPattern(word: string, sep: string) {
|
|
1564
|
-
const parts = [`${word}[^${sep}]+`]
|
|
1605
|
+
const parts = [`${word}[^${sep}]+`]
|
|
1565
1606
|
for (let index = 0; index < word.length; index += 1) {
|
|
1566
|
-
const prefix = word.slice(0, index)
|
|
1567
|
-
parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`)
|
|
1607
|
+
const prefix = word.slice(0, index)
|
|
1608
|
+
parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`)
|
|
1568
1609
|
}
|
|
1569
|
-
return `(?:${parts.join('|')})
|
|
1610
|
+
return `(?:${parts.join('|')})`
|
|
1570
1611
|
}
|
|
1571
1612
|
|
|
1572
1613
|
/**
|
|
@@ -1575,45 +1616,45 @@ function nonSegmentPattern(word: string, sep: string) {
|
|
|
1575
1616
|
* path on the hook.
|
|
1576
1617
|
*/
|
|
1577
1618
|
function transpiledPackageFilter(roots: string): RegExp | undefined {
|
|
1578
|
-
const sep = escapeRegex(path.sep)
|
|
1579
|
-
const names = getExternalPackagePolicy().transpiled?.()
|
|
1580
|
-
if (names?.length === 0) return undefined
|
|
1619
|
+
const sep = escapeRegex(path.sep)
|
|
1620
|
+
const names = getExternalPackagePolicy().transpiled?.()
|
|
1621
|
+
if (names?.length === 0) return undefined
|
|
1581
1622
|
const packages = names
|
|
1582
1623
|
? `(?:${names.map(name => escapeRegex(name).split('/').join(sep)).join('|')})${sep}`
|
|
1583
|
-
: ''
|
|
1584
|
-
return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
|
|
1624
|
+
: ''
|
|
1625
|
+
return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
|
|
1585
1626
|
}
|
|
1586
1627
|
|
|
1587
1628
|
function isTranspiledPackageFile(file: string) {
|
|
1588
|
-
const parts = file.split(path.sep)
|
|
1589
|
-
const nodeModules = parts.lastIndexOf('node_modules')
|
|
1590
|
-
if (nodeModules === -1) return false
|
|
1591
|
-
const first = parts[nodeModules + 1]
|
|
1592
|
-
if (!first) return false
|
|
1593
|
-
const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first
|
|
1594
|
-
return getExternalPackagePolicy().transpile(name)
|
|
1629
|
+
const parts = file.split(path.sep)
|
|
1630
|
+
const nodeModules = parts.lastIndexOf('node_modules')
|
|
1631
|
+
if (nodeModules === -1) return false
|
|
1632
|
+
const first = parts[nodeModules + 1]
|
|
1633
|
+
if (!first) return false
|
|
1634
|
+
const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first
|
|
1635
|
+
return getExternalPackagePolicy().transpile(name)
|
|
1595
1636
|
}
|
|
1596
1637
|
|
|
1597
1638
|
function virtualEntryPath(routeId: string) {
|
|
1598
|
-
return `${virtualEntryNamespace}:${routeId}
|
|
1639
|
+
return `${virtualEntryNamespace}:${routeId}`
|
|
1599
1640
|
}
|
|
1600
1641
|
|
|
1601
1642
|
function virtualEntryPlugin(entries: { route: RouteManifestEntry; source: string }[]): Plugin {
|
|
1602
|
-
const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
|
|
1643
|
+
const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
|
|
1603
1644
|
return {
|
|
1604
1645
|
name: 'pnext-virtual-client-entry',
|
|
1605
1646
|
setup(build) {
|
|
1606
1647
|
build.onResolve({ filter: new RegExp(`^${escapeRegex(virtualEntryNamespace)}:`) }, args => ({
|
|
1607
1648
|
path: args.path.slice(virtualEntryNamespace.length + 1),
|
|
1608
1649
|
namespace: virtualEntryNamespace,
|
|
1609
|
-
}))
|
|
1650
|
+
}))
|
|
1610
1651
|
build.onLoad({ filter: /.*/, namespace: virtualEntryNamespace }, args => {
|
|
1611
|
-
const contents = sources.get(args.path)
|
|
1612
|
-
if (contents === undefined) return undefined
|
|
1613
|
-
return { contents, loader: 'ts', resolveDir: process.cwd() }
|
|
1614
|
-
})
|
|
1652
|
+
const contents = sources.get(args.path)
|
|
1653
|
+
if (contents === undefined) return undefined
|
|
1654
|
+
return { contents, loader: 'ts', resolveDir: process.cwd() }
|
|
1655
|
+
})
|
|
1615
1656
|
},
|
|
1616
|
-
}
|
|
1657
|
+
}
|
|
1617
1658
|
}
|
|
1618
1659
|
|
|
1619
1660
|
// The shared route runtime: one virtual module per build, imported by every
|
|
@@ -1626,14 +1667,14 @@ function clientRuntimePlugin(facts: ClientRuntimeFacts): Plugin {
|
|
|
1626
1667
|
build.onResolve({ filter: new RegExp(`^${escapeRegex(CLIENT_RUNTIME_MODULE)}$`) }, () => ({
|
|
1627
1668
|
path: CLIENT_RUNTIME_MODULE,
|
|
1628
1669
|
namespace: CLIENT_RUNTIME_MODULE,
|
|
1629
|
-
}))
|
|
1670
|
+
}))
|
|
1630
1671
|
build.onLoad({ filter: /.*/, namespace: CLIENT_RUNTIME_MODULE }, () => ({
|
|
1631
1672
|
contents: clientRuntimeSource(facts),
|
|
1632
1673
|
loader: 'ts',
|
|
1633
1674
|
resolveDir: process.cwd(),
|
|
1634
|
-
}))
|
|
1675
|
+
}))
|
|
1635
1676
|
},
|
|
1636
|
-
}
|
|
1677
|
+
}
|
|
1637
1678
|
}
|
|
1638
1679
|
|
|
1639
1680
|
async function profileClientBuild<T>(
|
|
@@ -1642,25 +1683,18 @@ async function profileClientBuild<T>(
|
|
|
1642
1683
|
task: () => Promise<T>,
|
|
1643
1684
|
) {
|
|
1644
1685
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1645
|
-
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
1646
|
-
const start = performance.now()
|
|
1686
|
+
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
1687
|
+
const start = performance.now()
|
|
1647
1688
|
try {
|
|
1648
|
-
return await task()
|
|
1689
|
+
return await task()
|
|
1649
1690
|
} finally {
|
|
1650
|
-
const mode = dev ? 'dev' : 'prod'
|
|
1691
|
+
const mode = dev ? 'dev' : 'prod'
|
|
1651
1692
|
console.log(
|
|
1652
1693
|
`dev-profile client build ${mode} ${route.route} in ${formatDuration(performance.now() - start)}`,
|
|
1653
|
-
)
|
|
1694
|
+
)
|
|
1654
1695
|
}
|
|
1655
1696
|
}
|
|
1656
1697
|
|
|
1657
|
-
function formatDuration(durationMs: number) {
|
|
1658
|
-
const ms = Math.max(0, durationMs);
|
|
1659
|
-
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
1660
|
-
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
1661
|
-
return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
1698
|
function serverOnlyClientImportPlugin(): Plugin {
|
|
1665
1699
|
return {
|
|
1666
1700
|
name: 'pnext-server-only-client-import',
|
|
@@ -1674,26 +1708,26 @@ function serverOnlyClientImportPlugin(): Plugin {
|
|
|
1674
1708
|
},
|
|
1675
1709
|
],
|
|
1676
1710
|
}),
|
|
1677
|
-
)
|
|
1711
|
+
)
|
|
1678
1712
|
},
|
|
1679
|
-
}
|
|
1713
|
+
}
|
|
1680
1714
|
}
|
|
1681
1715
|
|
|
1682
1716
|
interface EsbuildLikeError extends Error {
|
|
1683
1717
|
errors?: {
|
|
1684
|
-
text?: string
|
|
1718
|
+
text?: string
|
|
1685
1719
|
location?: {
|
|
1686
|
-
file?: string
|
|
1687
|
-
}
|
|
1688
|
-
}[]
|
|
1720
|
+
file?: string
|
|
1721
|
+
}
|
|
1722
|
+
}[]
|
|
1689
1723
|
}
|
|
1690
1724
|
|
|
1691
1725
|
interface TraceParent {
|
|
1692
|
-
file: string
|
|
1693
|
-
specifier: string
|
|
1726
|
+
file: string
|
|
1727
|
+
specifier: string
|
|
1694
1728
|
}
|
|
1695
1729
|
|
|
1696
|
-
const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
|
|
1730
|
+
const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
|
|
1697
1731
|
|
|
1698
1732
|
function withClientImportTrace(
|
|
1699
1733
|
error: unknown,
|
|
@@ -1701,16 +1735,16 @@ function withClientImportTrace(
|
|
|
1701
1735
|
route: RouteManifestEntry,
|
|
1702
1736
|
entrySource: string,
|
|
1703
1737
|
) {
|
|
1704
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
1705
|
-
const trace = clientImportTrace(config, route, entrySource, error)
|
|
1706
|
-
if (!trace) return error
|
|
1707
|
-
const next = new Error(`${message}\n\n${trace}`)
|
|
1738
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1739
|
+
const trace = clientImportTrace(config, route, entrySource, error)
|
|
1740
|
+
if (!trace) return error
|
|
1741
|
+
const next = new Error(`${message}\n\n${trace}`)
|
|
1708
1742
|
if (error instanceof Error) {
|
|
1709
|
-
next.name = error.name
|
|
1710
|
-
next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack
|
|
1711
|
-
Object.assign(next, error)
|
|
1743
|
+
next.name = error.name
|
|
1744
|
+
next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack
|
|
1745
|
+
Object.assign(next, error)
|
|
1712
1746
|
}
|
|
1713
|
-
return next
|
|
1747
|
+
return next
|
|
1714
1748
|
}
|
|
1715
1749
|
|
|
1716
1750
|
function clientImportTrace(
|
|
@@ -1719,51 +1753,51 @@ function clientImportTrace(
|
|
|
1719
1753
|
entrySource: string,
|
|
1720
1754
|
error: unknown,
|
|
1721
1755
|
) {
|
|
1722
|
-
const unresolved = unresolvedImportFromError(error)
|
|
1723
|
-
if (!unresolved) return undefined
|
|
1756
|
+
const unresolved = unresolvedImportFromError(error)
|
|
1757
|
+
if (!unresolved) return undefined
|
|
1724
1758
|
|
|
1725
|
-
const entryFile = `${route.id}.ts
|
|
1726
|
-
const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined
|
|
1727
|
-
const parents = new Map<string, TraceParent>()
|
|
1728
|
-
const visited = new Set<string>()
|
|
1729
|
-
const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }]
|
|
1730
|
-
let unresolvedImporter: string | undefined
|
|
1759
|
+
const entryFile = `${route.id}.ts`
|
|
1760
|
+
const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined
|
|
1761
|
+
const parents = new Map<string, TraceParent>()
|
|
1762
|
+
const visited = new Set<string>()
|
|
1763
|
+
const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }]
|
|
1764
|
+
let unresolvedImporter: string | undefined
|
|
1731
1765
|
|
|
1732
1766
|
while (queue.length > 0) {
|
|
1733
|
-
const current = queue.shift()
|
|
1734
|
-
if (!current || visited.has(current.file)) continue
|
|
1735
|
-
visited.add(current.file)
|
|
1736
|
-
if (visited.size > 2000) break
|
|
1767
|
+
const current = queue.shift()
|
|
1768
|
+
if (!current || visited.has(current.file)) continue
|
|
1769
|
+
visited.add(current.file)
|
|
1770
|
+
if (visited.size > 2000) break
|
|
1737
1771
|
|
|
1738
1772
|
for (const specifier of localImports(current.file, current.source)) {
|
|
1739
|
-
const resolved = resolveTraceImport(config.root, current.file, specifier)
|
|
1773
|
+
const resolved = resolveTraceImport(config.root, current.file, specifier)
|
|
1740
1774
|
if (!resolved) {
|
|
1741
1775
|
if (
|
|
1742
1776
|
specifier === unresolved.specifier &&
|
|
1743
1777
|
(!failedFile || sameFile(current.file, failedFile))
|
|
1744
1778
|
) {
|
|
1745
|
-
unresolvedImporter = current.file
|
|
1746
|
-
queue.length = 0
|
|
1747
|
-
break
|
|
1779
|
+
unresolvedImporter = current.file
|
|
1780
|
+
queue.length = 0
|
|
1781
|
+
break
|
|
1748
1782
|
}
|
|
1749
|
-
continue
|
|
1783
|
+
continue
|
|
1750
1784
|
}
|
|
1751
1785
|
|
|
1752
|
-
if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
|
|
1786
|
+
if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
|
|
1753
1787
|
if (failedFile && sameFile(resolved, failedFile)) {
|
|
1754
|
-
unresolvedImporter = resolved
|
|
1755
|
-
queue.length = 0
|
|
1756
|
-
break
|
|
1788
|
+
unresolvedImporter = resolved
|
|
1789
|
+
queue.length = 0
|
|
1790
|
+
break
|
|
1757
1791
|
}
|
|
1758
1792
|
if (!visited.has(resolved) && !isAssetLike(resolved)) {
|
|
1759
|
-
queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
|
|
1793
|
+
queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
|
|
1760
1794
|
}
|
|
1761
1795
|
}
|
|
1762
1796
|
}
|
|
1763
1797
|
|
|
1764
|
-
if (!unresolvedImporter) return undefined
|
|
1765
|
-
const trace = tracePath(entryFile, unresolvedImporter, parents)
|
|
1766
|
-
if (trace.length === 0) return undefined
|
|
1798
|
+
if (!unresolvedImporter) return undefined
|
|
1799
|
+
const trace = tracePath(entryFile, unresolvedImporter, parents)
|
|
1800
|
+
if (trace.length === 0) return undefined
|
|
1767
1801
|
|
|
1768
1802
|
return [
|
|
1769
1803
|
`pnext client import trace for route ${route.route}:`,
|
|
@@ -1771,135 +1805,135 @@ function clientImportTrace(
|
|
|
1771
1805
|
(file, index) => `${index === 0 ? ' ' : ' -> '}${displayTraceFile(config, file)}`,
|
|
1772
1806
|
),
|
|
1773
1807
|
` -> ${unresolved.specifier} (unresolved in browser client bundle)`,
|
|
1774
|
-
].join('\n')
|
|
1808
|
+
].join('\n')
|
|
1775
1809
|
}
|
|
1776
1810
|
|
|
1777
1811
|
/** True when the build failed because an import could not be resolved. */
|
|
1778
1812
|
function hasUnresolvedImportError(error: unknown): boolean {
|
|
1779
|
-
const errors = (error as EsbuildLikeError).errors
|
|
1813
|
+
const errors = (error as EsbuildLikeError).errors
|
|
1780
1814
|
if (Array.isArray(errors)) {
|
|
1781
|
-
return errors.some(item => item.text?.startsWith('Could not resolve '))
|
|
1815
|
+
return errors.some(item => item.text?.startsWith('Could not resolve '))
|
|
1782
1816
|
}
|
|
1783
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
1784
|
-
return message.includes('Could not resolve ')
|
|
1817
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1818
|
+
return message.includes('Could not resolve ')
|
|
1785
1819
|
}
|
|
1786
1820
|
|
|
1787
1821
|
function unresolvedImportFromError(error: unknown) {
|
|
1788
|
-
const buildError = error as EsbuildLikeError
|
|
1789
|
-
const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '))
|
|
1790
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
1791
|
-
const text = diagnostic?.text ?? message
|
|
1792
|
-
const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1]
|
|
1793
|
-
if (!specifier) return undefined
|
|
1822
|
+
const buildError = error as EsbuildLikeError
|
|
1823
|
+
const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '))
|
|
1824
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1825
|
+
const text = diagnostic?.text ?? message
|
|
1826
|
+
const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1]
|
|
1827
|
+
if (!specifier) return undefined
|
|
1794
1828
|
return {
|
|
1795
1829
|
specifier,
|
|
1796
1830
|
file: diagnostic?.location?.file ?? errorFileFromMessage(message),
|
|
1797
|
-
}
|
|
1831
|
+
}
|
|
1798
1832
|
}
|
|
1799
1833
|
|
|
1800
1834
|
function errorFileFromMessage(message: string) {
|
|
1801
|
-
const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
|
|
1802
|
-
return match?.[1]
|
|
1835
|
+
const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
|
|
1836
|
+
return match?.[1]
|
|
1803
1837
|
}
|
|
1804
1838
|
|
|
1805
1839
|
function resolveTraceImport(root: string, fromFile: string, specifier: string) {
|
|
1806
|
-
if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
|
|
1807
|
-
return resolveImport(root, fromFile, specifier)
|
|
1840
|
+
if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
|
|
1841
|
+
return resolveImport(root, fromFile, specifier)
|
|
1808
1842
|
}
|
|
1809
1843
|
|
|
1810
1844
|
function resolveTraceFile(file: string) {
|
|
1811
1845
|
return canonicalTraceFile(
|
|
1812
1846
|
path.isAbsolute(file) ? path.resolve(file) : path.resolve(process.cwd(), file),
|
|
1813
|
-
)
|
|
1847
|
+
)
|
|
1814
1848
|
}
|
|
1815
1849
|
|
|
1816
1850
|
function resolveTraceCandidate(base: string) {
|
|
1817
|
-
const ext = path.extname(base)
|
|
1851
|
+
const ext = path.extname(base)
|
|
1818
1852
|
const candidates = ext
|
|
1819
1853
|
? [base]
|
|
1820
1854
|
: [
|
|
1821
1855
|
...traceSourceExtensions.map(extension => `${base}${extension}`),
|
|
1822
1856
|
...traceSourceExtensions.map(extension => path.join(base, `index${extension}`)),
|
|
1823
1857
|
base,
|
|
1824
|
-
]
|
|
1825
|
-
const resolved = candidates.find(isTraceFile)
|
|
1826
|
-
return resolved ? canonicalTraceFile(resolved) : undefined
|
|
1858
|
+
]
|
|
1859
|
+
const resolved = candidates.find(isTraceFile)
|
|
1860
|
+
return resolved ? canonicalTraceFile(resolved) : undefined
|
|
1827
1861
|
}
|
|
1828
1862
|
|
|
1829
1863
|
function isTraceFile(file: string) {
|
|
1830
1864
|
try {
|
|
1831
|
-
return statSync(file).isFile()
|
|
1865
|
+
return statSync(file).isFile()
|
|
1832
1866
|
} catch {
|
|
1833
|
-
return false
|
|
1867
|
+
return false
|
|
1834
1868
|
}
|
|
1835
1869
|
}
|
|
1836
1870
|
|
|
1837
1871
|
function readTextSyncSafe(file: string) {
|
|
1838
1872
|
try {
|
|
1839
|
-
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
1873
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
1840
1874
|
} catch {
|
|
1841
|
-
return ''
|
|
1875
|
+
return ''
|
|
1842
1876
|
}
|
|
1843
1877
|
}
|
|
1844
1878
|
|
|
1845
1879
|
function tracePath(entryFile: string, targetFile: string, parents: Map<string, TraceParent>) {
|
|
1846
|
-
const pathItems = [targetFile]
|
|
1847
|
-
let current = targetFile
|
|
1880
|
+
const pathItems = [targetFile]
|
|
1881
|
+
let current = targetFile
|
|
1848
1882
|
|
|
1849
1883
|
while (current !== entryFile) {
|
|
1850
|
-
const parent = parents.get(current)
|
|
1851
|
-
if (!parent) return []
|
|
1852
|
-
current = parent.file
|
|
1853
|
-
pathItems.push(current)
|
|
1884
|
+
const parent = parents.get(current)
|
|
1885
|
+
if (!parent) return []
|
|
1886
|
+
current = parent.file
|
|
1887
|
+
pathItems.push(current)
|
|
1854
1888
|
}
|
|
1855
1889
|
|
|
1856
|
-
return pathItems.reverse()
|
|
1890
|
+
return pathItems.reverse()
|
|
1857
1891
|
}
|
|
1858
1892
|
|
|
1859
1893
|
function sameFile(a: string, b: string) {
|
|
1860
|
-
return canonicalTraceFile(a) === canonicalTraceFile(b)
|
|
1894
|
+
return canonicalTraceFile(a) === canonicalTraceFile(b)
|
|
1861
1895
|
}
|
|
1862
1896
|
|
|
1863
1897
|
function displayTraceFile(config: ResolvedConfig, file: string) {
|
|
1864
|
-
if (!path.isAbsolute(file)) return `${file} (generated client entry)
|
|
1865
|
-
const root = canonicalTraceFile(config.root)
|
|
1866
|
-
const workspaceRoot = canonicalTraceFile(config.workspaceRoot)
|
|
1867
|
-
const resolvedFile = canonicalTraceFile(file)
|
|
1868
|
-
const rootRelative = path.relative(root, resolvedFile)
|
|
1869
|
-
if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative
|
|
1870
|
-
const workspaceRelative = path.relative(workspaceRoot, resolvedFile)
|
|
1898
|
+
if (!path.isAbsolute(file)) return `${file} (generated client entry)`
|
|
1899
|
+
const root = canonicalTraceFile(config.root)
|
|
1900
|
+
const workspaceRoot = canonicalTraceFile(config.workspaceRoot)
|
|
1901
|
+
const resolvedFile = canonicalTraceFile(file)
|
|
1902
|
+
const rootRelative = path.relative(root, resolvedFile)
|
|
1903
|
+
if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative
|
|
1904
|
+
const workspaceRelative = path.relative(workspaceRoot, resolvedFile)
|
|
1871
1905
|
if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative))
|
|
1872
|
-
return workspaceRelative
|
|
1873
|
-
return resolvedFile
|
|
1906
|
+
return workspaceRelative
|
|
1907
|
+
return resolvedFile
|
|
1874
1908
|
}
|
|
1875
1909
|
|
|
1876
1910
|
function canonicalTraceFile(file: string) {
|
|
1877
1911
|
try {
|
|
1878
|
-
return realpathSync.native(file)
|
|
1912
|
+
return realpathSync.native(file)
|
|
1879
1913
|
} catch {
|
|
1880
|
-
return path.resolve(file)
|
|
1914
|
+
return path.resolve(file)
|
|
1881
1915
|
}
|
|
1882
1916
|
}
|
|
1883
1917
|
|
|
1884
1918
|
/** One walked client source, with the stat that lets a later boot skip reading it. */
|
|
1885
1919
|
export interface ClientCacheSource {
|
|
1886
|
-
file: string
|
|
1887
|
-
mtimeMs: number
|
|
1888
|
-
size: number
|
|
1889
|
-
hash: string
|
|
1920
|
+
file: string
|
|
1921
|
+
mtimeMs: number
|
|
1922
|
+
size: number
|
|
1923
|
+
hash: string
|
|
1890
1924
|
}
|
|
1891
1925
|
|
|
1892
1926
|
export interface ClientCacheKeyParts {
|
|
1893
|
-
key: string
|
|
1894
|
-
staticHash: string
|
|
1895
|
-
sources: ClientCacheSource[]
|
|
1927
|
+
key: string
|
|
1928
|
+
staticHash: string
|
|
1929
|
+
sources: ClientCacheSource[]
|
|
1896
1930
|
}
|
|
1897
1931
|
|
|
1898
1932
|
/**
|
|
1899
1933
|
* The half of the key that is not a walked source: the client build pipeline's own sources, the
|
|
1900
1934
|
* generated entry, and the route's reference list. A handful of small reads, so it is cheap to
|
|
1901
1935
|
* recompute on every lookup - which is what lets the expensive source walk be validated from a
|
|
1902
|
-
* persisted index instead (see dev/client-key
|
|
1936
|
+
* persisted index instead (see dev/restart/client-key.ts).
|
|
1903
1937
|
*/
|
|
1904
1938
|
export async function clientCacheStaticHash(
|
|
1905
1939
|
route: RouteManifestEntry,
|
|
@@ -1908,18 +1942,18 @@ export async function clientCacheStaticHash(
|
|
|
1908
1942
|
) {
|
|
1909
1943
|
// The key must observe convention files added since the last build, so it
|
|
1910
1944
|
// never reads a memo an earlier call left behind.
|
|
1911
|
-
conventionCache.clear()
|
|
1912
|
-
const hash = createHash('sha256')
|
|
1945
|
+
conventionCache.clear()
|
|
1946
|
+
const hash = createHash('sha256')
|
|
1913
1947
|
for (const file of clientBuildPipelineFiles()) {
|
|
1914
|
-
hash.update(file)
|
|
1915
|
-
hash.update('\0')
|
|
1916
|
-
hash.update(await readText(file))
|
|
1917
|
-
hash.update('\0')
|
|
1948
|
+
hash.update(file)
|
|
1949
|
+
hash.update('\0')
|
|
1950
|
+
hash.update(await readText(file))
|
|
1951
|
+
hash.update('\0')
|
|
1918
1952
|
}
|
|
1919
1953
|
|
|
1920
1954
|
hash.update(
|
|
1921
1955
|
clientEntrySource({
|
|
1922
|
-
// Dev-only callers (client-key
|
|
1956
|
+
// Dev-only callers (restart/client-key): the key must move with the split
|
|
1923
1957
|
// flag or a toggled PNEXT_DYNAMIC_SPLIT would serve the other arm's entry.
|
|
1924
1958
|
deferredDynamicHref: devDeferredDynamicHref(true),
|
|
1925
1959
|
pageFile: route.client ? route.file : undefined,
|
|
@@ -1933,11 +1967,11 @@ export async function clientCacheStaticHash(
|
|
|
1933
1967
|
clientRootLayout: config ? hasClientRootLayout(config, route) : false,
|
|
1934
1968
|
shellLayoutOrder: config ? shellLayoutOrder(config, route) : [],
|
|
1935
1969
|
}),
|
|
1936
|
-
)
|
|
1937
|
-
hash.update('\0')
|
|
1938
|
-
hash.update(route.client ? route.file : '')
|
|
1939
|
-
hash.update(JSON.stringify(route.clientReferences))
|
|
1940
|
-
return hash.digest('hex')
|
|
1970
|
+
)
|
|
1971
|
+
hash.update('\0')
|
|
1972
|
+
hash.update(route.client ? route.file : '')
|
|
1973
|
+
hash.update(JSON.stringify(route.clientReferences))
|
|
1974
|
+
return hash.digest('hex')
|
|
1941
1975
|
}
|
|
1942
1976
|
|
|
1943
1977
|
/** The out-dir name: the static half plus every walked source's content hash. */
|
|
@@ -1945,18 +1979,18 @@ export function clientCacheKeyFrom(
|
|
|
1945
1979
|
staticHash: string,
|
|
1946
1980
|
sources: readonly { file: string; hash: string }[],
|
|
1947
1981
|
) {
|
|
1948
|
-
const hash = createHash('sha256').update(staticHash).update('\0')
|
|
1982
|
+
const hash = createHash('sha256').update(staticHash).update('\0')
|
|
1949
1983
|
for (const source of sources) {
|
|
1950
|
-
hash.update(source.file)
|
|
1951
|
-
hash.update('\0')
|
|
1952
|
-
hash.update(source.hash)
|
|
1953
|
-
hash.update('\0')
|
|
1984
|
+
hash.update(source.file)
|
|
1985
|
+
hash.update('\0')
|
|
1986
|
+
hash.update(source.hash)
|
|
1987
|
+
hash.update('\0')
|
|
1954
1988
|
}
|
|
1955
|
-
return hash.digest('hex').slice(0, 16)
|
|
1989
|
+
return hash.digest('hex').slice(0, 16)
|
|
1956
1990
|
}
|
|
1957
1991
|
|
|
1958
1992
|
export function clientSourceHash(source: string) {
|
|
1959
|
-
return Bun.hash(source).toString(36)
|
|
1993
|
+
return Bun.hash(source).toString(36)
|
|
1960
1994
|
}
|
|
1961
1995
|
|
|
1962
1996
|
/**
|
|
@@ -1965,13 +1999,13 @@ export function clientSourceHash(source: string) {
|
|
|
1965
1999
|
* hash that stat never described.
|
|
1966
2000
|
*/
|
|
1967
2001
|
export async function readClientCacheSource(file: string): Promise<ClientCacheSource> {
|
|
1968
|
-
const stats = await stat(file).catch(() => undefined)
|
|
2002
|
+
const stats = await stat(file).catch(() => undefined)
|
|
1969
2003
|
return {
|
|
1970
2004
|
file,
|
|
1971
2005
|
mtimeMs: stats?.mtimeMs ?? 0,
|
|
1972
2006
|
size: stats?.size ?? 0,
|
|
1973
2007
|
hash: clientSourceHash(await readText(file)),
|
|
1974
|
-
}
|
|
2008
|
+
}
|
|
1975
2009
|
}
|
|
1976
2010
|
|
|
1977
2011
|
export async function clientCacheKeyParts(
|
|
@@ -1980,11 +2014,11 @@ export async function clientCacheKeyParts(
|
|
|
1980
2014
|
config?: ResolvedConfig,
|
|
1981
2015
|
staticHash?: string,
|
|
1982
2016
|
): Promise<ClientCacheKeyParts> {
|
|
1983
|
-
const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
|
|
2017
|
+
const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
|
|
1984
2018
|
const sources = await Promise.all(
|
|
1985
2019
|
(await clientSourceFiles(route)).map(file => readClientCacheSource(file)),
|
|
1986
|
-
)
|
|
1987
|
-
return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
|
|
2020
|
+
)
|
|
2021
|
+
return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
|
|
1988
2022
|
}
|
|
1989
2023
|
|
|
1990
2024
|
export async function clientCacheKey(
|
|
@@ -1992,7 +2026,7 @@ export async function clientCacheKey(
|
|
|
1992
2026
|
nextCompat?: boolean,
|
|
1993
2027
|
config?: ResolvedConfig,
|
|
1994
2028
|
) {
|
|
1995
|
-
return (await clientCacheKeyParts(route, nextCompat, config)).key
|
|
2029
|
+
return (await clientCacheKeyParts(route, nextCompat, config)).key
|
|
1996
2030
|
}
|
|
1997
2031
|
|
|
1998
2032
|
function clientBuildPipelineFiles() {
|
|
@@ -2004,61 +2038,61 @@ function clientBuildPipelineFiles() {
|
|
|
2004
2038
|
path.join(import.meta.dirname, 'build.ts'),
|
|
2005
2039
|
path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts'),
|
|
2006
2040
|
path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts'),
|
|
2007
|
-
].filter(file => existsSync(file))
|
|
2041
|
+
].filter(file => existsSync(file))
|
|
2008
2042
|
}
|
|
2009
2043
|
|
|
2010
2044
|
async function clientSourceFiles(route: RouteManifestEntry) {
|
|
2011
2045
|
const entryFiles = [
|
|
2012
2046
|
...(route.client ? [route.file] : []),
|
|
2013
2047
|
...route.clientReferences.map(reference => reference.file),
|
|
2014
|
-
]
|
|
2015
|
-
const files = new Set<string>()
|
|
2048
|
+
]
|
|
2049
|
+
const files = new Set<string>()
|
|
2016
2050
|
|
|
2017
|
-
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
|
|
2051
|
+
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
|
|
2018
2052
|
|
|
2019
|
-
return [...files].sort()
|
|
2053
|
+
return [...files].sort()
|
|
2020
2054
|
}
|
|
2021
2055
|
|
|
2022
2056
|
// The walk fans out: every file joins `files` before its own read starts, so
|
|
2023
2057
|
// concurrent branches still visit each file once. Serially this was the whole
|
|
2024
2058
|
// cost of a route's client cache key (~1.8 s on a 300-module route).
|
|
2025
2059
|
async function collectClientSourceFile(file: string, files: Set<string>) {
|
|
2026
|
-
if (files.has(file) || !existsSync(file)) return
|
|
2027
|
-
files.add(file)
|
|
2028
|
-
if (isAssetLike(file)) return
|
|
2060
|
+
if (files.has(file) || !existsSync(file)) return
|
|
2061
|
+
files.add(file)
|
|
2062
|
+
if (isAssetLike(file)) return
|
|
2029
2063
|
|
|
2030
|
-
const source = rewriteLiteralDynamicCalls(await readText(file), file)
|
|
2064
|
+
const source = rewriteLiteralDynamicCalls(await readText(file), file)
|
|
2031
2065
|
await Promise.all(
|
|
2032
2066
|
[...localImports(file, source)].map(specifier => {
|
|
2033
|
-
const resolved = resolveImport(rootFromFile(file), file, specifier)
|
|
2034
|
-
return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
|
|
2067
|
+
const resolved = resolveImport(rootFromFile(file), file, specifier)
|
|
2068
|
+
return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
|
|
2035
2069
|
}),
|
|
2036
|
-
)
|
|
2070
|
+
)
|
|
2037
2071
|
}
|
|
2038
2072
|
|
|
2039
2073
|
// Value imports, re-exports and literal `import()` of one module, off the
|
|
2040
2074
|
// memoized parse (PERF-REWRITES #21) — type-only imports bind nothing and are
|
|
2041
2075
|
// already excluded there.
|
|
2042
2076
|
export function localImports(file: string, source: string) {
|
|
2043
|
-
const facts = scanFacts(file, source)
|
|
2044
|
-
const imports = new Set<string>()
|
|
2045
|
-
for (const edge of facts.imports) imports.add(edge.specifier)
|
|
2046
|
-
for (const edge of facts.dynamicImports) imports.add(edge.specifier)
|
|
2047
|
-
return imports
|
|
2077
|
+
const facts = scanFacts(file, source)
|
|
2078
|
+
const imports = new Set<string>()
|
|
2079
|
+
for (const edge of facts.imports) imports.add(edge.specifier)
|
|
2080
|
+
for (const edge of facts.dynamicImports) imports.add(edge.specifier)
|
|
2081
|
+
return imports
|
|
2048
2082
|
}
|
|
2049
2083
|
|
|
2050
2084
|
function isAssetLike(file: string) {
|
|
2051
|
-
return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
|
|
2085
|
+
return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
|
|
2052
2086
|
}
|
|
2053
2087
|
|
|
2054
2088
|
function rootFromFile(file: string) {
|
|
2055
|
-
const parts = file.split(path.sep)
|
|
2056
|
-
const srcApp = parts.lastIndexOf('src')
|
|
2089
|
+
const parts = file.split(path.sep)
|
|
2090
|
+
const srcApp = parts.lastIndexOf('src')
|
|
2057
2091
|
if (srcApp !== -1 && parts[srcApp + 1] === 'app')
|
|
2058
|
-
return parts.slice(0, srcApp).join(path.sep) || path.sep
|
|
2059
|
-
const app = parts.lastIndexOf('app')
|
|
2060
|
-
if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep
|
|
2061
|
-
return path.dirname(file)
|
|
2092
|
+
return parts.slice(0, srcApp).join(path.sep) || path.sep
|
|
2093
|
+
const app = parts.lastIndexOf('app')
|
|
2094
|
+
if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep
|
|
2095
|
+
return path.dirname(file)
|
|
2062
2096
|
}
|
|
2063
2097
|
|
|
2064
2098
|
// esbuild names every shared chunk "chunk-<hash>", which makes bundle reports
|
|
@@ -2068,96 +2102,96 @@ function rootFromFile(file: string) {
|
|
|
2068
2102
|
async function nameSharedClientChunks(outDir: string, metafile?: Metafile) {
|
|
2069
2103
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2070
2104
|
if (metafile && process.env.PNEXT_CLIENT_METAFILE) {
|
|
2071
|
-
await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
|
|
2105
|
+
await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
|
|
2072
2106
|
}
|
|
2073
|
-
const chunksDir = path.join(outDir, 'chunks')
|
|
2074
|
-
const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'))
|
|
2075
|
-
const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>()
|
|
2076
|
-
const renames = new Map<string, string>()
|
|
2107
|
+
const chunksDir = path.join(outDir, 'chunks')
|
|
2108
|
+
const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'))
|
|
2109
|
+
const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>()
|
|
2110
|
+
const renames = new Map<string, string>()
|
|
2077
2111
|
|
|
2078
2112
|
for (const file of files) {
|
|
2079
|
-
const basename = path.basename(file)
|
|
2080
|
-
if (!basename.startsWith('chunk-')) continue
|
|
2081
|
-
renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`)
|
|
2113
|
+
const basename = path.basename(file)
|
|
2114
|
+
if (!basename.startsWith('chunk-')) continue
|
|
2115
|
+
renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`)
|
|
2082
2116
|
}
|
|
2083
2117
|
|
|
2084
|
-
if (renames.size === 0) return renames
|
|
2118
|
+
if (renames.size === 0) return renames
|
|
2085
2119
|
|
|
2086
2120
|
await Promise.all(
|
|
2087
2121
|
[...renames].map(async ([from, to]) => {
|
|
2088
|
-
const fromPath = path.join(chunksDir, from)
|
|
2089
|
-
const toPath = path.join(chunksDir, to)
|
|
2090
|
-
if (from === to || !existsSync(fromPath)) return
|
|
2122
|
+
const fromPath = path.join(chunksDir, from)
|
|
2123
|
+
const toPath = path.join(chunksDir, to)
|
|
2124
|
+
if (from === to || !existsSync(fromPath)) return
|
|
2091
2125
|
try {
|
|
2092
|
-
await rename(fromPath, toPath)
|
|
2126
|
+
await rename(fromPath, toPath)
|
|
2093
2127
|
} catch (error) {
|
|
2094
2128
|
// Tolerate a concurrent build having already moved this chunk (ENOENT);
|
|
2095
2129
|
// the reference rewrite below still points at the renamed file.
|
|
2096
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2130
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2097
2131
|
}
|
|
2098
2132
|
// Move the external sourcemap alongside its chunk. The `sourceMappingURL`
|
|
2099
2133
|
// comment in the renamed chunk is rewritten below (replaceChunkNames matches
|
|
2100
2134
|
// the `chunk-<hash>.js` prefix inside `chunk-<hash>.js.map`), so the map file
|
|
2101
2135
|
// must adopt the new name to keep the link resolvable.
|
|
2102
|
-
const fromMap = `${fromPath}.map
|
|
2103
|
-
if (!existsSync(fromMap)) return
|
|
2136
|
+
const fromMap = `${fromPath}.map`
|
|
2137
|
+
if (!existsSync(fromMap)) return
|
|
2104
2138
|
try {
|
|
2105
|
-
await rename(fromMap, `${toPath}.map`)
|
|
2139
|
+
await rename(fromMap, `${toPath}.map`)
|
|
2106
2140
|
} catch (error) {
|
|
2107
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2141
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2108
2142
|
}
|
|
2109
2143
|
}),
|
|
2110
|
-
)
|
|
2144
|
+
)
|
|
2111
2145
|
|
|
2112
2146
|
// One read+rewrite per output file. An app with a route per page has hundreds
|
|
2113
2147
|
// of them, so they go out concurrently rather than one await at a time.
|
|
2114
|
-
const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'))
|
|
2148
|
+
const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'))
|
|
2115
2149
|
await Promise.all(
|
|
2116
2150
|
outputs.map(async file => {
|
|
2117
|
-
const source = await readText(file)
|
|
2118
|
-
const next = replaceChunkNames(source, renames)
|
|
2119
|
-
if (next !== source) await writeText(file, next)
|
|
2151
|
+
const source = await readText(file)
|
|
2152
|
+
const next = replaceChunkNames(source, renames)
|
|
2153
|
+
if (next !== source) await writeText(file, next)
|
|
2120
2154
|
}),
|
|
2121
|
-
)
|
|
2155
|
+
)
|
|
2122
2156
|
|
|
2123
|
-
return renames
|
|
2157
|
+
return renames
|
|
2124
2158
|
}
|
|
2125
2159
|
|
|
2126
2160
|
function hashSuffix(file: string) {
|
|
2127
|
-
return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
|
|
2161
|
+
return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
|
|
2128
2162
|
}
|
|
2129
2163
|
|
|
2130
2164
|
function chunkLabelsFromMetafile(metafile: Metafile) {
|
|
2131
|
-
const labels = new Map<string, string>()
|
|
2165
|
+
const labels = new Map<string, string>()
|
|
2132
2166
|
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
|
2133
|
-
const basename = path.basename(outputPath)
|
|
2134
|
-
if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue
|
|
2135
|
-
const weights = new Map<string, number>()
|
|
2167
|
+
const basename = path.basename(outputPath)
|
|
2168
|
+
if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue
|
|
2169
|
+
const weights = new Map<string, number>()
|
|
2136
2170
|
for (const [input, { bytesInOutput }] of Object.entries(output.inputs)) {
|
|
2137
|
-
const label = chunkInputLabel(input)
|
|
2138
|
-
if (!label) continue
|
|
2139
|
-
weights.set(label, (weights.get(label) ?? 0) + bytesInOutput)
|
|
2171
|
+
const label = chunkInputLabel(input)
|
|
2172
|
+
if (!label) continue
|
|
2173
|
+
weights.set(label, (weights.get(label) ?? 0) + bytesInOutput)
|
|
2140
2174
|
}
|
|
2141
|
-
const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
2142
|
-
if (top) labels.set(basename, top[0])
|
|
2175
|
+
const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
2176
|
+
if (top) labels.set(basename, top[0])
|
|
2143
2177
|
}
|
|
2144
|
-
return labels
|
|
2178
|
+
return labels
|
|
2145
2179
|
}
|
|
2146
2180
|
|
|
2147
2181
|
function chunkInputLabel(input: string) {
|
|
2148
|
-
const posix = input.split(path.sep).join('/')
|
|
2149
|
-
const packageIndex = posix.lastIndexOf('node_modules/')
|
|
2182
|
+
const posix = input.split(path.sep).join('/')
|
|
2183
|
+
const packageIndex = posix.lastIndexOf('node_modules/')
|
|
2150
2184
|
if (packageIndex !== -1) {
|
|
2151
|
-
const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/')
|
|
2152
|
-
const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0]
|
|
2153
|
-
return sanitizeChunkLabel(name ?? '')
|
|
2185
|
+
const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/')
|
|
2186
|
+
const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0]
|
|
2187
|
+
return sanitizeChunkLabel(name ?? '')
|
|
2154
2188
|
}
|
|
2155
2189
|
return sanitizeChunkLabel(
|
|
2156
2190
|
posix
|
|
2157
2191
|
.split('/')
|
|
2158
2192
|
.at(-1)
|
|
2159
2193
|
?.replace(/\.[^.]+$/, '') ?? '',
|
|
2160
|
-
)
|
|
2194
|
+
)
|
|
2161
2195
|
}
|
|
2162
2196
|
|
|
2163
2197
|
function sanitizeChunkLabel(label: string) {
|
|
@@ -2165,11 +2199,11 @@ function sanitizeChunkLabel(label: string) {
|
|
|
2165
2199
|
.replace(/^@/, '')
|
|
2166
2200
|
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
2167
2201
|
.replace(/^[-.]+|[-.]+$/g, '')
|
|
2168
|
-
.slice(0, 40)
|
|
2169
|
-
return clean || undefined
|
|
2202
|
+
.slice(0, 40)
|
|
2203
|
+
return clean || undefined
|
|
2170
2204
|
}
|
|
2171
2205
|
|
|
2172
2206
|
function replaceChunkNames(source: string, renames: Map<string, string>) {
|
|
2173
|
-
const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g')
|
|
2174
|
-
return source.replace(pattern, match => renames.get(match) ?? match)
|
|
2207
|
+
const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g')
|
|
2208
|
+
return source.replace(pattern, match => renames.get(match) ?? match)
|
|
2175
2209
|
}
|