@weaverse/next 0.1.0-alpha.8 → 1.0.0

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 (106) hide show
  1. package/README.md +155 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-LH6AAEIK.mjs} +115 -8
  3. package/dist/chunk-LH6AAEIK.mjs.map +1 -0
  4. package/dist/index.js +260 -30
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +167 -46
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +321 -91
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +205 -28
  11. package/dist/server.mjs.map +1 -1
  12. package/dist/types/client.d.ts +16 -0
  13. package/dist/types/client.d.ts.map +1 -0
  14. package/dist/types/hooks.d.ts +20 -0
  15. package/dist/types/hooks.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +34 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/item.d.ts +60 -0
  19. package/dist/types/item.d.ts.map +1 -0
  20. package/dist/types/loader.d.ts +25 -0
  21. package/dist/types/loader.d.ts.map +1 -0
  22. package/dist/types/main.d.ts +11 -0
  23. package/dist/types/main.d.ts.map +1 -0
  24. package/dist/types/provider.d.ts +82 -0
  25. package/dist/types/provider.d.ts.map +1 -0
  26. package/dist/types/registry.d.ts +15 -0
  27. package/dist/types/registry.d.ts.map +1 -0
  28. package/dist/types/renderer.d.ts +21 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +19 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +82 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +62 -0
  35. package/dist/types/root-provider.d.ts.map +1 -0
  36. package/dist/types/runtime.d.ts +113 -0
  37. package/dist/types/runtime.d.ts.map +1 -0
  38. package/dist/types/seo.d.ts +55 -0
  39. package/dist/types/seo.d.ts.map +1 -0
  40. package/dist/types/server/configs.d.ts +27 -0
  41. package/dist/types/server/configs.d.ts.map +1 -0
  42. package/dist/types/server/normalize-page-url.d.ts +19 -0
  43. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  44. package/dist/types/server/revalidate-handler.d.ts +55 -0
  45. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  46. package/dist/types/server/server-client.d.ts +21 -0
  47. package/dist/types/server/server-client.d.ts.map +1 -0
  48. package/dist/types/server.d.ts +19 -0
  49. package/dist/types/server.d.ts.map +1 -0
  50. package/dist/types/studio-bridge.d.ts +16 -0
  51. package/dist/types/studio-bridge.d.ts.map +1 -0
  52. package/dist/types/studio-connect.d.ts +27 -0
  53. package/dist/types/studio-connect.d.ts.map +1 -0
  54. package/dist/types/studio-router.d.ts +50 -0
  55. package/dist/types/studio-router.d.ts.map +1 -0
  56. package/dist/types/studio-script-src.d.ts +12 -0
  57. package/dist/types/studio-script-src.d.ts.map +1 -0
  58. package/dist/types/theme-settings-store.d.ts +18 -0
  59. package/dist/types/theme-settings-store.d.ts.map +1 -0
  60. package/dist/types/translation-context.d.ts +156 -0
  61. package/dist/types/translation-context.d.ts.map +1 -0
  62. package/dist/types/translation-store.d.ts +44 -0
  63. package/dist/types/translation-store.d.ts.map +1 -0
  64. package/dist/types/types.d.ts +665 -0
  65. package/dist/types/types.d.ts.map +1 -0
  66. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  67. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  68. package/dist/types/utils.d.ts +12 -0
  69. package/dist/types/utils.d.ts.map +1 -0
  70. package/package.json +12 -8
  71. package/src/client.ts +102 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +167 -0
  75. package/src/loader.ts +135 -0
  76. package/src/main.tsx +45 -0
  77. package/src/provider.tsx +225 -0
  78. package/src/registry.ts +63 -0
  79. package/src/renderer.tsx +100 -0
  80. package/src/request-info.ts +124 -0
  81. package/src/revalidate-item.ts +211 -0
  82. package/src/root-provider.tsx +182 -0
  83. package/src/runtime.ts +555 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +178 -0
  86. package/src/server/normalize-page-url.ts +73 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +728 -0
  89. package/src/server.ts +67 -0
  90. package/src/studio-bridge.tsx +82 -0
  91. package/src/studio-connect.tsx +88 -0
  92. package/src/studio-router.ts +85 -0
  93. package/src/studio-script-src.ts +128 -0
  94. package/src/theme-settings-store.ts +47 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +753 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-6PMZ77RA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -517
  102. package/dist/index.d.ts +0 -517
  103. package/dist/seo-CpN6tC36.d.mts +0 -477
  104. package/dist/seo-CpN6tC36.d.ts +0 -477
  105. package/dist/server.d.mts +0 -99
  106. package/dist/server.d.ts +0 -99
