@wular/pnext 0.0.20 → 0.0.22
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/package.json +1 -1
- package/src/cli/build/cache.ts +324 -0
- package/src/cli/build.ts +19 -3
- package/src/cli/create.ts +4 -4
- package/src/cli/migrate/package-json.ts +3 -2
- package/src/cli/serve/entry.ts +14 -4
- package/src/cli/serve/pipeline.ts +4 -0
- package/src/cli/start.ts +18 -11
- package/src/cli/typegen.ts +4 -1
- package/src/client/build.ts +13 -10
- package/src/client/entry.ts +201 -12
- package/src/client/router/runtime.ts +20 -6
- package/src/compat/metadata.ts +37 -4
- package/src/compat/next/config-loader.ts +7 -1
- package/src/compat/register/proxy.ts +3 -1
- package/src/compat/register/typed-routes.ts +3 -1
- package/src/compat/typecheck/check.ts +5 -1
- package/src/compat/validation/validate.ts +2 -0
- package/src/config.ts +7 -0
- package/src/dev/server.ts +2 -0
- package/src/extensions.ts +7 -1
- package/src/render/metadata.ts +53 -31
- package/src/render/renderer.ts +3 -2
- package/src/resolve/imports.ts +7 -1
- package/src/resolve/scan-facts.ts +7 -1
- package/src/runtime/fetch-host.ts +46 -2
- package/src/types.ts +84 -67
- package/src/utils/registry.ts +70 -0
package/src/render/metadata.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
MetadataLink,
|
|
8
8
|
MetadataOpenGraph,
|
|
9
9
|
MetadataRobots,
|
|
10
|
+
MetadataValue,
|
|
10
11
|
ResourceHint,
|
|
11
12
|
MetadataTwitter,
|
|
12
13
|
Viewport,
|
|
@@ -19,6 +20,13 @@ import {
|
|
|
19
20
|
import { escapeHtml, stringifyHtmlValue } from '../utils/html'
|
|
20
21
|
import { getTrailingSlashUrls, withBasePath } from '../routing/href'
|
|
21
22
|
|
|
23
|
+
/** Every metadata field is nullable: null means "not set", exactly like undefined. */
|
|
24
|
+
type MetaContent = MetadataValue | null | undefined
|
|
25
|
+
|
|
26
|
+
function absent(value: unknown): value is null | undefined {
|
|
27
|
+
return value === undefined || value === null
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
export type MetadataExport = Metadata | (() => Metadata | Promise<Metadata>)
|
|
23
31
|
|
|
24
32
|
export type ViewportExport = Viewport | (() => Viewport | Promise<Viewport>)
|
|
@@ -224,12 +232,15 @@ function prefixImageBasePath(image: MetadataImage): MetadataImage {
|
|
|
224
232
|
export function metadataTags(metadata: Metadata, context: MetadataContext): string {
|
|
225
233
|
const meta = metadata
|
|
226
234
|
const tags: string[] = []
|
|
227
|
-
const push = (name: string, content:
|
|
228
|
-
if (content)
|
|
235
|
+
const push = (name: string, content: MetaContent) => {
|
|
236
|
+
if (content)
|
|
237
|
+
tags.push(`<meta name="${escapeHtml(name)}" content="${escapeHtml(coerceString(content))}"/>`)
|
|
229
238
|
}
|
|
230
|
-
const pushProperty = (property: string, content:
|
|
239
|
+
const pushProperty = (property: string, content: MetaContent) => {
|
|
231
240
|
if (content)
|
|
232
|
-
tags.push(
|
|
241
|
+
tags.push(
|
|
242
|
+
`<meta property="${escapeHtml(property)}" content="${escapeHtml(coerceString(content))}"/>`,
|
|
243
|
+
)
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
push('description', meta.description)
|
|
@@ -263,8 +274,8 @@ export function metadataTags(metadata: Metadata, context: MetadataContext): stri
|
|
|
263
274
|
}
|
|
264
275
|
if (meta.facebook?.appId) pushProperty('fb:app_id', meta.facebook.appId)
|
|
265
276
|
for (const admin of list(meta.facebook?.admins)) pushProperty('fb:admins', admin)
|
|
266
|
-
if (meta.pinterest?.richPin
|
|
267
|
-
pushProperty('pinterest-rich-pin', meta.pinterest.richPin
|
|
277
|
+
if (!absent(meta.pinterest?.richPin)) {
|
|
278
|
+
pushProperty('pinterest-rich-pin', String(meta.pinterest.richPin))
|
|
268
279
|
}
|
|
269
280
|
for (const [property, value] of appLinkTags(meta.appLinks)) pushProperty(property, value)
|
|
270
281
|
|
|
@@ -278,13 +289,15 @@ export function metadataTags(metadata: Metadata, context: MetadataContext): stri
|
|
|
278
289
|
function openGraphTags(og: MetadataOpenGraph | undefined, context: MetadataContext): string[] {
|
|
279
290
|
if (!og) return []
|
|
280
291
|
const tags: string[] = []
|
|
281
|
-
const push = (property: string, content:
|
|
292
|
+
const push = (property: string, content: MetaContent) => {
|
|
282
293
|
if (content)
|
|
283
|
-
tags.push(
|
|
294
|
+
tags.push(
|
|
295
|
+
`<meta property="${escapeHtml(property)}" content="${escapeHtml(coerceString(content))}"/>`,
|
|
296
|
+
)
|
|
284
297
|
}
|
|
285
298
|
push('og:title', og.title === undefined ? undefined : resolveTitleChain([{ title: og.title }]))
|
|
286
299
|
push('og:description', og.description)
|
|
287
|
-
push('og:url',
|
|
300
|
+
push('og:url', resolveMetadataUrl(og.url, context))
|
|
288
301
|
push('og:site_name', og.siteName)
|
|
289
302
|
push('og:locale', og.locale)
|
|
290
303
|
push('og:type', og.type)
|
|
@@ -313,8 +326,9 @@ function openGraphTags(og: MetadataOpenGraph | undefined, context: MetadataConte
|
|
|
313
326
|
function twitterTags(twitter: MetadataTwitter | undefined, context: MetadataContext): string[] {
|
|
314
327
|
if (!twitter) return []
|
|
315
328
|
const tags: string[] = []
|
|
316
|
-
const push = (name: string, content:
|
|
317
|
-
if (content)
|
|
329
|
+
const push = (name: string, content: MetaContent) => {
|
|
330
|
+
if (content)
|
|
331
|
+
tags.push(`<meta name="${escapeHtml(name)}" content="${escapeHtml(coerceString(content))}"/>`)
|
|
318
332
|
}
|
|
319
333
|
push('twitter:card', twitterCard(twitter))
|
|
320
334
|
push('twitter:site', twitter.site)
|
|
@@ -334,10 +348,13 @@ function twitterTags(twitter: MetadataTwitter | undefined, context: MetadataCont
|
|
|
334
348
|
push('twitter:image:alt', image.alt)
|
|
335
349
|
push('twitter:image:type', image.type)
|
|
336
350
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
const players = twitter.players === undefined ? [] : [twitter.players].flat()
|
|
352
|
+
for (const player of players) {
|
|
353
|
+
push('twitter:player', resolveMetadataUrl(player.playerUrl, context))
|
|
354
|
+
push('twitter:player:stream', resolveMetadataUrl(player.streamUrl, context))
|
|
355
|
+
push('twitter:player:width', coerceString(player.width) || undefined)
|
|
356
|
+
push('twitter:player:height', coerceString(player.height) || undefined)
|
|
357
|
+
}
|
|
341
358
|
for (const [device, id] of Object.entries(twitter.app?.id ?? {})) {
|
|
342
359
|
push(`twitter:app:id:${device}`, id)
|
|
343
360
|
}
|
|
@@ -349,7 +366,7 @@ function twitterTags(twitter: MetadataTwitter | undefined, context: MetadataCont
|
|
|
349
366
|
|
|
350
367
|
// theme-color: string | { color, media } | array of those.
|
|
351
368
|
export function themeColorTags(value: Viewport['themeColor']): string {
|
|
352
|
-
if (value
|
|
369
|
+
if (absent(value)) return ''
|
|
353
370
|
const list = Array.isArray(value) ? value : [value]
|
|
354
371
|
return list
|
|
355
372
|
.map(entry => {
|
|
@@ -514,13 +531,13 @@ function coerceKeywords(value: Metadata['keywords']): string | undefined {
|
|
|
514
531
|
}
|
|
515
532
|
|
|
516
533
|
function normalizeKeywordList(value: Metadata['keywords']): (string | number)[] | undefined {
|
|
517
|
-
if (value
|
|
534
|
+
if (absent(value)) return undefined
|
|
518
535
|
return Array.isArray(value) ? value : [value]
|
|
519
536
|
}
|
|
520
537
|
|
|
521
538
|
// authors -> list of display names for meta[name=author].
|
|
522
539
|
function coerceAuthors(value: Metadata['authors']): string[] {
|
|
523
|
-
if (value
|
|
540
|
+
if (absent(value)) return []
|
|
524
541
|
const list = Array.isArray(value) ? value : [value]
|
|
525
542
|
return list
|
|
526
543
|
.map(author => (typeof author === 'string' ? author : author.name))
|
|
@@ -530,7 +547,7 @@ function coerceAuthors(value: Metadata['authors']): string[] {
|
|
|
530
547
|
|
|
531
548
|
// robots: string passthrough, or object -> "index, follow, noarchive" style.
|
|
532
549
|
function coerceRobots(value: Metadata['robots']): string | undefined {
|
|
533
|
-
if (value
|
|
550
|
+
if (absent(value)) return undefined
|
|
534
551
|
if (typeof value === 'string') return value
|
|
535
552
|
const flags: string[] = []
|
|
536
553
|
const r = value
|
|
@@ -601,7 +618,7 @@ function coerceCanonical(
|
|
|
601
618
|
context: MetadataContext,
|
|
602
619
|
): string | undefined {
|
|
603
620
|
const canonical = value?.canonical
|
|
604
|
-
if (canonical
|
|
621
|
+
if (absent(canonical)) return undefined
|
|
605
622
|
if (typeof canonical === 'string') return resolveMetadataUrl(canonical, context)
|
|
606
623
|
if (canonical instanceof URL) return canonical.href
|
|
607
624
|
if (typeof canonical === 'object' && 'url' in canonical)
|
|
@@ -687,7 +704,7 @@ function joinUrlPath(...parts: string[]) {
|
|
|
687
704
|
|
|
688
705
|
// Normalizes every icon form (string | URL | object | group) into <link> descriptors.
|
|
689
706
|
function coerceIcons(value: Metadata['icons'], context: MetadataContext): MetadataLink[] {
|
|
690
|
-
if (value
|
|
707
|
+
if (absent(value)) return []
|
|
691
708
|
if (isIconGroups(value)) {
|
|
692
709
|
return [
|
|
693
710
|
...iconList(value.icon, 'icon', context),
|
|
@@ -706,11 +723,11 @@ function isIconGroups(value: NonNullable<Metadata['icons']>): value is MetadataI
|
|
|
706
723
|
}
|
|
707
724
|
|
|
708
725
|
function iconList(
|
|
709
|
-
value: MetadataIcon | MetadataIcon[] | undefined,
|
|
726
|
+
value: MetadataIcon | MetadataIcon[] | null | undefined,
|
|
710
727
|
rel: string,
|
|
711
728
|
context: MetadataContext,
|
|
712
729
|
): MetadataLink[] {
|
|
713
|
-
if (value
|
|
730
|
+
if (absent(value)) return []
|
|
714
731
|
const list = Array.isArray(value) ? value : [value]
|
|
715
732
|
return list.map((icon): MetadataLink => {
|
|
716
733
|
if (typeof icon === 'string') return { rel, url: resolveMetadataUrl(icon, context) ?? icon }
|
|
@@ -725,7 +742,7 @@ function iconList(
|
|
|
725
742
|
}
|
|
726
743
|
|
|
727
744
|
function authorLinks(value: Metadata['authors'], context: MetadataContext): MetadataLink[] {
|
|
728
|
-
if (value
|
|
745
|
+
if (absent(value)) return []
|
|
729
746
|
return (Array.isArray(value) ? value : [value])
|
|
730
747
|
.filter((author): author is MetadataAuthor => typeof author === 'object' && author !== null)
|
|
731
748
|
.flatMap(author => {
|
|
@@ -775,7 +792,7 @@ function alternateLinks(value: Metadata['alternates'], context: MetadataContext)
|
|
|
775
792
|
for (const [type, entries] of Object.entries(value.types ?? {})) {
|
|
776
793
|
const listEntries = Array.isArray(entries) ? entries : [entries]
|
|
777
794
|
for (const entry of listEntries) {
|
|
778
|
-
const record =
|
|
795
|
+
const record: { url: unknown; title?: string } =
|
|
779
796
|
typeof entry === 'object' && !(entry instanceof URL) && entry !== null && 'url' in entry
|
|
780
797
|
? entry
|
|
781
798
|
: { url: entry }
|
|
@@ -794,8 +811,8 @@ function appleStartupLinks(
|
|
|
794
811
|
value: Metadata['appleWebApp'],
|
|
795
812
|
context: MetadataContext,
|
|
796
813
|
): MetadataLink[] {
|
|
797
|
-
if (!value || typeof value !== 'object' || value.startupImage
|
|
798
|
-
const images =
|
|
814
|
+
if (!value || typeof value !== 'object' || absent(value.startupImage)) return []
|
|
815
|
+
const images = [value.startupImage].flat()
|
|
799
816
|
return images.map(image => {
|
|
800
817
|
if (typeof image === 'string' || image instanceof URL) {
|
|
801
818
|
return {
|
|
@@ -853,8 +870,10 @@ function appLinkTags(value: Metadata['appLinks']): [string, string][] {
|
|
|
853
870
|
if (!value) return []
|
|
854
871
|
const tags: [string, string][] = []
|
|
855
872
|
for (const [platform, entries] of Object.entries(value)) {
|
|
856
|
-
for (const [
|
|
857
|
-
|
|
873
|
+
for (const target of entries === undefined ? [] : [entries].flat()) {
|
|
874
|
+
for (const [name, item] of Object.entries(target)) {
|
|
875
|
+
if (item !== undefined) tags.push([`al:${platform}:${name}`, coerceString(item)])
|
|
876
|
+
}
|
|
858
877
|
}
|
|
859
878
|
}
|
|
860
879
|
return tags
|
|
@@ -890,11 +909,14 @@ function resolveSocialMetadata(meta: Metadata): {
|
|
|
890
909
|
images: ownImages ? twitter?.images : og.images,
|
|
891
910
|
}
|
|
892
911
|
}
|
|
893
|
-
return {
|
|
912
|
+
return {
|
|
913
|
+
openGraph: og ? inherited(og, meta) : undefined,
|
|
914
|
+
twitter: twitter ? inherited(twitter, meta) : undefined,
|
|
915
|
+
}
|
|
894
916
|
}
|
|
895
917
|
|
|
896
918
|
// 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 }>(
|
|
919
|
+
function inherited<T extends { title?: Metadata['title']; description?: string | null }>(
|
|
898
920
|
block: T,
|
|
899
921
|
meta: Metadata,
|
|
900
922
|
): T {
|
package/src/render/renderer.ts
CHANGED
|
@@ -4470,6 +4470,7 @@ export function errorLogTrace(error: Error): string {
|
|
|
4470
4470
|
}
|
|
4471
4471
|
|
|
4472
4472
|
function logDevError(options: RenderOptions, error: Error, clientSsrError = false) {
|
|
4473
|
+
if (error.name === 'DynamicServerError') return
|
|
4473
4474
|
const seen = isBuildError(error) ? loggedBuildErrors : loggedRuntimeErrors
|
|
4474
4475
|
if (seen.has(error)) return
|
|
4475
4476
|
seen.add(error)
|
|
@@ -6733,7 +6734,7 @@ function anchorInlineSuspenseHoles(html: string): string {
|
|
|
6733
6734
|
.replace(/<!--\/\$s:[^>]+?-->/g, '</pnext-hole>')
|
|
6734
6735
|
}
|
|
6735
6736
|
|
|
6736
|
-
function clientSuspenseRefreshScript(nonce: string | undefined) {
|
|
6737
|
+
export function clientSuspenseRefreshScript(nonce: string | undefined) {
|
|
6737
6738
|
const nonceAttribute = nonce ? ` nonce="${escapeHtml(nonce)}"` : ''
|
|
6738
6739
|
// Graft each streamed island's content over its hole. The hole is a `<pnext-hole>` element rather
|
|
6739
6740
|
// than preact's `$s:ID` comment markers: hydration of an ancestor client layout removes comment
|
|
@@ -6873,7 +6874,7 @@ function streamPageSlotMode(options: RenderOptions): PageSlotMarkerMode {
|
|
|
6873
6874
|
return pageSlotIsMountTarget(options) ? 'materialize' : 'keep'
|
|
6874
6875
|
}
|
|
6875
6876
|
|
|
6876
|
-
function clientIslandMarkerBootstrapSource(mode: PageSlotMarkerMode = 'materialize') {
|
|
6877
|
+
export function clientIslandMarkerBootstrapSource(mode: PageSlotMarkerMode = 'materialize') {
|
|
6877
6878
|
// Pages that mount nothing on #pnext-page never need the wrapper div: the
|
|
6878
6879
|
// page's children stay direct children of <body>, matching Next's document
|
|
6879
6880
|
// structure (tests assert structural selectors like `body > p`). `keep`
|
package/src/resolve/imports.ts
CHANGED
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
} from './engine'
|
|
12
12
|
import { onExtensionHostReset } from '../extensions'
|
|
13
13
|
|
|
14
|
+
// `Bun` through globalThis: pnext ships TypeScript source, so app compilers without
|
|
15
|
+
// bun-types typecheck this file and a bare `Bun` identifier would fail them.
|
|
16
|
+
const { Bun: bun } = globalThis as unknown as {
|
|
17
|
+
Bun: { resolveSync(specifier: string, parent: string): string }
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// Conditions for "is this a local source file" lookups. Ordered to mirror the
|
|
15
21
|
// preference a bundler applies, though `exports` key order ultimately decides.
|
|
16
22
|
const localConditions = ['source', 'import', 'browser', 'node', 'default', 'require']
|
|
@@ -668,7 +674,7 @@ export function resolveRuntimeSpecifier(dir: string, specifier: string): string
|
|
|
668
674
|
const cached = runtimeResolveCache.get(key)
|
|
669
675
|
if (cached !== undefined) return cached
|
|
670
676
|
try {
|
|
671
|
-
const resolved =
|
|
677
|
+
const resolved = bun.resolveSync(specifier, dir)
|
|
672
678
|
runtimeResolveCache.set(key, resolved)
|
|
673
679
|
return resolved
|
|
674
680
|
} catch {
|
|
@@ -17,6 +17,12 @@ const parseSync: typeof import('oxc-parser').parseSync = (...args) =>
|
|
|
17
17
|
import { loadNative } from '../utils/native-require'
|
|
18
18
|
import { spliceSource } from '../runtime/module-transform'
|
|
19
19
|
|
|
20
|
+
// `Bun` through globalThis: pnext ships TypeScript source, so app compilers without
|
|
21
|
+
// bun-types typecheck this file and a bare `Bun` identifier would fail them.
|
|
22
|
+
const { Bun: bun } = globalThis as unknown as {
|
|
23
|
+
Bun: { hash(input: string | ArrayBufferView): { toString(radix?: number): string } }
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export interface ScanEdge {
|
|
21
27
|
specifier: string
|
|
22
28
|
/** Bound names: `default`, `*` (namespace object), or the imported names. */
|
|
@@ -187,7 +193,7 @@ export function rewriteFacts(file: string, source: string): RewriteFacts {
|
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
function parsed(file: string, source: string, lang = langForFile(file)) {
|
|
190
|
-
const key = `${lang}\0${source.length}\0${
|
|
196
|
+
const key = `${lang}\0${source.length}\0${bun.hash(source).toString()}`
|
|
191
197
|
const cached = memo.get(key)
|
|
192
198
|
if (cached) {
|
|
193
199
|
hits += 1
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
* always derives Host from the request URL and drops any supplied one. An app that forwards its
|
|
4
4
|
* inbound request headers to an upstream - every proxy/BFF route handler - would otherwise send
|
|
5
5
|
* the local Host upstream, an RFC 9110 7.2 violation that real upstreams reject. Match undici.
|
|
6
|
+
*
|
|
7
|
+
* Bun also ignores response-body cancellation: cancelling a fetch Response's stream leaves the
|
|
8
|
+
* upstream request in flight and its keep-alive socket checked out for good, so a passthrough proxy
|
|
9
|
+
* route whose client disconnects mid-stream leaks a connection per abort until later fetches to that
|
|
10
|
+
* host queue behind them forever. Only the fetch's own AbortSignal tears the request down, so each
|
|
11
|
+
* response body is tracked against its controller here and the serving layer fires it on disconnect.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
let installed = false
|
|
9
15
|
|
|
16
|
+
const upstreamAborts = new WeakMap<ReadableStream<Uint8Array>, AbortController>()
|
|
17
|
+
|
|
10
18
|
function isHostKey(key: unknown): boolean {
|
|
11
19
|
return typeof key === 'string' && key.length === 4 && key.toLowerCase() === 'host'
|
|
12
20
|
}
|
|
@@ -36,7 +44,43 @@ export function installFetchHostNormalization(): void {
|
|
|
36
44
|
const original = globalThis.fetch.bind(globalThis)
|
|
37
45
|
globalThis.fetch = function fetch(input: RequestInfo | URL, init?: RequestInit) {
|
|
38
46
|
const source = init?.headers ?? (input instanceof Request ? input.headers : undefined)
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const normalized =
|
|
48
|
+
source !== undefined && hasHost(source) ? { ...init, headers: withoutHost(source) } : init
|
|
49
|
+
return trackUpstream(original, input, normalized)
|
|
41
50
|
} as typeof globalThis.fetch
|
|
42
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Give every fetch an abort handle of its own, keyed on the body it streams back. */
|
|
54
|
+
function trackUpstream(
|
|
55
|
+
original: typeof globalThis.fetch,
|
|
56
|
+
input: RequestInfo | URL,
|
|
57
|
+
init: RequestInit | undefined,
|
|
58
|
+
): Promise<Response> {
|
|
59
|
+
const controller = new AbortController()
|
|
60
|
+
const caller = init?.signal ?? (input instanceof Request ? input.signal : undefined)
|
|
61
|
+
const signal = caller ? AbortSignal.any([caller, controller.signal]) : controller.signal
|
|
62
|
+
return original(input, { ...init, signal }).then(response => {
|
|
63
|
+
if (response.body) upstreamAborts.set(response.body, controller)
|
|
64
|
+
return response
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Tear down the upstream fetch `body` is streaming from once the client goes away. Bun.serve never
|
|
70
|
+
* cancels the body it is writing, so this disconnect is the only chance to release that socket.
|
|
71
|
+
* Zero-cost for any response that is not a fetch passthrough.
|
|
72
|
+
*/
|
|
73
|
+
export function abortUpstreamFetchOnDisconnect(
|
|
74
|
+
body: ReadableStream<Uint8Array> | null,
|
|
75
|
+
signal: AbortSignal | undefined,
|
|
76
|
+
): void {
|
|
77
|
+
if (!body || !signal || !upstreamAborts.has(body)) return
|
|
78
|
+
const abort = () => {
|
|
79
|
+
const controller = upstreamAborts.get(body)
|
|
80
|
+
if (!controller) return
|
|
81
|
+
upstreamAborts.delete(body)
|
|
82
|
+
controller.abort()
|
|
83
|
+
}
|
|
84
|
+
if (signal.aborted) abort()
|
|
85
|
+
else signal.addEventListener('abort', abort, { once: true })
|
|
86
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -95,7 +95,8 @@ export interface ResourceHint extends Omit<MetadataLink, 'url'> {
|
|
|
95
95
|
export type MetadataTitle =
|
|
96
96
|
string | number | { absolute?: string; default?: string; template?: string | null }
|
|
97
97
|
|
|
98
|
-
export type MetadataIcon =
|
|
98
|
+
export type MetadataIcon =
|
|
99
|
+
string | URL | (Omit<Partial<MetadataLink>, 'url'> & { url: string | URL })
|
|
99
100
|
|
|
100
101
|
export interface MetadataIconGroups {
|
|
101
102
|
icon?: MetadataIcon | MetadataIcon[]
|
|
@@ -116,7 +117,7 @@ export interface MetadataRobots {
|
|
|
116
117
|
nosnippet?: boolean
|
|
117
118
|
noimageindex?: boolean
|
|
118
119
|
nocache?: boolean
|
|
119
|
-
googleBot?: MetadataRobots & Record<string, string | number | boolean | undefined>
|
|
120
|
+
googleBot?: string | (MetadataRobots & Record<string, string | number | boolean | undefined>)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
export type MetadataImage =
|
|
@@ -134,7 +135,7 @@ export type MetadataImage =
|
|
|
134
135
|
export interface MetadataOpenGraph {
|
|
135
136
|
title?: MetadataTitle
|
|
136
137
|
description?: string
|
|
137
|
-
url?: string | URL
|
|
138
|
+
url?: string | URL | null
|
|
138
139
|
siteName?: string
|
|
139
140
|
locale?: string
|
|
140
141
|
type?: string
|
|
@@ -142,109 +143,125 @@ export interface MetadataOpenGraph {
|
|
|
142
143
|
videos?: MetadataImage | MetadataImage[]
|
|
143
144
|
audio?: MetadataImage | MetadataImage[]
|
|
144
145
|
publishedTime?: string
|
|
145
|
-
authors?: string | string[]
|
|
146
|
+
authors?: string | URL | (string | URL)[] | null
|
|
146
147
|
emails?: string | string[]
|
|
147
148
|
phoneNumbers?: string | string[]
|
|
148
149
|
faxNumbers?: string | string[]
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
export interface MetadataTwitterPlayer {
|
|
153
|
+
playerUrl?: string | URL
|
|
154
|
+
streamUrl?: string | URL
|
|
155
|
+
width?: string | number
|
|
156
|
+
height?: string | number
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
export interface MetadataTwitter {
|
|
152
160
|
card?: string
|
|
153
|
-
site?: string
|
|
154
|
-
siteId?: string
|
|
155
|
-
creator?: string
|
|
156
|
-
creatorId?: string
|
|
161
|
+
site?: string | null
|
|
162
|
+
siteId?: string | null
|
|
163
|
+
creator?: string | null
|
|
164
|
+
creatorId?: string | null
|
|
157
165
|
title?: MetadataTitle
|
|
158
|
-
description?: string
|
|
166
|
+
description?: string | null
|
|
159
167
|
images?: MetadataImage | MetadataImage[]
|
|
160
|
-
players?:
|
|
161
|
-
playerUrl?: string | URL
|
|
162
|
-
streamUrl?: string | URL
|
|
163
|
-
width?: string | number
|
|
164
|
-
height?: string | number
|
|
165
|
-
}
|
|
168
|
+
players?: MetadataTwitterPlayer | MetadataTwitterPlayer[]
|
|
166
169
|
app?: {
|
|
167
170
|
name?: string
|
|
168
|
-
id?: Record<string,
|
|
171
|
+
id?: Record<string, MetadataValue | undefined>
|
|
169
172
|
url?: Record<string, string | URL | undefined>
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
export type MetadataAlternate =
|
|
177
|
+
| string
|
|
178
|
+
| URL
|
|
179
|
+
| { url: string | URL; title?: string }
|
|
180
|
+
| (string | URL | { url: string | URL; title?: string })[]
|
|
181
|
+
|
|
173
182
|
export interface MetadataAlternates {
|
|
174
|
-
canonical?: string | URL | { url: string | URL }
|
|
175
|
-
languages?: Record<string,
|
|
176
|
-
media?: Record<string,
|
|
177
|
-
types?: Record<
|
|
178
|
-
string,
|
|
179
|
-
| string
|
|
180
|
-
| URL
|
|
181
|
-
| { url: string | URL; title?: string }
|
|
182
|
-
| (string | URL | { url: string | URL; title?: string })[]
|
|
183
|
-
>
|
|
183
|
+
canonical?: string | URL | { url: string | URL; title?: string } | null
|
|
184
|
+
languages?: Record<string, MetadataAlternate | null | undefined>
|
|
185
|
+
media?: Record<string, MetadataAlternate | null | undefined>
|
|
186
|
+
types?: Record<string, MetadataAlternate | null | undefined>
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
// Every field is nullable: a metadata object may explicitly null a field out to
|
|
190
|
+
// suppress an inherited value, and null reads the same as absent here.
|
|
186
191
|
export interface Metadata {
|
|
187
|
-
metadataBase?: string | URL
|
|
188
|
-
title?: MetadataTitle
|
|
189
|
-
description?: string
|
|
190
|
-
lang?: string
|
|
191
|
-
keywords?: string | number | (string | number)[]
|
|
192
|
-
authors?: string | MetadataAuthor | (string | MetadataAuthor)[]
|
|
193
|
-
generator?: string
|
|
194
|
-
applicationName?: string
|
|
195
|
-
referrer?: string
|
|
196
|
-
robots?: string | MetadataRobots
|
|
197
|
-
openGraph?: MetadataOpenGraph
|
|
198
|
-
twitter?: MetadataTwitter
|
|
199
|
-
alternates?: MetadataAlternates
|
|
192
|
+
metadataBase?: string | URL | null
|
|
193
|
+
title?: MetadataTitle | null
|
|
194
|
+
description?: string | null
|
|
195
|
+
lang?: string | null
|
|
196
|
+
keywords?: string | number | (string | number)[] | null
|
|
197
|
+
authors?: string | MetadataAuthor | (string | MetadataAuthor)[] | null
|
|
198
|
+
generator?: string | null
|
|
199
|
+
applicationName?: string | null
|
|
200
|
+
referrer?: string | null
|
|
201
|
+
robots?: string | MetadataRobots | null
|
|
202
|
+
openGraph?: MetadataOpenGraph | null
|
|
203
|
+
twitter?: MetadataTwitter | null
|
|
204
|
+
alternates?: MetadataAlternates | null
|
|
200
205
|
pagination?: {
|
|
201
|
-
previous?: string | URL
|
|
202
|
-
next?: string | URL
|
|
203
|
-
}
|
|
204
|
-
icons?: MetadataIcon | MetadataIcon[] | MetadataIconGroups
|
|
205
|
-
manifest?: string | URL
|
|
206
|
-
creator?: string
|
|
207
|
-
publisher?: string
|
|
206
|
+
previous?: string | URL | null
|
|
207
|
+
next?: string | URL | null
|
|
208
|
+
} | null
|
|
209
|
+
icons?: MetadataIcon | MetadataIcon[] | MetadataIconGroups | null
|
|
210
|
+
manifest?: string | URL | null
|
|
211
|
+
creator?: string | null
|
|
212
|
+
publisher?: string | null
|
|
208
213
|
formatDetection?: {
|
|
209
|
-
email?: boolean
|
|
210
|
-
address?: boolean
|
|
211
|
-
telephone?: boolean
|
|
212
|
-
url?: boolean
|
|
213
|
-
}
|
|
214
|
+
email?: boolean | null
|
|
215
|
+
address?: boolean | null
|
|
216
|
+
telephone?: boolean | null
|
|
217
|
+
url?: boolean | null
|
|
218
|
+
} | null
|
|
214
219
|
verification?: {
|
|
215
|
-
google?:
|
|
216
|
-
yahoo?:
|
|
217
|
-
yandex?:
|
|
218
|
-
other?: Record<string,
|
|
219
|
-
}
|
|
220
|
+
google?: MetadataValue | MetadataValue[] | null
|
|
221
|
+
yahoo?: MetadataValue | MetadataValue[] | null
|
|
222
|
+
yandex?: MetadataValue | MetadataValue[] | null
|
|
223
|
+
other?: Record<string, MetadataValue | MetadataValue[]>
|
|
224
|
+
} | null
|
|
220
225
|
itunes?: {
|
|
221
226
|
appId?: string
|
|
222
|
-
appArgument?: string
|
|
223
|
-
}
|
|
227
|
+
appArgument?: string | null
|
|
228
|
+
} | null
|
|
224
229
|
appleWebApp?:
|
|
225
230
|
| boolean
|
|
226
231
|
| {
|
|
227
232
|
capable?: boolean
|
|
228
|
-
title?: string
|
|
233
|
+
title?: string | null
|
|
229
234
|
statusBarStyle?: string
|
|
230
235
|
startupImage?:
|
|
231
236
|
| string
|
|
232
237
|
| URL
|
|
233
238
|
| { url: string | URL; media?: string }
|
|
234
239
|
| (string | URL | { url: string | URL; media?: string })[]
|
|
240
|
+
| null
|
|
235
241
|
}
|
|
242
|
+
| null
|
|
236
243
|
facebook?: {
|
|
237
244
|
appId?: string
|
|
238
|
-
admins?:
|
|
239
|
-
}
|
|
245
|
+
admins?: MetadataValue | MetadataValue[]
|
|
246
|
+
} | null
|
|
240
247
|
pinterest?: {
|
|
241
|
-
richPin?: boolean
|
|
242
|
-
}
|
|
243
|
-
appLinks?:
|
|
244
|
-
other?: Record<string,
|
|
248
|
+
richPin?: boolean | string | null
|
|
249
|
+
} | null
|
|
250
|
+
appLinks?: MetadataAppLinks | null
|
|
251
|
+
other?: Record<string, MetadataValue | MetadataValue[] | null>
|
|
245
252
|
links?: MetadataLink[]
|
|
246
253
|
}
|
|
247
254
|
|
|
255
|
+
/** A metadata value that renders to text; numbers are stringified. */
|
|
256
|
+
export type MetadataValue = string | number
|
|
257
|
+
|
|
258
|
+
export type MetadataAppLinkTarget = Record<string, MetadataValue | boolean | URL | undefined>
|
|
259
|
+
|
|
260
|
+
export type MetadataAppLinks = Record<
|
|
261
|
+
string,
|
|
262
|
+
MetadataAppLinkTarget | MetadataAppLinkTarget[] | undefined
|
|
263
|
+
>
|
|
264
|
+
|
|
248
265
|
export type ThemeColor = string | { color: string; media?: string }
|
|
249
266
|
|
|
250
267
|
export interface Viewport {
|
|
@@ -255,8 +272,8 @@ export interface Viewport {
|
|
|
255
272
|
userScalable?: boolean
|
|
256
273
|
viewportFit?: 'auto' | 'contain' | 'cover'
|
|
257
274
|
interactiveWidget?: 'resizes-visual' | 'resizes-content' | 'overlays-content'
|
|
258
|
-
themeColor?: ThemeColor | ThemeColor[]
|
|
259
|
-
colorScheme?: string
|
|
275
|
+
themeColor?: ThemeColor | ThemeColor[] | null
|
|
276
|
+
colorScheme?: string | null
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
export interface NextRequestCookie {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Which @wular/pnext a generated project depends on. `bunx @wular/pnext` reuses a cached copy of the
|
|
2
|
+
// package instead of re-resolving the dist-tag, so a stale laptop runs an old CLI — and an old CLI
|
|
3
|
+
// stamping its own version pins the new project to it. The range comes from the registry instead,
|
|
4
|
+
// with the running version as the offline fallback.
|
|
5
|
+
|
|
6
|
+
import { bold, cyan, dim } from './ansi'
|
|
7
|
+
import { pnextVersionRange } from './fs'
|
|
8
|
+
|
|
9
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@wular%2Fpnext'
|
|
10
|
+
// Short: create/migrate must not hang behind a slow or captive network.
|
|
11
|
+
const TIMEOUT_MS = 2000
|
|
12
|
+
|
|
13
|
+
async function fetchLatestPublishedVersion() {
|
|
14
|
+
const response = await fetch(REGISTRY_URL, {
|
|
15
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
16
|
+
// The abbreviated document — dist-tags plus versions, without the full metadata.
|
|
17
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
18
|
+
})
|
|
19
|
+
if (!response.ok) return undefined
|
|
20
|
+
const body = (await response.json()) as { 'dist-tags'?: { latest?: string } }
|
|
21
|
+
return body['dist-tags']?.latest
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Test seam: the create/migrate suites assert the stamped range and must never hit the network.
|
|
25
|
+
let fetchLatestVersion: () => Promise<string | undefined> = fetchLatestPublishedVersion
|
|
26
|
+
|
|
27
|
+
/** @internal Test-only. Returns a restore function. */
|
|
28
|
+
export function setLatestVersionFetcher(fetcher: () => Promise<string | undefined>) {
|
|
29
|
+
const previous = fetchLatestVersion
|
|
30
|
+
fetchLatestVersion = fetcher
|
|
31
|
+
return () => {
|
|
32
|
+
fetchLatestVersion = previous
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* '^' + the latest published @wular/pnext, for a project this CLI generates. Never throws and never
|
|
38
|
+
* blocks for long — any failure falls back to the running version. Warns when the CLI itself is
|
|
39
|
+
* stale, which is the symptom the user actually has to fix.
|
|
40
|
+
*/
|
|
41
|
+
export async function latestPnextVersionRange(command: 'create' | 'migrate') {
|
|
42
|
+
const running = pnextVersionRange().slice(1)
|
|
43
|
+
const latest = await fetchLatestVersion().catch(() => undefined)
|
|
44
|
+
if (!latest) return `^${running}`
|
|
45
|
+
if (isNewer(latest, running)) warnStaleCli(command, running, latest)
|
|
46
|
+
return `^${latest}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function warnStaleCli(command: string, running: string, latest: string) {
|
|
50
|
+
console.error(
|
|
51
|
+
`\n${bold('⚠ Your pnext CLI is out of date')} — running ${running}, latest is ${latest}.\n` +
|
|
52
|
+
dim(
|
|
53
|
+
` bunx reused a cached copy. This project depends on ^${latest}, but ${command} ran with ${running}.\n`,
|
|
54
|
+
) +
|
|
55
|
+
` Re-run with: ${cyan(`bunx @wular/pnext@latest ${command}`)}\n`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Numeric major.minor.patch only; anything unparseable counts as "not newer". */
|
|
60
|
+
function isNewer(candidate: string, current: string) {
|
|
61
|
+
const parts = (value: string) => value.split('-')[0]!.split('.').map(Number)
|
|
62
|
+
const [a, b] = [parts(candidate), parts(current)]
|
|
63
|
+
for (let index = 0; index < 3; index += 1) {
|
|
64
|
+
const left = a[index] ?? 0
|
|
65
|
+
const right = b[index] ?? 0
|
|
66
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return false
|
|
67
|
+
if (left !== right) return left > right
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|