Compare commits

...

19 Commits

Author SHA1 Message Date
5cf290b033 Update changelog v2.0.6 2021-07-08 01:08:57 +00:00
aec3da25b3 Bump version (changelog ignore) 2021-07-08 02:44:54 +02:00
66d7cb563d Merge pull request #18 from asashay/add-custom-props-to-theme-properties
Add possibility to add custom properties to theme.properties file
2021-07-08 02:43:37 +02:00
551e9c041e add possibility to add custom properties to theme.properties file 2021-07-06 15:52:14 +03:00
fffb6d5b5e Update changelog v2.0.5 2021-07-05 01:10:51 +00:00
ac0bfeb360 Bump version (changelog ignore) 2021-07-05 03:06:49 +02:00
7c30059ca3 Fix broken url for big stylesheet #16 2021-07-05 03:06:31 +02:00
fdb9ae6c40 Update changelog v2.0.4 2021-07-03 00:41:40 +00:00
3c82ffc0ab Bump version (changelog ignore) 2021-07-03 02:39:59 +02:00
5dd3103aba Fix: #7 2021-07-03 02:39:39 +02:00
84fc81f531 Update changelog v2.0.3 2021-06-30 20:05:50 +00:00
a20cbc62a5 Bump version (changelog ignore) 2021-06-30 22:01:25 +02:00
e6a93e2838 Escape double quote in ftl to js conversion #15 2021-06-30 22:01:01 +02:00
3cff54561f Update readme 2021-06-29 02:31:41 +02:00
e50a6a7876 Update changelog v2.0.2 2021-06-28 13:30:05 +00:00
b887ec839b Updagte README for implementing non incuded pages 2021-06-28 15:27:08 +02:00
465daa19a0 Update changelog v2.0.1 2021-06-28 05:04:20 +00:00
6c2b761d95 Bump version (changelog ignore) 2021-06-28 06:58:43 +02:00
0e8f95ce19 Update documentation for v2 2021-06-28 06:58:00 +02:00
11 changed files with 321 additions and 224 deletions

View File

@ -1,3 +1,31 @@
### **2.0.6** (2021-07-08)
- Merge pull request #18 from asashay/add-custom-props-to-theme-properties
Add possibility to add custom properties to theme.properties file
- add possibility to add custom properties to theme.properties file
### **2.0.5** (2021-07-05)
- Fix broken url for big stylesheet #16
### **2.0.4** (2021-07-03)
- Fix: #7
### **2.0.3** (2021-06-30)
- Escape double quote in ftl to js conversion #15
- Update readme
### **2.0.2** (2021-06-28)
- Updagte README for implementing non incuded pages
### **2.0.1** (2021-06-28)
- Update documentation for v2
# **2.0.0** (2021-06-28)
- Fix last bugs before relasing v2

View File

