Merge branch 'main' into feat/additional-account-pages

This commit is contained in:
George Litos 2024-03-02 13:41:48 +02:00
commit 2e6321342e
19 changed files with 374 additions and 143 deletions

View File

@ -3,9 +3,6 @@ on:
push:
branches:
- main
- v5
- v6
- v7
pull_request:
branches:
- main

View File

@ -40,7 +40,7 @@
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
## Sponsor 👼
## Sponsor
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
Their dedicated support helps us continue the development and maintenance of this project.
@ -123,6 +123,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
## 9.5
- Post build hook: You can now apply custom transformation to your theme files. [Learn more](https://docs.keycloakify.dev/build-options#postbuild-hook).
- You can now specify your option in the Keycloakify's Vite plugin instead in the package.json. [See example](https://docs.keycloakify.dev/build-options#themename).
## 9.4
**Vite Support! 🎉**

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "9.4.1",
"version": "9.5.8",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -125,6 +125,7 @@
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
"zod": "^3.17.10"
"zod": "^3.17.10",
"magic-string": "^0.30.7"
}
}

View File

@ -90,6 +90,7 @@ export declare namespace KcContext {
lastName?: string;
username?: string;
};
properties: Record<string, string | undefined>;
sessions: {
sessions: {
ipAddress: string;

View File

@ -8,8 +8,9 @@ import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcConte
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData } = params ?? {};
const { mockData, mockProperties } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
mockPageId?: PageId;
@ -82,6 +83,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
});
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
});
}
return { kcContext };
}

View File

@ -146,6 +146,17 @@ export const kcContextCommonMock: KcContext.Common = {
"email": "john.doe@code.gouv.fr",
"username": "doe_j"
},
"properties": {
"parent": "account-v1",
"kcButtonLargeClass": "btn-lg",
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"kcButtonPrimaryClass": "btn-primary",
"accountResourceProvider": "account-v1",
"styles":
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
"kcButtonClass": "btn",
"kcButtonDefaultClass": "btn-default"
},
"sessions": {
"sessions": [
{

View File

@ -10,3 +10,5 @@ export const retrocompatPostfix = "_retrocompat";
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const keycloakifyBuildOptionsForPostPostBuildScriptEnvName = "KEYCLOAKIFY_BUILD_OPTIONS_POST_POST_BUILD_SCRIPT";

View File

@ -181,34 +181,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
);
}
{
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
fs.writeFileSync(
totpFtlFilePath,
Buffer.from(
fs
.readFileSync(totpFtlFilePath)
.toString("utf8")
.replace(
[
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>"
].join("\n"),
[
" <#if totp.policy.supportedApplications?has_content>",
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>",
" </#if>"
].join("\n")
),
"utf8"
)
);
}
// Note, this is an optimization for reducing the size of the jar,
// For this version we know exactly which resources are used.
{

View File

@ -0,0 +1,25 @@
import { z } from "zod";
export type UserProvidedBuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
export const zUserProvidedBuildOptions = z.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
});

View File

