@wular/pnext 0.0.19 → 0.0.21

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.
Files changed (83) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/reference/compat.md +2 -2
  4. package/reference/getting-started.md +2 -2
  5. package/src/cli/adapters/vercel.ts +27 -0
  6. package/src/cli/analyze.ts +8 -7
  7. package/src/cli/boot/trace.ts +9 -3
  8. package/src/cli/build/cache.ts +324 -0
  9. package/src/cli/build.ts +42 -18
  10. package/src/cli/create.ts +4 -4
  11. package/src/cli/dev.ts +23 -0
  12. package/src/cli/migrate/package-json.ts +3 -2
  13. package/src/cli/migrate/report.ts +1 -1
  14. package/src/cli/migrate/run.ts +2 -2
  15. package/src/cli/migrate/scan.ts +128 -1
  16. package/src/cli/serve/pipeline.ts +42 -42
  17. package/src/cli/start.ts +20 -20
  18. package/src/client/build.ts +64 -35
  19. package/src/client/entry.ts +228 -16
  20. package/src/client/router/page-slot.ts +81 -0
  21. package/src/client/router/runtime.ts +107 -112
  22. package/src/compat/bundler/cjs-exports.ts +26 -20
  23. package/src/compat/client/css-order.ts +1 -1
  24. package/src/compat/client/navigation-scroll.ts +36 -40
  25. package/src/compat/css/modules.ts +6 -2
  26. package/src/compat/metadata.ts +0 -9
  27. package/src/compat/next/font/cache.ts +10 -22
  28. package/src/compat/next/font/runtime.ts +20 -0
  29. package/src/compat/next/legacy-request-apis.ts +41 -5
  30. package/src/compat/next/script.tsx +16 -24
  31. package/src/compat/react/client-lite.ts +1 -0
  32. package/src/compat/react/client.ts +11 -0
  33. package/src/compat/react/dom-client.ts +11 -2
  34. package/src/compat/react/dom-react-server.ts +4 -0
  35. package/src/compat/react/dom.ts +11 -2
  36. package/src/compat/react/internals.ts +65 -0
  37. package/src/compat/react/parity.ts +47 -0
  38. package/src/compat/react/preact.ts +31 -1
  39. package/src/compat/react/react-server.ts +7 -0
  40. package/src/compat/react/server.ts +16 -2
  41. package/src/compat/react/style-values.ts +68 -0
  42. package/src/compat/register/proxy.ts +3 -1
  43. package/src/compat/register/render.ts +14 -3
  44. package/src/compat/register/routing.ts +57 -7
  45. package/src/compat/register/typed-routes.ts +0 -2
  46. package/src/compat/tsconfig-defaults.ts +4 -0
  47. package/src/compat/typecheck/check.ts +26 -71
  48. package/src/compat/typecheck/worker.ts +3 -2
  49. package/src/compat/validation/validate.ts +2 -0
  50. package/src/compat-bootstrap.ts +4 -0
  51. package/src/css/build.ts +153 -14
  52. package/src/css/postcss.ts +32 -3
  53. package/src/dev/restart/client-key.ts +1 -1
  54. package/src/dev/restart/global-css.ts +12 -94
  55. package/src/dev/restart/record.ts +106 -0
  56. package/src/dev/restart/route-facts.ts +11 -90
  57. package/src/dev/server.ts +208 -41
  58. package/src/extensions.ts +69 -3
  59. package/src/render/hooks.ts +1 -1
  60. package/src/render/metadata.ts +89 -17
  61. package/src/render/renderer.ts +325 -138
  62. package/src/render/resource-hints.ts +62 -9
  63. package/src/render/slots.tsx +2 -1
  64. package/src/resolve/scan-facts.ts +13 -5
  65. package/src/routing/forwarded.ts +20 -7
  66. package/src/routing/handler.ts +6 -2
  67. package/src/routing/href.ts +11 -1
  68. package/src/routing/metadata-files.ts +16 -3
  69. package/src/routing/proxy.ts +6 -3
  70. package/src/routing/routes.ts +253 -34
  71. package/src/runtime/fetch-host.ts +42 -0
  72. package/src/runtime/fingerprint.ts +132 -0
  73. package/src/runtime/loader.ts +7 -5
  74. package/src/runtime/module-cache.ts +3 -36
  75. package/src/runtime/modules.ts +333 -77
  76. package/src/runtime/vendor-build.ts +34 -5
  77. package/src/runtime/vendor.ts +1 -18
  78. package/src/types.ts +6 -0
  79. package/src/utils/asset-hash.ts +22 -0
  80. package/src/utils/code.ts +41 -0
  81. package/src/utils/html.ts +4 -1
  82. package/src/utils/registry.ts +70 -0
  83. package/src/utils/serialize.ts +66 -7
