@wular/pnext 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/pnext +1 -1
- package/config/lint/base.js +7 -7
- package/config/ts/base.json +2 -4
- package/config/ts/react.json +2 -6
- package/package.json +23 -2
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/css.md +13 -9
- package/reference/dev.md +1 -1
- package/reference/metadata.md +7 -7
- package/reference/navigation.md +35 -28
- package/reference/performance.md +70 -70
- package/reference/rendering.md +17 -17
- package/reference/routing.md +17 -17
- package/reference/typegen.md +12 -8
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +113 -109
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +49 -52
- package/src/api/navigation.ts +59 -58
- package/src/api/server.ts +152 -144
- package/src/api/suspense.ts +4 -4
- package/src/cli/adapters/vercel-warm.ts +126 -121
- package/src/cli/adapters/vercel.ts +437 -443
- package/src/cli/analyze.ts +333 -144
- package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
- package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
- package/src/cli/build.ts +976 -978
- package/src/cli/create.ts +71 -63
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +88 -85
- package/src/cli/migrate/package-json.ts +73 -71
- package/src/cli/migrate/report.ts +32 -33
- package/src/cli/migrate/{index.ts → run.ts} +48 -49
- package/src/cli/migrate/scan.ts +46 -46
- package/src/cli/migrate/spinner.ts +9 -9
- package/src/cli/migrate/tsconfig.ts +35 -35
- package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
- package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
- package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
- package/src/cli/start.ts +65 -66
- package/src/{typegen.ts → cli/typegen.ts} +59 -59
- package/src/client/build.ts +794 -768
- package/src/client/chunk-fold.ts +245 -240
- package/src/client/{paths.ts → chunk-name.ts} +6 -6
- package/src/client/entry.ts +162 -147
- package/src/client/prebuilt.ts +231 -223
- package/src/client/profile.ts +29 -29
- package/src/client/react-compiler.ts +20 -15
- package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
- package/src/client/reference-stub.ts +51 -51
- package/src/client/reference.ts +19 -19
- package/src/{api → client}/router/events.ts +17 -17
- package/src/{api → client}/router/history.ts +16 -16
- package/src/{api → client}/router/hub.ts +52 -53
- package/src/{api/router.ts → client/router/index.ts} +61 -60
- package/src/{api → client}/router/policies.ts +24 -24
- package/src/{api → client}/router/runtime.ts +1988 -1992
- package/src/{api → client}/router/types.ts +93 -98
- package/src/compat/actions/client-plugin.ts +42 -43
- package/src/compat/actions/client-stub.ts +14 -14
- package/src/compat/actions/{action-client.ts → client.ts} +194 -193
- package/src/compat/actions/config.ts +63 -64
- package/src/compat/actions/detect.ts +68 -68
- package/src/compat/actions/discovery.ts +105 -105
- package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
- package/src/compat/actions/early-submit.ts +1 -1
- package/src/compat/actions/endpoint.ts +198 -198
- package/src/compat/actions/extensions.ts +811 -0
- package/src/compat/actions/flight.ts +23 -23
- package/src/compat/actions/form-state.ts +34 -34
- package/src/compat/actions/hoist.ts +382 -236
- package/src/compat/actions/ids.ts +7 -12
- package/src/compat/actions/index.ts +8 -8
- package/src/compat/actions/instances.ts +46 -46
- package/src/compat/actions/origin.ts +47 -48
- package/src/compat/actions/protocol.ts +22 -22
- package/src/compat/actions/registry.ts +19 -19
- package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
- package/src/compat/actions/rewrite.ts +125 -125
- package/src/compat/actions/{action-router.ts → router.ts} +10 -10
- package/src/compat/actions/serve.ts +132 -136
- package/src/compat/actions/server-tag.ts +4 -4
- package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
- package/src/compat/actions/unrecognized-error.ts +4 -4
- package/src/compat/{index.ts → aliases.ts} +112 -109
- package/src/compat/bundler/bun-externals.ts +18 -18
- package/src/compat/bundler/cjs-exports.ts +271 -223
- package/src/compat/bundler/config.ts +116 -111
- package/src/compat/bundler/externals.ts +12 -12
- package/src/compat/bundler/import-meta-url.ts +19 -19
- package/src/compat/bundler/modularize-imports.ts +36 -33
- package/src/compat/bundler/new-url-asset.ts +22 -24
- package/src/compat/bundler/optimize-package-imports.ts +111 -107
- package/src/compat/bundler/polyfill.ts +28 -28
- package/src/compat/bundler/react-compiler.ts +35 -29
- package/src/compat/bundler/react-profiler.tsx +11 -11
- package/src/compat/bundler/relay-transform.ts +48 -47
- package/src/compat/bundler/require-context.ts +119 -113
- package/src/compat/bundler/resolve-extensions.ts +19 -19
- package/src/compat/bundler/source-cache.ts +25 -25
- package/src/compat/bundler/static-imports.ts +7 -7
- package/src/compat/bundler/symlink-imports.ts +46 -46
- package/src/compat/bundler/tsconfig-paths.ts +13 -15
- package/src/compat/bundler/wasm.ts +58 -60
- package/src/compat/bundler/webpack-loaders.ts +254 -241
- package/src/compat/bundler/worker.ts +101 -104
- package/src/compat/cache/build-flags.ts +29 -29
- package/src/compat/cache/build-prerender-errors.ts +40 -44
- package/src/compat/cache/custom-handler.ts +60 -53
- package/src/compat/cache/fetch-patch.ts +240 -240
- package/src/compat/cache/handler.ts +27 -31
- package/src/compat/cache/modern-handler.ts +148 -126
- package/src/compat/cache/resume-data-cache.ts +47 -45
- package/src/compat/cache/revalidate.ts +233 -232
- package/src/compat/cache/runtime-error.ts +35 -35
- package/src/compat/cache/use-cache-transform.ts +442 -417
- package/src/compat/cache/use-cache.ts +650 -614
- package/src/compat/cache-control.ts +140 -142
- package/src/compat/client/base-path.ts +21 -20
- package/src/compat/client/css-order.ts +18 -18
- package/src/compat/client/errors/bare-boundary.ts +11 -11
- package/src/compat/client/errors/control-flow.ts +23 -23
- package/src/compat/client/errors/error-boundary.ts +61 -61
- package/src/compat/client/errors/global-error.ts +101 -91
- package/src/compat/client/errors/install.ts +71 -72
- package/src/compat/client/errors/lazy.ts +23 -23
- package/src/compat/client/errors/primitive-throw.ts +47 -45
- package/src/compat/client/errors/soft-refresh.ts +4 -4
- package/src/compat/client/link-status.ts +33 -33
- package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
- package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
- package/src/compat/client/navigation-scroll.ts +63 -63
- package/src/compat/client/optimistic-routing.ts +93 -88
- package/src/compat/client/prefetch-cache.ts +23 -24
- package/src/compat/client/route-announcer.ts +35 -35
- package/src/compat/client/segment-cache-policy.ts +20 -20
- package/src/compat/client/segment-cache.ts +309 -315
- package/src/compat/client/segment-prefetch.ts +127 -132
- package/src/compat/client/trailing-slash.ts +5 -4
- package/src/compat/css/chunking.ts +113 -116
- package/src/compat/css/inline-css.ts +21 -21
- package/src/compat/css/lightningcss.ts +37 -38
- package/src/compat/css/modules.ts +161 -175
- package/src/compat/css/nonce.ts +7 -7
- package/src/compat/css/sass-plugin.ts +18 -21
- package/src/compat/css/sass.ts +150 -152
- package/src/compat/css/styled-jsx-runtime.ts +27 -27
- package/src/compat/css/styled-jsx.ts +21 -21
- package/src/compat/edge-runtime.ts +27 -27
- package/src/compat/{adapter → export}/build-complete.ts +72 -75
- package/src/compat/export/client.ts +29 -31
- package/src/compat/export/{index.ts → emit.ts} +111 -110
- package/src/compat/export/standalone.ts +62 -54
- package/src/compat/image-optimizer/cache.ts +51 -49
- package/src/compat/image-optimizer/detect.ts +52 -52
- package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
- package/src/compat/image-optimizer/source.ts +77 -80
- package/src/compat/lifecycle/after-scope.ts +26 -26
- package/src/compat/lifecycle/after.ts +48 -45
- package/src/compat/lifecycle/error-funnel.ts +98 -102
- package/src/compat/lifecycle/error-serialize.ts +30 -26
- package/src/compat/lifecycle/error-ui.ts +67 -32
- package/src/compat/lifecycle/instrumentation-client.ts +36 -38
- package/src/compat/lifecycle/instrumentation.ts +85 -85
- package/src/compat/lifecycle/node-console.ts +11 -11
- package/src/compat/lifecycle/testmode.ts +160 -132
- package/src/compat/mdx/compile.ts +60 -57
- package/src/compat/mdx/plugin.ts +11 -11
- package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
- package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
- package/src/compat/metadata.ts +75 -81
- package/src/compat/next/cache.ts +68 -69
- package/src/compat/next/canonical-url.ts +9 -9
- package/src/compat/next/client-cache.ts +19 -19
- package/src/compat/next/client-navigation.ts +112 -116
- package/src/compat/next/client-only.ts +1 -1
- package/src/compat/next/client-script.tsx +99 -93
- package/src/compat/next/client-server.ts +10 -10
- package/src/compat/next/config-loader.ts +176 -173
- package/src/compat/next/config.ts +7 -7
- package/src/compat/next/constants.cjs +6 -6
- package/src/compat/next/constants.ts +6 -6
- package/src/compat/next/custom-server.ts +26 -24
- package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
- package/src/compat/next/dynamic.tsx +21 -18
- package/src/compat/next/error.tsx +52 -52
- package/src/compat/next/font/cache.ts +74 -74
- package/src/compat/next/font/google.ts +2 -2
- package/src/compat/next/font/index.ts +1 -1
- package/src/compat/next/font/local.ts +3 -3
- package/src/compat/next/font/runtime-client.ts +17 -15
- package/src/compat/next/font/runtime.ts +443 -408
- package/src/compat/next/font/shared.ts +120 -108
- package/src/compat/next/form.tsx +63 -63
- package/src/compat/next/head.tsx +2 -2
- package/src/compat/next/headers.ts +103 -95
- package/src/compat/next/image/client.tsx +220 -0
- package/src/compat/next/image/config.ts +56 -58
- package/src/compat/next/image/optimizer.ts +40 -36
- package/src/compat/next/image/patterns.ts +37 -40
- package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
- package/src/compat/next/image/shared.ts +65 -57
- package/src/compat/next/image/static-metadata.ts +98 -107
- package/src/compat/next/image/validate.ts +79 -88
- package/src/compat/next/image.tsx +19 -23
- package/src/compat/next/index.ts +1 -1
- package/src/compat/next/legacy-image.tsx +59 -60
- package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
- package/src/compat/next/link.tsx +158 -158
- package/src/compat/next/navigation.cjs +12 -3
- package/src/compat/next/navigation.ts +48 -50
- package/src/compat/next/offline.ts +27 -27
- package/src/compat/next/og.ts +121 -124
- package/src/compat/next/preferred-region.ts +13 -14
- package/src/compat/next/redirects.ts +58 -56
- package/src/compat/next/resource-hints.ts +73 -76
- package/src/compat/next/rewrites.ts +130 -133
- package/src/compat/next/root-params.ts +45 -45
- package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
- package/src/compat/next/router.cjs +4 -2
- package/src/compat/next/router.ts +58 -61
- package/src/compat/next/script.tsx +108 -108
- package/src/compat/next/server-only.ts +1 -1
- package/src/compat/next/server.ts +19 -19
- package/src/compat/next/svgr.ts +18 -17
- package/src/compat/next/telemetry.ts +24 -24
- package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
- package/src/compat/next/user-agent.ts +53 -49
- package/src/compat/next/web-vitals.ts +22 -24
- package/src/compat/otel/api.ts +41 -41
- package/src/compat/otel/client-trace-metadata.ts +25 -27
- package/src/compat/otel/fetch-span.ts +29 -29
- package/src/compat/otel/tracer.ts +331 -331
- package/src/compat/pages/api.ts +456 -0
- package/src/compat/pages/client-plugin.ts +36 -36
- package/src/compat/pages/router-state.ts +34 -34
- package/src/compat/pages/{index.ts → router.ts} +130 -135
- package/src/compat/ppr/io.ts +12 -12
- package/src/compat/ppr/missing-root-params.ts +34 -38
- package/src/compat/ppr/root-params-scan.ts +66 -66
- package/src/compat/ppr/root-params-transform.ts +24 -26
- package/src/compat/ppr/root-params.ts +30 -30
- package/src/compat/ppr/segment-config-incompat.ts +6 -7
- package/src/compat/protocol.ts +71 -70
- package/src/compat/react/action-state.ts +47 -48
- package/src/compat/react/client-lite.ts +15 -15
- package/src/compat/react/client.ts +4 -4
- package/src/compat/react/compiler-runtime.ts +11 -11
- package/src/compat/react/dom-client.ts +44 -44
- package/src/compat/react/dom-react-server.ts +10 -16
- package/src/compat/react/dom-server.ts +10 -10
- package/src/compat/react/dom.ts +52 -52
- package/src/compat/react/hooks-extra.ts +34 -35
- package/src/compat/react/parity.ts +64 -61
- package/src/compat/react/preact.ts +81 -82
- package/src/compat/react/react-server.ts +28 -28
- package/src/compat/react/router-shim.ts +1 -1
- package/src/compat/react/server-component-use.ts +8 -8
- package/src/compat/react/server-inserted-html.ts +30 -31
- package/src/compat/react/server.ts +53 -55
- package/src/compat/react/use.ts +32 -32
- package/src/compat/react/view-transition.ts +20 -20
- package/src/compat/register/actions.ts +35 -824
- package/src/compat/register/boot.ts +41 -41
- package/src/compat/register/build-tier.ts +5 -5
- package/src/compat/register/build.ts +74 -70
- package/src/compat/register/bundler.ts +161 -156
- package/src/compat/register/cache.ts +20 -20
- package/src/compat/register/client-errors.ts +3 -3
- package/src/compat/register/config.ts +6 -6
- package/src/compat/register/css-extras.ts +30 -34
- package/src/compat/register/edge-runtime.ts +3 -3
- package/src/compat/register/errors.ts +10 -12
- package/src/compat/register/export.ts +16 -16
- package/src/compat/register/font.ts +11 -11
- package/src/compat/register/hooks.ts +2 -2
- package/src/compat/register/image.ts +40 -40
- package/src/compat/register/index.ts +59 -62
- package/src/compat/register/instrumentation-client.ts +10 -10
- package/src/compat/register/lifecycle.ts +25 -28
- package/src/compat/register/mdx.ts +10 -10
- package/src/compat/register/middleware.ts +226 -16
- package/src/compat/register/otel.ts +80 -88
- package/src/compat/register/pages-api.ts +10 -463
- package/src/compat/register/ppr.ts +16 -16
- package/src/compat/register/protocol.ts +17 -18
- package/src/compat/register/proxy.ts +49 -51
- package/src/compat/register/render.ts +79 -76
- package/src/compat/register/routing.ts +162 -159
- package/src/compat/register/segment.ts +24 -1897
- package/src/compat/register/static-image.ts +3 -3
- package/src/compat/register/{misc.ts → taint.ts} +9 -9
- package/src/compat/register/typed-routes.ts +14 -14
- package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
- package/src/compat/register/validation.ts +18 -18
- package/src/compat/segment/loading-boundary.ts +43 -45
- package/src/compat/segment/page-slot.ts +69 -69
- package/src/compat/segment/serve.ts +1884 -0
- package/src/compat/segment/tree.ts +113 -112
- package/src/compat/segment/vary-key.ts +38 -38
- package/src/compat/segment/vary-params.ts +138 -142
- package/src/compat/static-params.ts +14 -12
- package/src/compat/tsconfig-defaults.ts +87 -91
- package/src/compat/typecheck/{index.ts → check.ts} +234 -212
- package/src/compat/typecheck/worker.ts +15 -12
- package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
- package/src/compat/typed-routes/manifest.ts +174 -170
- package/src/compat/typed-routes/typegen.ts +127 -110
- package/src/compat/validation/errors.ts +16 -19
- package/src/compat/validation/prerender-diagnostics.ts +521 -504
- package/src/compat/validation/{index.ts → validate.ts} +647 -648
- package/src/compat-bootstrap.ts +16 -16
- package/src/config.ts +69 -69
- package/src/css/build.ts +230 -227
- package/src/css/postcss.ts +79 -80
- package/src/css/worker.ts +14 -15
- package/src/dev/client-actions.ts +10 -10
- package/src/dev/client-chunk-store.ts +27 -27
- package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
- package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
- package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
- package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
- package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
- package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
- package/src/dev/server.ts +804 -820
- package/src/env.ts +46 -43
- package/src/extensions.ts +491 -478
- package/src/index.ts +8 -8
- package/src/internal.ts +20 -23
- package/src/{islands → render}/boundary-error.ts +3 -3
- package/src/render/hooks.ts +71 -71
- package/src/render/island-context.ts +14 -14
- package/src/render/metadata.ts +310 -310
- package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
- package/src/{ppr.ts → render/ppr.ts} +244 -245
- package/src/render/renderer.ts +2076 -2082
- package/src/render/resource-hints.ts +16 -17
- package/src/render/slots.tsx +224 -235
- package/src/{islands → render}/static-children.ts +9 -12
- package/src/{islands → render}/static-slots.ts +37 -37
- package/src/{cache/context.ts → request/cache.ts} +20 -20
- package/src/request/context.ts +107 -107
- package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
- package/src/resolve/engine.ts +90 -77
- package/src/resolve/imports.ts +475 -463
- package/src/resolve/scan-facts.ts +318 -296
- package/src/resolve/source-text.ts +37 -37
- package/src/{dynamic → resolve}/tree-shake.ts +132 -128
- package/src/routing/forwarded.ts +19 -19
- package/src/routing/handler.ts +84 -91
- package/src/routing/href.ts +69 -70
- package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
- package/src/{proxy.ts → routing/proxy.ts} +306 -312
- package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
- package/src/routing/routes.ts +827 -815
- package/src/routing/slots.ts +164 -160
- package/src/runtime/loader.ts +952 -0
- package/src/{dev → runtime}/module-cache.ts +309 -287
- package/src/{dev → runtime}/module-generations.ts +9 -9
- package/src/{dev → runtime}/module-transform.ts +81 -72
- package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
- package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
- package/src/runtime/vendor.ts +425 -404
- package/src/styles.d.ts +9 -0
- package/src/types.ts +320 -335
- package/src/utils/ansi.ts +5 -5
- package/src/utils/{source.ts → code.ts} +15 -12
- package/src/utils/content-type.ts +3 -3
- package/src/utils/decode.ts +2 -2
- package/src/utils/dev-profile.ts +13 -13
- package/src/utils/error-log.ts +6 -6
- package/src/utils/esbuild.ts +18 -18
- package/src/utils/fs-cache.ts +13 -13
- package/src/utils/fs.ts +57 -49
- package/src/utils/html.ts +20 -24
- package/src/utils/native-require.ts +8 -8
- package/src/utils/serialize.ts +139 -146
- package/src/utils/verbose.ts +18 -18
- package/src/cli/analyze-print.ts +0 -181
- package/src/compat/middleware/manifest.ts +0 -210
- package/src/compat/next/image-client.tsx +0 -215
- package/src/compat/next/link-usage.ts +0 -29
- package/src/css/index.ts +0 -2
- package/src/render/index.ts +0 -1
- package/src/style-modules.d.ts +0 -9
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 { 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,58 @@ 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
|
-
const aliases = { ...getImportAliasExtensions().aliases(config, 'client') }
|
|
1223
|
+
const aliases = { ...getImportAliasExtensions().aliases(config, 'client') }
|
|
1193
1224
|
// Suspense-free tier: the app's `react` imports resolve to the compat-free lite shim, so the
|
|
1194
1225
|
// bundle ships preact core + hooks without preact/compat (see clientSuspenseFree).
|
|
1195
1226
|
if (reactLite && aliases.react) {
|
|
1196
|
-
aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
|
|
1227
|
+
aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
|
|
1197
1228
|
}
|
|
1198
|
-
const specifiers = Object.keys(aliases)
|
|
1229
|
+
const specifiers = Object.keys(aliases)
|
|
1199
1230
|
return {
|
|
1200
1231
|
name: 'pnext-import-alias',
|
|
1201
1232
|
setup(build) {
|
|
1202
1233
|
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
|
|
1234
|
+
if (aliases[args.path]) return undefined
|
|
1235
|
+
const message = getImportAliasExtensions().missingImportError(config, args.path)
|
|
1236
|
+
return message ? { errors: [{ text: message }] } : undefined
|
|
1237
|
+
})
|
|
1238
|
+
if (specifiers.length === 0) return
|
|
1208
1239
|
build.onResolve(
|
|
1209
1240
|
{ filter: new RegExp(`^(${specifiers.map(escapeRegex).join('|')})$`) },
|
|
1210
1241
|
async args => {
|
|
1211
|
-
const target = aliases[args.path]
|
|
1212
|
-
if (!target) return undefined
|
|
1213
|
-
if (path.isAbsolute(target)) return { path: target }
|
|
1242
|
+
const target = aliases[args.path]
|
|
1243
|
+
if (!target) return undefined
|
|
1244
|
+
if (path.isAbsolute(target)) return { path: target }
|
|
1214
1245
|
return build.resolve(target, {
|
|
1215
1246
|
kind: args.kind,
|
|
1216
1247
|
importer: args.importer,
|
|
1217
1248
|
namespace: args.namespace,
|
|
1218
1249
|
resolveDir: args.resolveDir,
|
|
1219
|
-
})
|
|
1250
|
+
})
|
|
1220
1251
|
},
|
|
1221
|
-
)
|
|
1252
|
+
)
|
|
1222
1253
|
},
|
|
1223
|
-
}
|
|
1254
|
+
}
|
|
1224
1255
|
}
|
|
1225
1256
|
|
|
1226
1257
|
// preact core + preact/compat + react/react-dom/next are single-instance: they must ALWAYS resolve
|
|
@@ -1230,11 +1261,11 @@ function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
|
|
|
1230
1261
|
// `preact` to the linked copy while pnext-internal compat code keeps resolving to the alias -
|
|
1231
1262
|
// shipping TWO physical preact cores. That breaks single-instance option hooks, context identity and
|
|
1232
1263
|
// error interception. Never let the linked-package resolver shadow these; the alias owns them.
|
|
1233
|
-
const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)
|
|
1264
|
+
const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/
|
|
1234
1265
|
|
|
1235
1266
|
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')
|
|
1267
|
+
const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts')
|
|
1268
|
+
const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts')
|
|
1238
1269
|
return {
|
|
1239
1270
|
name: 'pnext-linked-package-client-resolve',
|
|
1240
1271
|
setup(build) {
|
|
@@ -1243,36 +1274,36 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
|
|
|
1243
1274
|
// outputs under node_modules paths, e.g. @next/third-parties client
|
|
1244
1275
|
// components) where no sibling ./preact exists; only a real sibling
|
|
1245
1276
|
// file (an app's own ./preact.*) opts out.
|
|
1246
|
-
if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
|
|
1277
|
+
if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
|
|
1247
1278
|
const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
|
|
1248
1279
|
existsSync(path.join(args.resolveDir, `preact${ext}`)),
|
|
1249
|
-
)
|
|
1250
|
-
if (hasRealSibling) return undefined
|
|
1251
|
-
return { path: compatPreact }
|
|
1252
|
-
})
|
|
1280
|
+
)
|
|
1281
|
+
if (hasRealSibling) return undefined
|
|
1282
|
+
return { path: compatPreact }
|
|
1283
|
+
})
|
|
1253
1284
|
// One build's worth of memo, keyed on exactly what esbuild hands us: the resolver walks
|
|
1254
1285
|
// node_modules and probes package targets per call, and a directory of components importing
|
|
1255
1286
|
// the same few packages asks the same question repeatedly. Per-BUILD, so a linked package's
|
|
1256
1287
|
// layout changing between builds is still seen - esbuild already assumes it is fixed within one.
|
|
1257
|
-
const linked = new Map<string, string | undefined>()
|
|
1288
|
+
const linked = new Map<string, string | undefined>()
|
|
1258
1289
|
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)
|
|
1290
|
+
if (frameworkAliasedSpecifier.test(args.path)) return undefined
|
|
1291
|
+
const dir = args.resolveDir || config.root
|
|
1292
|
+
const key = `${dir}\0${args.path}`
|
|
1293
|
+
let resolved = linked.get(key)
|
|
1263
1294
|
if (resolved === undefined && !linked.has(key)) {
|
|
1264
1295
|
resolved = resolveLinkedPackageSpecifier(
|
|
1265
1296
|
config.root,
|
|
1266
1297
|
path.join(dir, 'pnext-resolve.ts'),
|
|
1267
1298
|
args.path,
|
|
1268
1299
|
['browser', 'style', 'import', 'default'],
|
|
1269
|
-
)
|
|
1270
|
-
linked.set(key, resolved)
|
|
1300
|
+
)
|
|
1301
|
+
linked.set(key, resolved)
|
|
1271
1302
|
}
|
|
1272
|
-
return resolved ? { path: resolved } : undefined
|
|
1273
|
-
})
|
|
1303
|
+
return resolved ? { path: resolved } : undefined
|
|
1304
|
+
})
|
|
1274
1305
|
},
|
|
1275
|
-
}
|
|
1306
|
+
}
|
|
1276
1307
|
}
|
|
1277
1308
|
|
|
1278
1309
|
// Mirror of the server build's static-asset seam for the CLIENT bundle. A static image import from a
|
|
@@ -1280,7 +1311,7 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
|
|
|
1280
1311
|
// object - with the asset emitted under `/_next/static/media/...` - rather than a bare `file` loader
|
|
1281
1312
|
// URL string. Uses core's `staticAssetModule` extension seam so compat (next/image) supplies the
|
|
1282
1313
|
// descriptor and pure-core apps fall back to the generic URL module.
|
|
1283
|
-
const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])
|
|
1314
|
+
const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/
|
|
1284
1315
|
|
|
1285
1316
|
function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
|
|
1286
1317
|
return {
|
|
@@ -1291,66 +1322,66 @@ function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
|
|
|
1291
1322
|
// `*.svg`) preempts the generic static-image resolver: defer so the
|
|
1292
1323
|
// compat loader-rule plugin runs the chain instead.
|
|
1293
1324
|
if (getAssetExtensions().hasLoaderRuleFor(args.path)) {
|
|
1294
|
-
return undefined
|
|
1325
|
+
return undefined
|
|
1295
1326
|
}
|
|
1296
1327
|
return {
|
|
1297
1328
|
path: resolveAssetPath(args.path, args.resolveDir),
|
|
1298
1329
|
namespace: 'pnext-client-static-asset',
|
|
1299
|
-
}
|
|
1300
|
-
})
|
|
1330
|
+
}
|
|
1331
|
+
})
|
|
1301
1332
|
build.onLoad({ filter: /.*/, namespace: 'pnext-client-static-asset' }, async args => ({
|
|
1302
1333
|
contents: await staticImageModuleSource(config, args.path),
|
|
1303
1334
|
loader: 'js',
|
|
1304
|
-
}))
|
|
1335
|
+
}))
|
|
1305
1336
|
},
|
|
1306
|
-
}
|
|
1337
|
+
}
|
|
1307
1338
|
}
|
|
1308
1339
|
|
|
1309
1340
|
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}
|
|
1341
|
+
const { sourcePath, hash } = splitAssetHash(specifier)
|
|
1342
|
+
const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)
|
|
1343
|
+
return `${resolved}${hash}`
|
|
1313
1344
|
}
|
|
1314
1345
|
|
|
1315
1346
|
function splitAssetHash(specifier: string) {
|
|
1316
|
-
const index = specifier.search(/[?#]/)
|
|
1347
|
+
const index = specifier.search(/[?#]/)
|
|
1317
1348
|
return index === -1
|
|
1318
1349
|
? { sourcePath: specifier, hash: '' }
|
|
1319
|
-
: { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
|
|
1350
|
+
: { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
|
|
1320
1351
|
}
|
|
1321
1352
|
|
|
1322
1353
|
async function staticImageModuleSource(config: ResolvedConfig, file: string) {
|
|
1323
|
-
const { sourcePath } = splitAssetHash(file)
|
|
1324
|
-
const bytes = new Uint8Array(await readFile(sourcePath))
|
|
1325
|
-
const emitted: string[] = []
|
|
1354
|
+
const { sourcePath } = splitAssetHash(file)
|
|
1355
|
+
const bytes = new Uint8Array(await readFile(sourcePath))
|
|
1356
|
+
const emitted: string[] = []
|
|
1326
1357
|
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)
|
|
1358
|
+
emitted.push(relative)
|
|
1359
|
+
return `/${relative}`
|
|
1360
|
+
}
|
|
1361
|
+
const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
|
|
1362
|
+
const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
|
|
1332
1363
|
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)
|
|
1364
|
+
const target = path.join(config.outPath, 'public', ...relative.split('/'))
|
|
1365
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
1366
|
+
if (!existsSync(target)) await copyFile(sourcePath, target)
|
|
1336
1367
|
}
|
|
1337
|
-
return source
|
|
1368
|
+
return source
|
|
1338
1369
|
}
|
|
1339
1370
|
|
|
1340
1371
|
// Core's generic static-asset module (no compat override): emit under a hashed
|
|
1341
1372
|
// `/_next/static/media` URL and export the URL string as default. Mirrors the
|
|
1342
|
-
// identically named helper in runtime/
|
|
1373
|
+
// identically named helper in runtime/loader.ts + runtime/modules.ts.
|
|
1343
1374
|
function coreStaticAssetModule(
|
|
1344
1375
|
sourcePath: string,
|
|
1345
1376
|
bytes: Uint8Array,
|
|
1346
1377
|
emit: (relative: string) => string,
|
|
1347
1378
|
): 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
|
|
1379
|
+
const ext = path.extname(sourcePath).toLowerCase() || '.bin'
|
|
1380
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8)
|
|
1381
|
+
const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
1382
|
+
const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`)
|
|
1383
|
+
const src = emit(relative)
|
|
1384
|
+
return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`
|
|
1354
1385
|
}
|
|
1355
1386
|
|
|
1356
1387
|
/**
|
|
@@ -1366,91 +1397,93 @@ function coreStaticAssetModule(
|
|
|
1366
1397
|
* loader; scoped to the source roots, so third-party node_modules .js keeps esbuild's default loader.
|
|
1367
1398
|
*/
|
|
1368
1399
|
function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
1369
|
-
const sourceRewrite = nextCompatEnabled(config)
|
|
1370
|
-
const compiler = getCompatModeExtensions().reactCompilerOptions(config)
|
|
1371
|
-
const asyncPre = hasClientSourceAsyncPreTransforms()
|
|
1400
|
+
const sourceRewrite = nextCompatEnabled(config)
|
|
1401
|
+
const compiler = getCompatModeExtensions().reactCompilerOptions(config)
|
|
1402
|
+
const asyncPre = hasClientSourceAsyncPreTransforms()
|
|
1372
1403
|
// Under a source root, and under a root only first-party source plus transpiled packages - said in
|
|
1373
1404
|
// the FILTER, not the callback: a dependency's files are the bulk of a real graph, so a callback
|
|
1374
1405
|
// would decline most of them one at a time.
|
|
1375
|
-
const roots = clientSourceRoots(config)
|
|
1376
|
-
|
|
1406
|
+
const roots = clientSourceRoots(config)
|
|
1407
|
+
.map(root => escapeRegex(root))
|
|
1408
|
+
.join('|')
|
|
1409
|
+
const sep = escapeRegex(path.sep)
|
|
1377
1410
|
const filter = new RegExp(
|
|
1378
1411
|
`^(?:${roots})(?:${sep}${nonSegmentPattern('node_modules', sep)})*${sep}[^${sep}]*\\.(?:[cm]?[jt]sx?)$`,
|
|
1379
|
-
)
|
|
1380
|
-
const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
|
|
1412
|
+
)
|
|
1413
|
+
const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
|
|
1381
1414
|
// One in-flight pipeline per file for the whole build: `warm()` and the
|
|
1382
1415
|
// onLoad hook share it, so a warmed file is already done (or in flight on
|
|
1383
1416
|
// oxc's threadpool) by the time esbuild asks for it, and a component shared
|
|
1384
1417
|
// by twenty routes is prepared once.
|
|
1385
|
-
const prepared = new Map<string, Promise<OnLoadOutput>>()
|
|
1418
|
+
const prepared = new Map<string, Promise<OnLoadOutput>>()
|
|
1386
1419
|
// Deferred dynamic refs THIS pipeline rewrote — persisted as the out-dir sidecar.
|
|
1387
|
-
const deferredRefs = new Map<string, DeferredDynamicRef>()
|
|
1420
|
+
const deferredRefs = new Map<string, DeferredDynamicRef>()
|
|
1388
1421
|
// The text esbuild actually parsed, kept so the prebuilt seam can read a
|
|
1389
1422
|
// file's framework imports off the POST-transform source — a compat rewrite
|
|
1390
1423
|
// can add or rename one, and the seam must offer exactly what is there.
|
|
1391
|
-
const loaded = new Map<string, string>()
|
|
1424
|
+
const loaded = new Map<string, string>()
|
|
1392
1425
|
|
|
1393
1426
|
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
|
|
1427
|
+
const existing = prepared.get(resolved)
|
|
1428
|
+
if (existing) return existing
|
|
1429
|
+
const pending = runPipeline(resolved)
|
|
1430
|
+
prepared.set(resolved, pending)
|
|
1431
|
+
return pending
|
|
1399
1432
|
}
|
|
1400
1433
|
|
|
1401
1434
|
async function runPipeline(resolved: string): Promise<OnLoadOutput> {
|
|
1402
|
-
const inNodeModules = resolved.split(path.sep).includes('node_modules')
|
|
1435
|
+
const inNodeModules = resolved.split(path.sep).includes('node_modules')
|
|
1403
1436
|
// A nested node_modules under a source root is a dependency, not app
|
|
1404
1437
|
// source: only an explicitly transpiled package opts into the rewrites,
|
|
1405
1438
|
// and none of them are ever React-Compiled.
|
|
1406
|
-
if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
|
|
1407
|
-
clientProfile.count('loadFiles')
|
|
1439
|
+
if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
|
|
1440
|
+
clientProfile.count('loadFiles')
|
|
1408
1441
|
// Build-scoped cache: the route-fact walk has already read most of these,
|
|
1409
1442
|
// 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
|
|
1443
|
+
const original = await clientProfile.timeAsync('read', () => readSourceText(resolved))
|
|
1444
|
+
let contents = original
|
|
1412
1445
|
// Worker bundling runs first and awaits: it consumes `import.meta.url`,
|
|
1413
1446
|
// which the sync chain inlines.
|
|
1414
1447
|
if (asyncPre) {
|
|
1415
|
-
contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
|
|
1448
|
+
contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
|
|
1416
1449
|
}
|
|
1417
1450
|
contents = clientProfile.time('chain', () =>
|
|
1418
1451
|
applyClientSourceTransforms(contents, resolved, config.root),
|
|
1419
|
-
)
|
|
1420
|
-
contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
|
|
1452
|
+
)
|
|
1453
|
+
contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
|
|
1421
1454
|
if (dev && devDynamicSplitEnabled()) {
|
|
1422
1455
|
contents = rewriteDeferredDynamicImports(
|
|
1423
1456
|
contents,
|
|
1424
1457
|
resolved,
|
|
1425
1458
|
specifier => resolveImport(rootFromFile(resolved), resolved, specifier),
|
|
1426
1459
|
target => {
|
|
1427
|
-
const id = clientReferenceId(target.file, target.exportName)
|
|
1428
|
-
registerDeferredDynamicRef(id, target)
|
|
1429
|
-
deferredRefs.set(id, target)
|
|
1430
|
-
return deferredDynamicChunkHref({ id })
|
|
1460
|
+
const id = clientReferenceId(target.file, target.exportName)
|
|
1461
|
+
registerDeferredDynamicRef(id, target)
|
|
1462
|
+
deferredRefs.set(id, target)
|
|
1463
|
+
return deferredDynamicChunkHref({ id })
|
|
1431
1464
|
},
|
|
1432
|
-
)
|
|
1465
|
+
)
|
|
1433
1466
|
}
|
|
1434
1467
|
if (compiler && !inNodeModules && shouldReactCompile(contents, resolved)) {
|
|
1435
1468
|
contents = await clientProfile.timeAsync('reactCompiler', () =>
|
|
1436
1469
|
reactCompiled(resolved, contents, compiler),
|
|
1437
|
-
)
|
|
1470
|
+
)
|
|
1438
1471
|
}
|
|
1439
1472
|
// Unchanged sources go back to esbuild's native loader; only .js/.mjs
|
|
1440
1473
|
// 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) }
|
|
1474
|
+
loaded.set(resolved, contents)
|
|
1475
|
+
if (contents === original && !/\.m?js$/.test(resolved)) return undefined
|
|
1476
|
+
return { contents, loader: clientSourceLoader(resolved) }
|
|
1444
1477
|
}
|
|
1445
1478
|
|
|
1446
1479
|
return {
|
|
1447
1480
|
/** The parsed text of a file this build has loaded, if it has. */
|
|
1448
1481
|
sourceOf(file: string) {
|
|
1449
|
-
return loaded.get(file)
|
|
1482
|
+
return loaded.get(file)
|
|
1450
1483
|
},
|
|
1451
1484
|
/** Deferred dynamic refs rewritten during this build (dev split). */
|
|
1452
1485
|
deferredRefs() {
|
|
1453
|
-
return deferredRefs
|
|
1486
|
+
return deferredRefs
|
|
1454
1487
|
},
|
|
1455
1488
|
/**
|
|
1456
1489
|
* Start the pipeline for sources the route table already names, before esbuild runs. Without it
|
|
@@ -1459,15 +1492,15 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
|
1459
1492
|
*/
|
|
1460
1493
|
/** `warm` for whole routes - every client source the route table names. */
|
|
1461
1494
|
warmRoutes(routes: RouteManifestEntry[]) {
|
|
1462
|
-
this.warm(routeClientSources(routes))
|
|
1495
|
+
this.warm(routeClientSources(routes))
|
|
1463
1496
|
},
|
|
1464
1497
|
warm(files: Iterable<string>) {
|
|
1465
1498
|
for (const file of files) {
|
|
1466
|
-
const resolved = path.resolve(file)
|
|
1467
|
-
if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
|
|
1499
|
+
const resolved = path.resolve(file)
|
|
1500
|
+
if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
|
|
1468
1501
|
// Errors surface at the onLoad await, where esbuild attributes them to
|
|
1469
1502
|
// the importing module; nothing must reject on the warm path.
|
|
1470
|
-
prepare(resolved).catch(() => undefined)
|
|
1503
|
+
prepare(resolved).catch(() => undefined)
|
|
1471
1504
|
}
|
|
1472
1505
|
},
|
|
1473
1506
|
/**
|
|
@@ -1483,24 +1516,24 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
|
|
|
1483
1516
|
// namespace: 'file' — an onLoad without a namespace matches EVERY
|
|
1484
1517
|
// namespace, which would hijack virtual modules (e.g. the server-action
|
|
1485
1518
|
// 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)))
|
|
1519
|
+
build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)))
|
|
1487
1520
|
// Second hook, not a wider first one: `transpilePackages` opts a
|
|
1488
1521
|
// dependency's own files into the same pipeline, and they are the only
|
|
1489
1522
|
// node_modules paths that ever produce contents.
|
|
1490
1523
|
if (transpiled) {
|
|
1491
1524
|
build.onLoad({ filter: transpiled, namespace: 'file' }, args =>
|
|
1492
1525
|
prepare(path.resolve(args.path)),
|
|
1493
|
-
)
|
|
1526
|
+
)
|
|
1494
1527
|
}
|
|
1495
1528
|
},
|
|
1496
|
-
}
|
|
1529
|
+
}
|
|
1497
1530
|
},
|
|
1498
|
-
}
|
|
1531
|
+
}
|
|
1499
1532
|
}
|
|
1500
1533
|
|
|
1501
|
-
type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
|
|
1534
|
+
type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
|
|
1502
1535
|
|
|
1503
|
-
export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline
|
|
1536
|
+
export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
|
|
1504
1537
|
|
|
1505
1538
|
/**
|
|
1506
1539
|
* Open the client-source pipeline early so the build can feed it routes as the fact scan resolves
|
|
@@ -1509,64 +1542,64 @@ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
|
|
|
1509
1542
|
* otherwise create for itself.
|
|
1510
1543
|
*/
|
|
1511
1544
|
export function startClientSources(config: ResolvedConfig): ClientSourcePipeline {
|
|
1512
|
-
conventionCache.clear()
|
|
1513
|
-
return createClientSourcePipeline(config)
|
|
1545
|
+
conventionCache.clear()
|
|
1546
|
+
return createClientSourcePipeline(config)
|
|
1514
1547
|
}
|
|
1515
1548
|
|
|
1516
1549
|
// The compiled-source cache is keyed on the file and a hash of its post-rewrite
|
|
1517
1550
|
// input, so a rebuild of an unchanged file skips the oxc pass. It holds the
|
|
1518
1551
|
// PROMISE, so two concurrent loads of the same source (two routes sharing a
|
|
1519
1552
|
// component) share one compile instead of racing two.
|
|
1520
|
-
const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
|
|
1553
|
+
const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
|
|
1521
1554
|
|
|
1522
1555
|
function reactCompiled(
|
|
1523
1556
|
file: string,
|
|
1524
1557
|
source: string,
|
|
1525
1558
|
options: NonNullable<ReturnType<CompatModeExtensions['reactCompilerOptions']>>,
|
|
1526
1559
|
) {
|
|
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
|
|
1560
|
+
const hash = Bun.hash.wyhash(`${options.target}\0${source}`)
|
|
1561
|
+
const cached = reactCompiledCache.get(file)
|
|
1562
|
+
if (cached?.hash === hash) return cached.code
|
|
1563
|
+
const code = transformReactCompiler(source, file, options)
|
|
1564
|
+
reactCompiledCache.set(file, { hash, code })
|
|
1565
|
+
return code
|
|
1533
1566
|
}
|
|
1534
1567
|
|
|
1535
1568
|
// config.root can sit outside the workspace root (and vice versa), so both
|
|
1536
1569
|
// anchor the filter — as does each one's realpath, since esbuild reports
|
|
1537
1570
|
// resolved paths through the OS realpath.
|
|
1538
1571
|
function clientSourceRoots(config: ResolvedConfig) {
|
|
1539
|
-
const roots = new Set<string>()
|
|
1572
|
+
const roots = new Set<string>()
|
|
1540
1573
|
for (const root of [config.root, config.workspaceRoot]) {
|
|
1541
|
-
const resolved = path.resolve(root)
|
|
1542
|
-
roots.add(resolved)
|
|
1574
|
+
const resolved = path.resolve(root)
|
|
1575
|
+
roots.add(resolved)
|
|
1543
1576
|
try {
|
|
1544
|
-
roots.add(realpathSync.native(resolved))
|
|
1577
|
+
roots.add(realpathSync.native(resolved))
|
|
1545
1578
|
} catch {
|
|
1546
1579
|
// A root that cannot be realpath'd is already covered by its literal form.
|
|
1547
1580
|
}
|
|
1548
1581
|
}
|
|
1549
|
-
return [...roots]
|
|
1582
|
+
return [...roots]
|
|
1550
1583
|
}
|
|
1551
1584
|
|
|
1552
1585
|
// `.cjs` keeps the plain js loader: CommonJS never carries JSX, and parsing a
|
|
1553
1586
|
// `a < b` comparison as JSX would fail the build.
|
|
1554
1587
|
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'
|
|
1588
|
+
if (file.endsWith('.tsx')) return 'tsx'
|
|
1589
|
+
if (/\.[cm]?ts$/.test(file)) return 'ts'
|
|
1590
|
+
return file.endsWith('.cjs') ? 'js' : 'jsx'
|
|
1558
1591
|
}
|
|
1559
1592
|
|
|
1560
1593
|
// A path segment that is NOT `word`, spelled positively: esbuild's Go regexp has
|
|
1561
1594
|
// no lookahead, so the negation enumerates "differs at some position / ends
|
|
1562
1595
|
// early / runs longer".
|
|
1563
1596
|
function nonSegmentPattern(word: string, sep: string) {
|
|
1564
|
-
const parts = [`${word}[^${sep}]+`]
|
|
1597
|
+
const parts = [`${word}[^${sep}]+`]
|
|
1565
1598
|
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}]*`)
|
|
1599
|
+
const prefix = word.slice(0, index)
|
|
1600
|
+
parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`)
|
|
1568
1601
|
}
|
|
1569
|
-
return `(?:${parts.join('|')})
|
|
1602
|
+
return `(?:${parts.join('|')})`
|
|
1570
1603
|
}
|
|
1571
1604
|
|
|
1572
1605
|
/**
|
|
@@ -1575,45 +1608,45 @@ function nonSegmentPattern(word: string, sep: string) {
|
|
|
1575
1608
|
* path on the hook.
|
|
1576
1609
|
*/
|
|
1577
1610
|
function transpiledPackageFilter(roots: string): RegExp | undefined {
|
|
1578
|
-
const sep = escapeRegex(path.sep)
|
|
1579
|
-
const names = getExternalPackagePolicy().transpiled?.()
|
|
1580
|
-
if (names?.length === 0) return undefined
|
|
1611
|
+
const sep = escapeRegex(path.sep)
|
|
1612
|
+
const names = getExternalPackagePolicy().transpiled?.()
|
|
1613
|
+
if (names?.length === 0) return undefined
|
|
1581
1614
|
const packages = names
|
|
1582
1615
|
? `(?:${names.map(name => escapeRegex(name).split('/').join(sep)).join('|')})${sep}`
|
|
1583
|
-
: ''
|
|
1584
|
-
return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
|
|
1616
|
+
: ''
|
|
1617
|
+
return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
|
|
1585
1618
|
}
|
|
1586
1619
|
|
|
1587
1620
|
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)
|
|
1621
|
+
const parts = file.split(path.sep)
|
|
1622
|
+
const nodeModules = parts.lastIndexOf('node_modules')
|
|
1623
|
+
if (nodeModules === -1) return false
|
|
1624
|
+
const first = parts[nodeModules + 1]
|
|
1625
|
+
if (!first) return false
|
|
1626
|
+
const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first
|
|
1627
|
+
return getExternalPackagePolicy().transpile(name)
|
|
1595
1628
|
}
|
|
1596
1629
|
|
|
1597
1630
|
function virtualEntryPath(routeId: string) {
|
|
1598
|
-
return `${virtualEntryNamespace}:${routeId}
|
|
1631
|
+
return `${virtualEntryNamespace}:${routeId}`
|
|
1599
1632
|
}
|
|
1600
1633
|
|
|
1601
1634
|
function virtualEntryPlugin(entries: { route: RouteManifestEntry; source: string }[]): Plugin {
|
|
1602
|
-
const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
|
|
1635
|
+
const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
|
|
1603
1636
|
return {
|
|
1604
1637
|
name: 'pnext-virtual-client-entry',
|
|
1605
1638
|
setup(build) {
|
|
1606
1639
|
build.onResolve({ filter: new RegExp(`^${escapeRegex(virtualEntryNamespace)}:`) }, args => ({
|
|
1607
1640
|
path: args.path.slice(virtualEntryNamespace.length + 1),
|
|
1608
1641
|
namespace: virtualEntryNamespace,
|
|
1609
|
-
}))
|
|
1642
|
+
}))
|
|
1610
1643
|
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
|
-
})
|
|
1644
|
+
const contents = sources.get(args.path)
|
|
1645
|
+
if (contents === undefined) return undefined
|
|
1646
|
+
return { contents, loader: 'ts', resolveDir: process.cwd() }
|
|
1647
|
+
})
|
|
1615
1648
|
},
|
|
1616
|
-
}
|
|
1649
|
+
}
|
|
1617
1650
|
}
|
|
1618
1651
|
|
|
1619
1652
|
// The shared route runtime: one virtual module per build, imported by every
|
|
@@ -1626,14 +1659,14 @@ function clientRuntimePlugin(facts: ClientRuntimeFacts): Plugin {
|
|
|
1626
1659
|
build.onResolve({ filter: new RegExp(`^${escapeRegex(CLIENT_RUNTIME_MODULE)}$`) }, () => ({
|
|
1627
1660
|
path: CLIENT_RUNTIME_MODULE,
|
|
1628
1661
|
namespace: CLIENT_RUNTIME_MODULE,
|
|
1629
|
-
}))
|
|
1662
|
+
}))
|
|
1630
1663
|
build.onLoad({ filter: /.*/, namespace: CLIENT_RUNTIME_MODULE }, () => ({
|
|
1631
1664
|
contents: clientRuntimeSource(facts),
|
|
1632
1665
|
loader: 'ts',
|
|
1633
1666
|
resolveDir: process.cwd(),
|
|
1634
|
-
}))
|
|
1667
|
+
}))
|
|
1635
1668
|
},
|
|
1636
|
-
}
|
|
1669
|
+
}
|
|
1637
1670
|
}
|
|
1638
1671
|
|
|
1639
1672
|
async function profileClientBuild<T>(
|
|
@@ -1642,25 +1675,18 @@ async function profileClientBuild<T>(
|
|
|
1642
1675
|
task: () => Promise<T>,
|
|
1643
1676
|
) {
|
|
1644
1677
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1645
|
-
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
1646
|
-
const start = performance.now()
|
|
1678
|
+
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
1679
|
+
const start = performance.now()
|
|
1647
1680
|
try {
|
|
1648
|
-
return await task()
|
|
1681
|
+
return await task()
|
|
1649
1682
|
} finally {
|
|
1650
|
-
const mode = dev ? 'dev' : 'prod'
|
|
1683
|
+
const mode = dev ? 'dev' : 'prod'
|
|
1651
1684
|
console.log(
|
|
1652
1685
|
`dev-profile client build ${mode} ${route.route} in ${formatDuration(performance.now() - start)}`,
|
|
1653
|
-
)
|
|
1686
|
+
)
|
|
1654
1687
|
}
|
|
1655
1688
|
}
|
|
1656
1689
|
|
|
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
1690
|
function serverOnlyClientImportPlugin(): Plugin {
|
|
1665
1691
|
return {
|
|
1666
1692
|
name: 'pnext-server-only-client-import',
|
|
@@ -1674,26 +1700,26 @@ function serverOnlyClientImportPlugin(): Plugin {
|
|
|
1674
1700
|
},
|
|
1675
1701
|
],
|
|
1676
1702
|
}),
|
|
1677
|
-
)
|
|
1703
|
+
)
|
|
1678
1704
|
},
|
|
1679
|
-
}
|
|
1705
|
+
}
|
|
1680
1706
|
}
|
|
1681
1707
|
|
|
1682
1708
|
interface EsbuildLikeError extends Error {
|
|
1683
1709
|
errors?: {
|
|
1684
|
-
text?: string
|
|
1710
|
+
text?: string
|
|
1685
1711
|
location?: {
|
|
1686
|
-
file?: string
|
|
1687
|
-
}
|
|
1688
|
-
}[]
|
|
1712
|
+
file?: string
|
|
1713
|
+
}
|
|
1714
|
+
}[]
|
|
1689
1715
|
}
|
|
1690
1716
|
|
|
1691
1717
|
interface TraceParent {
|
|
1692
|
-
file: string
|
|
1693
|
-
specifier: string
|
|
1718
|
+
file: string
|
|
1719
|
+
specifier: string
|
|
1694
1720
|
}
|
|
1695
1721
|
|
|
1696
|
-
const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
|
|
1722
|
+
const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
|
|
1697
1723
|
|
|
1698
1724
|
function withClientImportTrace(
|
|
1699
1725
|
error: unknown,
|
|
@@ -1701,16 +1727,16 @@ function withClientImportTrace(
|
|
|
1701
1727
|
route: RouteManifestEntry,
|
|
1702
1728
|
entrySource: string,
|
|
1703
1729
|
) {
|
|
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}`)
|
|
1730
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1731
|
+
const trace = clientImportTrace(config, route, entrySource, error)
|
|
1732
|
+
if (!trace) return error
|
|
1733
|
+
const next = new Error(`${message}\n\n${trace}`)
|
|
1708
1734
|
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)
|
|
1735
|
+
next.name = error.name
|
|
1736
|
+
next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack
|
|
1737
|
+
Object.assign(next, error)
|
|
1712
1738
|
}
|
|
1713
|
-
return next
|
|
1739
|
+
return next
|
|
1714
1740
|
}
|
|
1715
1741
|
|
|
1716
1742
|
function clientImportTrace(
|
|
@@ -1719,51 +1745,51 @@ function clientImportTrace(
|
|
|
1719
1745
|
entrySource: string,
|
|
1720
1746
|
error: unknown,
|
|
1721
1747
|
) {
|
|
1722
|
-
const unresolved = unresolvedImportFromError(error)
|
|
1723
|
-
if (!unresolved) return undefined
|
|
1748
|
+
const unresolved = unresolvedImportFromError(error)
|
|
1749
|
+
if (!unresolved) return undefined
|
|
1724
1750
|
|
|
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
|
|
1751
|
+
const entryFile = `${route.id}.ts`
|
|
1752
|
+
const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined
|
|
1753
|
+
const parents = new Map<string, TraceParent>()
|
|
1754
|
+
const visited = new Set<string>()
|
|
1755
|
+
const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }]
|
|
1756
|
+
let unresolvedImporter: string | undefined
|
|
1731
1757
|
|
|
1732
1758
|
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
|
|
1759
|
+
const current = queue.shift()
|
|
1760
|
+
if (!current || visited.has(current.file)) continue
|
|
1761
|
+
visited.add(current.file)
|
|
1762
|
+
if (visited.size > 2000) break
|
|
1737
1763
|
|
|
1738
1764
|
for (const specifier of localImports(current.file, current.source)) {
|
|
1739
|
-
const resolved = resolveTraceImport(config.root, current.file, specifier)
|
|
1765
|
+
const resolved = resolveTraceImport(config.root, current.file, specifier)
|
|
1740
1766
|
if (!resolved) {
|
|
1741
1767
|
if (
|
|
1742
1768
|
specifier === unresolved.specifier &&
|
|
1743
1769
|
(!failedFile || sameFile(current.file, failedFile))
|
|
1744
1770
|
) {
|
|
1745
|
-
unresolvedImporter = current.file
|
|
1746
|
-
queue.length = 0
|
|
1747
|
-
break
|
|
1771
|
+
unresolvedImporter = current.file
|
|
1772
|
+
queue.length = 0
|
|
1773
|
+
break
|
|
1748
1774
|
}
|
|
1749
|
-
continue
|
|
1775
|
+
continue
|
|
1750
1776
|
}
|
|
1751
1777
|
|
|
1752
|
-
if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
|
|
1778
|
+
if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
|
|
1753
1779
|
if (failedFile && sameFile(resolved, failedFile)) {
|
|
1754
|
-
unresolvedImporter = resolved
|
|
1755
|
-
queue.length = 0
|
|
1756
|
-
break
|
|
1780
|
+
unresolvedImporter = resolved
|
|
1781
|
+
queue.length = 0
|
|
1782
|
+
break
|
|
1757
1783
|
}
|
|
1758
1784
|
if (!visited.has(resolved) && !isAssetLike(resolved)) {
|
|
1759
|
-
queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
|
|
1785
|
+
queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
|
|
1760
1786
|
}
|
|
1761
1787
|
}
|
|
1762
1788
|
}
|
|
1763
1789
|
|
|
1764
|
-
if (!unresolvedImporter) return undefined
|
|
1765
|
-
const trace = tracePath(entryFile, unresolvedImporter, parents)
|
|
1766
|
-
if (trace.length === 0) return undefined
|
|
1790
|
+
if (!unresolvedImporter) return undefined
|
|
1791
|
+
const trace = tracePath(entryFile, unresolvedImporter, parents)
|
|
1792
|
+
if (trace.length === 0) return undefined
|
|
1767
1793
|
|
|
1768
1794
|
return [
|
|
1769
1795
|
`pnext client import trace for route ${route.route}:`,
|
|
@@ -1771,135 +1797,135 @@ function clientImportTrace(
|
|
|
1771
1797
|
(file, index) => `${index === 0 ? ' ' : ' -> '}${displayTraceFile(config, file)}`,
|
|
1772
1798
|
),
|
|
1773
1799
|
` -> ${unresolved.specifier} (unresolved in browser client bundle)`,
|
|
1774
|
-
].join('\n')
|
|
1800
|
+
].join('\n')
|
|
1775
1801
|
}
|
|
1776
1802
|
|
|
1777
1803
|
/** True when the build failed because an import could not be resolved. */
|
|
1778
1804
|
function hasUnresolvedImportError(error: unknown): boolean {
|
|
1779
|
-
const errors = (error as EsbuildLikeError).errors
|
|
1805
|
+
const errors = (error as EsbuildLikeError).errors
|
|
1780
1806
|
if (Array.isArray(errors)) {
|
|
1781
|
-
return errors.some(item => item.text?.startsWith('Could not resolve '))
|
|
1807
|
+
return errors.some(item => item.text?.startsWith('Could not resolve '))
|
|
1782
1808
|
}
|
|
1783
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
1784
|
-
return message.includes('Could not resolve ')
|
|
1809
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1810
|
+
return message.includes('Could not resolve ')
|
|
1785
1811
|
}
|
|
1786
1812
|
|
|
1787
1813
|
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
|
|
1814
|
+
const buildError = error as EsbuildLikeError
|
|
1815
|
+
const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '))
|
|
1816
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1817
|
+
const text = diagnostic?.text ?? message
|
|
1818
|
+
const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1]
|
|
1819
|
+
if (!specifier) return undefined
|
|
1794
1820
|
return {
|
|
1795
1821
|
specifier,
|
|
1796
1822
|
file: diagnostic?.location?.file ?? errorFileFromMessage(message),
|
|
1797
|
-
}
|
|
1823
|
+
}
|
|
1798
1824
|
}
|
|
1799
1825
|
|
|
1800
1826
|
function errorFileFromMessage(message: string) {
|
|
1801
|
-
const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
|
|
1802
|
-
return match?.[1]
|
|
1827
|
+
const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
|
|
1828
|
+
return match?.[1]
|
|
1803
1829
|
}
|
|
1804
1830
|
|
|
1805
1831
|
function resolveTraceImport(root: string, fromFile: string, specifier: string) {
|
|
1806
|
-
if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
|
|
1807
|
-
return resolveImport(root, fromFile, specifier)
|
|
1832
|
+
if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
|
|
1833
|
+
return resolveImport(root, fromFile, specifier)
|
|
1808
1834
|
}
|
|
1809
1835
|
|
|
1810
1836
|
function resolveTraceFile(file: string) {
|
|
1811
1837
|
return canonicalTraceFile(
|
|
1812
1838
|
path.isAbsolute(file) ? path.resolve(file) : path.resolve(process.cwd(), file),
|
|
1813
|
-
)
|
|
1839
|
+
)
|
|
1814
1840
|
}
|
|
1815
1841
|
|
|
1816
1842
|
function resolveTraceCandidate(base: string) {
|
|
1817
|
-
const ext = path.extname(base)
|
|
1843
|
+
const ext = path.extname(base)
|
|
1818
1844
|
const candidates = ext
|
|
1819
1845
|
? [base]
|
|
1820
1846
|
: [
|
|
1821
1847
|
...traceSourceExtensions.map(extension => `${base}${extension}`),
|
|
1822
1848
|
...traceSourceExtensions.map(extension => path.join(base, `index${extension}`)),
|
|
1823
1849
|
base,
|
|
1824
|
-
]
|
|
1825
|
-
const resolved = candidates.find(isTraceFile)
|
|
1826
|
-
return resolved ? canonicalTraceFile(resolved) : undefined
|
|
1850
|
+
]
|
|
1851
|
+
const resolved = candidates.find(isTraceFile)
|
|
1852
|
+
return resolved ? canonicalTraceFile(resolved) : undefined
|
|
1827
1853
|
}
|
|
1828
1854
|
|
|
1829
1855
|
function isTraceFile(file: string) {
|
|
1830
1856
|
try {
|
|
1831
|
-
return statSync(file).isFile()
|
|
1857
|
+
return statSync(file).isFile()
|
|
1832
1858
|
} catch {
|
|
1833
|
-
return false
|
|
1859
|
+
return false
|
|
1834
1860
|
}
|
|
1835
1861
|
}
|
|
1836
1862
|
|
|
1837
1863
|
function readTextSyncSafe(file: string) {
|
|
1838
1864
|
try {
|
|
1839
|
-
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
1865
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
1840
1866
|
} catch {
|
|
1841
|
-
return ''
|
|
1867
|
+
return ''
|
|
1842
1868
|
}
|
|
1843
1869
|
}
|
|
1844
1870
|
|
|
1845
1871
|
function tracePath(entryFile: string, targetFile: string, parents: Map<string, TraceParent>) {
|
|
1846
|
-
const pathItems = [targetFile]
|
|
1847
|
-
let current = targetFile
|
|
1872
|
+
const pathItems = [targetFile]
|
|
1873
|
+
let current = targetFile
|
|
1848
1874
|
|
|
1849
1875
|
while (current !== entryFile) {
|
|
1850
|
-
const parent = parents.get(current)
|
|
1851
|
-
if (!parent) return []
|
|
1852
|
-
current = parent.file
|
|
1853
|
-
pathItems.push(current)
|
|
1876
|
+
const parent = parents.get(current)
|
|
1877
|
+
if (!parent) return []
|
|
1878
|
+
current = parent.file
|
|
1879
|
+
pathItems.push(current)
|
|
1854
1880
|
}
|
|
1855
1881
|
|
|
1856
|
-
return pathItems.reverse()
|
|
1882
|
+
return pathItems.reverse()
|
|
1857
1883
|
}
|
|
1858
1884
|
|
|
1859
1885
|
function sameFile(a: string, b: string) {
|
|
1860
|
-
return canonicalTraceFile(a) === canonicalTraceFile(b)
|
|
1886
|
+
return canonicalTraceFile(a) === canonicalTraceFile(b)
|
|
1861
1887
|
}
|
|
1862
1888
|
|
|
1863
1889
|
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)
|
|
1890
|
+
if (!path.isAbsolute(file)) return `${file} (generated client entry)`
|
|
1891
|
+
const root = canonicalTraceFile(config.root)
|
|
1892
|
+
const workspaceRoot = canonicalTraceFile(config.workspaceRoot)
|
|
1893
|
+
const resolvedFile = canonicalTraceFile(file)
|
|
1894
|
+
const rootRelative = path.relative(root, resolvedFile)
|
|
1895
|
+
if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative
|
|
1896
|
+
const workspaceRelative = path.relative(workspaceRoot, resolvedFile)
|
|
1871
1897
|
if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative))
|
|
1872
|
-
return workspaceRelative
|
|
1873
|
-
return resolvedFile
|
|
1898
|
+
return workspaceRelative
|
|
1899
|
+
return resolvedFile
|
|
1874
1900
|
}
|
|
1875
1901
|
|
|
1876
1902
|
function canonicalTraceFile(file: string) {
|
|
1877
1903
|
try {
|
|
1878
|
-
return realpathSync.native(file)
|
|
1904
|
+
return realpathSync.native(file)
|
|
1879
1905
|
} catch {
|
|
1880
|
-
return path.resolve(file)
|
|
1906
|
+
return path.resolve(file)
|
|
1881
1907
|
}
|
|
1882
1908
|
}
|
|
1883
1909
|
|
|
1884
1910
|
/** One walked client source, with the stat that lets a later boot skip reading it. */
|
|
1885
1911
|
export interface ClientCacheSource {
|
|
1886
|
-
file: string
|
|
1887
|
-
mtimeMs: number
|
|
1888
|
-
size: number
|
|
1889
|
-
hash: string
|
|
1912
|
+
file: string
|
|
1913
|
+
mtimeMs: number
|
|
1914
|
+
size: number
|
|
1915
|
+
hash: string
|
|
1890
1916
|
}
|
|
1891
1917
|
|
|
1892
1918
|
export interface ClientCacheKeyParts {
|
|
1893
|
-
key: string
|
|
1894
|
-
staticHash: string
|
|
1895
|
-
sources: ClientCacheSource[]
|
|
1919
|
+
key: string
|
|
1920
|
+
staticHash: string
|
|
1921
|
+
sources: ClientCacheSource[]
|
|
1896
1922
|
}
|
|
1897
1923
|
|
|
1898
1924
|
/**
|
|
1899
1925
|
* The half of the key that is not a walked source: the client build pipeline's own sources, the
|
|
1900
1926
|
* generated entry, and the route's reference list. A handful of small reads, so it is cheap to
|
|
1901
1927
|
* recompute on every lookup - which is what lets the expensive source walk be validated from a
|
|
1902
|
-
* persisted index instead (see dev/client-key
|
|
1928
|
+
* persisted index instead (see dev/restart/client-key.ts).
|
|
1903
1929
|
*/
|
|
1904
1930
|
export async function clientCacheStaticHash(
|
|
1905
1931
|
route: RouteManifestEntry,
|
|
@@ -1908,18 +1934,18 @@ export async function clientCacheStaticHash(
|
|
|
1908
1934
|
) {
|
|
1909
1935
|
// The key must observe convention files added since the last build, so it
|
|
1910
1936
|
// never reads a memo an earlier call left behind.
|
|
1911
|
-
conventionCache.clear()
|
|
1912
|
-
const hash = createHash('sha256')
|
|
1937
|
+
conventionCache.clear()
|
|
1938
|
+
const hash = createHash('sha256')
|
|
1913
1939
|
for (const file of clientBuildPipelineFiles()) {
|
|
1914
|
-
hash.update(file)
|
|
1915
|
-
hash.update('\0')
|
|
1916
|
-
hash.update(await readText(file))
|
|
1917
|
-
hash.update('\0')
|
|
1940
|
+
hash.update(file)
|
|
1941
|
+
hash.update('\0')
|
|
1942
|
+
hash.update(await readText(file))
|
|
1943
|
+
hash.update('\0')
|
|
1918
1944
|
}
|
|
1919
1945
|
|
|
1920
1946
|
hash.update(
|
|
1921
1947
|
clientEntrySource({
|
|
1922
|
-
// Dev-only callers (client-key
|
|
1948
|
+
// Dev-only callers (restart/client-key): the key must move with the split
|
|
1923
1949
|
// flag or a toggled PNEXT_DYNAMIC_SPLIT would serve the other arm's entry.
|
|
1924
1950
|
deferredDynamicHref: devDeferredDynamicHref(true),
|
|
1925
1951
|
pageFile: route.client ? route.file : undefined,
|
|
@@ -1933,11 +1959,11 @@ export async function clientCacheStaticHash(
|
|
|
1933
1959
|
clientRootLayout: config ? hasClientRootLayout(config, route) : false,
|
|
1934
1960
|
shellLayoutOrder: config ? shellLayoutOrder(config, route) : [],
|
|
1935
1961
|
}),
|
|
1936
|
-
)
|
|
1937
|
-
hash.update('\0')
|
|
1938
|
-
hash.update(route.client ? route.file : '')
|
|
1939
|
-
hash.update(JSON.stringify(route.clientReferences))
|
|
1940
|
-
return hash.digest('hex')
|
|
1962
|
+
)
|
|
1963
|
+
hash.update('\0')
|
|
1964
|
+
hash.update(route.client ? route.file : '')
|
|
1965
|
+
hash.update(JSON.stringify(route.clientReferences))
|
|
1966
|
+
return hash.digest('hex')
|
|
1941
1967
|
}
|
|
1942
1968
|
|
|
1943
1969
|
/** The out-dir name: the static half plus every walked source's content hash. */
|
|
@@ -1945,18 +1971,18 @@ export function clientCacheKeyFrom(
|
|
|
1945
1971
|
staticHash: string,
|
|
1946
1972
|
sources: readonly { file: string; hash: string }[],
|
|
1947
1973
|
) {
|
|
1948
|
-
const hash = createHash('sha256').update(staticHash).update('\0')
|
|
1974
|
+
const hash = createHash('sha256').update(staticHash).update('\0')
|
|
1949
1975
|
for (const source of sources) {
|
|
1950
|
-
hash.update(source.file)
|
|
1951
|
-
hash.update('\0')
|
|
1952
|
-
hash.update(source.hash)
|
|
1953
|
-
hash.update('\0')
|
|
1976
|
+
hash.update(source.file)
|
|
1977
|
+
hash.update('\0')
|
|
1978
|
+
hash.update(source.hash)
|
|
1979
|
+
hash.update('\0')
|
|
1954
1980
|
}
|
|
1955
|
-
return hash.digest('hex').slice(0, 16)
|
|
1981
|
+
return hash.digest('hex').slice(0, 16)
|
|
1956
1982
|
}
|
|
1957
1983
|
|
|
1958
1984
|
export function clientSourceHash(source: string) {
|
|
1959
|
-
return Bun.hash(source).toString(36)
|
|
1985
|
+
return Bun.hash(source).toString(36)
|
|
1960
1986
|
}
|
|
1961
1987
|
|
|
1962
1988
|
/**
|
|
@@ -1965,13 +1991,13 @@ export function clientSourceHash(source: string) {
|
|
|
1965
1991
|
* hash that stat never described.
|
|
1966
1992
|
*/
|
|
1967
1993
|
export async function readClientCacheSource(file: string): Promise<ClientCacheSource> {
|
|
1968
|
-
const stats = await stat(file).catch(() => undefined)
|
|
1994
|
+
const stats = await stat(file).catch(() => undefined)
|
|
1969
1995
|
return {
|
|
1970
1996
|
file,
|
|
1971
1997
|
mtimeMs: stats?.mtimeMs ?? 0,
|
|
1972
1998
|
size: stats?.size ?? 0,
|
|
1973
1999
|
hash: clientSourceHash(await readText(file)),
|
|
1974
|
-
}
|
|
2000
|
+
}
|
|
1975
2001
|
}
|
|
1976
2002
|
|
|
1977
2003
|
export async function clientCacheKeyParts(
|
|
@@ -1980,11 +2006,11 @@ export async function clientCacheKeyParts(
|
|
|
1980
2006
|
config?: ResolvedConfig,
|
|
1981
2007
|
staticHash?: string,
|
|
1982
2008
|
): Promise<ClientCacheKeyParts> {
|
|
1983
|
-
const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
|
|
2009
|
+
const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
|
|
1984
2010
|
const sources = await Promise.all(
|
|
1985
2011
|
(await clientSourceFiles(route)).map(file => readClientCacheSource(file)),
|
|
1986
|
-
)
|
|
1987
|
-
return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
|
|
2012
|
+
)
|
|
2013
|
+
return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
|
|
1988
2014
|
}
|
|
1989
2015
|
|
|
1990
2016
|
export async function clientCacheKey(
|
|
@@ -1992,7 +2018,7 @@ export async function clientCacheKey(
|
|
|
1992
2018
|
nextCompat?: boolean,
|
|
1993
2019
|
config?: ResolvedConfig,
|
|
1994
2020
|
) {
|
|
1995
|
-
return (await clientCacheKeyParts(route, nextCompat, config)).key
|
|
2021
|
+
return (await clientCacheKeyParts(route, nextCompat, config)).key
|
|
1996
2022
|
}
|
|
1997
2023
|
|
|
1998
2024
|
function clientBuildPipelineFiles() {
|
|
@@ -2004,61 +2030,61 @@ function clientBuildPipelineFiles() {
|
|
|
2004
2030
|
path.join(import.meta.dirname, 'build.ts'),
|
|
2005
2031
|
path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts'),
|
|
2006
2032
|
path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts'),
|
|
2007
|
-
].filter(file => existsSync(file))
|
|
2033
|
+
].filter(file => existsSync(file))
|
|
2008
2034
|
}
|
|
2009
2035
|
|
|
2010
2036
|
async function clientSourceFiles(route: RouteManifestEntry) {
|
|
2011
2037
|
const entryFiles = [
|
|
2012
2038
|
...(route.client ? [route.file] : []),
|
|
2013
2039
|
...route.clientReferences.map(reference => reference.file),
|
|
2014
|
-
]
|
|
2015
|
-
const files = new Set<string>()
|
|
2040
|
+
]
|
|
2041
|
+
const files = new Set<string>()
|
|
2016
2042
|
|
|
2017
|
-
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
|
|
2043
|
+
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
|
|
2018
2044
|
|
|
2019
|
-
return [...files].sort()
|
|
2045
|
+
return [...files].sort()
|
|
2020
2046
|
}
|
|
2021
2047
|
|
|
2022
2048
|
// The walk fans out: every file joins `files` before its own read starts, so
|
|
2023
2049
|
// concurrent branches still visit each file once. Serially this was the whole
|
|
2024
2050
|
// cost of a route's client cache key (~1.8 s on a 300-module route).
|
|
2025
2051
|
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
|
|
2052
|
+
if (files.has(file) || !existsSync(file)) return
|
|
2053
|
+
files.add(file)
|
|
2054
|
+
if (isAssetLike(file)) return
|
|
2029
2055
|
|
|
2030
|
-
const source = rewriteLiteralDynamicCalls(await readText(file), file)
|
|
2056
|
+
const source = rewriteLiteralDynamicCalls(await readText(file), file)
|
|
2031
2057
|
await Promise.all(
|
|
2032
2058
|
[...localImports(file, source)].map(specifier => {
|
|
2033
|
-
const resolved = resolveImport(rootFromFile(file), file, specifier)
|
|
2034
|
-
return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
|
|
2059
|
+
const resolved = resolveImport(rootFromFile(file), file, specifier)
|
|
2060
|
+
return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
|
|
2035
2061
|
}),
|
|
2036
|
-
)
|
|
2062
|
+
)
|
|
2037
2063
|
}
|
|
2038
2064
|
|
|
2039
2065
|
// Value imports, re-exports and literal `import()` of one module, off the
|
|
2040
2066
|
// memoized parse (PERF-REWRITES #21) — type-only imports bind nothing and are
|
|
2041
2067
|
// already excluded there.
|
|
2042
2068
|
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
|
|
2069
|
+
const facts = scanFacts(file, source)
|
|
2070
|
+
const imports = new Set<string>()
|
|
2071
|
+
for (const edge of facts.imports) imports.add(edge.specifier)
|
|
2072
|
+
for (const edge of facts.dynamicImports) imports.add(edge.specifier)
|
|
2073
|
+
return imports
|
|
2048
2074
|
}
|
|
2049
2075
|
|
|
2050
2076
|
function isAssetLike(file: string) {
|
|
2051
|
-
return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
|
|
2077
|
+
return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
|
|
2052
2078
|
}
|
|
2053
2079
|
|
|
2054
2080
|
function rootFromFile(file: string) {
|
|
2055
|
-
const parts = file.split(path.sep)
|
|
2056
|
-
const srcApp = parts.lastIndexOf('src')
|
|
2081
|
+
const parts = file.split(path.sep)
|
|
2082
|
+
const srcApp = parts.lastIndexOf('src')
|
|
2057
2083
|
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)
|
|
2084
|
+
return parts.slice(0, srcApp).join(path.sep) || path.sep
|
|
2085
|
+
const app = parts.lastIndexOf('app')
|
|
2086
|
+
if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep
|
|
2087
|
+
return path.dirname(file)
|
|
2062
2088
|
}
|
|
2063
2089
|
|
|
2064
2090
|
// esbuild names every shared chunk "chunk-<hash>", which makes bundle reports
|
|
@@ -2068,96 +2094,96 @@ function rootFromFile(file: string) {
|
|
|
2068
2094
|
async function nameSharedClientChunks(outDir: string, metafile?: Metafile) {
|
|
2069
2095
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2070
2096
|
if (metafile && process.env.PNEXT_CLIENT_METAFILE) {
|
|
2071
|
-
await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
|
|
2097
|
+
await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
|
|
2072
2098
|
}
|
|
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>()
|
|
2099
|
+
const chunksDir = path.join(outDir, 'chunks')
|
|
2100
|
+
const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'))
|
|
2101
|
+
const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>()
|
|
2102
|
+
const renames = new Map<string, string>()
|
|
2077
2103
|
|
|
2078
2104
|
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`)
|
|
2105
|
+
const basename = path.basename(file)
|
|
2106
|
+
if (!basename.startsWith('chunk-')) continue
|
|
2107
|
+
renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`)
|
|
2082
2108
|
}
|
|
2083
2109
|
|
|
2084
|
-
if (renames.size === 0) return renames
|
|
2110
|
+
if (renames.size === 0) return renames
|
|
2085
2111
|
|
|
2086
2112
|
await Promise.all(
|
|
2087
2113
|
[...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
|
|
2114
|
+
const fromPath = path.join(chunksDir, from)
|
|
2115
|
+
const toPath = path.join(chunksDir, to)
|
|
2116
|
+
if (from === to || !existsSync(fromPath)) return
|
|
2091
2117
|
try {
|
|
2092
|
-
await rename(fromPath, toPath)
|
|
2118
|
+
await rename(fromPath, toPath)
|
|
2093
2119
|
} catch (error) {
|
|
2094
2120
|
// Tolerate a concurrent build having already moved this chunk (ENOENT);
|
|
2095
2121
|
// the reference rewrite below still points at the renamed file.
|
|
2096
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2122
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2097
2123
|
}
|
|
2098
2124
|
// Move the external sourcemap alongside its chunk. The `sourceMappingURL`
|
|
2099
2125
|
// comment in the renamed chunk is rewritten below (replaceChunkNames matches
|
|
2100
2126
|
// the `chunk-<hash>.js` prefix inside `chunk-<hash>.js.map`), so the map file
|
|
2101
2127
|
// must adopt the new name to keep the link resolvable.
|
|
2102
|
-
const fromMap = `${fromPath}.map
|
|
2103
|
-
if (!existsSync(fromMap)) return
|
|
2128
|
+
const fromMap = `${fromPath}.map`
|
|
2129
|
+
if (!existsSync(fromMap)) return
|
|
2104
2130
|
try {
|
|
2105
|
-
await rename(fromMap, `${toPath}.map`)
|
|
2131
|
+
await rename(fromMap, `${toPath}.map`)
|
|
2106
2132
|
} catch (error) {
|
|
2107
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2133
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
2108
2134
|
}
|
|
2109
2135
|
}),
|
|
2110
|
-
)
|
|
2136
|
+
)
|
|
2111
2137
|
|
|
2112
2138
|
// One read+rewrite per output file. An app with a route per page has hundreds
|
|
2113
2139
|
// 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'))
|
|
2140
|
+
const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'))
|
|
2115
2141
|
await Promise.all(
|
|
2116
2142
|
outputs.map(async file => {
|
|
2117
|
-
const source = await readText(file)
|
|
2118
|
-
const next = replaceChunkNames(source, renames)
|
|
2119
|
-
if (next !== source) await writeText(file, next)
|
|
2143
|
+
const source = await readText(file)
|
|
2144
|
+
const next = replaceChunkNames(source, renames)
|
|
2145
|
+
if (next !== source) await writeText(file, next)
|
|
2120
2146
|
}),
|
|
2121
|
-
)
|
|
2147
|
+
)
|
|
2122
2148
|
|
|
2123
|
-
return renames
|
|
2149
|
+
return renames
|
|
2124
2150
|
}
|
|
2125
2151
|
|
|
2126
2152
|
function hashSuffix(file: string) {
|
|
2127
|
-
return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
|
|
2153
|
+
return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
|
|
2128
2154
|
}
|
|
2129
2155
|
|
|
2130
2156
|
function chunkLabelsFromMetafile(metafile: Metafile) {
|
|
2131
|
-
const labels = new Map<string, string>()
|
|
2157
|
+
const labels = new Map<string, string>()
|
|
2132
2158
|
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>()
|
|
2159
|
+
const basename = path.basename(outputPath)
|
|
2160
|
+
if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue
|
|
2161
|
+
const weights = new Map<string, number>()
|
|
2136
2162
|
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)
|
|
2163
|
+
const label = chunkInputLabel(input)
|
|
2164
|
+
if (!label) continue
|
|
2165
|
+
weights.set(label, (weights.get(label) ?? 0) + bytesInOutput)
|
|
2140
2166
|
}
|
|
2141
|
-
const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
2142
|
-
if (top) labels.set(basename, top[0])
|
|
2167
|
+
const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
2168
|
+
if (top) labels.set(basename, top[0])
|
|
2143
2169
|
}
|
|
2144
|
-
return labels
|
|
2170
|
+
return labels
|
|
2145
2171
|
}
|
|
2146
2172
|
|
|
2147
2173
|
function chunkInputLabel(input: string) {
|
|
2148
|
-
const posix = input.split(path.sep).join('/')
|
|
2149
|
-
const packageIndex = posix.lastIndexOf('node_modules/')
|
|
2174
|
+
const posix = input.split(path.sep).join('/')
|
|
2175
|
+
const packageIndex = posix.lastIndexOf('node_modules/')
|
|
2150
2176
|
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 ?? '')
|
|
2177
|
+
const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/')
|
|
2178
|
+
const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0]
|
|
2179
|
+
return sanitizeChunkLabel(name ?? '')
|
|
2154
2180
|
}
|
|
2155
2181
|
return sanitizeChunkLabel(
|
|
2156
2182
|
posix
|
|
2157
2183
|
.split('/')
|
|
2158
2184
|
.at(-1)
|
|
2159
2185
|
?.replace(/\.[^.]+$/, '') ?? '',
|
|
2160
|
-
)
|
|
2186
|
+
)
|
|
2161
2187
|
}
|
|
2162
2188
|
|
|
2163
2189
|
function sanitizeChunkLabel(label: string) {
|
|
@@ -2165,11 +2191,11 @@ function sanitizeChunkLabel(label: string) {
|
|
|
2165
2191
|
.replace(/^@/, '')
|
|
2166
2192
|
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
2167
2193
|
.replace(/^[-.]+|[-.]+$/g, '')
|
|
2168
|
-
.slice(0, 40)
|
|
2169
|
-
return clean || undefined
|
|
2194
|
+
.slice(0, 40)
|
|
2195
|
+
return clean || undefined
|
|
2170
2196
|
}
|
|
2171
2197
|
|
|
2172
2198
|
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)
|
|
2199
|
+
const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g')
|
|
2200
|
+
return source.replace(pattern, match => renames.get(match) ?? match)
|
|
2175
2201
|
}
|