import { z } from "zod"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import { id } from "tsafe/id"; import { parse as urlParse } from "url"; import { typeGuard } from "tsafe/typeGuard"; import { symToStr } from "tsafe/symToStr"; const bundlers = ["mvn", "keycloakify", "none"] as const; type Bundler = (typeof bundlers)[number]; type ParsedPackageJson = { name: string; version: string; homepage?: string; keycloakify?: { /** @deprecated: use extraLoginPages instead */ extraPages?: string[]; extraLoginPages?: string[]; extraAccountPages?: string[]; extraThemeProperties?: string[]; areAppAndKeycloakServerSharingSameDomain?: boolean; artifactId?: string; groupId?: string; bundler?: Bundler; keycloakVersionDefaultAssets?: string; }; }; const zParsedPackageJson = z.object({ "name": z.string(), "version": z.string(), "homepage": z.string().optional(), "keycloakify": z .object({ "extraPages": z.array(z.string()).optional(), "extraLoginPages": z.array(z.string()).optional(), "extraAccountPages": z.array(z.string()).optional(), "extraThemeProperties": z.array(z.string()).optional(), "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(), "artifactId": z.string().optional(), "groupId": z.string().optional(), "bundler": z.enum(bundlers).optional(), "keycloakVersionDefaultAssets": z.string().optional() }) .optional() }); assert, ParsedPackageJson>>(); /** Consolidated build option gathered form CLI arguments and config in package.json */ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets; export namespace BuildOptions { export type Common = { isSilent: boolean; version: string; themeName: string; extraLoginPages: string[] | undefined; extraAccountPages: string[] | undefined; extraThemeProperties?: string[]; groupId: string; artifactId: string; bundler: Bundler; keycloakVersionDefaultAssets: string; }; export type Standalone = Common & { isStandalone: true; urlPathname: string | undefined; }; export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains; export namespace ExternalAssets { export type CommonExternalAssets = Common & { isStandalone: false; }; export type SameDomain = CommonExternalAssets & { areAppAndKeycloakServerSharingSameDomain: true; }; export type DifferentDomains = CommonExternalAssets & { areAppAndKeycloakServerSharingSameDomain: false; urlOrigin: string; urlPathname: string | undefined; }; } } export function readBuildOptions(params: { packageJson: string; CNAME: string | undefined; isExternalAssetsCliParamProvided: boolean; isSilent: boolean; }): BuildOptions { const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params; const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson)); const url = (() => { const { homepage } = parsedPackageJson; let url: URL | undefined = undefined; if (homepage !== undefined) { url = new URL(homepage); } if (CNAME !== undefined) { url = new URL(`https://${CNAME.replace(/\s+$/, "")}`); } if (url === undefined) { return undefined; } return { "origin": url.origin, "pathname": (() => { const out = url.pathname.replace(/([^/])$/, "$1/"); return out === "/" ? undefined : out; })() }; })(); const common: BuildOptions.Common = (() => { const { name, keycloakify = {}, version, homepage } = parsedPackageJson; const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } = keycloakify ?? {}; const themeName = name .replace(/^@(.*)/, "$1") .split("/") .join("-"); return { themeName, "bundler": (() => { const { KEYCLOAKIFY_BUNDLER } = process.env; assert( typeGuard( KEYCLOAKIFY_BUNDLER, [undefined, ...id(bundlers)].includes(KEYCLOAKIFY_BUNDLER) ), `${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}` ); return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify"; })(), "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`, "groupId": (() => { const fallbackGroupId = `${themeName}.keycloak`; return ( process.env.KEYCLOAKIFY_GROUP_ID ?? groupId ?? (!homepage ? fallbackGroupId : urlParse(homepage) .host?.replace(/:[0-9]+$/, "") ?.split(".") .reverse() .join(".") ?? fallbackGroupId) + ".keycloak" ); })(), "version": process.env.KEYCLOAKIFY_VERSION ?? version, "extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])], extraAccountPages, extraThemeProperties, isSilent, "keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3" }; })(); if (isExternalAssetsCliParamProvided) { const commonExternalAssets = id({ ...common, "isStandalone": false }); if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) { return id({ ...commonExternalAssets, "areAppAndKeycloakServerSharingSameDomain": true }); } else { assert( url !== undefined, [ "Can't compile in external assets mode if we don't know where", "the app will be hosted.", "You should provide a homepage field in the package.json (or create a", "public/CNAME file.", "Alternatively, if your app and the Keycloak server are on the same domain, ", "eg https://example.com is your app and https://example.com/auth is the keycloak", 'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }', "in your package.json" ].join(" ") ); return id({ ...commonExternalAssets, "areAppAndKeycloakServerSharingSameDomain": false, "urlOrigin": url.origin, "urlPathname": url.pathname }); } } return id({ ...common, "isStandalone": true, "urlPathname": url?.pathname }); }