package/src/extensions.ts CHANGED
@@ -376,6 +376,17 @@ export interface RenderExtensions {
376
376
  context: VaryParamsTrackingContext,
377
377
  ) => T
378
378
 
379
+ /**
380
+ * Give a `params`/`searchParams` promise Next 15's transitional SYNC surface (the resolved keys
381
+ * readable without `await`), behind compat.next.legacyRequestAPIs. The SAME promise comes back —
382
+ * callers hand a settled thenable to `use()`. Default: identity.
383
+ */
384
+ legacySyncProps: <T extends object>(
385
+ promise: Promise<T>,
386
+ kind: 'params' | 'searchParams',
387
+ value: T,
388
+ ) => Promise<T>
389
+
379
390
  /** Optional opaque state lifecycle around partial prerenders. */
380
391
  prerenderSidecar: PrerenderSidecarExtension
381
392
  }
@@ -476,6 +487,7 @@ function createRenderExtensions(): RenderExtensions {
476
487
  formatErrorLog: () => undefined,
477
488
  globalNotFoundEnabled: () => false,
478
489
  trackVaryParams: value => value,
490
+ legacySyncProps: promise => promise,
479
491
  prerenderSidecar: {
480
492
  begin: () => undefined,
481
493
  collect: () => Promise.resolve(undefined),
@@ -704,8 +716,11 @@ export function registerRevalidateBypassToken(token: () => string | undefined):
704
716
  * Run every registered response finalizer against `response` before it is
705
717
  * flushed, then return the response to send. Finalizers see request info + the
706
718
  * outgoing status/headers and mutate them in place; when a finalizer changes the
707
- * status (immutable on a Response) the body is re-wrapped with the new status. A
708
- * no-finalizer app (pure core) returns the response untouched.
719
+ * status (immutable on a Response) the body is re-wrapped with the new status.
720
+ *
721
+ * The document cache-control default is applied LAST, and only when nothing
722
+ * upstream claimed the header: an ISR/`use cache` route's SWR value, a route
723
+ * handler's own header and compat's RSC `private, no-store` all win over it.
709
724
  */
710
725
  export async function finalizeResponse(
711
726
  response: Response,
@@ -714,10 +729,10 @@ export async function finalizeResponse(
714
729
  routeKind: ResponseRouteKind
715
730
  routeMode?: ResponseRouteMode
716
731
  hints?: Record<string, unknown>
732
+ dev?: boolean
717
733
  },
718
734
  ): Promise<Response> {
719
735
  const finalizers = activeHost.request.responseFinalizers
720
- if (finalizers.length === 0) return response
721
736
  const ctx: ResponseFinalizerContext = {
722
737
  request,
723
738
  routeKind: info.routeKind,
@@ -727,6 +742,7 @@ export async function finalizeResponse(
727
742
  headers: response.headers,
728
743
  }
729
744
  for (const finalizer of finalizers) await finalizer(ctx)
745
+ applyDocumentCacheControl(ctx, info.dev === true)
730
746
  if (ctx.status === response.status) return response
731
747
  return new Response(response.body, {
732
748
  status: ctx.status,
@@ -735,6 +751,56 @@ export async function finalizeResponse(
735
751
  })
736
752
  }
737
753
 
754
+ /**
755
+ * What Next answers a DOCUMENT with, byte for byte (measured against stock
756
+ * next@16.2.12, `next start` and `next dev`; see tests/compat/conformance):
757
+ *
758
+ * dev, every document `no-cache, must-revalidate`
759
+ * prod, per-request document `private, no-cache, no-store, max-age=0, must-revalidate`
760
+ * prod, pure prerender (200) `s-maxage=31536000`
761
+ * prod, ISR / `use cache` `s-maxage=<revalidate>, stale-while-revalidate=<expire-revalidate>`
762
+ * route handler nothing, unless it sets `revalidate`
763
+ *
764
+ * Without one of these an intermediary is free to apply heuristic freshness to
765
+ * a personalised document. A non-200 is never a cacheable prerender in Next
766
+ * (its prerendered 404 still answers with the per-request value), so status
767
+ * gates the static case rather than the route's build-time disposition alone —
768
+ * which is also why no 404/500 render spells its own `no-store` any more: the
769
+ * prod value below already carries it, in Next's words.
770
+ */
771
+ const DYNAMIC_DOCUMENT_CACHE_CONTROL = 'private, no-cache, no-store, max-age=0, must-revalidate'
772
+ const DEV_DOCUMENT_CACHE_CONTROL = 'no-cache, must-revalidate'
773
+ const DOCUMENT_EXPIRE_SECONDS = 31536000
774
+
775
+ function applyDocumentCacheControl(ctx: ResponseFinalizerContext, dev: boolean): void {
776
+ if (ctx.routeKind !== 'html') return
777
+ if (ctx.headers.has('cache-control')) return
778
+ if (!(ctx.headers.get('content-type') ?? '').startsWith('text/html')) return
779
+ if (dev) {
780
+ ctx.headers.set('cache-control', DEV_DOCUMENT_CACHE_CONTROL)
781
+ return
782
+ }
783
+ if (ctx.status !== 200) {
784
+ ctx.headers.set('cache-control', DYNAMIC_DOCUMENT_CACHE_CONTROL)
785
+ return
786
+ }
787
+ // Compat sets the SWR value itself (its expireTime is configurable and it also
788
+ // covers pages-router getStaticProps); this only catches a core ISR document,
789
+ // which would otherwise be told never to cache a page that is cacheable for N.
790
+ const revalidate = ctx.routeMode === 'isr' ? ctx.hints?.revalidateSeconds : undefined
791
+ if (typeof revalidate === 'number' && revalidate > 0) {
792
+ const swr = Math.max(0, DOCUMENT_EXPIRE_SECONDS - revalidate)
793
+ ctx.headers.set('cache-control', `s-maxage=${revalidate}, stale-while-revalidate=${swr}`)
794
+ return
795
+ }
796
+ ctx.headers.set(
797
+ 'cache-control',
798
+ ctx.routeMode === 'static'
799
+ ? `s-maxage=${DOCUMENT_EXPIRE_SECONDS}`
800
+ : DYNAMIC_DOCUMENT_CACHE_CONTROL,
801
+ )
802
+ }
803
+
738
804
  export function setRequestExtensions(
739
805
  overrides: Partial<Omit<RequestExtensions, 'interceptors' | 'responseFinalizers'>>,
740
806
  ): void {
@@ -22,7 +22,7 @@ import { restoreWithExtensionHost } from '../extensions'
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  /** Next compat is on (server actions, next/* modules, authInterrupts). */
25
- export function nextCompatEnabled(config: ResolvedConfig): boolean {
25
+ export function nextCompatEnabled(config: Pick<ResolvedConfig, 'compat'>): boolean {
26
26
  return Boolean(config.compat?.next)
27
27
  }
28
28
 
@@ -60,12 +60,23 @@ export function metadataContext(page: MetadataRenderPage): MetadataContext {
60
60
  }
61
61
  }
62
62
 
63
- export function headLinks(page: MetadataRenderPage, context: MetadataContext) {
64
- const canonical = coerceCanonical(page.metadata.alternates, context)
65
- const links: (MetadataLink | ResourceHint)[] = [
66
- ...page.fontPreloads,
67
- ...(page.resourceHints ?? []),
68
- ...stylesheetLinks(page.stylesheets),
63
+ // Links describing the render's own bytes, not metadata - so they stay in the
64
+ // head even when metadata streams into the body.
65
+ export function assetLinks(page: MetadataRenderPage) {
66
+ return assetLinkGroups(page).filter(Boolean).join('\n')
67
+ }
68
+
69
+ export function assetLinkGroups(page: MetadataRenderPage) {
70
+ const connects: ResourceHint[] = []
71
+ const preloads: ResourceHint[] = []
72
+ const lateHints: ResourceHint[] = []
73
+ for (const hint of page.resourceHints ?? []) {
74
+ if (hint.rel === 'preconnect' || hint.rel === 'dns-prefetch') connects.push(hint)
75
+ else if (hint.rel === 'preload' && (hint.as === 'font' || hint.as === 'image'))
76
+ preloads.push(hint)
77
+ else lateHints.push(hint)
78
+ }
79
+ const bulk: (MetadataLink | ResourceHint)[] = [
69
80
  ...(page.modulePreloads ?? []).map(url => ({ rel: 'modulepreload', url })),
70
81
  ...(page.dynamicScriptPreloads ?? []).map(url => ({
71
82
  rel: 'preload',
@@ -76,6 +87,20 @@ export function headLinks(page: MetadataRenderPage, context: MetadataContext) {
76
87
  crossOrigin: 'anonymous' as const,
77
88
  fetchPriority: 'low' as const,
78
89
  })),
90
+ ]
91
+ return [
92
+ renderLinks(connects),
93
+ renderLinks([...page.fontPreloads, ...preloads]),
94
+ renderLinks(stylesheetLinks(page.stylesheets)),
95
+ renderLinks(bulk),
96
+ renderLinks(lateHints),
97
+ ]
98
+ }
99
+
100
+ // Links derived from `metadata` - these travel with it into the body.
101
+ export function metadataOnlyLinks(page: MetadataRenderPage, context: MetadataContext) {
102
+ const canonical = coerceCanonical(page.metadata.alternates, context)
103
+ const links: (MetadataLink | ResourceHint)[] = [
79
104
  ...(canonical ? [{ rel: 'canonical', url: canonical }] : []),
80
105
  ...staticMetadataLinks(page),
81
106
  ...authorLinks(page.metadata.authors, context),
@@ -86,6 +111,14 @@ export function headLinks(page: MetadataRenderPage, context: MetadataContext) {
86
111
  ...coerceIcons(page.metadata.icons, context),
87
112
  ...(page.metadata.links ?? []),
88
113
  ]
114
+ return renderLinks(links)
115
+ }
116
+
117
+ export function headLinks(page: MetadataRenderPage, context: MetadataContext) {
118
+ return [assetLinks(page), metadataOnlyLinks(page, context)].filter(Boolean).join('\n')
119
+ }
120
+
121
+ function renderLinks(links: (MetadataLink | ResourceHint)[]) {
89
122
  return links.map(link => ` <link ${linkAttrs(link)}/>`).join('\n')
90
123
  }
91
124
 
@@ -122,11 +155,13 @@ function staticMetadataLinks(page: MetadataRenderPage): MetadataLink[] {
122
155
  ...(!hasExplicitManifest && metadata.manifest ? [staticMetadataLink(metadata.manifest)] : []),
123
156
  ...metadata.icons.map(staticMetadataLink),
124
157
  ...(metadata.dynamicIcons ?? []),
125
- ...metadata.appleIcons.map(staticMetadataLink),
126
- ...(metadata.dynamicAppleIcons ?? []),
158
+ // Root icons share the icon bucket. Keeping them after apple icons made
159
+ // the link order differ from Next whenever both conventions were present.
127
160
  ...(!hasExplicitIcons && metadata.icons.length === 0
128
161
  ? metadata.rootIcons.map(staticMetadataLink)
129
162
  : []),
163
+ ...metadata.appleIcons.map(staticMetadataLink),
164
+ ...(metadata.dynamicAppleIcons ?? []),
130
165
  ])
131
166
  }
132
167
 
@@ -233,8 +268,9 @@ export function metadataTags(metadata: Metadata, context: MetadataContext): stri
233
268
  }
234
269
  for (const [property, value] of appLinkTags(meta.appLinks)) pushProperty(property, value)
235
270
 
236
- tags.push(...openGraphTags(meta.openGraph, context))
237
- tags.push(...twitterTags(meta.twitter ?? twitterFromOpenGraph(meta.openGraph), context))
271
+ const social = resolveSocialMetadata(meta)
272
+ tags.push(...openGraphTags(social.openGraph, context))
273
+ tags.push(...twitterTags(social.twitter, context))
238
274
 
239
275
  return tags.join('\n ')
240
276
  }
@@ -831,17 +867,53 @@ function twitterCard(twitter: MetadataTwitter) {
831
867
  return twitter.images === undefined ? 'summary' : 'summary_large_image'
832
868
  }
833
869
 
834
- function twitterFromOpenGraph(
835
- openGraph: MetadataOpenGraph | undefined,
836
- ): MetadataTwitter | undefined {
837
- if (!openGraph) return undefined
870
+ // og/twitter resolve in one final pass over the merged chain: twitter auto-fills
871
+ // from openGraph, then whichever of the two exists inherits the page
872
+ // title/description it still lacks. Emission is that pass - nothing between the
873
+ // merge and here reads these fields, and the parent handed to generateMetadata
874
+ // is pre-merge by design.
875
+ function resolveSocialMetadata(meta: Metadata): {
876
+ openGraph?: MetadataOpenGraph
877
+ twitter?: MetadataTwitter
878
+ } {
879
+ const og = meta.openGraph
880
+ let twitter = meta.twitter
881
+ if (og) {
882
+ const ownImages = twitter && 'images' in twitter && twitter.images
883
+ twitter = {
884
+ ...twitter,
885
+ // card follows twitter's OWN images: an og image inherited below never
886
+ // upgrades a declared twitter block to summary_large_image.
887
+ card: twitterCard(twitter ?? { images: og.images }),
888
+ title: titleText(twitter?.title) === undefined ? og.title : twitter?.title,
889
+ description: twitter?.description || og.description,
890
+ images: ownImages ? twitter?.images : og.images,
891
+ }
892
+ }
893
+ return { openGraph: og && inherited(og, meta), twitter: twitter && inherited(twitter, meta) }
894
+ }
895
+
896
+ // A block that renders no title/description of its own falls back to the page's.
897
+ function inherited<T extends { title?: Metadata['title']; description?: string }>(
898
+ block: T,
899
+ meta: Metadata,
900
+ ): T {
901
+ const own = titleText(block.title)
902
+ if (own !== undefined && block.description) return block
838
903
  return {
839
- title: openGraph.title,
840
- description: openGraph.description,
841
- images: openGraph.images,
904
+ ...block,
905
+ title: own === undefined ? titleText(meta.title) : block.title,
906
+ description: block.description || meta.description,
842
907
  }
843
908
  }
844
909
 
910
+ // The title a chain entry actually renders, or undefined when it renders none
911
+ // (a bare `template`, an empty string) and so counts as missing for inheritance.
912
+ function titleText(title: Metadata['title']): string | undefined {
913
+ if (title === undefined) return undefined
914
+ return resolveTitleChain([{ title }]) || undefined
915
+ }
916
+
845
917
  export function mergeViewport(...items: Viewport[]) {
846
918
  return items.reduce<Viewport>(
847
919
  (viewport, item) => ({