import { isSome } from "@jet/environment"; import { URL } from "@jet/environment/util/urls"; import { Path, Parameters } from "../../foundation/network/url-constants"; import { normalizeLocale, deriveLocaleForUrl } from "../locale"; /** * Generate the routes property for a `RouteProvider` and a canonical URL factory * function for a {@link RoutableIntent} (non-detail page). * * `makeIntent` should be a function that accepts a single parameter, * `options: Omit`, and returns the intent. * * `path` is the static part of the path. For example, in `'/us/browse?l=es'`, * `'/browse'` is the static part of the path. Path can contain multiple * segments like `'/foo/bar'`, but it does not support parameters like * `'/foo/{id}'`. * * `expectedQueryParameters` are the names of the query parameters for the routes * that also double as properties on the intent. This instructs the intent * builder on which properties to provide to `makeIntent`. For example, * `SearchResultsPageIntent`'s `RouteProvider` recognizes a `'term'` query param, which * is then stored in the intent as `{ term: 'valueFromParam' }`. For that case, * this array should be `['term']`. Put another way, this should be a list of the * non-optional (non-nullable, etc.) string properties on the intent: * * ```typescript * interface SearchResultsPageIntent extends RouteableIntent { * $kind: 'SearchResultsPageIntent'; // not this one * term: string; // this one should be included! * facet: Opt; // but it doesn't include this; it's optional * } * ``` * * NOTE: If you receive a type error like `Type 'string' is not assignable * to type 'never'` for this argument, it means that your array of parameters * doesn't match the intent definition. In the above example, if we added a * `originalTerm: string` to the intent, then passing ['term'] would fail, * because 'originalTerm' is missing. Adding it does mean, however, that you * expect users to be able to pass it as a query parameter * (ex. /search?term=foo&originalTerm=bar). If that's unintended, you may want * to consider making this property Opt<> (and also maybe ?). * * Usage Example: For a contrived intent `FooIntent`, * * ```typescript * interface FooIntent extends RouteableIntent { * $kind: 'FooIntent'; * bar: string; * } * ``` * * If we expect users to be able to route to this intent via `/us/foo?bar=baz` * or `/foo?bar=baz&l=es`, then in the IntentController's file: * * ```typescript * const { * routes, * makeCanonicalUrl, * } = generateRoutes(makeFooIntent, '/foo', ['bar']); * ``` * * And then, `routes` can be used in the `RouteProvider`: * * ```typescript * export const FooIntentController: RouteProvider & ... = { * routes, * * // ... * }; * ``` * * And then in perform, when returning a `Page`, append the `canonicalURL`: * * ```typescript * async perform(intent: FooIntent, objectGraph: ObjectGraph): Promise { * // ... * return { * ...page, * canonicalURL: makeCanonicalUrl(intent), * }; * }, * ``` */ export function generateRoutes(makeIntent, path, requiredQueryParameters = [], extras) { const ruleExtras = { exclusions: extras === null || extras === void 0 ? void 0 : extras.exclusions }; const queryParameters = requiredQueryParameters.slice(); if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) { queryParameters.push(...extras.optionalQuery.map((p) => `${p}?`)); } return { routes(objectGraph) { var _a; return [ { rules: [ { ...ruleExtras, path, query: [`${Parameters.language}?`, ...queryParameters], }, { ...ruleExtras, path: `/{${Path.storeFront}}${path}`, query: [`${Parameters.language}?`, ...queryParameters], }, ...((_a = extras === null || extras === void 0 ? void 0 : extras.extraRules) !== null && _a !== void 0 ? _a : []), ], handler(_, parameters) { const { [Path.storeFront]: storefront, [Parameters.language]: language, ...rest } = parameters; const typedRestParams = rest; return makeIntent({ ...typedRestParams, ...normalizeLocale(objectGraph, { storefront, language, }), }); }, }, ]; }, makeCanonicalUrl(objectGraph, intent) { const url = buildBaseUrl(objectGraph, path, intent); // Replace dynamic segments with `Intent` properties const pathComponents = url.pathComponents(); for (const [i, pathComponent] of pathComponents.entries()) { if (pathComponent.startsWith("{") && pathComponent.endsWith("}")) { const variableInPathComponent = pathComponent.substring(1, pathComponent.length - 1); pathComponents[i] = intent[variableInPathComponent]; } } url.set("pathname", "/" + pathComponents.join("/")); // Set query params const sortedQueryParameters = requiredQueryParameters.slice(); // Sorted for consistent order sortedQueryParameters.sort((a, b) => a.localeCompare(b)); for (const queryParameter of sortedQueryParameters) { if (!(queryParameter in intent)) { throw new Error(`expected queryParmeters to contain: ${queryParameter}`); } const value = intent[queryParameter]; // NOTE: TypeScript is unfortunately not able to see that // parameters[queryParameter] will always be string. We prove // this in the type. queryParameters is required to contain the // string names of all keys on I with a string value (see // RequiredIntentOptions). This escape hatch is preferable // within this function as it means that the function is // externally typesafe. If a user forgets to pass in params, // there will be a type error. url.param(queryParameter, value); } if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) { const optionalParams = extras.optionalQuery.slice(); optionalParams.sort((a, b) => a.localeCompare(b)); optionalParams.forEach((query) => { const value = intent[query]; if (value) { url.param(query, value); } }); } return url.toString(); }, }; } /** * Builds the basis for a "canonical URL" given a {@linkcode path} and {@linkcode locale} * * The locale data will be encoded into the resulting {@linkcode URL}; other "dynamic segments" * within {@linkcode path} are expected to be replaced separately */ function buildBaseUrl(objectGraph, path, locale) { const url = new URL("https://apps.apple.com"); const { storefront, language } = deriveLocaleForUrl(objectGraph, locale); url.path(storefront); // Actual path (ex. browse in https://apps.apple.com/us/browse) // substr(1), because URL.path doesn't expect a leading slash url.path(path.substr(1)); if (isSome(language)) { url.param("l", language); } return url; } //# sourceMappingURL=generate-routes.js.map