Implement a mechanism to overload kcContext

This commit is contained in:
garronej
2021-06-28 04:04:48 +02:00
parent e160882db9
commit eac28f97b8
25 changed files with 604 additions and 48 deletions

View File

@ -8,7 +8,7 @@ import {
} from "../replaceImportFromStatic";
import fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
export const pageIds = [

View File

@ -3,7 +3,7 @@
import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
import { id } from "evt/tools/typeSafety/id";
import { id } from "tsafe/id";
type TransformSourceCode =
(params: {

View File

@ -1,8 +1,6 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage";
@ -10,8 +8,6 @@ export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.E
const { msg } = useKcMessage();
assert(kcContext.message !== undefined);
const { message, client } = kcContext;
return (

View File

@ -1,6 +1,6 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { doExtends } from "evt/tools/typeSafety/doExtends";
import { doExtends } from "tsafe/doExtends";
/** Class names can be provided as an array or separated by whitespace */
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };

View File

@ -1,7 +1,7 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { doExtends } from "evt/tools/typeSafety/doExtends";
import { doExtends } from "tsafe/doExtends";
import type { MessageKey } from "../i18n/useKcMessage";
import type { LanguageLabel } from "../i18n/KcLanguageTag";
@ -150,7 +150,8 @@ export declare namespace KcContextBase {
pageId: "error.ftl";
client?: {
baseUrl?: string;
}
},
message: NonNullable<Common["message"]>;
};
export type LoginResetPassword = Common & {

View File

@ -1,28 +1,93 @@
import type { KcContextBase } from "./KcContextBase";
import type { KcContextBase } from "./KcContextBase";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import type { DeepPartial } from "../tools/DeepPartial";
import { deepAssign } from "../tools/deepAssign";
export function getKcContext<KcContextExtended extends { pageId: string; } = never>(
export type ExtendsKcContextBase<
KcContextExtended extends ({ pageId: string; } | undefined)
> =
KcContextExtended extends undefined ?
KcContextBase :
AndByDiscriminatingKey<
"pageId",
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
export function getKcContext<KcContextExtended extends ({ pageId: string; } | undefined) = undefined>(
params?: {
mockPageId?: KcContextBase["pageId"] | KcContextExtended["pageId"];
kcContextExtendedMock?: KcContextExtended[];
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
}
): { kcContext: (KcContextBase | KcContextExtended & KcContextBase.Common) | undefined; } {
): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined; } {
const { mockPageId, kcContextExtendedMock } = params ?? { "mockPageId": false };
const {
mockPageId,
mockData
} = params ?? {};
if (mockPageId !== undefined) {
return {
"pageId": mockPageId,
...(kcContextMocks.find(({ pageId }) => pageId === mockPageId) ?? kcContextCommonMock),
...(kcContextExtendedMock?.find(({ pageId }) => pageId === mockPageId) ?? {})
} as any;
//TODO maybe trow if no mock fo custom page
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn([
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n"));
}
const kcContext: any = { "pageId": mockPageId };
deepAssign({
"target": kcContext,
"source": kcContextCommonMock
});
if (kcContextDefaultMock !== undefined) {
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock
});
}
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
return (window as any)[ftlValuesGlobalName];
return {
"kcContext":
typeof window === "undefined" ?
undefined :
(window as any)[ftlValuesGlobalName]
};
}

View File

@ -5,12 +5,15 @@ import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
import { id } from "tsafe/id";
import { join as pathJoin } from "path";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContextBase.Common = {
"url": {
"loginAction": "#",
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
"resourcesPath": pathJoin(PUBLIC_URL, resourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
},
@ -95,7 +98,8 @@ export const kcContextCommonMock: KcContextBase.Common = {
"languageTag": "tr"
}
],
"current": null as any
//"current": null as any
"current": "English"
},
"auth": {
"showUsername": false,
@ -110,6 +114,7 @@ export const kcContextCommonMock: KcContextBase.Common = {
"isAppInitiatedAction": false,
};
Object.defineProperty(
kcContextCommonMock.locale!,
"current",
@ -188,6 +193,10 @@ export const kcContextMocks: KcContextBase[] = [
"pageId": "error.ftl",
"client": {
"baseUrl": "#"
},
"message": {
"type": "error",
"summary": "This is the error message"
}
}),
id<KcContextBase.LoginResetPassword>({

View File

@ -0,0 +1,43 @@
import { KcContextBase } from "./KcContextBase";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string; }>=
AndByDiscriminatingKey<
"pageId",
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
type KcContextExtended =
{ pageId: "register.ftl"; authorizedMailDomains: string[]; } |
{ pageId: "my-extra-page-1.ftl"; } |
{ pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
const y: ExtendsKcContextBase<KcContextExtended> = null as any;
if (y.pageId === "register.ftl") {
y.authorizedMailDomains;
y.realm.displayName;
y.register
}
if (y.pageId === "my-extra-page-1.ftl") {
y.realm.displayName;
}
if (y.pageId === "my-extra-page-2.ftl") {
y.realm
y.someCustomValue
}

View File

@ -1,5 +1,5 @@
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "tsafe/objectKeys";
import { kcMessages } from "./kcMessages/login";
export type KcLanguageTag = keyof typeof kcMessages;

View File

@ -1,7 +1,7 @@
import { kcMessages } from "../generated_kcMessages/login";
import { Evt } from "evt";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "tsafe/objectKeys";
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());

View File

@ -2,22 +2,39 @@
import { createUseGlobalState } from "powerhooks";
import { getKcContext } from "../getKcContext";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
import type { StatefulEvt } from "powerhooks";
import { KcLanguageTag } from "./KcLanguageTag";
const { kcContext } = getKcContext();
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
const wrap = createUseGlobalState(
"kcLanguageTag",
() => getBestMatchAmongKcLanguageTag(
kcContext?.locale?.current ??
navigator.language
),
() => {
const { kcContext } = getKcContext();
const languageLike =
kcContext?.locale?.current ??
(
typeof navigator === "undefined" ?
undefined :
navigator.language
);
if (languageLike === undefined) {
return "en";
}
return getBestMatchAmongKcLanguageTag(languageLike);
},
{ "persistance": "localStorage" }
);
export const { useKcLanguageTag } = wrap;
export function getEvtKcLanguage() {
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
return wrap.evtKcLanguageTag;
}

View File

@ -0,0 +1,35 @@
export type AndByDiscriminatingKey<
DiscriminatingKey extends string,
U1 extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
export declare namespace AndByDiscriminatingKey {
export type Tf1<
DiscriminatingKey extends string,
U1,
U1Again extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
U1 extends Pick<U2, DiscriminatingKey> ?
Tf2<DiscriminatingKey, U1, U2, U1Again> :
U1;
export type Tf2<
DiscriminatingKey extends string,
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> =
U2 extends Pick<SingletonU1, DiscriminatingKey> ?
U2 & SingletonU1 :
U2 extends Pick<U1, DiscriminatingKey> ?
never :
U2;
}

4
src/lib/tools/DeepPartial.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};

View File

@ -1,2 +1,2 @@
export { assert } from "evt/tools/typeSafety/assert";
export { assert } from "tsafe/assert";

View File

@ -0,0 +1,56 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
export function deepAssign(
params: {
target: Record<string, unknown>;
source: Record<string, unknown>;
}
) {
const { target, source } = params;
Object.keys(source).forEach(key => {
var dereferencedSource = source[key];
if (
target[key] === undefined ||
!(dereferencedSource instanceof Object)
) {
Object.defineProperty(
target,
key,
{
"enumerable": true,
"value": dereferencedSource
}
);
return;
}
const dereferencedTarget = target[key];
if (dereferencedSource instanceof Array) {
assert(is<unknown[]>(dereferencedTarget));
assert(is<unknown[]>(dereferencedSource));
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
return;
}
assert(is<Record<string, unknown>>(dereferencedTarget));
assert(is<Record<string, unknown>>(dereferencedSource));
deepAssign({
"target": dereferencedTarget,
"source": dereferencedSource
});
});
}

View File

@ -0,0 +1,4 @@
export function deepClone<T>(arg: T): T {
return JSON.parse(JSON.stringify(arg));
}

View File

@ -1,6 +1,6 @@
import { join as pathJoin } from "path";
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
import { generateKeycloakThemeResources } from "../../bin/build-keycloak-theme/generateKeycloakThemeResources";
import {
setupSampleReactProject,
sampleReactProjectDirPath

View File

@ -6,7 +6,7 @@ import {
} from "./setupSampleReactProject";
import * as st from "scripting-tools";
import { join as pathJoin } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
setupSampleReactProject();

View File

@ -3,7 +3,7 @@ import { 
replaceImportsFromStaticInJsCode,
replaceImportsInCssCode,
generateCssCodeToDefineGlobals
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
} from "../../bin/build-keycloak-theme/replaceImportFromStatic";
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": `

View File

@ -1,7 +1,7 @@
import { getProjectRoot } from "../bin/tools/getProjectRoot";
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "../bin/tools/downloadAndUnzip";
import { downloadAndUnzip } from "../../bin/tools/downloadAndUnzip";
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");

View File

@ -0,0 +1,207 @@
import { getKcContext } from "../../lib/getKcContext";
import type { KcContextBase } from "../../lib/getKcContext";
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
import { same } from "evt/tools/inDepth";
import { doExtends } from "tsafe/doExtends";
import { assert } from "tsafe/assert";
import { kcContextMocks, kcContextCommonMock } from "../../lib/getKcContext/kcContextMocks";
import { deepClone } from "../../lib/tools/deepClone";
import type { Any } from "ts-toolbelt";
const authorizedMailDomains = [
"example.com",
"another-example.com",
"*.yet-another-example.com",
"*.example.com",
"hello-world.com"
];
const displayName = "this is an overwritten common value";
const aNonStandardValue = "a non standard value";
type KcContextExtended = {
pageId: "register.ftl";
authorizedMailDomains: string[];
} | {
pageId: "my-extra-page-1.ftl";
} | {
pageId: "my-extra-page-2.ftl";
aNonStandardValue: string;
};
function getKcContextProxy(
params: {
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
}
) {
const { mockPageId } = params;
const { kcContext } = getKcContext<KcContextExtended>({
mockPageId,
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName }
},
{
"pageId": "register.ftl",
authorizedMailDomains
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue
}
]
});
return { kcContext };
}
{
const pageId= "login.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
//@ts-expect-error
doExtends<Any.Equals<typeof kcContext, any>, 1>();
assert(kcContext?.pageId === pageId);
doExtends<typeof kcContext, KcContextBase.Login>();
assert(same(
//NOTE: deepClone for printIfExists or other functions...
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
mock.realm.displayName = displayName;
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "register.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
//@ts-expect-error
doExtends<Any.Equals<typeof kcContext, any>, 1>();
assert(kcContext?.pageId === pageId);
doExtends<typeof kcContext, KcContextBase.Register>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { authorizedMailDomains });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-2.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
//@ts-expect-error
doExtends<Any.Equals<typeof kcContext, any>, 1>();
assert(kcContext?.pageId === pageId);
//@ts-expect-error
doExtends<typeof kcContext, KcContextBase>();
doExtends<typeof kcContext, KcContextBase.Common>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId, aNonStandardValue });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-1.ftl";
console.log("We expect a warning here =>");
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
//@ts-expect-error
doExtends<Any.Equals<typeof kcContext, any>, 1>();
assert(kcContext?.pageId === pageId);
//@ts-expect-error
doExtends<typeof kcContext, KcContextBase>();
doExtends<typeof kcContext, KcContextBase.Common>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const { kcContext } = getKcContext();
//@ts-expect-error
doExtends<Any.Equals<typeof kcContext, any>, 1>();
doExtends<typeof kcContext, KcContextBase | undefined>();
}

2
src/test/lib/index.ts Normal file
View File

@ -0,0 +1,2 @@
import "./getKcContext";

View File

@ -0,0 +1,91 @@
import { AndByDiscriminatingKey } from "../../../lib/tools/AndByDiscriminatingKey";
import { doExtends } from "tsafe/doExtends";
type Base =
{ pageId: "a"; onlyA: string; } |
{ pageId: "b"; onlyB: string; } |
{ pageId: "only base"; onlyBase: string; };
type Extension =
{ pageId: "a"; onlyExtA: string; } |
{ pageId: "b"; onlyExtB: string; } |
{ pageId: "only ext"; onlyExt: string; };
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
type Expected =
{ pageId: "a"; onlyA: string; onlyExtA: string; } |
{ pageId: "b"; onlyB: string; onlyExtB: string; } |
{ pageId: "only base"; onlyBase: string; } |
{ pageId: "only ext"; onlyExt: string; };
doExtends<Got, Expected>();
doExtends<Expected, Got>();
const x: Got = null as any;
if (x.pageId === "a") {
x.onlyA;
x.onlyExtA;
//@ts-expect-error
x.onlyB;
//@ts-expect-error
x.onlyBase;
//@ts-expect-error
x.onlyExt;
}
if (x.pageId === "b") {
x.onlyB;
x.onlyExtB;
//@ts-expect-error
x.onlyA;
//@ts-expect-error
x.onlyBase;
//@ts-expect-error
x.onlyExt;
}
if (x.pageId === "only base") {
x.onlyBase;
//@ts-expect-error
x.onlyA;
//@ts-expect-error
x.onlyB;
//@ts-expect-error
x.onlyExt;
}
if (x.pageId === "only ext") {
x.onlyExt;
//@ts-expect-error
x.onlyA;
//@ts-expect-error
x.onlyB;
//@ts-expect-error
x.onlyBase;
}