package/src/seo.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { PageSEOData } from '@weaverse/schema'
2
+ import type { Metadata } from 'next'
3
+ import type { WeaverseNextLoaderData, WeaverseNextPageData } from './types'
4
+
5
+ /** Object containing an optional Weaverse page for SEO extraction. */
6
+ export interface WeaverseNextSeoPageContainer {
7
+ /** Page whose published SEO fields should be formatted. */
8
+ page?: WeaverseNextPageData | null
9
+ }
10
+
11
+ /** Input accepted by {@link getWeaverseNextSeoMetadata}. */
12
+ export type WeaverseNextSeoInput =
13
+ | WeaverseNextLoaderData
14
+ | WeaverseNextPageData
15
+ | WeaverseNextSeoPageContainer
16
+ | null
17
+ | undefined
18
+
19
+ /** Open Graph metadata accepted by Next.js's `Metadata` API. */
20
+ export type NextOpenGraph = NonNullable<Metadata['openGraph']>
21
+
22
+ /** Twitter card metadata accepted by Next.js's `Metadata` API. */
23
+ export type NextTwitter = NonNullable<Metadata['twitter']>
24
+
25
+ /**
26
+ * Weaverse page SEO expressed as a subset of Next's `Metadata`. Assignable to
27
+ * `Metadata` as-is: `const metadata: Metadata = getWeaverseNextSeoMetadata(data)`.
28
+ */
29
+ export interface WeaverseNextSeoMetadata {
30
+ /** Canonical URL settings for Next's metadata API. */
31
+ alternates?: {
32
+ /** Preferred URL for search indexing. */
33
+ canonical?: string
34
+ }
35
+ /** Page meta description. */
36
+ description?: string
37
+ /** Page meta keywords. */
38
+ keywords?: string
39
+ /** Open Graph metadata normalized to Next's supported union. */
40
+ openGraph?: NextOpenGraph
41
+ /** Search crawler indexing and link-following directives. */
42
+ robots: {
43
+ /** Whether crawlers may follow links on the page. */
44
+ follow: boolean
45
+ /** Whether crawlers may index the page. */
46
+ index: boolean
47
+ }
48
+ /** Page title. */
49
+ title?: string
50
+ /** Twitter card metadata normalized for Next's metadata API. */
51
+ twitter?: NextTwitter
52
+ }
53
+
54
+ function hasOpenGraphContent(seo: PageSEOData['openGraph']): boolean {
55
+ return Boolean(seo?.title || seo?.description || seo?.image || seo?.type)
56
+ }
57
+
58
+ function hasTwitterContent(seo: PageSEOData['twitter']): boolean {
59
+ return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType)
60
+ }
61
+
62
+ function normalizeTwitterCard(
63
+ cardType: NonNullable<PageSEOData['twitter']>['cardType']
64
+ ): 'summary' | 'summary_large_image' {
65
+ return cardType === 'summary_large_image' ? 'summary_large_image' : 'summary'
66
+ }
67
+
68
+ /**
69
+ * Map a Builder Open Graph type onto Next's `OpenGraphType` union. Every
70
+ * Builder value except `product` exists in Next's union and is preserved
71
+ * verbatim; `product` is not modeled by Next and makes `generateMetadata()`
72
+ * throw, so it degrades to `website` — the closest renderable equivalent.
73
+ */
74
+ function normalizeOpenGraphType(
75
+ type: NonNullable<PageSEOData['openGraph']>['type']
76
+ ): 'article' | 'profile' | 'video.other' | 'website' {
77
+ if (type === 'article' || type === 'profile' || type === 'video.other') {
78
+ return type
79
+ }
80
+ return 'website'
81
+ }
82
+
83
+ /**
84
+ * Convert Builder SEO fields into metadata accepted by Next.js.
85
+ *
86
+ * Unsupported Open Graph product types and incomplete Twitter app/player
87
+ * cards are safely downgraded to renderable Next.js variants.
88
+ */
89
+ export function formatWeaverseNextSeoMetadata(
90
+ seo: PageSEOData | null | undefined
91
+ ): WeaverseNextSeoMetadata {
92
+ let index = seo?.robots?.index !== false
93
+ let follow = seo?.robots?.follow !== false
94
+ let metadata: WeaverseNextSeoMetadata = {
95
+ robots: { index, follow },
96
+ }
97
+
98
+ if (!seo) {
99
+ return metadata
100
+ }
101
+
102
+ if (seo.title) {
103
+ metadata.title = seo.title
104
+ }
105
+ if (seo.description) {
106
+ metadata.description = seo.description
107
+ }
108
+ if (seo.keywords) {
109
+ metadata.keywords = seo.keywords
110
+ }
111
+ if (seo.canonicalUrl) {
112
+ metadata.alternates = { canonical: seo.canonicalUrl }
113
+ }
114
+
115
+ let openGraph = seo.openGraph
116
+ if (hasOpenGraphContent(openGraph)) {
117
+ metadata.openGraph = {
118
+ title: openGraph?.title,
119
+ description: openGraph?.description,
120
+ images: openGraph?.image ? [openGraph.image] : undefined,
121
+ type: normalizeOpenGraphType(openGraph?.type),
122
+ }
123
+ }
124
+
125
+ let twitter = seo.twitter
126
+ if (hasTwitterContent(twitter)) {
127
+ // Builder does not collect the descriptors Next requires for `app` cards
128
+ // (Next throws when they are missing) or `player` cards (which render an
129
+ // empty, useless card without a player URL and dimensions). Downgrade both
130
+ // to `summary` rather than emitting metadata Next cannot render.
131
+ metadata.twitter = {
132
+ card: normalizeTwitterCard(twitter?.cardType),
133
+ title: twitter?.title,
134
+ description: twitter?.description,
135
+ images: twitter?.image ? [twitter.image] : undefined,
136
+ }
137
+ }
138
+
139
+ return metadata
140
+ }
141
+
142
+ /**
143
+ * Read page SEO from loader data or a page payload and format it for Next.js
144
+ * `generateMetadata`. Missing data returns indexable/followable robot defaults.
145
+ */
146
+ export function getWeaverseNextSeoMetadata(
147
+ data: WeaverseNextSeoInput
148
+ ): WeaverseNextSeoMetadata {
149
+ if (!data) {
150
+ return formatWeaverseNextSeoMetadata(null)
151
+ }
152
+ let page =
153
+ 'items' in data && 'id' in data
154
+ ? (data as WeaverseNextPageData)
155
+ : ((data as WeaverseNextSeoPageContainer).page ?? null)
156
+ return formatWeaverseNextSeoMetadata(page?.seo ?? null)
157
+ }
@@ -0,0 +1,178 @@
1
+ import type {
2
+ WeaverseNextBaseConfigs,
3
+ WeaverseNextRequestContext,
4
+ } from '../types'
5
+
6
+ const DEFAULT_WEAVERSE_HOST = 'https://studio.weaverse.io'
7
+ const DEFAULT_WEAVERSE_API_BASE = 'https://api.weaverse.io'
8
+
9
+ /**
10
+ * Weaverse-controlled apex domains the Studio bridge / data API may be served
11
+ * from (matched on the host itself and any subdomain). Kept in sync with
12
+ * Hydrogen's `isTrustedStudioHost`.
13
+ */
14
+ const TRUSTED_STUDIO_DOMAINS = ['weaverse.io', 'weaverse.dev']
15
+
16
+ /**
17
+ * Whether `host` is an origin we trust to resolve page/theme data. The
18
+ * `weaverseHost` query param is attacker-controllable, so a crafted
19
+ * `?weaverseHost=` must be rejected before it can point server-side fetches at
20
+ * an arbitrary origin (SSRF). Only `weaverse.io`/`weaverse.dev` and subdomains
21
+ * over https are trusted.
22
+ */
23
+ function isTrustedStudioHost(host: string): boolean {
24
+ let parsed: URL
25
+ try {
26
+ parsed = new URL(host)
27
+ } catch {
28
+ return false
29
+ }
30
+ if (parsed.protocol !== 'https:') {
31
+ return false
32
+ }
33
+ let { hostname } = parsed
34
+ return TRUSTED_STUDIO_DOMAINS.some(
35
+ (domain) => hostname === domain || hostname.endsWith(`.${domain}`)
36
+ )
37
+ }
38
+
39
+ function toUrl(value: string | URL): URL | null {
40
+ try {
41
+ return typeof value === 'string'
42
+ ? new URL(value, 'http://localhost')
43
+ : value
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Extract the request search params from the explicit Next request context.
51
+ * Prefers `searchParams`, falls back to parsing `url`.
52
+ */
53
+ export function getContextSearchParams(
54
+ context?: WeaverseNextRequestContext
55
+ ): URLSearchParams {
56
+ if (context?.searchParams) {
57
+ return new URLSearchParams(context.searchParams)
58
+ }
59
+ if (context?.url) {
60
+ let url = toUrl(context.url)
61
+ if (url) {
62
+ return new URLSearchParams(url.searchParams)
63
+ }
64
+ }
65
+ return new URLSearchParams()
66
+ }
67
+
68
+ /**
69
+ * Last value of a (possibly duplicated) query key. Matches Hydrogen's
70
+ * `getRequestQueries`, which assigns while iterating and so keeps the last
71
+ * occurrence — Studio appends its own control params, so a stale earlier value
72
+ * must not win.
73
+ */
74
+ function lastParam(
75
+ searchParams: URLSearchParams,
76
+ key: string
77
+ ): string | undefined {
78
+ let values = searchParams.getAll(key)
79
+ return values.length ? values.at(-1) : undefined
80
+ }
81
+
82
+ function coerceBool(value: string | undefined): boolean {
83
+ return value === 'true'
84
+ }
85
+
86
+ function readEnv(
87
+ env: Record<string, string | undefined> | undefined,
88
+ key: string
89
+ ): string | undefined {
90
+ let fromConfig = env?.[key]
91
+ if (fromConfig !== undefined) {
92
+ return fromConfig
93
+ }
94
+ if (typeof process !== 'undefined' && process.env) {
95
+ return process.env[key]
96
+ }
97
+ return
98
+ }
99
+
100
+ /** Overrides used while resolving request-scoped Weaverse configuration. */
101
+ export interface ResolveConfigsOptions {
102
+ /** Environment values; missing keys fall back to `process.env`. */
103
+ env?: Record<string, string | undefined>
104
+ /** Public data API base override. */
105
+ weaverseApiBase?: string
106
+ /** Studio origin override. */
107
+ weaverseHost?: string
108
+ /** Studio asset version override. */
109
+ weaverseVersion?: string
110
+ }
111
+
112
+ /**
113
+ * Derive Weaverse configs from the request context and env, close to Hydrogen's
114
+ * `getWeaverseConfigs`. Resolves the trusted host, public API base, design /
115
+ * preview / revision flags, and public env — but never resolves the final
116
+ * `projectId` (that priority chain, which may call an async resolver, lives in
117
+ * the server client). Query/env projectId candidates are returned for the
118
+ * client to combine with function/string inputs.
119
+ */
120
+ export function getWeaverseNextConfigs(
121
+ context?: WeaverseNextRequestContext,
122
+ options: ResolveConfigsOptions = {}
123
+ ): WeaverseNextBaseConfigs {
124
+ let searchParams = getContextSearchParams(context)
125
+ let { env } = options
126
+
127
+ let queryHost = lastParam(searchParams, 'weaverseHost')
128
+ // A trusted query host is canonicalized to its origin before use, so a crafted
129
+ // `?weaverseHost=https://studio.weaverse.io/evil?x=1` can only ever resolve to
130
+ // `https://studio.weaverse.io` — never carrying an attacker path/query into the
131
+ // server-side API base.
132
+ let trustedUrlHost =
133
+ queryHost && isTrustedStudioHost(queryHost) ? new URL(queryHost).origin : ''
134
+
135
+ let configuredHost =
136
+ options.weaverseHost || readEnv(env, 'WEAVERSE_HOST') || ''
137
+ let weaverseHost = trustedUrlHost || configuredHost || DEFAULT_WEAVERSE_HOST
138
+
139
+ // Public data reads default to the edge proxy, but an explicit custom host
140
+ // (staging/self-hosted Weaverse) must stay the API base so those deployments
141
+ // don't fetch page/config data from the production proxy.
142
+ let weaverseApiBase =
143
+ trustedUrlHost ||
144
+ options.weaverseApiBase ||
145
+ readEnv(env, 'WEAVERSE_PUBLIC_API_BASE') ||
146
+ (configuredHost && configuredHost !== DEFAULT_WEAVERSE_HOST
147
+ ? configuredHost
148
+ : DEFAULT_WEAVERSE_API_BASE)
149
+
150
+ let isRevisionPreview =
151
+ context?.isRevisionPreview ?? searchParams.has('__revisionId')
152
+ let isDesignMode =
153
+ context?.isDesignMode ?? coerceBool(lastParam(searchParams, 'isDesignMode'))
154
+ let isPreviewMode =
155
+ context?.isPreviewMode ??
156
+ coerceBool(lastParam(searchParams, 'isPreviewMode'))
157
+
158
+ return {
159
+ queryProjectId: lastParam(searchParams, 'weaverseProjectId') || '',
160
+ envProjectId: readEnv(env, 'WEAVERSE_PROJECT_ID') || '',
161
+ weaverseHost,
162
+ weaverseApiBase,
163
+ weaverseVersion:
164
+ lastParam(searchParams, 'weaverseVersion') ||
165
+ options.weaverseVersion ||
166
+ '',
167
+ isDesignMode,
168
+ isPreviewMode,
169
+ isRevisionPreview,
170
+ sectionType:
171
+ context?.sectionType || lastParam(searchParams, 'sectionType') || '',
172
+ publicEnv: {
173
+ PUBLIC_STORE_DOMAIN: readEnv(env, 'PUBLIC_STORE_DOMAIN') || '',
174
+ PUBLIC_STOREFRONT_API_TOKEN:
175
+ readEnv(env, 'PUBLIC_STOREFRONT_API_TOKEN') || '',
176
+ },
177
+ }
178
+ }
@@ -0,0 +1,73 @@
1
+ import { withPathPrefix } from '../request-info'
2
+ import type { WeaverseNextRequestContext } from '../types'
3
+
4
+ // Query params that influence Weaverse page resolution on the Builder:
5
+ // revision previews, design-mode flags, design-mode draft item overrides,
6
+ // and `weaverse*` control params (host, version, project id). Everything
7
+ // else — utm_*, fbclid, gclid, storefront filters — is irrelevant to which
8
+ // page the Builder returns and would otherwise fragment any fetch cache keyed
9
+ // on the request body. `weaverseDraftItem` is the current draft item param;
10
+ // `__weaverseDraftItem` is kept as a legacy compatibility alias.
11
+ const KEPT_PARAM_RE =
12
+ /^(?:__revisionId|__weaverseDraftItem|isDesignMode|weaverse)/
13
+ const DATA_SUFFIX = '.data'
14
+
15
+ /**
16
+ * Normalize the storefront URL sent in the Builder page API request body.
17
+ *
18
+ * The Builder reads only the pathname (locale/type/handle fallback) and the
19
+ * Weaverse/Studio control params from this URL, so keep exactly those and drop
20
+ * tracking params. Params are sorted for a stable, cache-friendly string.
21
+ *
22
+ * Ported from `@weaverse/hydrogen`'s `normalizePageUrl`, but tolerant of a
23
+ * relative URL (Next route handlers may only know the pathname).
24
+ */
25
+ export function normalizeNextPageUrl(url: string): string {
26
+ let parsed: URL
27
+ try {
28
+ parsed = new URL(url)
29
+ } catch {
30
+ try {
31
+ parsed = new URL(url, 'http://localhost')
32
+ } catch {
33
+ return url
34
+ }
35
+ }
36
+
37
+ let kept = [...parsed.searchParams].filter(([key]) => KEPT_PARAM_RE.test(key))
38
+ kept.sort(([a], [b]) => a.localeCompare(b))
39
+ let search = new URLSearchParams(kept).toString()
40
+
41
+ let { pathname } = parsed
42
+ if (pathname.endsWith(DATA_SUFFIX)) {
43
+ pathname = pathname.slice(0, -DATA_SUFFIX.length)
44
+ }
45
+ return `${parsed.origin}${pathname}${search ? `?${search}` : ''}`
46
+ }
47
+
48
+ /**
49
+ * Build an absolute-ish request URL string from the explicit request context,
50
+ * preferring `url`, then `pathname` + `searchParams`. Used as the input to
51
+ * {@link normalizeNextPageUrl}.
52
+ */
53
+ export function resolveRequestUrl(
54
+ context?: WeaverseNextRequestContext
55
+ ): string {
56
+ if (context?.url) {
57
+ let raw = String(context.url)
58
+ let url = new URL(raw, 'http://localhost')
59
+ let pathname = withPathPrefix(url.pathname, context.i18n)
60
+ // Unchanged when there is nothing to prefix, so a relative or bare-origin
61
+ // URL is passed on exactly as the app gave it.
62
+ if (pathname === url.pathname) {
63
+ return raw
64
+ }
65
+ url.pathname = pathname
66
+ return url.toString()
67
+ }
68
+ let pathname = withPathPrefix(context?.pathname || '/', context?.i18n)
69
+ let search = context?.searchParams
70
+ ? new URLSearchParams(context.searchParams).toString()
71
+ : ''
72
+ return `http://localhost${pathname}${search ? `?${search}` : ''}`
73
+ }