@wular/pnext 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// `experimental.optimizePackageImports` (COMPAT). A barrel package re-exports its whole catalogue
|
|
2
|
+
// from one entry and the vendor pipeline compiles the ENTRY, so a route naming four icons makes the
|
|
3
|
+
// server evaluate the whole barrel.
|
|
4
|
+
//
|
|
5
|
+
// The fix is Next's: for a listed package, rewrite `import { A, B } from 'pkg'` into per-member
|
|
6
|
+
// imports of the LEAF modules its barrel re-exports them from. Each leaf becomes an ordinary vendor
|
|
7
|
+
// demand, so the existing per-package grouping compiles the demanded subpaths as entries of one
|
|
8
|
+
// `splitting: true` build - the shared graph stays one instance.
|
|
9
|
+
//
|
|
10
|
+
// Unlike `modularizeImports` (a user-written path template), the leaf map is DERIVED from the
|
|
11
|
+
// barrel itself, so nothing here encodes a package's layout. Derivation is also the safety gate:
|
|
12
|
+
// the entry must be re-exports and nothing else - no declarations, no side effects - or the package
|
|
13
|
+
// is left alone, which is why an over-broad list cannot produce wrong code.
|
|
14
|
+
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { parseSync } from 'oxc-parser';
|
|
18
|
+
import { rewriteFacts, type ImportBinding } from '../../resolve/scan-facts';
|
|
19
|
+
import { spliceSource } from '../../dev/module-transform';
|
|
20
|
+
import { resolvePackageSpecifier, resolveVendorPackageSpecifier } from '../../resolve/imports';
|
|
21
|
+
import { withSniff, type ServerSourceTransform } from '../../extensions';
|
|
22
|
+
import { specifierSniffTokens } from '../../utils/source';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Next's default `optimizePackageImports` set - the barrels big enough that compiling the entry is the
|
|
26
|
+
* dominant cost. Extended by the config option. Every name is still gated by the derivation below, so a
|
|
27
|
+
* package that is not a pure re-export barrel costs nothing but one parse.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_OPTIMIZED_PACKAGES = [
|
|
30
|
+
'lucide-react',
|
|
31
|
+
'date-fns',
|
|
32
|
+
'lodash-es',
|
|
33
|
+
'ramda',
|
|
34
|
+
'antd',
|
|
35
|
+
'react-bootstrap',
|
|
36
|
+
'ahooks',
|
|
37
|
+
'@ant-design/icons',
|
|
38
|
+
'@headlessui/react',
|
|
39
|
+
'@headlessui-float/react',
|
|
40
|
+
'@heroicons/react/20/solid',
|
|
41
|
+
'@heroicons/react/24/solid',
|
|
42
|
+
'@heroicons/react/24/outline',
|
|
43
|
+
'@visx/visx',
|
|
44
|
+
'@tremor/react',
|
|
45
|
+
'@mui/material',
|
|
46
|
+
'@mui/icons-material',
|
|
47
|
+
'@material-ui/core',
|
|
48
|
+
'@material-ui/icons',
|
|
49
|
+
'@tabler/icons-react',
|
|
50
|
+
'@phosphor-icons/react',
|
|
51
|
+
'react-use',
|
|
52
|
+
'recharts',
|
|
53
|
+
'rxjs',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function optimizedPackageSet(configured: readonly string[]): string[] {
|
|
57
|
+
// Rewriting a barrel changes what the vendor pipeline compiles, so it is also
|
|
58
|
+
// what a slow or wrong artifact has to be bisected against — and how the
|
|
59
|
+
// bench A/Bs it (same seam shape as `PNEXT_VENDOR_GROUP=0`).
|
|
60
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
61
|
+
if (process.env.PNEXT_OPTIMIZE_PACKAGE_IMPORTS === '0') return [];
|
|
62
|
+
return [...new Set([...DEFAULT_OPTIMIZED_PACKAGES, ...configured])];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Where one exported name really lives: the barrel's re-export target. */
|
|
66
|
+
interface BarrelMember {
|
|
67
|
+
/** The leaf module, relative to the barrel entry. */
|
|
68
|
+
source: string;
|
|
69
|
+
/** The name to take from it: `default`, `*`, or an export name. */
|
|
70
|
+
imported: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The exported names a pure re-export barrel publishes, and their leaves. */
|
|
74
|
+
type BarrelMap = ReadonlyMap<string, BarrelMember>;
|
|
75
|
+
|
|
76
|
+
const barrelMaps = new Map<string, BarrelMap | undefined>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The leaf map of a barrel entry, or undefined when the entry is not a pure
|
|
80
|
+
* re-export barrel. Cached per entry file: one parse per package per process,
|
|
81
|
+
* however many modules import it.
|
|
82
|
+
*/
|
|
83
|
+
function barrelMap(entry: string): BarrelMap | undefined {
|
|
84
|
+
if (barrelMaps.has(entry)) return barrelMaps.get(entry);
|
|
85
|
+
let map: BarrelMap | undefined;
|
|
86
|
+
try {
|
|
87
|
+
map = parseBarrel(entry);
|
|
88
|
+
} catch {
|
|
89
|
+
map = undefined;
|
|
90
|
+
}
|
|
91
|
+
barrelMaps.set(entry, map);
|
|
92
|
+
return map;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseBarrel(entry: string): BarrelMap | undefined {
|
|
96
|
+
const source = readFileSync(entry, 'utf8');
|
|
97
|
+
const parsed = parseSync(entry, source, { lang: 'js' });
|
|
98
|
+
if (parsed.errors.length > 0) return undefined;
|
|
99
|
+
|
|
100
|
+
// A namespace re-export (`import * as icons from './icons/index.js'; export
|
|
101
|
+
// { icons }`) is two statements; the local binding is only allowed to leave
|
|
102
|
+
// the module through that export, or the barrel is doing something else.
|
|
103
|
+
const namespaces = new Map<string, string>();
|
|
104
|
+
for (const node of parsed.program.body) {
|
|
105
|
+
if (node.type === 'ImportDeclaration') {
|
|
106
|
+
const specifiers = node.specifiers ?? [];
|
|
107
|
+
const only = specifiers[0];
|
|
108
|
+
if (specifiers.length !== 1 || only?.type !== 'ImportNamespaceSpecifier') return undefined;
|
|
109
|
+
namespaces.set(only.local.name, node.source.value);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
113
|
+
// `export const x = …` is a definition, not a re-export: not a barrel.
|
|
114
|
+
if (node.declaration) return undefined;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// `export * from` hides its names in the target, so a demanded name cannot
|
|
118
|
+
// be attributed to a leaf; anything else is code with a side effect.
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const members = new Map<string, BarrelMember>();
|
|
123
|
+
for (const statement of parsed.module.staticExports) {
|
|
124
|
+
for (const record of statement.entries) {
|
|
125
|
+
if (record.isType) continue;
|
|
126
|
+
const exported = record.exportName.name;
|
|
127
|
+
if (!exported) return undefined;
|
|
128
|
+
const request = record.moduleRequest?.value;
|
|
129
|
+
if (request) {
|
|
130
|
+
const kind: string = record.importName.kind;
|
|
131
|
+
if (kind === 'AllButDefault') return undefined;
|
|
132
|
+
const imported =
|
|
133
|
+
kind === 'Default' ? 'default' : kind === 'All' ? '*' : record.importName.name;
|
|
134
|
+
if (!imported) return undefined;
|
|
135
|
+
members.set(exported, { source: request, imported });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const local = record.localName.name;
|
|
139
|
+
const namespace = local === null ? undefined : namespaces.get(local);
|
|
140
|
+
if (!namespace) return undefined;
|
|
141
|
+
members.set(exported, { source: namespace, imported: '*' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return members.size > 0 ? members : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The barrel entry, but only when both layers resolve the specifier to the SAME
|
|
149
|
+
* file: a package that splits its entry per condition publishes a different
|
|
150
|
+
* catalogue per layer, and one rewritten source is compiled for both.
|
|
151
|
+
*/
|
|
152
|
+
function barrelEntry(root: string, file: string, specifier: string) {
|
|
153
|
+
const client = resolveVendorPackageSpecifier(root, file, specifier, CLIENT_CONDITIONS);
|
|
154
|
+
if (!client) return undefined;
|
|
155
|
+
return resolveVendorPackageSpecifier(root, file, specifier, SERVER_CONDITIONS) === client
|
|
156
|
+
? client
|
|
157
|
+
: undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The bare specifier that names a leaf module - what the rewrite emits, so the leaf goes through the
|
|
162
|
+
* ordinary package-resolution and vendor-grouping paths rather than a raw path only this transform could
|
|
163
|
+
* resolve. Verified by re-resolving it: a package whose `exports` map withholds the subpath resolves to
|
|
164
|
+
* something else, and then the member is left on the barrel. Verified under BOTH layers' conditions,
|
|
165
|
+
* because one rewritten source is compiled for both.
|
|
166
|
+
*/
|
|
167
|
+
function leafSpecifier(root: string, entry: string, member: BarrelMember): string | undefined {
|
|
168
|
+
const key = `${entry}\0${member.source}`;
|
|
169
|
+
if (leafSpecifiers.has(key)) return leafSpecifiers.get(key);
|
|
170
|
+
const specifier = resolveLeafSpecifier(root, entry, member);
|
|
171
|
+
leafSpecifiers.set(key, specifier);
|
|
172
|
+
return specifier;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const leafSpecifiers = new Map<string, string | undefined>();
|
|
176
|
+
|
|
177
|
+
const CLIENT_CONDITIONS = ['module', 'import'];
|
|
178
|
+
const SERVER_CONDITIONS = ['react-server', 'node', 'module', 'import'];
|
|
179
|
+
|
|
180
|
+
function resolveLeafSpecifier(root: string, entry: string, member: BarrelMember) {
|
|
181
|
+
if (!member.source.startsWith('.')) return undefined;
|
|
182
|
+
const file = path.resolve(path.dirname(entry), member.source);
|
|
183
|
+
const packageDir = packageDirOf(entry);
|
|
184
|
+
if (!packageDir) return undefined;
|
|
185
|
+
const name = packageNameOfDir(packageDir);
|
|
186
|
+
const relative = path.relative(packageDir, file);
|
|
187
|
+
if (!name || relative.startsWith('..') || path.isAbsolute(relative)) return undefined;
|
|
188
|
+
const specifier = `${name}/${relative.split(path.sep).join('/')}`;
|
|
189
|
+
for (const conditions of [CLIENT_CONDITIONS, SERVER_CONDITIONS]) {
|
|
190
|
+
if (resolvePackageSpecifier(root, entry, specifier, conditions) !== file) return undefined;
|
|
191
|
+
}
|
|
192
|
+
return specifier;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The installed package a barrel entry belongs to, by its `node_modules` segment. */
|
|
196
|
+
function packageDirOf(entry: string) {
|
|
197
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
198
|
+
const index = entry.lastIndexOf(marker);
|
|
199
|
+
if (index < 0) return undefined;
|
|
200
|
+
const rest = entry.slice(index + marker.length).split(path.sep);
|
|
201
|
+
const segments = rest[0]?.startsWith('@') ? rest.slice(0, 2) : rest.slice(0, 1);
|
|
202
|
+
return path.join(entry.slice(0, index + marker.length), ...segments);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function packageNameOfDir(packageDir: string) {
|
|
206
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
207
|
+
const index = packageDir.lastIndexOf(marker);
|
|
208
|
+
if (index < 0) return undefined;
|
|
209
|
+
return packageDir.slice(index + marker.length).split(path.sep).join('/');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Rewrite one matched import into per-leaf imports, or undefined to leave it alone - a default/namespace
|
|
214
|
+
* import of the barrel itself, a type-only clause, or a name the barrel does not re-export from a
|
|
215
|
+
* resolvable leaf.
|
|
216
|
+
*/
|
|
217
|
+
function expandBarrelImport(
|
|
218
|
+
bindings: readonly ImportBinding[],
|
|
219
|
+
map: BarrelMap,
|
|
220
|
+
root: string,
|
|
221
|
+
entry: string,
|
|
222
|
+
): string | undefined {
|
|
223
|
+
if (bindings.length === 0) return undefined;
|
|
224
|
+
const statements: string[] = [];
|
|
225
|
+
for (const binding of bindings) {
|
|
226
|
+
// A barrel publishes its types off the same entry, so `{ Icon, type Props }`
|
|
227
|
+
// is the common shape. The type binding is erased before anything resolves
|
|
228
|
+
// it, so dropping it is what lets the value bindings split at all.
|
|
229
|
+
if (binding.type) continue;
|
|
230
|
+
if (binding.imported === 'default' || binding.imported === '*') return undefined;
|
|
231
|
+
const member = map.get(binding.imported);
|
|
232
|
+
if (!member) return undefined;
|
|
233
|
+
const specifier = leafSpecifier(root, entry, member);
|
|
234
|
+
if (!specifier) return undefined;
|
|
235
|
+
const target = JSON.stringify(specifier);
|
|
236
|
+
if (member.imported === 'default') statements.push(`import ${binding.local} from ${target};`);
|
|
237
|
+
else if (member.imported === '*') statements.push(`import * as ${binding.local} from ${target};`);
|
|
238
|
+
else {
|
|
239
|
+
const named =
|
|
240
|
+
member.imported === binding.local ? binding.local : `${member.imported} as ${binding.local}`;
|
|
241
|
+
statements.push(`import { ${named} } from ${target};`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return statements.length > 0 ? statements.join(' ') : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The transform. Registered on both layers' chains: one rewritten source is
|
|
249
|
+
* compiled for the RSC graph and the browser graph, which is why the leaf
|
|
250
|
+
* specifier has to resolve identically under both.
|
|
251
|
+
*/
|
|
252
|
+
export function createOptimizePackageImportsTransform(
|
|
253
|
+
packages: readonly string[],
|
|
254
|
+
root: string,
|
|
255
|
+
): ServerSourceTransform {
|
|
256
|
+
if (packages.length === 0) return withSniff([], source => source);
|
|
257
|
+
const optimized = new Set(packages);
|
|
258
|
+
return withSniff(specifierSniffTokens(packages), (source, file) => {
|
|
259
|
+
if (!source.includes('import')) return source;
|
|
260
|
+
const edits: { start: number; end: number; value: string }[] = [];
|
|
261
|
+
for (const statement of rewriteFacts(file, source).imports) {
|
|
262
|
+
if (!optimized.has(statement.specifier)) continue;
|
|
263
|
+
const entry = barrelEntry(root, file, statement.specifier);
|
|
264
|
+
const map = entry && barrelMap(entry);
|
|
265
|
+
if (!map || !entry) continue;
|
|
266
|
+
const expanded = expandBarrelImport(statement.bindings, map, root, entry);
|
|
267
|
+
if (expanded !== undefined) {
|
|
268
|
+
edits.push({ start: statement.start, end: statement.end, value: expanded });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return edits.length > 0 ? spliceSource(source, edits) : source;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// core-js polyfill injection (COMPAT - experimental.swcEnvOptions).
|
|
2
|
+
//
|
|
3
|
+
// When an app enables `experimental.swcEnvOptions`, SWC ships core-js polyfills for the JS features the
|
|
4
|
+
// targeted browsers lack. pnext does not run SWC's feature analysis; it mirrors the user-visible outcome
|
|
5
|
+
// by adding the matching `core-js/modules/*` import to the client module that uses the feature - the
|
|
6
|
+
// same placement SWC's usage mode picks, so the polyfill runs ahead of that module and ships in its
|
|
7
|
+
// chunk. Gated on swcEnvOptions being configured, so a pure app pays nothing.
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import type { ResolvedConfig } from '../../config';
|
|
13
|
+
|
|
14
|
+
// No-module polyfills chunk (COMPAT - Next parity).
|
|
15
|
+
//
|
|
16
|
+
// Next ships a `polyfills-<hash>.js` chunk referenced by a `<script noModule>` tag: legacy browsers with
|
|
17
|
+
// no ES-module support load it, module-capable browsers skip every `noModule` script. pnext only ever
|
|
18
|
+
// targets module-capable runtimes, so the chunk's body is inert - it exists so the served document
|
|
19
|
+
// carries the same `noModule` script tag Next emits, which some suites assert on.
|
|
20
|
+
|
|
21
|
+
/** Inert body for the no-module polyfills chunk (never runs in module browsers). */
|
|
22
|
+
export const POLYFILLS_NOMODULE_SOURCE = '!function(){"use strict"}();\n';
|
|
23
|
+
|
|
24
|
+
let cachedPolyfillsFileName: string | undefined;
|
|
25
|
+
|
|
26
|
+
/** Content-hashed filename for the emitted no-module polyfills chunk. */
|
|
27
|
+
export function polyfillsChunkFileName(): string {
|
|
28
|
+
if (cachedPolyfillsFileName) return cachedPolyfillsFileName;
|
|
29
|
+
const hash = createHash('sha256').update(POLYFILLS_NOMODULE_SOURCE).digest('hex').slice(0, 16);
|
|
30
|
+
return (cachedPolyfillsFileName = `polyfills-${hash}.js`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** App-absolute served path of the polyfills chunk (before assetPrefix). */
|
|
34
|
+
export function polyfillsChunkPath(): string {
|
|
35
|
+
return `/_next/static/chunks/${polyfillsChunkFileName()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The core-js module SWC's usage mode emits per feature, keyed by the syntax
|
|
39
|
+
// that pulls it in. Extend as more features are needed.
|
|
40
|
+
const POLYFILL_FEATURES: { usage: RegExp; module: string }[] = [
|
|
41
|
+
{ usage: /\.replaceAll\s*\(/, module: 'core-js/modules/es.string.replace-all.js' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Sniff tokens for the client transform below (one per feature usage). */
|
|
45
|
+
export const POLYFILL_USAGE_TOKENS = POLYFILL_FEATURES.map(feature => feature.usage);
|
|
46
|
+
|
|
47
|
+
// core-js resolves from the app, not from pnext. One require per root, reused
|
|
48
|
+
// across the transform's per-module calls.
|
|
49
|
+
const polyfillRequires = new Map<string, NodeJS.Require>();
|
|
50
|
+
|
|
51
|
+
function polyfillRequire(config: ResolvedConfig) {
|
|
52
|
+
const existing = polyfillRequires.get(config.root);
|
|
53
|
+
if (existing) return existing;
|
|
54
|
+
const created = createRequire(path.join(config.root, 'pnext-polyfill-resolve.cjs'));
|
|
55
|
+
polyfillRequires.set(config.root, created);
|
|
56
|
+
return created;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Prepend the core-js imports for the features `source` uses, mirroring SWC's usage mode: the polyfill
|
|
61
|
+
* belongs to the MODULE that needs it, so it lands in that module's chunk (an `inject` into every entry
|
|
62
|
+
* would instead be hoisted into a shared chunk of its own, away from the user code). A module core-js
|
|
63
|
+
* does not ship - or that is not installed - is skipped, never a build failure.
|
|
64
|
+
*
|
|
65
|
+
* Only app sources, like SWC: polyfilling a dependency (or pnext's own client runtime) would make
|
|
66
|
+
* core-js shared across entries and split back out into the very standalone chunk this placement avoids.
|
|
67
|
+
*/
|
|
68
|
+
export function rewriteUsagePolyfillImports(
|
|
69
|
+
source: string,
|
|
70
|
+
file: string,
|
|
71
|
+
config: ResolvedConfig,
|
|
72
|
+
): string {
|
|
73
|
+
const inApp = file.startsWith(`${config.root}${path.sep}`);
|
|
74
|
+
if (!inApp || file.includes(`${path.sep}node_modules${path.sep}`)) return source;
|
|
75
|
+
const require = polyfillRequire(config);
|
|
76
|
+
const imports = POLYFILL_FEATURES.flatMap(feature => {
|
|
77
|
+
if (!feature.usage.test(source)) return [];
|
|
78
|
+
try {
|
|
79
|
+
return [`import ${JSON.stringify(require.resolve(feature.module))};`];
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (imports.length === 0) return source;
|
|
85
|
+
// After the directive prologue: a `'use client'` module must keep it first.
|
|
86
|
+
const directive = /^(?:\s*['"][^'"]+['"];?\s*)*/.exec(source)?.[0] ?? '';
|
|
87
|
+
return `${directive}${imports.join('\n')}\n${source.slice(directive.length)}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// `Profiler` import splitting (COMPAT). The React Compiler pass itself is core's
|
|
3
|
+
// (client/react-compiler.ts), driven by the client-source loader; next.config's
|
|
4
|
+
// `reactCompiler` flag reaches it through the compat-mode options seam.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { rewriteFacts } from '../../resolve/scan-facts';
|
|
9
|
+
import { spliceSource } from '../../dev/module-transform';
|
|
10
|
+
|
|
11
|
+
const profilerRuntimePath = path.join(import.meta.dirname, 'react-profiler.tsx');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Split `Profiler` out of a `react` named import onto pnext's own Profiler
|
|
15
|
+
* shim. Folded onto the module record, so the statement is
|
|
16
|
+
* matched by shape rather than by a line-anchored regex.
|
|
17
|
+
*/
|
|
18
|
+
export function rewriteReactProfilerImport(source: string, file = 'module.tsx'): string {
|
|
19
|
+
if (!source.includes('Profiler')) return source;
|
|
20
|
+
const edits: { start: number; end: number; value: string }[] = [];
|
|
21
|
+
for (const statement of rewriteFacts(file, source).imports) {
|
|
22
|
+
if (statement.specifier !== 'react') continue;
|
|
23
|
+
const profilers = statement.bindings.filter(binding => binding.imported === 'Profiler');
|
|
24
|
+
if (profilers.length === 0) continue;
|
|
25
|
+
|
|
26
|
+
const rest = statement.bindings.filter(binding => binding.imported !== 'Profiler');
|
|
27
|
+
const named = rest.filter(binding => binding.imported !== 'default' && binding.imported !== '*');
|
|
28
|
+
const other = rest.filter(binding => binding.imported === 'default' || binding.imported === '*');
|
|
29
|
+
const indent = importIndent(source, statement.start);
|
|
30
|
+
const clause = named.map(printBinding).join(', ');
|
|
31
|
+
const prefix = other.map(binding => (binding.imported === '*' ? `* as ${binding.local}` : binding.local)).join(', ');
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
if (prefix && clause) lines.push(`${indent}import ${prefix}, { ${clause} } from 'react';`);
|
|
34
|
+
else if (prefix) lines.push(`${indent}import ${prefix} from 'react';`);
|
|
35
|
+
else if (clause) lines.push(`${indent}import { ${clause} } from 'react';`);
|
|
36
|
+
for (const profiler of profilers) {
|
|
37
|
+
lines.push(
|
|
38
|
+
`${indent}import { Profiler as ${profiler.local} } from ${JSON.stringify(profilerRuntimePath)};`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
edits.push({ start: statement.start, end: statement.end, value: lines.join('\n').slice(indent.length) });
|
|
42
|
+
}
|
|
43
|
+
return edits.length > 0 ? spliceSource(source, edits) : source;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printBinding(binding: { imported: string; local: string; type: boolean }) {
|
|
47
|
+
const name = binding.imported === binding.local ? binding.local : `${binding.imported} as ${binding.local}`;
|
|
48
|
+
return binding.type ? `type ${name}` : name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Leading whitespace of the statement's own line (the splice keeps the first one). */
|
|
52
|
+
function importIndent(source: string, start: number) {
|
|
53
|
+
const lineStart = source.lastIndexOf('\n', start - 1) + 1;
|
|
54
|
+
const indent = source.slice(lineStart, start);
|
|
55
|
+
return /^[ \t]*$/.test(indent) ? indent : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rewriteReactProfilerServerSource(source: string, file?: string): string {
|
|
59
|
+
return rewriteReactProfilerImport(source, file);
|
|
60
|
+
}
|
|
61
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Fragment, type ComponentChildren } from 'preact';
|
|
2
|
+
import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
export interface ProfilerProps {
|
|
5
|
+
id: string;
|
|
6
|
+
children?: ComponentChildren;
|
|
7
|
+
onRender?: (
|
|
8
|
+
id: string,
|
|
9
|
+
phase: 'mount' | 'update',
|
|
10
|
+
actualDuration: number,
|
|
11
|
+
baseDuration: number,
|
|
12
|
+
startTime: number,
|
|
13
|
+
commitTime: number,
|
|
14
|
+
) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Profiler({ id, children, onRender }: ProfilerProps) {
|
|
18
|
+
const phase = useRef<'mount' | 'update'>('mount');
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
const now = typeof performance === 'undefined' ? 0 : performance.now();
|
|
21
|
+
onRender?.(id, phase.current, 0, 0, now, now);
|
|
22
|
+
phase.current = 'update';
|
|
23
|
+
});
|
|
24
|
+
return <Fragment>{children}</Fragment>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// next.config `compiler.relay` (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// Next runs the Relay SWC transform, which replaces every graphql-tagged template with a reference to
|
|
4
|
+
// the artifact relay-compiler generated for that operation. Without the transform the tag survives into
|
|
5
|
+
// the bundle and relay-runtime's `graphql()` throws its "Unexpected invocation at runtime" invariant on
|
|
6
|
+
// the first render.
|
|
7
|
+
//
|
|
8
|
+
// pnext has no SWC, so this is a plain source transform: for each tagged template it reads the
|
|
9
|
+
// operation/fragment name out of the GraphQL body, resolves the matching artifact on disk, hoists an
|
|
10
|
+
// `import * as <binding>` and substitutes the binding for the tag. Passing the module namespace rather
|
|
11
|
+
// than the default export is exactly what the babel/SWC transforms emit - relay-runtime's `getNode()`
|
|
12
|
+
// unwraps `node.default` for ES-module artifacts and takes the object as-is for the CommonJS ones.
|
|
13
|
+
//
|
|
14
|
+
// Everything is resolved relative to `process.cwd()`, matching the SWC transform's root. That is what
|
|
15
|
+
// makes the multi-project fixture work: each sub-project is built with its own cwd.
|
|
16
|
+
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { withSniff, type ServerSourceTransform } from '../../extensions';
|
|
20
|
+
import type { RelayCompilerConfig } from './config';
|
|
21
|
+
|
|
22
|
+
/** The artifact extension relay-compiler emits for each `language` setting. */
|
|
23
|
+
function artifactExtensions(language: RelayCompilerConfig['language']): string[] {
|
|
24
|
+
// Try the configured language's extension first, then the other one: a
|
|
25
|
+
// project can point `artifactDirectory` at output generated with a different
|
|
26
|
+
// `language` than next.config declares (the multi-project fixture reads its
|
|
27
|
+
// `language` from a sibling project's relay config).
|
|
28
|
+
return language === 'typescript' ? ['.ts', '.js'] : ['.js', '.ts'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `graphql`-tagged templates. GraphQL has no backticks, so a lazy scan is exact. */
|
|
32
|
+
function relayTagPattern(): RegExp {
|
|
33
|
+
return /(?<![\w$.])graphql\s*`([\s\S]*?)`/g;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const OPERATION_NAME = /\b(?:query|mutation|subscription|fragment)\s+([A-Za-z_][\w]*)/;
|
|
37
|
+
|
|
38
|
+
function isUnder(dir: string, file: string): boolean {
|
|
39
|
+
const relative = path.relative(dir, file);
|
|
40
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Insert hoisted imports after any leading directive prologue ('use client' /
|
|
44
|
+
// 'use server' / 'use strict') and optional shebang, so a client module's
|
|
45
|
+
// `'use client'` stays the first statement.
|
|
46
|
+
function injectAfterDirectives(source: string, injection: string): string {
|
|
47
|
+
const prologue = /^(\uFEFF?(?:#![^\n]*\n)?(?:[ \t]*(['"])use [\w-]+\2[ \t]*;?[ \t]*(?:\r?\n|$))*)/.exec(
|
|
48
|
+
source,
|
|
49
|
+
);
|
|
50
|
+
const index = prologue ? prologue[0].length : 0;
|
|
51
|
+
return `${source.slice(0, index)}${injection}\n${source.slice(index)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a source transform applying `compiler.relay`. Returns a no-op transform
|
|
56
|
+
* when nothing is configured so pure-core apps pay nothing.
|
|
57
|
+
*/
|
|
58
|
+
export function createRelayTransform(relay: RelayCompilerConfig | undefined): ServerSourceTransform {
|
|
59
|
+
if (!relay) return withSniff([], source => source);
|
|
60
|
+
|
|
61
|
+
const cwd = path.resolve(process.cwd());
|
|
62
|
+
const srcDir = path.resolve(cwd, relay.src);
|
|
63
|
+
const artifactDir = relay.artifactDirectory
|
|
64
|
+
? path.resolve(cwd, relay.artifactDirectory)
|
|
65
|
+
: undefined;
|
|
66
|
+
const extensions = artifactExtensions(relay.language);
|
|
67
|
+
const resolved = new Map<string, string | undefined>();
|
|
68
|
+
|
|
69
|
+
/** The on-disk artifact for one operation name, or undefined when absent. */
|
|
70
|
+
function artifactFor(fileDir: string, name: string): string | undefined {
|
|
71
|
+
// Without `artifactDirectory`, relay-compiler writes `__generated__` next
|
|
72
|
+
// to the source file.
|
|
73
|
+
const directory = artifactDir ?? path.join(fileDir, '__generated__');
|
|
74
|
+
const key = `${directory}\0${name}`;
|
|
75
|
+
if (resolved.has(key)) return resolved.get(key);
|
|
76
|
+
let found: string | undefined;
|
|
77
|
+
for (const extension of extensions) {
|
|
78
|
+
const candidate = path.join(directory, `${name}.graphql${extension}`);
|
|
79
|
+
if (existsSync(candidate)) {
|
|
80
|
+
found = candidate;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
resolved.set(key, found);
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return withSniff(['graphql'], (source, file) => {
|
|
89
|
+
if (!source.includes('graphql`') && !/graphql\s*`/.test(source)) return source;
|
|
90
|
+
const absolute = path.resolve(file);
|
|
91
|
+
// Next scopes the transform to `src`. Files outside the build cwd entirely
|
|
92
|
+
// (materialized copies) are not covered by that check, so only skip a file
|
|
93
|
+
// that lives under the cwd but outside the configured source root.
|
|
94
|
+
if (isUnder(cwd, absolute) && !isUnder(srcDir, absolute)) return source;
|
|
95
|
+
|
|
96
|
+
const bindings = new Map<string, string>();
|
|
97
|
+
const replaced = source.replace(relayTagPattern(), (match, body: string) => {
|
|
98
|
+
const name = OPERATION_NAME.exec(body)?.[1];
|
|
99
|
+
if (!name) return match;
|
|
100
|
+
const artifact = artifactFor(path.dirname(absolute), name);
|
|
101
|
+
if (!artifact) return match;
|
|
102
|
+
let binding = bindings.get(artifact);
|
|
103
|
+
if (!binding) {
|
|
104
|
+
binding = `__pnext_relay_${bindings.size}`;
|
|
105
|
+
bindings.set(artifact, binding);
|
|
106
|
+
}
|
|
107
|
+
return binding;
|
|
108
|
+
});
|
|
109
|
+
if (bindings.size === 0) return source;
|
|
110
|
+
|
|
111
|
+
const imports = [...bindings]
|
|
112
|
+
.map(([artifact, binding]) => `import * as ${binding} from ${JSON.stringify(artifact)};`)
|
|
113
|
+
.join('\n');
|
|
114
|
+
return injectAfterDirectives(replaced, imports);
|
|
115
|
+
});
|
|
116
|
+
}
|