mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
1c69ce3fef
* fix(pages-router): load custom _app/_document via resolved file paths in dev The dev server imported pages/_app and pages/_document by extensionless id and relied on the module runner applying custom resolve.extensions (e.g. ".page.tsx" from pageExtensions). The runner no longer resolves those, so apps using pageExtensions silently lost their custom App/Document (the load failure is swallowed). Import the file path resolved by findFileWithExts instead, at all three call sites. * test(e2e): add pages-router-complex example app and behaviour suite A deliberately convoluted Pages Router app that serves as a compatibility target: the kinds of patterns that only surface in large, long-lived enterprise pages-router codebases, each pinned by a Playwright spec whose behaviour is verified against real Next.js (73/73 under next dev --webpack). Highlights: custom pageExtensions everywhere (middleware.page.ts, instrumentation.page.ts), app-shell getInitialProps with a TTL-memoised chrome fetch and embedded-shell/draft bypasses, class _document with its own GIP, a zone route dimension driven by middleware rewrites and a real i18next/react-i18next runtime, catch-all routes with static-sibling precedence and cacheable-404/redirect hygiene, record-driven template branching on a dynamic/static/dynamic route, an urql-style server-snapshot data layer, shallow routing + router.events + next/compat/router + next/navigation hooks in pages, next/image with a custom loader, draft-mode gateway APIs, a function-form next.config, and a Cloudflare setup matching the vinext init scaffold. The playwright project (pages-router-complex, port 4199) is intentionally not in the CI e2e matrix: vinext currently passes 59/73, and the failures are the compat backlog documented in the example README. * fix(pages-router): cover compound page extensions in CI
185 lines
4.8 KiB
TypeScript
185 lines
4.8 KiB
TypeScript
/**
|
|
* The remaining slots of the provider pyramid. Each one is small, but their
|
|
* nesting order is load-bearing:
|
|
* sign-in > copy > formatting > viewport > runtime-settings > critique >
|
|
* graph-handle > edge-probe > trials > side-panel.
|
|
*/
|
|
|
|
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
|
|
// --- Sign-in state ---------------------------------------------------------
|
|
|
|
type SignInState = {
|
|
standing: "guest" | "known";
|
|
faulted: boolean;
|
|
};
|
|
|
|
const SignInContext = createContext<SignInState>({
|
|
standing: "guest",
|
|
faulted: false,
|
|
});
|
|
|
|
export const SignInStateProvider = ({
|
|
withSessionBridge,
|
|
surfaceFaults,
|
|
children,
|
|
}: {
|
|
withSessionBridge?: boolean;
|
|
surfaceFaults?: boolean;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
const value = useMemo<SignInState>(
|
|
() => ({
|
|
standing: withSessionBridge ? "guest" : "guest",
|
|
faulted: surfaceFaults ? false : false,
|
|
}),
|
|
[withSessionBridge, surfaceFaults],
|
|
);
|
|
return <SignInContext.Provider value={value}>{children}</SignInContext.Provider>;
|
|
};
|
|
|
|
export const useSignInState = () => useContext(SignInContext);
|
|
|
|
// --- Viewport band ---------------------------------------------------------
|
|
|
|
type ViewportBand = "narrow" | "mid" | "broad";
|
|
|
|
const ViewportBandContext = createContext<ViewportBand>("broad");
|
|
|
|
export const ViewportBandProvider = ({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) => {
|
|
const [band, setBand] = useState<ViewportBand>("broad");
|
|
|
|
useEffect(() => {
|
|
const measure = () =>
|
|
setBand(
|
|
window.innerWidth < 640 ? "narrow" : window.innerWidth < 1024 ? "mid" : "broad",
|
|
);
|
|
measure();
|
|
window.addEventListener("resize", measure);
|
|
return () => window.removeEventListener("resize", measure);
|
|
}, []);
|
|
|
|
return (
|
|
<ViewportBandContext.Provider value={band}>
|
|
{children}
|
|
</ViewportBandContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useViewportBand = () => useContext(ViewportBandContext);
|
|
|
|
// --- Critique portal settings ---------------------------------------------
|
|
|
|
type CritiquePortalSettings = {
|
|
expressEndpoint: string;
|
|
gatewayRest?: { baseUrl: string; apiKey: string };
|
|
};
|
|
|
|
const CritiquePortalContext = createContext<CritiquePortalSettings>({
|
|
expressEndpoint: "",
|
|
});
|
|
|
|
export const CritiquePortalProvider = ({
|
|
settings,
|
|
children,
|
|
}: {
|
|
settings: CritiquePortalSettings;
|
|
children: React.ReactNode;
|
|
}) => (
|
|
<CritiquePortalContext.Provider value={settings}>
|
|
{children}
|
|
</CritiquePortalContext.Provider>
|
|
);
|
|
|
|
export const useCritiquePortal = () => useContext(CritiquePortalContext);
|
|
|
|
// --- Edge-probe (edge-injected experimentation) data -----------------------
|
|
|
|
export type EdgeProbeData = Record<string, unknown>;
|
|
|
|
const EdgeProbeContext = createContext<{
|
|
data: EdgeProbeData;
|
|
hasActiveProbe: (id: number) => boolean;
|
|
}>({ data: {}, hasActiveProbe: () => false });
|
|
|
|
export const EdgeProbeProvider = ({
|
|
data = {},
|
|
children,
|
|
}: {
|
|
data?: EdgeProbeData;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
const value = useMemo(
|
|
() => ({
|
|
data,
|
|
hasActiveProbe: (id: number) =>
|
|
Array.isArray(data["activeProbes"]) &&
|
|
(data["activeProbes"] as number[]).includes(id),
|
|
}),
|
|
[data],
|
|
);
|
|
return <EdgeProbeContext.Provider value={value}>{children}</EdgeProbeContext.Provider>;
|
|
};
|
|
|
|
export const useEdgeProbe = () => useContext(EdgeProbeContext);
|
|
|
|
// --- Side panel (slide-out surface fed by the data edge) -------------------
|
|
|
|
const SidePanelContext = createContext<{
|
|
dataEdgeUrl: string;
|
|
isOpen: boolean;
|
|
setOpen: (open: boolean) => void;
|
|
}>({ dataEdgeUrl: "", isOpen: false, setOpen: () => {} });
|
|
|
|
export const SidePanelProvider = ({
|
|
dataEdgeUrl,
|
|
children,
|
|
}: {
|
|
dataEdgeUrl: string;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
const [isOpen, setOpen] = useState(false);
|
|
const value = useMemo(
|
|
() => ({ dataEdgeUrl, isOpen, setOpen }),
|
|
[dataEdgeUrl, isOpen],
|
|
);
|
|
return (
|
|
<SidePanelContext.Provider value={value}>{children}</SidePanelContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useSidePanel = () => useContext(SidePanelContext);
|
|
|
|
// --- Engagement kit hook ---------------------------------------------------
|
|
|
|
export const useEngageKit = ({
|
|
product,
|
|
engageSettings,
|
|
}: {
|
|
product: string;
|
|
engageSettings: { appId?: string; apiKey?: string };
|
|
}): void => {
|
|
useEffect(() => {
|
|
if (engageSettings.appId && engageSettings.apiKey) {
|
|
// A real integration would boot a third-party SDK here.
|
|
(window as unknown as Record<string, unknown>)["__ATLAS_ENGAGE__"] = {
|
|
product,
|
|
appId: engageSettings.appId,
|
|
};
|
|
}
|
|
}, [product, engageSettings.appId, engageSettings.apiKey]);
|
|
};
|
|
|
|
// --- Beacon consent hook ---------------------------------------------------
|
|
|
|
export const useBeaconConsent = (): void => {
|
|
useEffect(() => {
|
|
// Consent tooling would gate the beacon sink here; the hook slot itself
|
|
// is what the shell architecture requires.
|
|
}, []);
|
|
};
|