@ -24,8 +24,6 @@
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
**V2 is not yet documented, most users should stick with v1.x.x**
# Motivations
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
@ -64,7 +62,7 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
- [How to use](#how-to-use)
- [Setting up the build tool](#setting-up-the-build-tool)
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
- [Changing the look **and** feel](#changing-the-look-and-feel)
- [Advanced pages configuration](#advanced-pages-configuration)
- [Hot reload](#hot-reload)
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
- [Support for Terms and conditions](#support-for-terms-and-conditions)
@ -136,10 +134,12 @@ import { App } from "./<wherever>/App";
import {
KcApp,
defaultKcProps,
kcContext
getKcContext
} from "keycloakify";
import { css } from "tss-react";
const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" });
reactDom.render(
@ -163,10 +163,12 @@ import { App } from "./<wherever>/App";
import {
KcApp,
defaultKcProps,
kcContext
getKcContext
} from "keycloakify";
import { css } from "tss-react";
const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" });
reactDom.render(
@ -201,20 +203,25 @@ and the result you can expect:
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
</p>
### Changing the look **and** feel
### Advanced pages configuration
If you want to really re-implement the pages, the best approach is to
create your own version of the [`<KcApp />`](https://github.com/garronej/keycloakify/blob/develop/src/lib/components/KcApp.tsx).
Copy/past some of [the components](https://github.com/garronej/keycloakify/tree/develop/src/lib/components) provided by this module and start hacking around.
If you want to go beyond only customizing the CSS you can re-implement some of the
pages or event add new ones.
You can find an example of such customization [here](https://github.com/InseeFrLab/onyxia-ui/tree/master/src/app/components/KcApp).
If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel).
If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/app/components/KcApp).
The web app is in production [here](https://datalab.sspcloud.fr).
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
Note that you dont have to re write **all** components, only the ones that you most need customized.
Look at [here for example](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L112-L120).
We want to have our very own login and register page, so we wrote customs [Login.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/master/src/app/components/KcApp/Login.tsx) and [Register.txs](https://github.com/InseeFrLab/onyxia-ui/blob/master/src/app/components/KcApp/Register.tsx), we import them [here](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L9-L10) and use them [here](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L113-L114).
We don't want to bother, however, customizing `login-reset-password.ftl`. We are fine using the component from [the default theme](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L13) with just some [CSS customization](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L103-L110).
Main takeaways are:
- You must declare your custom pages in the package.json. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/package.json#L17-L22)
- (TS only) You must declare theses page in the type argument of the getter
function for the `kcContext` in order to have the correct typings. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L16-L21)
- (TS only) If you use Keycloak plugins that defines non standard `.ftl` values
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
that define `authorizedMailDomains` in `register.ftl`) you should
declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13)
- You should provide sample data for all the non standard value if you want to be able
to debug the page outside of keycloak. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L28-L43)
WARNING: If you chose to go this way use:
```json
@ -227,16 +234,19 @@ in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might
### Hot reload
Rebuild the theme each time you make a change to see the result is not practical.
If you want to test your login screens outside of Keycloak, in [storybook](https://storybook.js.org/)
for example you can use `kcContextMocks`.
If you want to test your login screens outside of Keycloak you can mock a given `kcContext`:
```tsx
import {
KcApp,
defaultKcProps,
kcContextMocks
getKcContext
} from "keycloakify";
const { kcContext } = getKcContext({
"mockPageId": "login.ftl"
});
reactDom.render(
<KcApp
kcContext={kcContextMocks.kcLoginContext}
@ -278,16 +288,10 @@ Then to load your own therms of services using [like this](https://github.com/ga
# Some pages still have the default theme. Why?
**NEW in v1.2 it is now much more easy to add support for custom pages since the
Keycloak context is now automatically converted into a JavaScript object (kcContext).
In v2 (coming soon) it won't be required to fork for adding support for custom pages.**
This project only support the most common user facing pages of Keycloak login.
This project only support out of the box the most common user facing pages of Keycloak login.
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
If you need to customize pages that are not supported yet you can submit an issue about it and wait for me get it implemented.
If you can't wait, PR are welcome! [Here](https://github.com/InseeFrLab/keycloakify/commit/0163459ad6b1ad0afcc34fae5f3cc28dbcf8b4a7) is the commit that adds support
for the `login-otp.ftl` page. You can use it as a model for implementing other pages.
If you need to customize pages that are not supported yet or if you need to implement some non standard `.ftl` pages please refer to [Advanced pages configuration](#advanced-pages-configuration).
# GitHub Actions

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "2.0.0",
"version": "2.0.6",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",

View File

@ -19,6 +19,7 @@ export function main() {
console.log("🔏 Building the keycloak theme...⌚");
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
@ -55,7 +56,8 @@ export function main() {
};
})(),
extraPagesId
extraPagesId,
extraThemeProperties
});
const { jarFilePath } = generateJavaStackFiles({

View File

@ -113,7 +113,7 @@
<#attempt>
"${object?no_esc}"
"${object?replace('"', '\\"')?no_esc}"
<#recover>
/* couldn't convert into string non hash, non method, non boolean, non enumerable object */
undefined;

View File

@ -23,12 +23,13 @@ export function generateKeycloakThemeResources(
//If urlOrigin is not undefined then it means --externals-assets
urlOrigin: undefined | string;
extraPagesId: string[];
extraThemeProperties: string[];
}
) {
const {
themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath,
urlPathname, urlOrigin, extraPagesId
urlPathname, urlOrigin, extraPagesId, extraThemeProperties
} = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
@ -166,7 +167,10 @@ export function generateKeycloakThemeResources(
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from("parent=keycloak", "utf8")
Buffer.from(
"parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")),
"utf8"
)
);
}

View File

@ -5,19 +5,40 @@ import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
export function replaceImportsFromStaticInJsCode(
params: {
jsCode: string;
urlOrigin: undefined | string;
urlOrigin: undefined | string;
}
): { fixedJsCode: string; } {
/*
NOTE:
When we have urlOrigin defined it means that
we are building with --external-assets
so we have to make sur that the fixed js code will run
inside and outside keycloak.
When urlOrigin isn't defined we can assume the fixedJsCode
will always run in keycloak context.
*/
const { jsCode, urlOrigin } = params;
const fixedJsCode = jsCode.replace(
/([a-z]+\.[a-z]+)\+"static\//g,
(...[, group]) =>
urlOrigin === undefined ?
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
);
const fixedJsCode =
jsCode
.replace(
/([a-z]+\.[a-z]+)\+"static\//g,
(...[, group]) =>
urlOrigin === undefined ?
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
)
.replace(
/".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/,
(...[, group1, group2, group3]) =>
urlOrigin === undefined ?
`".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},` :
`".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`
);
return { fixedJsCode };

View File

@ -8,9 +8,9 @@ import { deepAssign } from "../tools/deepAssign";
export type ExtendsKcContextBase<
KcContextExtended extends ({ pageId: string; } | undefined)
KcContextExtended extends { pageId: string; }
> =
KcContextExtended extends undefined ?
[KcContextExtended] extends [never] ?
KcContextBase :
AndByDiscriminatingKey<
"pageId",
@ -18,7 +18,7 @@ export type ExtendsKcContextBase<
KcContextBase
>;
export function getKcContext<KcContextExtended extends ({ pageId: string; } | undefined) = undefined>(
export function getKcContext<KcContextExtended extends { pageId: string; } = never>(
params?: {
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
@ -51,22 +51,15 @@ export function getKcContext<KcContextExtended extends ({ pageId: string; } | un
}
const kcContext: any = { "pageId": mockPageId };
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextCommonMock
"source": kcContextDefaultMock !== undefined ?
kcContextDefaultMock :
{ "pageId": mockPageId, ...kcContextCommonMock, }
});
if (kcContextDefaultMock !== undefined) {
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock
});
}
if (partialKcContextCustomMock !== undefined) {
deepAssign({

View File

@ -2,6 +2,7 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
//Warning: Be mindful that because of array this is not idempotent.
export function deepAssign(
params: {
target: Record<string, unknown>;

View File

@ -14,6 +14,7 @@ generateKeycloakThemeResources({
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
"urlPathname": "/keycloakify-demo-app/",
"urlOrigin": undefined,
"extraPagesId": ["my-custom-page.ftl"]
"extraPagesId": ["my-custom-page.ftl"],
"extraThemeProperties": ["env=test"]
});

View File

@ -6,202 +6,245 @@ 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 { 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 authorizedMailDomains = [
"example.com",
"another-example.com",
"*.yet-another-example.com",
"*.example.com",
"hello-world.com"
];
const displayName = "this is an overwritten common value";
const displayName = "this is an overwritten common value";
const aNonStandardValue = "a non standard value";
const aNonStandardValue1 = "a non standard value 1";
const aNonStandardValue2 = "a non standard value 2";
type KcContextExtended = {
pageId: "register.ftl";
authorizedMailDomains: string[];
} | {
pageId: "my-extra-page-1.ftl";
} | {
pageId: "my-extra-page-2.ftl";
aNonStandardValue: string;
};
type KcContextExtended = {
pageId: "register.ftl";
authorizedMailDomains: string[];
} | {
pageId: "info.ftl";
aNonStandardValue1: string;
} | {
pageId: "my-extra-page-1.ftl";
} | {
pageId: "my-extra-page-2.ftl";
aNonStandardValue2: string;
};
const getKcContextProxy = (
params: {
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
}
) => {
const { mockPageId } = params;
const { kcContext } = getKcContext<KcContextExtended>({
mockPageId,
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName }
},
{
"pageId": "info.ftl",
aNonStandardValue1
},
{
"pageId": "register.ftl",
authorizedMailDomains
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue2
}
]
});
return { kcContext };
};
{
const pageId = "login.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
doExtends<Any.Equals<typeof kcContext, KcContextBase.Login>, 1>();
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}`);
function getKcContextProxy(
params: {
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
}
) {
const { mockPageId } = params;
{
const pageId = "info.ftl";
const { kcContext } = getKcContext<KcContextExtended>({
mockPageId,
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName }
},
{
"pageId": "register.ftl",
authorizedMailDomains
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue
}
]
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
doExtends<Any.Equals<typeof kcContext, KcContextBase.Info & { pageId: typeof pageId; aNonStandardValue1: string; }>, 1>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { aNonStandardValue1 });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "register.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
doExtends<Any.Equals<typeof kcContext, KcContextBase.Register & { pageId: typeof pageId; authorizedMailDomains: string[]; }>, 1>();
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 });
assert(kcContext?.pageId === pageId);
doExtends<Any.Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; aNonStandardValue2: string; }>, 1>();
kcContext.aNonStandardValue2;
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId, aNonStandardValue2 });
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 });
assert(kcContext?.pageId === pageId);
doExtends<Any.Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; }>, 1>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
}
{
const pageId = "login.ftl";
const { kcContext } = getKcContext({
"mockPageId": pageId
});
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>();
doExtends<Any.Equals<typeof kcContext, KcContextBase | undefined>, 1>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { authorizedMailDomains });
return mock;
})()
deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)
));
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}`);
console.log("PASS no extension");
}
{
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<Any.Equals<typeof kcContext, KcContextBase | undefined>, 1>();
doExtends<typeof kcContext, KcContextBase | undefined>();
assert(kcContext === undefined);
console.log("PASS no extension, no mock");
}