Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 93 additions & 14 deletions .github/scripts/audit-app-zone-shell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ export function extractRoutes(source) {
let match;

while ((match = routeRegex.exec(source)) !== null) {
const routeBlock = match[0];
const sourcePath = normalizeSource(match[1]);
const destination = match[2];
const deepDestination = routeBlock.match(
/deepDestination:\s*"([^"]+)"/,
)?.[1];
if (!sourcePath) continue;
if (SKIP_IF_DESTINATION_CONTAINS.some((part) => destination.includes(part))) {
continue;
Expand All @@ -100,6 +104,7 @@ export function extractRoutes(source) {
routes.set(sourcePath, {
source: sourcePath,
destination,
...(deepDestination ? { deepDestination } : {}),
});
}

Expand All @@ -118,6 +123,52 @@ function isSameOrNestedPath(path, basePath) {
return path === base || path.startsWith(`${base}/`);
}

function templateBasePath(pathname) {
const markerIndex = pathname.indexOf("/:path*");
if (markerIndex === -1) return trimTrailingPath(pathname);
return trimTrailingPath(pathname.slice(0, markerIndex));
}

function pathSuffix(path, basePath) {
const base = trimTrailingPath(basePath);
if (base === "/") return path;
return path.slice(base.length);
}

function searchWithoutTemplateParams(locUrl, templateUrl) {
const searchParams = new URLSearchParams(locUrl.search);

for (const [key, value] of templateUrl.searchParams) {
const values = searchParams.getAll(key);
let removedTemplateValue = false;
searchParams.delete(key);

for (const candidate of values) {
if (!removedTemplateValue && candidate === value) {
removedTemplateValue = true;
continue;
}
searchParams.append(key, candidate);
}
}

const query = searchParams.toString();
return query ? `?${query}` : "";
}

function sourcePathFromDestinationUrl(locUrl, destinationUrl, route, basePath) {
if (
locUrl.origin !== destinationUrl.origin ||
!isSameOrNestedPath(locUrl.pathname, basePath)
) {
return null;
}

const suffix = pathSuffix(locUrl.pathname, basePath);
const search = searchWithoutTemplateParams(locUrl, destinationUrl);
return `${trimTrailingPath(route.source)}${suffix}${search}${locUrl.hash}`;
}

export function sourcePathFromSitemapLoc(loc, route, baseUrl) {
let locUrl;
try {
Expand All @@ -128,6 +179,9 @@ export function sourcePathFromSitemapLoc(loc, route, baseUrl) {

const sourceUrl = new URL(resolveUrl(baseUrl, route.source));
const destinationUrl = new URL(resolveUrl(baseUrl, route.destination));
const deepDestinationUrl = route.deepDestination
? new URL(resolveUrl(baseUrl, route.deepDestination))
: null;

if (
locUrl.origin === sourceUrl.origin &&
Expand All @@ -136,32 +190,57 @@ export function sourcePathFromSitemapLoc(loc, route, baseUrl) {
return `${locUrl.pathname}${locUrl.search}${locUrl.hash}`;
}

if (
locUrl.origin === destinationUrl.origin &&
isSameOrNestedPath(locUrl.pathname, destinationUrl.pathname)
) {
const destinationBasePath = trimTrailingPath(destinationUrl.pathname);
const suffix =
destinationBasePath === "/"
? locUrl.pathname
: locUrl.pathname.slice(destinationBasePath.length);
return `${trimTrailingPath(route.source)}${suffix}${locUrl.search}${locUrl.hash}`;
const sourceFromBaseDestination = sourcePathFromDestinationUrl(
locUrl,
destinationUrl,
route,
trimTrailingPath(destinationUrl.pathname),
);
if (sourceFromBaseDestination) return sourceFromBaseDestination;

if (deepDestinationUrl) {
const sourceFromDeepDestination = sourcePathFromDestinationUrl(
locUrl,
deepDestinationUrl,
route,
templateBasePath(deepDestinationUrl.pathname),
);
if (sourceFromDeepDestination) return sourceFromDeepDestination;
}

return null;
}

function mergeSourceSearch(destinationUrl, sourceUrl) {
for (const [key, value] of sourceUrl.searchParams) {
destinationUrl.searchParams.append(key, value);
}
}

export function resolveDestinationForSource(route, sourcePath, baseUrl) {
const destinationUrl = new URL(resolveUrl(baseUrl, route.destination));
const sourceUrl = new URL(resolveUrl(baseUrl, sourcePath));
const routeSourceBase = trimTrailingPath(route.source);
const suffix =
routeSourceBase === "/"
? sourceUrl.pathname
: sourceUrl.pathname.slice(routeSourceBase.length);
: pathSuffix(sourceUrl.pathname, routeSourceBase);

const destinationTemplate =
suffix && route.deepDestination ? route.deepDestination : route.destination;
const destinationUrl = new URL(resolveUrl(baseUrl, destinationTemplate));

if (suffix) {
if (destinationUrl.pathname.includes("/:path*")) {
destinationUrl.pathname = destinationUrl.pathname.replace(
":path*",
suffix.replace(/^\/+/, ""),
);
} else {
destinationUrl.pathname = joinPaths(destinationUrl.pathname, suffix);
}
}

destinationUrl.pathname = joinPaths(destinationUrl.pathname, suffix);
destinationUrl.search = sourceUrl.search;
mergeSourceSearch(destinationUrl, sourceUrl);
destinationUrl.hash = sourceUrl.hash;
return destinationUrl.toString();
}
Expand Down
69 changes: 69 additions & 0 deletions .github/scripts/audit-app-zone-shell.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ describe("extractRoutes", () => {
{ source: "/:countryId/marriage", destination: "https://marriage.example.com" },
{ source: "/us/salternative", destination: "https://salt.example.com/us/salternative" },
{ source: "/us/salternative", destination: "https://new-salt.example.com/us/salternative" },
{
source: "/us/pe84",
destination: "https://pe84.example.com/us/pe84/calculator",
deepDestination: "https://pe84.example.com/us/pe84/:path*",
},
{ source: "/api/:path*", destination: "https://api.example.com/:path*" },
{ source: "/us/_zones/foo", destination: "https://example.com/_zones/foo" },
{ source: "/us/:slug", destination: "https://example.com/:slug" },
Expand All @@ -44,6 +49,11 @@ describe("extractRoutes", () => {
source: "/us/marriage",
destination: "https://marriage.example.com",
},
{
source: "/us/pe84",
destination: "https://pe84.example.com/us/pe84/calculator",
deepDestination: "https://pe84.example.com/us/pe84/:path*",
},
{
source: "/us/salternative",
destination: "https://new-salt.example.com/us/salternative",
Expand Down Expand Up @@ -101,6 +111,65 @@ describe("sitemap route mapping", () => {
"https://wptra.vercel.app/us/working-parents-tax-relief-act/calculator?tab=policy#aggregate",
);
});

test("uses custom deepDestination templates for PR fallback URLs", () => {
const pe84Route = {
source: "/us/pe84",
destination: "https://april-fools-2026-two.vercel.app/us/pe84/calculator",
deepDestination: "https://april-fools-2026-two.vercel.app/us/pe84/:path*",
};

assert.equal(
sourcePathFromSitemapLoc(
"https://april-fools-2026-two.vercel.app/us/pe84/savings?view=chart#result",
pe84Route,
"https://policyengine.org",
),
"/us/pe84/savings?view=chart#result",
);
assert.equal(
resolveDestinationForSource(
pe84Route,
"/us/pe84/savings?view=chart#result",
"https://policyengine.org",
),
"https://april-fools-2026-two.vercel.app/us/pe84/savings?view=chart#result",
);
assert.equal(
resolveDestinationForSource(
pe84Route,
"/us/pe84",
"https://policyengine.org",
),
"https://april-fools-2026-two.vercel.app/us/pe84/calculator",
);
});

test("preserves static destination query params without leaking them into source paths", () => {
const ukMarriageRoute = {
source: "/uk/marriage",
destination: "https://marriage-zeta-beryl.vercel.app/us/marriage?country=uk",
deepDestination:
"https://marriage-zeta-beryl.vercel.app/us/marriage/:path*?country=uk",
};

assert.equal(
sourcePathFromSitemapLoc(
"https://marriage-zeta-beryl.vercel.app/us/marriage/couples?country=uk&tab=chart#summary",
ukMarriageRoute,
"https://policyengine.org",
),
"/uk/marriage/couples?tab=chart#summary",
);
assert.equal(
resolveDestinationForSource(
ukMarriageRoute,
"/uk/marriage/couples?tab=chart#summary",
"https://policyengine.org",
),
"https://marriage-zeta-beryl.vercel.app/us/marriage/couples?country=uk&tab=chart#summary",
);
});
});

describe("inspectTopShellData", () => {
Expand Down
Loading