@ -47,10 +47,15 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
throw new Error("Keycloakify's Vite plugin output not found");
}
const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
const userProvidedBuildOptions = {
...userProvidedBuildOptionsFromPackageJson,
...resolvedViteConfig?.userProvidedBuildOptions
};
const themeNames = (() => {
if (parsedPackageJson.keycloakify?.themeName === undefined) {
if (userProvidedBuildOptions.themeName === undefined) {
return [
parsedPackageJson.name
.replace(/^@(.*)/, "$1")
@ -59,11 +64,11 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
];
}
if (typeof parsedPackageJson.keycloakify.themeName === "string") {
return [parsedPackageJson.keycloakify.themeName];
if (typeof userProvidedBuildOptions.themeName === "string") {
return [userProvidedBuildOptions.themeName];
}
return parsedPackageJson.keycloakify.themeName;
return userProvidedBuildOptions.themeName;
})();
const reactAppBuildDirPath = (() => {
@ -72,9 +77,9 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
break webpack;
}
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
@ -94,13 +99,13 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames,
"extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties,
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
parsedPackageJson.keycloakify?.groupId ??
userProvidedBuildOptions.groupId ??
(parsedPackageJson.homepage === undefined
? fallbackGroupId
: urlParse(parsedPackageJson.homepage)
@ -110,20 +115,23 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true,
"loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"doCreateJar": userProvidedBuildOptions.doCreateJar ?? true,
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
reactAppRootDirPath,
reactAppBuildDirPath,
"keycloakifyBuildDirPath": (() => {
if (parsedPackageJson.keycloakify?.keycloakifyBuildDirPath !== undefined) {
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`;
return pathJoin(
reactAppRootDirPath,
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
);
})(),
"publicDirPath": (() => {
webpack: {
@ -179,7 +187,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
})(),
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true,
"doBuildRetrocompatAccountTheme": userProvidedBuildOptions.doBuildRetrocompatAccountTheme ?? true,
npmWorkspaceRootDirPath
};
}

View File

@ -3,41 +3,20 @@ import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { type UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
keycloakify?: UserProvidedBuildOptions;
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
})
.optional()
"keycloakify": zUserProvidedBuildOptions.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();

View File

@ -5,19 +5,22 @@ import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
import { UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
userProvidedBuildOptions: UserProvidedBuildOptions;
};
const zResolvedViteConfig = z.object({
"buildDir": z.string(),
"publicDir": z.string(),
"assetsDir": z.string(),
"urlPathname": z.string().optional()
"urlPathname": z.string().optional(),
"userProvidedBuildOptions": zUserProvidedBuildOptions
});
{

View File

@ -431,7 +431,7 @@
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
</#if>
<#local keys = "">
@ -463,9 +463,10 @@
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
@ -488,24 +489,33 @@
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || (
"applications.ftl" == pageId &&
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
is_subpath(path, ["applications", "applications"]) &&
(
key == "realm" ||
key == "container"
)
) || (
"applications.ftl" == pageId &&
"masterAdminClient" == key
are_same_path(path, ["user"]) &&
key == "delegateForUpdate"
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if (
["register.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
key == "attemptedUsername" && are_same_path(path, ["auth"])
)>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]>
</#attempt>
</#if>
@ -658,9 +668,9 @@
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#function is_subpath path searchedPath>
<#if path?size != searchedPath?size>
<#if path?size < searchedPath?size>
<#return false>
</#if>
@ -668,8 +678,14 @@
<#list path as property>
<#if i == searchedPath?size >
<#continue>
</#if>
<#local searchedProperty=searchedPath[i]>
<#local i+= 1>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
@ -686,11 +702,13 @@
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
<#function are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function>
</script>

View File

@ -9,6 +9,7 @@ import { getLogger } from "../tools/logger";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants";
export async function main() {
const buildOptions = readBuildOptions({
@ -36,9 +37,31 @@ export async function main() {
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
const containerKeycloakVersion = "23.0.6";
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
if (buildOptions.doCreateJar) {
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
child_process.execSync("npx vite", {
"cwd": buildOptions.reactAppRootDirPath,
"env": {
...process.env,
[keycloakifyBuildOptionsForPostPostBuildScriptEnvName]: JSON.stringify(buildOptions)
}
});
create_jar: {
if (!buildOptions.doCreateJar) {
break create_jar;
}
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
const jarDirPath = pathDirname(jarFilePath);
@ -59,16 +82,6 @@ export async function main() {
);
}
const containerKeycloakVersion = "23.0.6";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
logger.log(
[
"",

View File

@ -84,7 +84,7 @@ export declare namespace KcContext {
description?: string;
attributes: Record<string, string>;
};
isAppInitiatedAction: boolean;
isAppInitiatedAction?: boolean;
messagesPerField: {
/**
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
@ -116,6 +116,7 @@ export declare namespace KcContext {
*/
exists: (fieldName: string) => boolean;
};
properties: Record<string, string | undefined>;
};
export type SamlPostForm = Common & {
@ -244,6 +245,17 @@ export declare namespace KcContext {
export type Terms = Common & {
pageId: "terms.ftl";
//NOTE: Optional because maybe it wasn't defined in older keycloak versions.
user?: {
id: string;
username: string;
attributes: Record<string, string[]>;
email: string;
emailVerified: boolean;
firstName?: string;
lastName?: string;
markedForEviction?: boolean;
};
};
export type LoginDeviceVerifyUserCode = Common & {

View File

@ -12,8 +12,9 @@ import { symToStr } from "tsafe/symToStr";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData } = params ?? {};
const { mockData, mockProperties } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
mockPageId?: PageId;
@ -141,6 +142,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
}
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
});
}
return { kcContext };
}

View File

@ -236,7 +236,127 @@ export const kcContextCommonMock: KcContext.Common = {
"attributes": {}
},
"scripts": [],
"isAppInitiatedAction": false
"isAppInitiatedAction": false,
"properties": {
"kcLogoIdP-facebook": "fa fa-facebook",
"parent": "keycloak",
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",
"kcLogoIdP-bitbucket": "fa fa-bitbucket",
"kcAuthenticatorWebAuthnClass": "fa fa-key list-view-pf-icon-lg",
"kcWebAuthnDefaultIcon": "pficon pficon-key",
"kcLogoIdP-stackoverflow": "fa fa-stack-overflow",
"kcSelectAuthListItemClass": "pf-l-stack__item select-auth-box-parent pf-l-split",
"kcLogoIdP-microsoft": "fa fa-windows",
"kcLocaleItemClass": "pf-c-dropdown__menu-item",
"kcLoginOTPListItemHeaderClass": "pf-c-tile__header",
"kcLoginOTPListItemIconBodyClass": "pf-c-tile__icon",
"kcInputHelperTextAfterClass": "pf-c-form__helper-text pf-c-form__helper-text-after",
"kcFormClass": "form-horizontal",
"kcSelectAuthListClass": "pf-l-stack select-auth-container",
"kcInputClassRadioCheckboxLabelDisabled": "pf-m-disabled",
"kcSelectAuthListItemIconClass": "pf-l-split__item select-auth-box-icon",
"kcRecoveryCodesWarning": "kc-recovery-codes-warning",
"kcFormSettingClass": "login-pf-settings",
"kcWebAuthnBLE": "fa fa-bluetooth-b",
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcSelectAuthListItemArrowIconClass": "fa fa-angle-right fa-lg",
"meta": "viewport==width=device-width,initial-scale=1",
"styles": "css/login.css css/tile.css",
"kcFeedbackAreaClass": "col-md-12",
"kcLogoIdP-google": "fa fa-google",
"kcCheckLabelClass": "pf-c-check__label",
"kcSelectAuthListItemFillClass": "pf-l-split__item pf-m-fill",
"kcAuthenticatorDefaultClass": "fa fa-list list-view-pf-icon-lg",
"kcLogoIdP-gitlab": "fa fa-gitlab",
"kcFormAreaClass": "col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcInputClassRadioLabel": "pf-c-radio__label",
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
"kcSelectAuthListItemHeadingClass": "pf-l-stack__item select-auth-box-headline pf-c-title",
"kcInfoAreaClass": "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
"kcLogoLink": "http://www.keycloak.org",
"kcContainerClass": "container-fluid",
"kcSelectAuthListItemTitle": "select-auth-box-paragraph",
"kcHtmlClass": "login-pf",
"kcLoginOTPListItemTitleClass": "pf-c-tile__title",
"locales": "ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"serviceTitle": "CodeGouv",
"kcLogoIdP-openshift-v4": "pf-icon pf-icon-openshift",
"kcWebAuthnUnknownIcon": "pficon pficon-key unknown-transport-class",
"kcFormSocialAccountNameClass": "kc-social-provider-name",
"kcLogoIdP-openshift-v3": "pf-icon pf-icon-openshift",
"kcLoginOTPListInputClass": "pf-c-tile__input",
"kcWebAuthnUSB": "fa fa-usb",
"kcInputClassRadio": "pf-c-radio",
"kcWebAuthnKeyIcon": "pficon pficon-key",
"kcFeedbackInfoIcon": "fa fa-fw fa-info-circle",
"kcCommonLogoIdP": "kc-social-provider-logo kc-social-gray",
"stylesCommon":
"web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css",
"kcRecoveryCodesActions": "kc-recovery-codes-actions",
"kcFormGroupHeader": "pf-c-form__group",
"kcFormSocialAccountSectionClass": "kc-social-section kc-social-gray",
"kcLogoIdP-instagram": "fa fa-instagram",
"kcAlertClass": "pf-c-alert pf-m-inline",
"kcHeaderClass": "login-pf-page-header",
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormSocialAccountLinkClass": "pf-c-login__main-footer-links-item-link",
"kcLocaleMainClass": "pf-c-dropdown",
"kcTextareaClass": "form-control",
"kcButtonBlockClass": "pf-m-block",
"kcButtonClass": "pf-c-button",
"kcWebAuthnNFC": "fa fa-wifi",
"kcLocaleClass": "col-xs-12 col-sm-1",
"kcInputClassCheckboxInput": "pf-c-check__input",
"kcFeedbackErrorIcon": "fa fa-fw fa-exclamation-circle",
"kcInputLargeClass": "input-lg",
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
"kcRecoveryCodesList": "kc-recovery-codes-list",
"kcFormSocialAccountListClass": "pf-c-login__main-footer-links kc-social-links",
"kcAlertTitleClass": "pf-c-alert__title kc-feedback-text",
"kcAuthenticatorPasswordClass": "fa fa-unlock list-view-pf-icon-lg",
"kcCheckInputClass": "pf-c-check__input",
"kcLogoIdP-linkedin": "fa fa-linkedin",
"kcLogoIdP-twitter": "fa fa-twitter",
"kcFeedbackWarningIcon": "fa fa-fw fa-exclamation-triangle",
"kcResetFlowIcon": "pficon pficon-arrow fa",
"kcSelectAuthListItemIconPropertyClass": "fa-2x select-auth-box-icon-properties",
"kcFeedbackSuccessIcon": "fa fa-fw fa-check-circle",
"kcLoginOTPListClass": "pf-c-tile",
"kcSrOnlyClass": "sr-only",
"kcFormSocialAccountListGridClass": "pf-l-grid kc-social-grid",
"kcButtonDefaultClass": "btn-default",
"kcFormGroupErrorClass": "has-error",
"kcSelectAuthListItemDescriptionClass": "pf-l-stack__item select-auth-box-desc",
"kcSelectAuthListItemBodyClass": "pf-l-split__item pf-l-stack",
"import": "common/keycloak",
"kcWebAuthnInternal": "pficon pficon-key",
"kcSelectAuthListItemArrowClass": "pf-l-split__item select-auth-box-arrow",
"kcCheckClass": "pf-c-check",
"kcContentClass": "col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
"kcLogoClass": "login-pf-brand",
"kcLoginOTPListItemIconClass": "fa fa-mobile",
"kcLoginClass": "login-pf-page",
"kcSignUpClass": "login-pf-signup",
"kcButtonLargeClass": "btn-lg",
"kcFormCardClass": "card-pf",
"kcLocaleListClass": "pf-c-dropdown__menu pf-m-align-right",
"kcInputClass": "pf-c-form-control",
"kcFormGroupClass": "form-group",
"kcLogoIdP-paypal": "fa fa-paypal",
"kcInputClassCheckbox": "pf-c-check",
"kcRecoveryCodesConfirmation": "kc-recovery-codes-confirmation",
"kcInputClassRadioInput": "pf-c-radio__input",
"kcFormSocialAccountListButtonClass": "pf-c-button pf-m-control pf-m-block kc-social-item kc-social-gray",
"kcInputClassCheckboxLabel": "pf-c-check__label",
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormHeaderClass": "login-pf-header",
"kcFormSocialAccountGridItem": "pf-l-grid__item",
"kcButtonPrimaryClass": "pf-m-primary",
"kcInputHelperTextBeforeClass": "pf-c-form__helper-text pf-c-form__helper-text-before",
"kcLogoIdP-github": "fa fa-github",
"kcLabelClass": "pf-c-form__label pf-c-form__label-text"
}
};
const loginUrl = {

View File

@ -1,31 +1,66 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import type { Plugin } from "vite";
import * as fs from "fs";
import { resolvedViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, keycloak_resources } from "../bin/constants";
import {
resolvedViteConfigJsonBasename,
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir,
keycloak_resources,
keycloakifyBuildOptionsForPostPostBuildScriptEnvName
} from "../bin/constants";
import type { ResolvedViteConfig } from "../bin/keycloakify/buildOptions/resolvedViteConfig";
import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
import { copyKeycloakResourcesToPublic } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../bin/keycloakify/buildOptions";
import type { UserProvidedBuildOptions } from "../bin/keycloakify/buildOptions/UserProvidedBuildOptions";
import MagicString from "magic-string";
export type Params = UserProvidedBuildOptions & {
postBuild?: (buildOptions: Omit<BuildOptions, "bundler">) => Promise<void>;
};
export function keycloakify(params?: Params) {
const { postBuild, ...userProvidedBuildOptions } = params ?? {};
export function keycloakify() {
let reactAppRootDirPath: string | undefined = undefined;
let urlPathname: string | undefined = undefined;
let buildDirPath: string | undefined = undefined;
let command: "build" | "serve" | undefined = undefined;
let shouldGenerateSourcemap: boolean | undefined = undefined;
const plugin = {
"name": "keycloakify" as const,
"configResolved": async resolvedConfig => {
shouldGenerateSourcemap = resolvedConfig.build.sourcemap !== false;
run_post_build_script: {
const buildOptionJson = process.env[keycloakifyBuildOptionsForPostPostBuildScriptEnvName];
if (buildOptionJson === undefined) {
break run_post_build_script;
}
if (postBuild === undefined) {
process.exit(0);
}
const buildOptions: BuildOptions = JSON.parse(buildOptionJson);
await postBuild(buildOptions);
process.exit(0);
}
command = resolvedConfig.command;
reactAppRootDirPath = resolvedConfig.root;
urlPathname = (() => {
let out = resolvedConfig.env.BASE_URL;
if (out.startsWith(".") && command === "build") {
if (out.startsWith(".") && command === "build" && resolvedConfig.envPrefix?.includes("STORYBOOK_") !== true) {
throw new Error(
[
`BASE_URL=${out} is not supported By Keycloakify. Use an absolute URL instead.`,
@ -67,7 +102,8 @@ export function keycloakify() {
"publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
"assetsDir": resolvedConfig.build.assetsDir,
"buildDir": resolvedConfig.build.outDir,
urlPathname
urlPathname,
userProvidedBuildOptions
}),
null,
2
@ -82,6 +118,7 @@ export function keycloakify() {
},
"transform": (code, id) => {
assert(command !== undefined);
assert(shouldGenerateSourcemap !== undefined);
if (command !== "build") {
return;
@ -89,49 +126,53 @@ export function keycloakify() {
assert(reactAppRootDirPath !== undefined);
let transformedCode: string | undefined = undefined;
{
const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
replace_import_meta_env_base_url_in_source_code: {
{
const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
if (!isWithinSourceDirectory) {
break replace_import_meta_env_base_url_in_source_code;
}
if (!isWithinSourceDirectory) {
return;
}
{
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
if (!isTypeScriptFile && !isJavascriptFile) {
break replace_import_meta_env_base_url_in_source_code;
}
}
if (transformedCode === undefined) {
transformedCode = code;
}
transformedCode = replaceAll(
transformedCode,
"import.meta.env.BASE_URL",
[
`(`,
`(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`,
`"${urlPathname ?? "/"}":`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
`)`
].join("")
);
}
if (transformedCode === undefined) {
{
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
if (!isTypeScriptFile && !isJavascriptFile) {
return;
}
}
const transformedCode = new MagicString(code);
transformedCode.replaceAll(
/import\.meta\.env(?:(?:\.BASE_URL)|(?:\["BASE_URL"\]))/g,
[
`(`,
`(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`,
`"${urlPathname ?? "/"}":`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
`)`
].join("")
);
if (!transformedCode.hasChanged()) {
return;
}
if (!shouldGenerateSourcemap) {
return transformedCode.toString();
}
const map = transformedCode.generateMap({
"source": id,
"includeContent": true,
"hires": true
});
return {
"code": transformedCode
"code": transformedCode.toString(),
"map": map.toString()
};
},
"closeBundle": async () => {

View File

@ -1665,7 +1665,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@^1.4.10":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
@ -8352,6 +8352,13 @@ lz-string@^1.4.4:
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
magic-string@^0.30.7:
version "0.30.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505"
integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"