Create a storybook friendly getKcContext
This commit is contained in:
parent
fd49c2fd23
commit
d1c7491704
@ -81,7 +81,7 @@
|
|||||||
"@types/make-fetch-happen": "^10.0.1",
|
"@types/make-fetch-happen": "^10.0.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "^18.0.35",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/yauzl": "^2.10.0",
|
"@types/yauzl": "^2.10.0",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "18.1.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"scripting-tools": "^0.19.13",
|
"scripting-tools": "^0.19.13",
|
||||||
|
@ -3,6 +3,7 @@ import Fallback from "keycloakify/account/Fallback";
|
|||||||
export default Fallback;
|
export default Fallback;
|
||||||
|
|
||||||
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||||
|
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
|
||||||
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||||
|
|
||||||
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
106
src/account/kcContext/createGetKcContext.ts
Normal file
106
src/account/kcContext/createGetKcContext.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
|
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||||
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
|
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||||
|
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
||||||
|
|
||||||
|
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
|
}) {
|
||||||
|
const { mockData } = params ?? {};
|
||||||
|
|
||||||
|
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] = ExtendKcContext<KcContextExtension>["pageId"]>(params?: {
|
||||||
|
mockPageId?: PageId;
|
||||||
|
storyParams?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||||
|
}): { kcContext: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }> | undefined } {
|
||||||
|
const { mockPageId, storyParams } = params ?? {};
|
||||||
|
|
||||||
|
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||||
|
|
||||||
|
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||||
|
//TODO maybe trow if no mock fo custom page
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||||
|
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||||
|
].join(" "),
|
||||||
|
"background: red; color: yellow; font-size: medium"
|
||||||
|
);
|
||||||
|
|
||||||
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
const partialKcContextCustomMock = (() => {
|
||||||
|
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||||
|
|
||||||
|
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (mockDataPick !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": mockDataPick
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storyParams !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": storyParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(out).length === 0 ? undefined : out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realKcContext === undefined) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "kcContext": realKcContext as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getKcContext };
|
||||||
|
}
|
@ -1,78 +1,19 @@
|
|||||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
|
||||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
import { createGetKcContext } from "./createGetKcContext";
|
||||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
|
||||||
import { pathBasename } from "keycloakify/tools/pathBasename";
|
|
||||||
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
|
||||||
import { symToStr } from "tsafe/symToStr";
|
|
||||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
|
||||||
|
|
||||||
|
/** @deprecated: Use createGetKcContext instead */
|
||||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||||
const { mockPageId, mockData } = params ?? {};
|
const { mockPageId, mockData } = params ?? {};
|
||||||
|
|
||||||
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
const { getKcContext } = createGetKcContext({
|
||||||
|
mockData
|
||||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
|
||||||
//TODO maybe trow if no mock fo custom page
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
|
||||||
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
|
||||||
].join(" "),
|
|
||||||
"background: red; color: yellow; font-size: medium"
|
|
||||||
);
|
|
||||||
|
|
||||||
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 = {};
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (partialKcContextCustomMock !== undefined) {
|
const { kcContext } = getKcContext({ mockPageId });
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": partialKcContextCustomMock
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kcContext };
|
return { kcContext };
|
||||||
}
|
|
||||||
|
|
||||||
if (realKcContext === undefined) {
|
|
||||||
return { "kcContext": undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
|
|
||||||
return { "kcContext": undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const { url } = realKcContext;
|
|
||||||
|
|
||||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { "kcContext": realKcContext };
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ export default Fallback;
|
|||||||
|
|
||||||
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
|
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
|
||||||
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
|
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
|
||||||
|
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
|
||||||
export { createUseI18n } from "keycloakify/login/i18n/i18n";
|
export { createUseI18n } from "keycloakify/login/i18n/i18n";
|
||||||
|
|
||||||
export type { PageProps } from "keycloakify/login/pages/PageProps";
|
export type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
164
src/login/kcContext/createGetKcContext.ts
Normal file
164
src/login/kcContext/createGetKcContext.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import type { KcContext, Attribute } from "./KcContext";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||||
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
|
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
|
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||||
|
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { loginThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
||||||
|
|
||||||
|
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
|
}) {
|
||||||
|
const { mockData } = params ?? {};
|
||||||
|
|
||||||
|
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] = ExtendKcContext<KcContextExtension>["pageId"]>(params?: {
|
||||||
|
mockPageId?: PageId;
|
||||||
|
storyParams?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||||
|
}): { kcContext: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }> | undefined } {
|
||||||
|
const { mockPageId, storyParams } = params ?? {};
|
||||||
|
|
||||||
|
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||||
|
|
||||||
|
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||||
|
//TODO maybe trow if no mock fo custom page
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||||
|
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||||
|
].join(" "),
|
||||||
|
"background: red; color: yellow; font-size: medium"
|
||||||
|
);
|
||||||
|
|
||||||
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
const partialKcContextCustomMock = (() => {
|
||||||
|
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||||
|
|
||||||
|
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (mockDataPick !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": mockDataPick
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storyParams !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": storyParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(out).length === 0 ? undefined : out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
|
||||||
|
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
|
||||||
|
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
|
||||||
|
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
|
||||||
|
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { attributes } = kcContextDefaultMock.profile;
|
||||||
|
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes = [];
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName = {};
|
||||||
|
|
||||||
|
const partialAttributes = [
|
||||||
|
...((partialKcContextCustomMock as DeepPartial<KcContext.RegisterUserProfile>).profile?.attributes ?? [])
|
||||||
|
].filter(exclude(undefined));
|
||||||
|
|
||||||
|
attributes.forEach(attribute => {
|
||||||
|
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
||||||
|
|
||||||
|
const augmentedAttribute: Attribute = {} as any;
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": augmentedAttribute,
|
||||||
|
"source": attribute
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialAttribute !== undefined) {
|
||||||
|
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": augmentedAttribute,
|
||||||
|
"source": partialAttribute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
|
||||||
|
});
|
||||||
|
|
||||||
|
partialAttributes
|
||||||
|
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
|
||||||
|
.forEach(partialAttribute => {
|
||||||
|
const { name } = partialAttribute;
|
||||||
|
|
||||||
|
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
||||||
|
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
||||||
|
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realKcContext === undefined) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "kcContext": realKcContext as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getKcContext };
|
||||||
|
}
|
@ -1,136 +1,19 @@
|
|||||||
import type { KcContext, Attribute } from "./KcContext";
|
|
||||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
|
||||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import { exclude } from "tsafe/exclude";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
import { createGetKcContext } from "./createGetKcContext";
|
||||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
|
||||||
import { pathBasename } from "keycloakify/tools/pathBasename";
|
|
||||||
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
|
||||||
import { symToStr } from "tsafe/symToStr";
|
|
||||||
import { loginThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
|
||||||
|
|
||||||
|
/** @deprecated: Use createGetKcContext instead */
|
||||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||||
const { mockPageId, mockData } = params ?? {};
|
const { mockPageId, mockData } = params ?? {};
|
||||||
|
|
||||||
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
const { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||||
|
mockData
|
||||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
|
||||||
//TODO maybe trow if no mock fo custom page
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
|
||||||
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
|
||||||
].join(" "),
|
|
||||||
"background: red; color: yellow; font-size: medium"
|
|
||||||
);
|
|
||||||
|
|
||||||
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 = {};
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (partialKcContextCustomMock !== undefined) {
|
const { kcContext } = getKcContext({ mockPageId });
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": partialKcContextCustomMock
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
|
|
||||||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
|
|
||||||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
|
|
||||||
) {
|
|
||||||
assert(
|
|
||||||
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
|
|
||||||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
|
|
||||||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { attributes } = kcContextDefaultMock.profile;
|
|
||||||
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes = [];
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName = {};
|
|
||||||
|
|
||||||
const partialAttributes = [
|
|
||||||
...((partialKcContextCustomMock as DeepPartial<KcContext.RegisterUserProfile>).profile?.attributes ?? [])
|
|
||||||
].filter(exclude(undefined));
|
|
||||||
|
|
||||||
attributes.forEach(attribute => {
|
|
||||||
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
|
||||||
|
|
||||||
const augmentedAttribute: Attribute = {} as any;
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": augmentedAttribute,
|
|
||||||
"source": attribute
|
|
||||||
});
|
|
||||||
|
|
||||||
if (partialAttribute !== undefined) {
|
|
||||||
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": augmentedAttribute,
|
|
||||||
"source": partialAttribute
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
|
|
||||||
});
|
|
||||||
|
|
||||||
partialAttributes
|
|
||||||
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
|
|
||||||
.forEach(partialAttribute => {
|
|
||||||
const { name } = partialAttribute;
|
|
||||||
|
|
||||||
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
|
||||||
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
|
||||||
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kcContext };
|
return { kcContext };
|
||||||
}
|
|
||||||
|
|
||||||
if (realKcContext === undefined) {
|
|
||||||
return { "kcContext": undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
|
|
||||||
return { "kcContext": undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const { url } = realKcContext;
|
|
||||||
|
|
||||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { "kcContext": realKcContext };
|
|
||||||
}
|
}
|
||||||
|
249
test/lib/createGetKcContext.spec.ts
Normal file
249
test/lib/createGetKcContext.spec.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
|
||||||
|
import type { ExtendKcContext } from "keycloakify/login/kcContext/getKcContextFromWindow";
|
||||||
|
import type { KcContext } from "keycloakify/login/kcContext";
|
||||||
|
import { same } from "evt/tools/inDepth";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "keycloakify/login/kcContext/kcContextMocks";
|
||||||
|
import { deepClone } from "keycloakify/tools/deepClone";
|
||||||
|
import { expect, it, describe } from "vitest";
|
||||||
|
|
||||||
|
describe("createGetKcContext", () => {
|
||||||
|
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 aNonStandardValue1 = "a non standard value 1";
|
||||||
|
const aNonStandardValue2 = "a non standard value 2";
|
||||||
|
|
||||||
|
type KcContextExtension =
|
||||||
|
| {
|
||||||
|
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: ExtendKcContext<KcContextExtension>["pageId"] }) => {
|
||||||
|
const { mockPageId } = params;
|
||||||
|
|
||||||
|
const { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||||
|
"mockData": [
|
||||||
|
{
|
||||||
|
"pageId": "login.ftl",
|
||||||
|
"realm": { displayName }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "info.ftl",
|
||||||
|
aNonStandardValue1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "register.ftl",
|
||||||
|
authorizedMailDomains
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "my-extra-page-2.ftl",
|
||||||
|
aNonStandardValue2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({
|
||||||
|
mockPageId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
};
|
||||||
|
it("has proper API for login.ftl", () => {
|
||||||
|
const pageId = "login.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
assert<Equals<typeof kcContext, KcContext.Login>>();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
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;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has a proper API for info.ftl", () => {
|
||||||
|
const pageId = "info.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
|
||||||
|
assert<
|
||||||
|
Equals<
|
||||||
|
typeof kcContext,
|
||||||
|
KcContext.Info & {
|
||||||
|
pageId: typeof pageId;
|
||||||
|
aNonStandardValue1: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
Object.assign(mock, { aNonStandardValue1 });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("has a proper API for register.ftl", () => {
|
||||||
|
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; ...
|
||||||
|
assert<
|
||||||
|
Equals<
|
||||||
|
typeof kcContext,
|
||||||
|
KcContext.Register & {
|
||||||
|
pageId: typeof pageId;
|
||||||
|
authorizedMailDomains: string[];
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
Object.assign(mock, { authorizedMailDomains });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("has a proper API for my-extra-page-2.ftl", () => {
|
||||||
|
const pageId = "my-extra-page-2.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
assert<
|
||||||
|
Equals<
|
||||||
|
typeof kcContext,
|
||||||
|
KcContext.Common & {
|
||||||
|
pageId: typeof pageId;
|
||||||
|
aNonStandardValue2: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
|
||||||
|
kcContext.aNonStandardValue2;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId, aNonStandardValue2 });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("has a proper API for my-extra-page-1.ftl", () => {
|
||||||
|
const pageId = "my-extra-page-1.ftl";
|
||||||
|
|
||||||
|
console.log("We expect a warning here =>");
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
assert<Equals<typeof kcContext, KcContext.Common & { pageId: typeof pageId }>>();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("returns the proper mock for login.ftl", () => {
|
||||||
|
const pageId = "login.ftl";
|
||||||
|
|
||||||
|
const { getKcContext } = createGetKcContext();
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({
|
||||||
|
"mockPageId": pageId
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<typeof kcContext, KcContext.Login | undefined>>();
|
||||||
|
|
||||||
|
assert(same(deepClone(kcContext), deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)));
|
||||||
|
});
|
||||||
|
it("returns undefined when no mock is specified", () => {
|
||||||
|
const { getKcContext } = createGetKcContext();
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
|
assert<Equals<typeof kcContext, KcContext | undefined>>();
|
||||||
|
|
||||||
|
assert(kcContext === undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mock are properly overwriten", () => {
|
||||||
|
const { getKcContext } = createGetKcContext();
|
||||||
|
|
||||||
|
const displayName = "myDisplayName";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({
|
||||||
|
"mockPageId": "login.ftl",
|
||||||
|
"storyParams": {
|
||||||
|
"realm": {
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<typeof kcContext, KcContext.Login | undefined>>();
|
||||||
|
|
||||||
|
assert(kcContext?.realm.displayName === displayName);
|
||||||
|
});
|
||||||
|
});
|
@ -213,7 +213,7 @@ describe("getKcContext", () => {
|
|||||||
|
|
||||||
assert(same(deepClone(kcContext), deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)));
|
assert(same(deepClone(kcContext), deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)));
|
||||||
});
|
});
|
||||||
it("returns the proper mock for login.ftl", () => {
|
it("returns undefined when no mock is specified", () => {
|
||||||
const { kcContext } = getKcContext();
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
assert<Equals<typeof kcContext, KcContext | undefined>>();
|
assert<Equals<typeof kcContext, KcContext | undefined>>();
|
||||||
|
26
yarn.lock
26
yarn.lock
@ -2873,7 +2873,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*":
|
"@types/react@*", "@types/react@^18.0.35":
|
||||||
version "18.0.35"
|
version "18.0.35"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.35.tgz#192061cb1044fe01f2d3a94272cd35dd50502741"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.35.tgz#192061cb1044fe01f2d3a94272cd35dd50502741"
|
||||||
integrity sha512-6Laome31HpetaIUGFWl1VQ3mdSImwxtFZ39rh059a1MNnKGqBpC88J6NJ8n/Is3Qx7CefDGLgf/KhN/sYCf7ag==
|
integrity sha512-6Laome31HpetaIUGFWl1VQ3mdSImwxtFZ39rh059a1MNnKGqBpC88J6NJ8n/Is3Qx7CefDGLgf/KhN/sYCf7ag==
|
||||||
@ -2882,15 +2882,6 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/react@18.0.9":
|
|
||||||
version "18.0.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
|
|
||||||
integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
|
|
||||||
dependencies:
|
|
||||||
"@types/prop-types" "*"
|
|
||||||
"@types/scheduler" "*"
|
|
||||||
csstype "^3.0.2"
|
|
||||||
|
|
||||||
"@types/retry@*":
|
"@types/retry@*":
|
||||||
version "0.12.2"
|
version "0.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
|
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
|
||||||
@ -9629,14 +9620,7 @@ react-sizeme@^3.0.1:
|
|||||||
shallowequal "^1.1.0"
|
shallowequal "^1.1.0"
|
||||||
throttle-debounce "^3.0.1"
|
throttle-debounce "^3.0.1"
|
||||||
|
|
||||||
react@18.1.0:
|
react@^18.0, react@^18.2.0:
|
||||||
version "18.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
|
|
||||||
integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.1.0"
|
|
||||||
|
|
||||||
react@^18.0:
|
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
@ -11456,9 +11440,9 @@ upath@^1.1.1:
|
|||||||
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
|
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
|
||||||
|
|
||||||
update-browserslist-db@^1.0.10:
|
update-browserslist-db@^1.0.10:
|
||||||
version "1.0.10"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
|
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
|
||||||
integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
|
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
escalade "^3.1.1"
|
escalade "^3.1.1"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user