jamdesk 1.1.127 → 1.1.129
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/dist/commands/broken-links.d.ts +2 -0
- package/dist/commands/broken-links.d.ts.map +1 -1
- package/dist/commands/broken-links.js +11 -2
- package/dist/commands/broken-links.js.map +1 -1
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +5 -0
- package/dist/lib/deps.js.map +1 -1
- package/package.json +2 -1
- package/vendored/app/api/markdown-export/[project]/[...slug]/route.ts +71 -1
- package/vendored/app/layout.tsx +8 -0
- package/vendored/components/AIActionsMenu.tsx +72 -3
- package/vendored/components/layout/EmbedLinkInterceptor.tsx +37 -0
- package/vendored/components/layout/LayoutWrapper.tsx +29 -1
- package/vendored/components/layout/PageColumns.tsx +10 -5
- package/vendored/components/navigation/Breadcrumb.tsx +6 -1
- package/vendored/components/navigation/SocialFooter.tsx +40 -17
- package/vendored/lib/api-spec-menu-gate.ts +49 -0
- package/vendored/lib/api-spec-offer.ts +50 -0
- package/vendored/lib/api-specs-bundle.ts +255 -0
- package/vendored/lib/api-specs-markdown-hint.ts +49 -0
- package/vendored/lib/api-specs-route.ts +45 -0
- package/vendored/lib/docs-types.ts +1 -1
- package/vendored/lib/heading-extractor.ts +34 -30
- package/vendored/lib/layout-helpers.tsx +19 -2
- package/vendored/lib/middleware-helpers.ts +29 -0
- package/vendored/lib/render-doc-page.tsx +93 -76
- package/vendored/lib/scanner-blocklist.ts +7 -0
- package/vendored/lib/static-artifacts.ts +39 -9
- package/vendored/lib/static-file-route.ts +35 -1
- package/vendored/scripts/github-slugger-regex.cjs +13 -0
- package/vendored/scripts/validate-links.cjs +136 -22
- package/vendored/themes/jam/variables.css +8 -0
- package/vendored/workspace-package-lock.json +128 -120
|
@@ -365,6 +365,8 @@ interface DocsChromeProps {
|
|
|
365
365
|
customCss: string | null;
|
|
366
366
|
customJs: string | null;
|
|
367
367
|
children: React.ReactNode;
|
|
368
|
+
embed?: boolean;
|
|
369
|
+
embedTheme?: 'light' | 'dark' | 'auto';
|
|
368
370
|
}
|
|
369
371
|
|
|
370
372
|
/**
|
|
@@ -383,6 +385,8 @@ export async function DocsChrome({
|
|
|
383
385
|
customCss,
|
|
384
386
|
customJs,
|
|
385
387
|
children,
|
|
388
|
+
embed,
|
|
389
|
+
embedTheme,
|
|
386
390
|
}: DocsChromeProps): Promise<React.ReactElement> {
|
|
387
391
|
// Lowercase to match docs-config canonical case — `data-theme="nebula"` CSS
|
|
388
392
|
// selectors are case-sensitive, so `theme: "NEBULA"` from disk would silently
|
|
@@ -410,6 +414,11 @@ export async function DocsChrome({
|
|
|
410
414
|
const appearanceDefault = config.appearance?.default || 'system';
|
|
411
415
|
const appearanceStrict = config.appearance?.strict || false;
|
|
412
416
|
|
|
417
|
+
// In embed mode, an explicit ?theme=dark|light forces that theme so the
|
|
418
|
+
// panel can match the host app; theme=auto (or omitted) keeps system.
|
|
419
|
+
const embedForcedTheme =
|
|
420
|
+
embed && (embedTheme === 'light' || embedTheme === 'dark') ? embedTheme : undefined;
|
|
421
|
+
|
|
413
422
|
const linkPrefix = config.hostAtDocs ? '/docs' : '';
|
|
414
423
|
|
|
415
424
|
// Skip building the IIFE string in dev — both the script render below and
|
|
@@ -464,6 +473,11 @@ export async function DocsChrome({
|
|
|
464
473
|
}}
|
|
465
474
|
/>
|
|
466
475
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
476
|
+
{/* Embed render (widget modal): default every in-frame link to a new tab.
|
|
477
|
+
Clicking a doc link should open the full docs site in a new tab, not
|
|
478
|
+
navigate the modal iframe into a chrome'd (non-embed) page. Scoped to
|
|
479
|
+
embed — this <head> only renders when x-jd-layout=embed. */}
|
|
480
|
+
{embed && <base target="_blank" />}
|
|
467
481
|
{config.fonts && (
|
|
468
482
|
<>
|
|
469
483
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
@@ -646,11 +660,14 @@ export async function DocsChrome({
|
|
|
646
660
|
)}
|
|
647
661
|
<ThemeProvider
|
|
648
662
|
defaultTheme={appearanceDefault}
|
|
649
|
-
forcedTheme={
|
|
663
|
+
forcedTheme={
|
|
664
|
+
embedForcedTheme ??
|
|
665
|
+
(appearanceStrict ? (appearanceDefault === 'system' ? undefined : appearanceDefault as 'light' | 'dark') : undefined)
|
|
666
|
+
}
|
|
650
667
|
>
|
|
651
668
|
<LinkPrefixProvider prefix={linkPrefix}>
|
|
652
669
|
<ProjectSlugProvider slug={resolvedProjectSlug || ''}>
|
|
653
|
-
<LayoutWrapper config={config}>
|
|
670
|
+
<LayoutWrapper config={config} embed={embed}>
|
|
654
671
|
{children}
|
|
655
672
|
</LayoutWrapper>
|
|
656
673
|
</ProjectSlugProvider>
|
|
@@ -504,6 +504,31 @@ export function getMcpApiPath(projectSlug: string): string {
|
|
|
504
504
|
return `/api/mcp/${projectSlug}`;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
export type EmbedTheme = 'light' | 'dark' | 'auto';
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* True when the request opts into the chrome-less embed render.
|
|
511
|
+
*
|
|
512
|
+
* Supports `?embed=1` (canonical) and `?layout=embed` (convenience alias).
|
|
513
|
+
*
|
|
514
|
+
* @param searchParams - URL search params from the incoming request
|
|
515
|
+
* @returns true if this request should render without site chrome
|
|
516
|
+
*/
|
|
517
|
+
export function isEmbedRequest(searchParams: URLSearchParams): boolean {
|
|
518
|
+
return searchParams.get('embed') === '1' || searchParams.get('layout') === 'embed';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Forced theme for an embed render; defaults to 'auto' (prefers-color-scheme).
|
|
523
|
+
*
|
|
524
|
+
* @param searchParams - URL search params from the incoming request
|
|
525
|
+
* @returns 'dark' | 'light' when explicitly set, otherwise 'auto'
|
|
526
|
+
*/
|
|
527
|
+
export function getEmbedTheme(searchParams: URLSearchParams): EmbedTheme {
|
|
528
|
+
const t = searchParams.get('theme');
|
|
529
|
+
return t === 'dark' || t === 'light' ? t : 'auto';
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
/**
|
|
508
533
|
* Check if this is a chat request that needs routing.
|
|
509
534
|
*
|
|
@@ -629,6 +654,7 @@ const TRUSTED_PROXY_HEADERS = [
|
|
|
629
654
|
// attacker on a subdomain ALSO toggle the "owner can sign in"
|
|
630
655
|
// copy. Strip them all from the inbound request.
|
|
631
656
|
'x-jd-layout',
|
|
657
|
+
'x-jd-embed-theme', // forced-theme for embed render — never client-supplied
|
|
632
658
|
'x-jd-custom-domain',
|
|
633
659
|
'x-jd-project-name',
|
|
634
660
|
'x-jd-project-logo',
|
|
@@ -678,6 +704,9 @@ export interface BuildProjectHeadersOptions {
|
|
|
678
704
|
* Strips any client-supplied copies of those headers from the inbound
|
|
679
705
|
* request to prevent header smuggling.
|
|
680
706
|
*
|
|
707
|
+
* Strips (but does NOT set) x-jd-layout and x-jd-embed-theme — callers own
|
|
708
|
+
* those after this call; see TRUSTED_PROXY_HEADERS for the full strip list.
|
|
709
|
+
*
|
|
681
710
|
* @param projectSlug - Resolved project slug
|
|
682
711
|
* @param existingHeaders - Existing request headers to clone
|
|
683
712
|
* @param hostAtDocs - Whether docs are hosted at /docs
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
import { notFound } from 'next/navigation';
|
|
14
14
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
15
15
|
import type { Metadata } from 'next';
|
|
16
|
-
import type { AnchorHTMLAttributes, ReactElement } from 'react';
|
|
16
|
+
import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react';
|
|
17
17
|
import { MDXComponents } from '@/components/mdx/MDXComponents';
|
|
18
18
|
import { Breadcrumb } from '@/components/navigation/Breadcrumb';
|
|
19
19
|
import { TableOfContents } from '@/components/navigation/TableOfContents';
|
|
20
20
|
import { PageColumns } from '@/components/layout/PageColumns';
|
|
21
|
+
import { EmbedLinkInterceptor } from '@/components/layout/EmbedLinkInterceptor';
|
|
21
22
|
import { PageNavigation } from '@/components/navigation/PageNavigation';
|
|
22
23
|
import { SocialFooter } from '@/components/navigation/SocialFooter';
|
|
23
24
|
import { ApiPageWrapper } from '@/components/mdx/ApiPage';
|
|
@@ -82,6 +83,7 @@ import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
|
82
83
|
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
83
84
|
import { AIActionsMenu } from '@/components/AIActionsMenu';
|
|
84
85
|
import { getContextualOptions } from '@/lib/contextual-defaults';
|
|
86
|
+
import { withApiSpecDownload } from '@/lib/api-spec-menu-gate';
|
|
85
87
|
import { SnippetComponents } from '@/components/snippets/ProjectSnippets';
|
|
86
88
|
|
|
87
89
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
@@ -274,6 +276,13 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
274
276
|
const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
|
|
275
277
|
const isIsr = isIsrMode();
|
|
276
278
|
|
|
279
|
+
// Chrome-less embed render (widget modal iframe). `x-jd-layout=embed` is set
|
|
280
|
+
// by proxy.ts. In embed mode we drop the footer + TOC and break cross-page
|
|
281
|
+
// links out of the iframe. `wrap` is a no-op on normal pages so the link
|
|
282
|
+
// interceptor only mounts when actually embedded.
|
|
283
|
+
const embed = requestHeaders?.get('x-jd-layout') === 'embed';
|
|
284
|
+
const wrap = (n: ReactNode) => embed ? <EmbedLinkInterceptor>{n}</EmbedLinkInterceptor> : n;
|
|
285
|
+
|
|
277
286
|
if (isIsr) {
|
|
278
287
|
if (!projectSlug) notFound();
|
|
279
288
|
const exists = await projectExists(projectSlug);
|
|
@@ -499,6 +508,12 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
499
508
|
|
|
500
509
|
const contextualOptions = getContextualOptions(config);
|
|
501
510
|
const hasAiActions = contextualOptions.length > 0;
|
|
511
|
+
const apiMenuOptions = withApiSpecDownload(contextualOptions, {
|
|
512
|
+
isIsr,
|
|
513
|
+
isApiPage,
|
|
514
|
+
pageHasOpenApi: !!data.openapi,
|
|
515
|
+
config,
|
|
516
|
+
});
|
|
502
517
|
|
|
503
518
|
const proseClasses = 'prose max-w-none';
|
|
504
519
|
|
|
@@ -531,86 +546,88 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
531
546
|
if (isApiPage) {
|
|
532
547
|
return (
|
|
533
548
|
<>{jsonLdScript}<ApiPageWrapper hasCodePanels={hasCodePanels}>
|
|
534
|
-
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
<
|
|
540
|
-
<
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
549
|
+
{wrap(
|
|
550
|
+
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10 flex-1 min-w-0">
|
|
551
|
+
<Breadcrumb slug={slug} config={config} hidden={embed} />
|
|
552
|
+
|
|
553
|
+
{data.title && (
|
|
554
|
+
<header className="mb-4 sm:mb-6">
|
|
555
|
+
<div className="flex items-center gap-3">
|
|
556
|
+
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-theme-text-primary tracking-tight">
|
|
557
|
+
{data.title}
|
|
558
|
+
</h1>
|
|
559
|
+
{rssIcon}
|
|
560
|
+
{hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={apiMenuOptions} projectName={config.name} /></div>}
|
|
561
|
+
</div>
|
|
562
|
+
{data.description && (
|
|
563
|
+
<p className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3">
|
|
564
|
+
{data.description}
|
|
565
|
+
</p>
|
|
566
|
+
)}
|
|
567
|
+
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={apiMenuOptions} projectName={config.name} /></div>}
|
|
568
|
+
</header>
|
|
569
|
+
)}
|
|
570
|
+
|
|
571
|
+
<div className={proseClasses}>
|
|
572
|
+
{mdxApiMethod && mdxApiPath && (
|
|
573
|
+
mdxEndpointData && playgroundDisplay !== 'none' ? (
|
|
574
|
+
<OpenApiEndpoint
|
|
575
|
+
endpoint={mdxEndpointData}
|
|
576
|
+
playgroundOnly
|
|
577
|
+
playgroundDisplay={playgroundDisplay}
|
|
578
|
+
authMethod={resolvedMdxAuth.method}
|
|
579
|
+
authHeaderName={resolvedMdxAuth.headerName}
|
|
580
|
+
serverUrl={fallbackServerUrl}
|
|
581
|
+
proxyEnabled={proxyEnabled}
|
|
582
|
+
languages={config.api?.examples?.languages}
|
|
583
|
+
/>
|
|
584
|
+
) : (
|
|
585
|
+
<ApiEndpoint
|
|
586
|
+
method={mdxApiMethod}
|
|
587
|
+
path={mdxApiPath}
|
|
588
|
+
baseUrl={fallbackServerUrl}
|
|
589
|
+
/>
|
|
590
|
+
)
|
|
550
591
|
)}
|
|
551
|
-
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
552
|
-
</header>
|
|
553
|
-
)}
|
|
554
592
|
|
|
555
|
-
|
|
556
|
-
{mdxApiMethod && mdxApiPath && (
|
|
557
|
-
mdxEndpointData && playgroundDisplay !== 'none' ? (
|
|
593
|
+
{openApiEndpointData && (
|
|
558
594
|
<OpenApiEndpoint
|
|
559
|
-
endpoint={
|
|
560
|
-
|
|
595
|
+
endpoint={openApiEndpointData}
|
|
596
|
+
codeExamples={openApiCodeExamples || undefined}
|
|
561
597
|
playgroundDisplay={playgroundDisplay}
|
|
562
|
-
authMethod={
|
|
563
|
-
authHeaderName={
|
|
598
|
+
authMethod={resolvedOpenApiAuth.method}
|
|
599
|
+
authHeaderName={resolvedOpenApiAuth.headerName}
|
|
564
600
|
serverUrl={fallbackServerUrl}
|
|
565
601
|
proxyEnabled={proxyEnabled}
|
|
566
602
|
languages={config.api?.examples?.languages}
|
|
567
603
|
/>
|
|
568
|
-
)
|
|
569
|
-
<ApiEndpoint
|
|
570
|
-
method={mdxApiMethod}
|
|
571
|
-
path={mdxApiPath}
|
|
572
|
-
baseUrl={fallbackServerUrl}
|
|
573
|
-
/>
|
|
574
|
-
)
|
|
575
|
-
)}
|
|
576
|
-
|
|
577
|
-
{openApiEndpointData && (
|
|
578
|
-
<OpenApiEndpoint
|
|
579
|
-
endpoint={openApiEndpointData}
|
|
580
|
-
codeExamples={openApiCodeExamples || undefined}
|
|
581
|
-
playgroundDisplay={playgroundDisplay}
|
|
582
|
-
authMethod={resolvedOpenApiAuth.method}
|
|
583
|
-
authHeaderName={resolvedOpenApiAuth.headerName}
|
|
584
|
-
serverUrl={fallbackServerUrl}
|
|
585
|
-
proxyEnabled={proxyEnabled}
|
|
586
|
-
languages={config.api?.examples?.languages}
|
|
587
|
-
/>
|
|
588
|
-
)}
|
|
604
|
+
)}
|
|
589
605
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
606
|
+
{!openApiEndpointData && openApiError && (
|
|
607
|
+
<OpenApiError message={openApiError} slug={slug.join('/')} />
|
|
608
|
+
)}
|
|
593
609
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
+
<StepSlugProvider entries={stepEntries}>
|
|
611
|
+
<MDXRemote
|
|
612
|
+
source={openApiEndpointData
|
|
613
|
+
? content.replace(/<ResponseExample>[\s\S]*?<\/ResponseExample>/g, '')
|
|
614
|
+
: content}
|
|
615
|
+
components={AllComponentsWithInline}
|
|
616
|
+
options={{
|
|
617
|
+
...mdxSecurityOptions,
|
|
618
|
+
mdxOptions: {
|
|
619
|
+
...getCommonMdxOptions(config, highlighter),
|
|
620
|
+
recmaPlugins: [recmaCompoundComponents],
|
|
621
|
+
},
|
|
622
|
+
}}
|
|
623
|
+
/>
|
|
624
|
+
</StepSlugProvider>
|
|
625
|
+
</div>
|
|
610
626
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
627
|
+
{!embed && <PageNavigation currentSlug={slug.join('/')} config={config} />}
|
|
628
|
+
<SocialFooter config={config} hidden={data.hideFooter} embed={embed} projectSlug={projectSlug ?? undefined} />
|
|
629
|
+
</article>,
|
|
630
|
+
)}
|
|
614
631
|
</ApiPageWrapper></>
|
|
615
632
|
);
|
|
616
633
|
}
|
|
@@ -631,9 +648,9 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
631
648
|
</StepSlugProvider>
|
|
632
649
|
);
|
|
633
650
|
|
|
634
|
-
const articleContent = (
|
|
651
|
+
const articleContent = wrap(
|
|
635
652
|
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
|
|
636
|
-
<Breadcrumb slug={slug} config={config} />
|
|
653
|
+
<Breadcrumb slug={slug} config={config} hidden={embed} />
|
|
637
654
|
|
|
638
655
|
{data.title && (
|
|
639
656
|
<header className="mb-6 sm:mb-10">
|
|
@@ -656,10 +673,10 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
656
673
|
<div className={proseClasses}>
|
|
657
674
|
{hasView ? <ViewWrapper>{mdxContent}</ViewWrapper> : mdxContent}
|
|
658
675
|
|
|
659
|
-
<PageNavigation currentSlug={slug.join('/')} config={config} isWideMode={isWideMode || hasPanel} />
|
|
676
|
+
{!embed && <PageNavigation currentSlug={slug.join('/')} config={config} isWideMode={isWideMode || hasPanel} />}
|
|
660
677
|
</div>
|
|
661
|
-
<SocialFooter config={config} hidden={data.hideFooter} projectSlug={projectSlug ?? undefined} />
|
|
662
|
-
</article
|
|
678
|
+
<SocialFooter config={config} hidden={data.hideFooter} embed={embed} projectSlug={projectSlug ?? undefined} />
|
|
679
|
+
</article>,
|
|
663
680
|
);
|
|
664
681
|
|
|
665
682
|
if (hasPanel) {
|
|
@@ -669,7 +686,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
669
686
|
return (
|
|
670
687
|
<>
|
|
671
688
|
{jsonLdScript}
|
|
672
|
-
<PageColumns toc={<TableOfContents content={content} />} isWideMode={isWideMode}>
|
|
689
|
+
<PageColumns toc={<TableOfContents content={content} />} isWideMode={isWideMode} embed={embed}>
|
|
673
690
|
{articleContent}
|
|
674
691
|
</PageColumns>
|
|
675
692
|
</>
|
|
@@ -197,6 +197,13 @@ export function classifyScannerProbe(pathname: string): ScannerCategory | null {
|
|
|
197
197
|
const wellKnownMatch = /^\/(?:[a-z0-9][a-z0-9-]{0,15}\/)?\.well-known\/(.*)$/.exec(lower);
|
|
198
198
|
if (wellKnownMatch && !/(?:^|\/)\.[^/]/.test(wellKnownMatch[1])) return null;
|
|
199
199
|
|
|
200
|
+
// Carve-out: api-specs.zip is the one .zip this platform legitimately serves —
|
|
201
|
+
// the request-time bundle of every OpenAPI spec a docs site references, streamed
|
|
202
|
+
// by app/api-specs.zip/route.ts (and the /docs hostAtDocs variant). Without this,
|
|
203
|
+
// the backup-archive blocklist below fast-404s it as a `*.zip` probe. Exact match
|
|
204
|
+
// only — no other .zip path is exempt.
|
|
205
|
+
if (lower === '/api-specs.zip' || lower === '/docs/api-specs.zip') return null;
|
|
206
|
+
|
|
200
207
|
// Dotfile or dot-dir, at the root or at any segment boundary.
|
|
201
208
|
// Matches: /.git, /.env, /.aws/credentials, /admin/.env, /backend/.git/config
|
|
202
209
|
// Does NOT match: /blog/page.with.dots, /api/v1.2/endpoint (dot is
|
|
@@ -454,6 +454,8 @@ export interface GeneratedArtifacts {
|
|
|
454
454
|
llmsFullTxt: string;
|
|
455
455
|
robotsTxt: string;
|
|
456
456
|
rssFeed: string | null;
|
|
457
|
+
/** changelog.json for the embeddable widget — built from the same updates as rssFeed. */
|
|
458
|
+
changelog: string;
|
|
457
459
|
}
|
|
458
460
|
|
|
459
461
|
/**
|
|
@@ -482,18 +484,21 @@ export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArti
|
|
|
482
484
|
: '';
|
|
483
485
|
const robotsTxt = generateRobotsTxt({ baseUrl, hostAtDocs, noindex });
|
|
484
486
|
|
|
485
|
-
//
|
|
487
|
+
// Extract <Update> entries once — they power BOTH the RSS feed and
|
|
488
|
+
// changelog.json, so sharing the single pass keeps the widget's "latest" and
|
|
489
|
+
// feed.xml structurally in sync (and avoids scanning every rss page twice).
|
|
490
|
+
const updates = rssPages && rssPages.length > 0 ? extractUpdatesFromPages(rssPages) : [];
|
|
491
|
+
|
|
486
492
|
let rssFeed: string | null = null;
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
name, description, baseUrl, hostAtDocs, updates,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
493
|
+
if (updates.length > 0) {
|
|
494
|
+
rssFeed = generateRssFeed({
|
|
495
|
+
name, description, baseUrl, hostAtDocs, updates,
|
|
496
|
+
});
|
|
494
497
|
}
|
|
495
498
|
|
|
496
|
-
|
|
499
|
+
const changelog = generateChangelog(updates);
|
|
500
|
+
|
|
501
|
+
return { sitemap, llmsTxt, llmsFullTxt, robotsTxt, rssFeed, changelog };
|
|
497
502
|
}
|
|
498
503
|
|
|
499
504
|
// =============================================================================
|
|
@@ -614,6 +619,31 @@ export function extractUpdatesFromPages(pages: RssPageInfo[]): UpdateEntry[] {
|
|
|
614
619
|
return updates;
|
|
615
620
|
}
|
|
616
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Emit changelog metadata (newest <Update> first) for the embeddable
|
|
624
|
+
* "What's New" widget's unread indicator. Consumes the SAME UpdateEntry[]
|
|
625
|
+
* that powers feed.xml (extractUpdatesFromPages), so the widget's "latest"
|
|
626
|
+
* always agrees with the RSS feed and inherits its rss:true gating (docs
|
|
627
|
+
* pages that merely demonstrate <Update> are excluded). Entries without a
|
|
628
|
+
* parseable date are dropped (can't be ordered). Stable id = date+slug so a
|
|
629
|
+
* label typo doesn't reset every visitor's seen-state. Ties on the same date
|
|
630
|
+
* are broken by label slug (not source order) so the chosen `latest` — and
|
|
631
|
+
* thus the unread id — stays identical across builds even if page iteration
|
|
632
|
+
* order shifts; otherwise a reorder would silently reset every visitor's dot.
|
|
633
|
+
*/
|
|
634
|
+
export function generateChangelog(updates: UpdateEntry[]): string {
|
|
635
|
+
const dated = updates
|
|
636
|
+
.filter((u) => u.date && !Number.isNaN(Date.parse(u.date)))
|
|
637
|
+
.map((u) => ({ u, ts: Date.parse(u.date as string) }))
|
|
638
|
+
.sort((a, b) => b.ts - a.ts
|
|
639
|
+
|| labelToSlug(a.u.label).localeCompare(labelToSlug(b.u.label)));
|
|
640
|
+
const top = dated[0]?.u ?? null;
|
|
641
|
+
const latest = top
|
|
642
|
+
? { id: `${top.date}-${labelToSlug(top.label)}`, label: top.label, date: top.date }
|
|
643
|
+
: null;
|
|
644
|
+
return JSON.stringify({ version: 1, latest, count: dated.length });
|
|
645
|
+
}
|
|
646
|
+
|
|
617
647
|
/**
|
|
618
648
|
* Generate RSS 2.0 XML feed from update entries.
|
|
619
649
|
*/
|
|
@@ -15,9 +15,10 @@ import { log } from '@/lib/logger';
|
|
|
15
15
|
import { isIsrMode } from '@/lib/page-isr-helpers';
|
|
16
16
|
import { fetchStaticFile } from '@/lib/r2-content';
|
|
17
17
|
|
|
18
|
-
/** Filenames served via createStaticFileHandler (
|
|
18
|
+
/** Filenames served via createStaticFileHandler (7 at root + 7 at /docs). */
|
|
19
19
|
export const STATIC_FILE_NAMES = [
|
|
20
20
|
'sitemap.xml', 'robots.txt', 'llms.txt', 'llms-full.txt', 'feed.xml', 'search-data.json',
|
|
21
|
+
'changelog.json',
|
|
21
22
|
] as const;
|
|
22
23
|
|
|
23
24
|
/** All CDN paths for static file routes — used by revalidation to purge CDN cache. */
|
|
@@ -41,6 +42,14 @@ const CONTENT_TYPES: Record<string, string> = {
|
|
|
41
42
|
*
|
|
42
43
|
* `search-data.json` is intentionally NOT in this list: a real user landing
|
|
43
44
|
* on the upstream subdomain would still hit it from the in-page search UI.
|
|
45
|
+
*
|
|
46
|
+
* `changelog.json` is intentionally NOT in this list either, and must stay out:
|
|
47
|
+
* the embeddable widget always fetches it from `<slug>.jamdesk.app` (custom
|
|
48
|
+
* domains can't serve the `?embed=1` render), and that subdomain carries
|
|
49
|
+
* `x-jd-noindex: true` whenever the project has a custom domain. Suppressing it
|
|
50
|
+
* here would 404 the metadata fetch and permanently break the widget's unread
|
|
51
|
+
* dot for every custom-domain customer. It's a deliberately public artifact
|
|
52
|
+
* (served with `Access-Control-Allow-Origin: *`), not a URL-inventory file.
|
|
44
53
|
*/
|
|
45
54
|
const NOINDEX_SUPPRESSED_FILES = new Set([
|
|
46
55
|
'sitemap.xml', 'llms.txt', 'llms-full.txt', 'feed.xml',
|
|
@@ -84,6 +93,31 @@ function getContentType(filename: string): string {
|
|
|
84
93
|
return CONTENT_TYPES[ext] || 'application/octet-stream';
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Like createStaticFileHandler, but adds Access-Control-Allow-Origin: * so the
|
|
98
|
+
* file can be fetched cross-origin (the embeddable widget runs on the
|
|
99
|
+
* customer's site and reads changelog.json from the project's jamdesk.app
|
|
100
|
+
* origin). Simple GET — no preflight needed.
|
|
101
|
+
*
|
|
102
|
+
* PUBLIC ARTIFACTS ONLY. `*` makes every byte this serves readable cross-origin
|
|
103
|
+
* by any website. Only ever wrap genuinely public files (changelog.json). Do
|
|
104
|
+
* NOT reuse this for project-private or auth-scoped routes — that would widen
|
|
105
|
+
* them to the entire web.
|
|
106
|
+
*/
|
|
107
|
+
export function createCorsStaticFileHandler(
|
|
108
|
+
filename: string,
|
|
109
|
+
label: string,
|
|
110
|
+
contentTypeOverride?: string,
|
|
111
|
+
cacheControlOverride?: string,
|
|
112
|
+
): (request: NextRequest) => Promise<NextResponse> {
|
|
113
|
+
const handler = createStaticFileHandler(filename, label, contentTypeOverride, cacheControlOverride);
|
|
114
|
+
return async function GET(request: NextRequest): Promise<NextResponse> {
|
|
115
|
+
const response = await handler(request);
|
|
116
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
117
|
+
return response;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
/**
|
|
88
122
|
* Create a GET handler that serves a static file from R2.
|
|
89
123
|
*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// AUTO-GENERATED — do not edit by hand.
|
|
2
|
+
// Byte-for-byte copy of github-slugger@2.0.0's removal regex, transformed ESM->CJS so the CJS link validator
|
|
3
|
+
// (validate-links.cjs) can require it. github-slugger is what rehype-slug
|
|
4
|
+
// uses to assign real heading ids; replicating it keeps #fragment validation
|
|
5
|
+
// aligned with the true DOM anchors. Regenerate after upgrading github-slugger:
|
|
6
|
+
// node scripts/gen-github-slugger-regex.cjs
|
|
7
|
+
// A drift test (validate-links-slug.test.ts) fails if this diverges.
|
|
8
|
+
|
|
9
|
+
// This module is generated by `script/`.
|
|
10
|
+
/* eslint-disable no-control-regex, no-misleading-character-class, no-useless-escape */
|
|
11
|
+
const regex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g
|
|
12
|
+
|
|
13
|
+
module.exports = { regex };
|