Compare commits

..

81 Commits

Author SHA1 Message Date
6e71da62f0 Bump version 2024-03-02 09:29:50 +01:00
5bf33aae75 Better support for environnement variables 2024-03-02 09:29:36 +01:00
06b2dc63ff Bump version 2024-03-02 09:02:17 +01:00
1bb0c9dfc2 Add generic type for kcContext.properties 2024-03-02 09:01:57 +01:00
a2b167e120 Bump version 2024-02-29 21:57:54 +01:00
0909a4b7cc Remove dead code, (I didn't fully understand how it landed here in the first place) 2024-02-29 21:57:37 +01:00
fd7d2bb9bf Bump version 2024-02-27 23:55:40 +01:00
63c40fd816 Add type definition for the user property in the kcContext of the terms.ftl page 2024-02-27 23:55:06 +01:00
0569fa5e58 Bump version 2024-02-27 23:32:07 +01:00
ba74952e0b #513 2024-02-27 23:19:55 +01:00
20c28f785a Bump version 2024-02-27 06:52:17 +01:00
e9b249ddc7 Fix ftl script bug and definitively address #512 and #432 2024-02-27 06:52:17 +01:00
604bb484a3 Update README.md 2024-02-25 21:02:31 +01:00
010c93793a Bump version 2024-02-24 05:38:59 +01:00
dc1d4a66f4 keycloakifyBuildDirPath was non absolute 2024-02-24 05:38:42 +01:00
8ef633d7ef Bump version 2024-02-24 05:09:08 +01:00
2176d33da1 Support generating source map 2024-02-24 05:08:39 +01:00
5b794e2d22 Bump version 2024-02-23 20:02:41 +01:00
ccd75d56c5 Run the post build script in the react app directory 2024-02-23 19:38:01 +01:00
b700066833 Release candidate 2024-02-23 19:22:28 +01:00
546ee006d3 Rename postBuildScript to postBuild, make the params of the Vite plugin optional 2024-02-23 19:22:14 +01:00
7f333a6a36 Release candidate 2024-02-23 19:16:57 +01:00
ae757ee371 Enable to provide the configuration to the Vite plugin, enable user to provide a post build script #148 2024-02-23 19:16:53 +01:00
69936750d5 Bump version 2024-02-21 20:43:34 +01:00
442bfa4ed6 Update types to reflect what is actually there on the kcContext 2024-02-21 20:43:18 +01:00
2a88e6802f Bump version 2024-02-18 11:28:29 +01:00
bcc8b12e13 Enable to statically build storybook in Vite project 2024-02-18 11:28:17 +01:00
9b974505eb Update ci 2024-02-17 04:15:31 +01:00
29b1c26771 Bump version 2024-02-17 03:47:03 +01:00
02db20d98b Enable to release on v8 branch 2024-02-17 03:47:03 +01:00
757354df7d Follow up on #406 2024-02-17 03:47:03 +01:00
563518cf46 Remove poll 2024-02-13 15:07:14 +01:00
7c42d9082a Remove broken badge 2024-02-13 04:47:14 +01:00
040284af71 Reference Vite doc 2024-02-13 04:46:36 +01:00
34f64184d9 Bump version 2024-02-13 01:33:31 +01:00
b9abd74156 Create a .gitignore that matches all in the build_keycloak directory 2024-02-13 01:33:15 +01:00
a1c0bfda6c Bump version 2024-02-13 01:13:26 +01:00
617dcef09d Merge pull request #499 from keycloakify/vite
Vite
2024-02-13 01:04:54 +01:00
d9c406800a Fix tests 2024-02-12 23:57:21 +01:00
54b869def1 Release candidate 2024-02-12 23:43:21 +01:00
d80a583979 Better test for isStorybook 2024-02-12 23:42:31 +01:00
99bfd7379b Release candidate 2024-02-12 13:43:34 +01:00
5f257382fa Prevent accumulation of assets in build dir 2024-02-12 13:43:12 +01:00
e3e6847c82 Prevent users from having to use any 2024-02-12 13:27:07 +01:00
4ee0823acb Bump version 2024-02-12 01:47:21 +01:00
d466123b1c Use Keycloak 23.0.6 2024-02-12 01:41:08 +01:00
21cbc14a48 Release candidate 2024-02-12 01:34:54 +01:00
b2f2c3e386 Disalow releative basename in vite config 2024-02-12 01:34:34 +01:00
b03340ed10 Do not dynamically import "en" to make vite happy 2024-02-12 00:33:12 +01:00
5b563d8e9b Improve privacy on the buildinfo file that might be served 2024-02-12 00:32:18 +01:00
2790487fc7 Release candidate 2024-02-11 23:59:23 +01:00
ad5a368065 Run vite configResolved in dev mode as well 2024-02-11 23:59:10 +01:00
7c0a631a9a Fix crash in vite-plugin 2024-02-11 23:54:28 +01:00
4a8920749a release candidate 2024-02-11 23:48:09 +01:00
8ab118dd06 Remove path-browserify dependency 2024-02-11 23:47:58 +01:00
e6661cb898 Fix build 2024-02-11 23:22:37 +01:00
1671850714 Release candidate 2024-02-11 20:21:05 +01:00
d568bafe04 Remove unessesary file 2024-02-11 20:20:52 +01:00
43dcce8478 Add cache to getNpmWorkspaceRootDirPath 2024-02-11 20:20:38 +01:00
ad70a4cffd Make Vite run copy-keaycloak-resources-to-public 2024-02-11 20:15:18 +01:00
6d4a948dd8 minor refactor 2024-02-11 19:25:52 +01:00
839ba6a964 Improve monorepo support 2024-02-11 18:28:58 +01:00
b5cfdb9d0a Store vite plugin output in cache dir path 2024-02-11 16:17:58 +01:00
9706338182 Release candidate 2024-02-11 12:05:05 +01:00
05f52c3d23 Remove unessesary log 2024-02-11 12:04:50 +01:00
df3acb6932 Release candidate 2024-02-11 10:38:12 +01:00
b3c242595e Fix PUBLIC_URL typing 2024-02-11 10:37:22 +01:00
26985f8d81 Release candidate 2024-02-11 00:21:21 +01:00
05e5e4efec When is storybook, don't print mock related warning in console 2024-02-11 00:21:04 +01:00
4d67f16e94 Release candidate 2024-02-10 20:40:59 +01:00
334ec1870a Throw if unrecognized bundler when getting BASE_URL for mocks 2024-02-10 20:40:41 +01:00
ef5e4fccd3 Replace the common asset path url in the ftl (upstream) to help pepole figure out what's going on 2024-02-10 20:04:08 +01:00
8535edcfd4 Relase candidate 2024-02-10 19:51:39 +01:00
bda76200d7 Base url that works everywhere in mocks 2024-02-10 19:51:24 +01:00
db0dc96cc7 Release candidate 2024-02-09 17:56:57 +01:00
6d62b5a150 Don't replace process.env.PUBLIC_URL 2024-02-09 17:56:28 +01:00
217439d673 Export polyfill of process.env.PUBLIC_URL 2024-02-09 17:52:26 +01:00
1f79a8f7dc Deprecate keycloakJsAdapter 2024-02-09 17:52:03 +01:00
7596786b18 Release candidate 2024-02-08 23:37:14 +01:00
2540b06c94 Tweak the resources of the default theme that are kept 2024-02-08 23:36:52 +01:00
43eeaf3002 Remove misleading comment in start-keycloak-container script 2024-02-08 14:41:07 +01:00
51 changed files with 1136 additions and 960 deletions

View File

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

View File

@ -14,9 +14,6 @@
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/keycloakify">
</a>
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
</a>
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
@ -43,11 +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)!
> 📣 I've observed that a few people have unstarred the project recently.
> I'm concerned that I may have inadvertently introduced some misinformation in the documentation, leading to frustration.
> If you're having a negative experience, [please let me know so I can resolve the issue](https://github.com/keycloakify/keycloakify/discussions/507).
## 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.
@ -130,6 +123,20 @@ 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! 🎉**
- [The starter is now a Vite project](https://github.com/keycloakify/keycloakify-starter).
The Webpack based starter is accessible [here](https://github.com/keycloakify/keycloakify-starter-cra).
- CRA (Webpack) remains supported for the forseable future.
- If you have a CRA Keycloakify theme that you wish to migrate to Vite checkout [this migration guide](https://docs.keycloakify.dev/migration-guides/cra-greater-than-vite).
## 9.0
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "9.4.0-rc.3",
"version": "9.5.8",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -119,13 +119,13 @@
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
"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

@ -3,7 +3,7 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { getLogger } from "../src/bin/tools/logger";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
@ -19,7 +19,9 @@ const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "23.0.4";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
@ -31,7 +33,8 @@ async function main() {
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
"cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
}
});
@ -72,14 +75,13 @@ async function main() {
return;
}
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
""
`//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY"
].join("\n");
languages.forEach(language => {
@ -92,6 +94,7 @@ async function main() {
Buffer.from(
[
generatedFileHeader,
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
@ -110,10 +113,15 @@ async function main() {
Buffer.from(
[
generatedFileHeader,
`import * as en from "./en";`,
"",
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
" switch (currentLanguageTag) {",
...languages.map(language => ` case "${language}": return import("./${language}");`),
` case "en": return en;`,
...languages
.filter(language => language !== "en")
.map(language => ` case "${language}": return import("./${language}");`),
' default: return { "default": {} };',
" }",
" })();",

View File

@ -1,14 +0,0 @@
import fs from "fs";
import path from "path";
import zodToJsonSchema from "zod-to-json-schema";
import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
const jsonSchemaName = "keycloakifyPackageJsonSchema";
const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
const baseProperties = {
// merges package.json schema with keycloakify properties
"allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
};
fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));

View File

@ -1,11 +1,11 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
const singletonDependencies: string[] = ["react", "@types/react"];
const rootDirPath = getProjectRoot();
const rootDirPath = getThisCodebaseRootDirPath();
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync(

21
src/PUBLIC_URL.ts Normal file
View File

@ -0,0 +1,21 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert";
/**
* This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
* This works both in your main app and in your Keycloak theme.
*/
export const PUBLIC_URL = (() => {
const kcContext = (window as any)[nameOfTheGlobal];
if (kcContext === undefined || process.env.NODE_ENV === "development") {
assert(
process.env.PUBLIC_URL !== undefined,
`If you use keycloakify/PUBLIC_URL you should be in Webpack and thus process.env.PUBLIC_URL should be defined`
);
return process.env.PUBLIC_URL;
}
return `${kcContext.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}`;
})();

View File

@ -90,6 +90,7 @@ export declare namespace KcContext {
lastName?: string;
username?: string;
};
properties: Record<string, string | undefined>;
};
export type Password = Common & {

View File

@ -1,16 +1,16 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { isStorybook } from "keycloakify/lib/isStorybook";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
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;
@ -27,7 +27,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
warn_that_mock_is_enbaled: {
if (isStorybook) {
break warn_that_mock_is_enbaled;
}
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
}
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
@ -77,6 +83,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
});
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
});
}
return { kcContext };
}
@ -88,8 +101,6 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
return { "kcContext": realKcContext as any };
}

View File

@ -1,12 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
@ -15,7 +13,7 @@ export const kcContextCommonMock: KcContext.Common = {
"themeName": "my-theme-name",
"url": {
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
@ -147,6 +145,17 @@ export const kcContextCommonMock: KcContext.Common = {
"lastName": "doe",
"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"
}
};

View File

@ -1,10 +1,11 @@
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { KcContext } from "keycloakify/account/kcContext";
export type PageProps<KcContext, I18nExtended extends I18n> = {
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
kcContext: NarowedKcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>;

View File

@ -2,7 +2,7 @@ export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const resolvedViteConfigJsonBasename = ".keycloakifyViteConfig.json";
export const resolvedViteConfigJsonBasename = "vite.json";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
@ -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

@ -1,17 +1,44 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { downloadKeycloakStaticResources, type BuildOptionsLike } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { readBuildOptions } from "./keycloakify/buildOptions";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
import { readThisNpmProjectVersion } from "./tools/readThisNpmProjectVersion";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
(async () => {
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2)
export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
const { processArgv } = params;
const buildOptions = readBuildOptions({ processArgv });
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const { keycloakifyBuildinfoRaw } = generateKeycloakifyBuildinfoRaw({
destDirPath,
"keycloakifyVersion": readThisNpmProjectVersion(),
buildOptions
});
const reservedDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { "force": true, "recursive": true });
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
@ -24,13 +51,13 @@ import * as fs from "fs";
}
})(),
themeType,
"themeDirPath": reservedDirPath,
"themeDirPath": destDirPath,
buildOptions
});
}
fs.writeFileSync(
pathJoin(reservedDirPath, "README.txt"),
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
@ -42,5 +69,44 @@ import * as fs from "fs";
fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(buildOptions.reactAppRootDirPath, reservedDirPath)} directory created.`);
})();
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
}
export function generateKeycloakifyBuildinfoRaw(params: {
destDirPath: string;
keycloakifyVersion: string;
buildOptions: BuildOptionsLike & {
loginThemeResourcesFromKeycloakVersion: string;
};
}) {
const { destDirPath, keycloakifyVersion, buildOptions } = params;
const { cacheDirPath, npmWorkspaceRootDirPath, loginThemeResourcesFromKeycloakVersion, ...rest } = buildOptions;
assert<Equals<typeof rest, {}>>(true);
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion,
"buildOptions": {
loginThemeResourcesFromKeycloakVersion,
"cacheDirPath": pathRelative(destDirPath, cacheDirPath),
"npmWorkspaceRootDirPath": pathRelative(destDirPath, npmWorkspaceRootDirPath)
}
},
null,
2
);
return { keycloakifyBuildinfoRaw };
}
async function main() {
await copyKeycloakResourcesToPublic({
"processArgv": process.argv.slice(2)
});
}
if (require.main === module) {
main();
}

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { downloadAndUnzip } from "./downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
@ -13,6 +13,7 @@ import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -21,11 +22,10 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
"doUseCache": true,
"cacheDirPath": buildOptions.cacheDirPath,
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
buildOptions,
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
@ -62,9 +62,9 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
// Note, this is an optimization for reducing the size of the jar
remove_unused_node_modules: {
const pathOfNodeModules = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
if (!fs.existsSync(pathOfNodeModules)) {
if (!fs.existsSync(nodeModuleDirPath)) {
break remove_unused_node_modules;
}
@ -114,8 +114,8 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
];
transformCodebase({
"srcDirPath": pathOfNodeModules,
"destDirPath": pathOfNodeModules,
"srcDirPath": nodeModuleDirPath,
"destDirPath": nodeModuleDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
@ -140,6 +140,33 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
});
}
// Just like node_modules
remove_unused_lib: {
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
if (!fs.existsSync(libDirPath)) {
break remove_unused_lib;
}
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace", "pficon"];
transformCodebase({
"srcDirPath": libDirPath,
"destDirPath": libDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
});
}
last_account_v1_transformations: {
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
break last_account_v1_transformations;
@ -154,67 +181,25 @@ 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.
{
const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
const usedCommonResourceRelativeFilePaths = [
const toKeepPrefixes = [
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
pathJoin("patternfly", "dist", "css", fileBasename)
),
...[
"OpenSans-Light-webfont.woff2",
"OpenSans-Regular-webfont.woff2",
"OpenSans-Bold-webfont.woff2",
"OpenSans-Semibold-webfont.woff2",
"OpenSans-Bold-webfont.woff",
"OpenSans-Light-webfont.woff",
"OpenSans-Regular-webfont.woff",
"OpenSans-Semibold-webfont.woff",
"OpenSans-Regular-webfont.ttf",
"OpenSans-Light-webfont.ttf",
"OpenSans-Semibold-webfont.ttf",
"OpenSans-Bold-webfont.ttf"
].map(fileBasename => pathJoin("patternfly", "dist", "fonts", fileBasename))
pathJoin("patternfly", "dist", "fonts")
];
transformCodebase({
"srcDirPath": nodeModulesDirPath,
"destDirPath": nodeModulesDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (!usedCommonResourceRelativeFilePaths.includes(fileRelativePath)) {
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
});

203
src/bin/downloadAndUnzip.ts Normal file
View File

@ -0,0 +1,203 @@
import { createHash } from "crypto";
import { mkdir, writeFile, unlink } from "fs/promises";
import fetch from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { transformCodebase } from "./tools/transformCodebase";
import { unzip, zip } from "./tools/unzip";
import { rm } from "./tools/fs.rm";
import * as child_process from "child_process";
import { existsAsync } from "./tools/fs.existsAsync";
import type { BuildOptions } from "./keycloakify/buildOptions";
import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadAndUnzip(params: {
url: string;
destDirPath: string;
specificDirsToExtract?: string[];
preCacheTransform?: {
actionCacheId: string;
action: (params: { destDirPath: string }) => Promise<void>;
};
buildOptions: BuildOptionsLike;
}) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
const { extractDirPath, zipFilePath } = (() => {
const zipFileBasenameWithoutExt = generateFileNameFromURL({
url,
"preCacheTransform":
preCacheTransform === undefined
? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
}
});
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
return { zipFilePath, extractDirPath };
})();
download_zip_and_transform: {
if (await existsAsync(zipFilePath)) {
break download_zip_and_transform;
}
const { response, isFromRemoteCache } = await (async () => {
const proxyFetchOptions = await getProxyFetchOptions({
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
});
const response = await fetch(
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
proxyFetchOptions
);
if (response.status === 200) {
return {
response,
"isFromRemoteCache": true
};
}
return {
"response": await fetch(url, proxyFetchOptions),
"isFromRemoteCache": false
};
})();
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
* does not support node-fetch 3.x. So we stick around with this band-aid until
* octokit upgrades.
*/
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
if (isFromRemoteCache) {
break download_zip_and_transform;
}
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
break download_zip_and_transform;
}
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
try {
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
} catch (error) {
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
throw error;
}
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
upload_to_remot_cache_if_admin: {
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
if (githubToken === undefined) {
break upload_to_remot_cache_if_admin;
}
console.log("uploading to remote cache");
try {
child_process.execSync(`which putasset`);
} catch {
child_process.execSync(`npm install -g putasset`);
}
try {
child_process.execFileSync("putasset", [
"--owner",
"keycloakify",
"--repo",
"keycloakify",
"--tag",
"v0.0.1",
"--filename",
zipFilePath,
"--token",
githubToken
]);
} catch {
console.log("upload failed, asset probably already exists in remote cache");
}
}
}
await unzip(zipFilePath, extractDirPath);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
await rm(extractDirPath, { "recursive": true });
}
function generateFileNameFromURL(params: {
url: string;
preCacheTransform:
| {
actionCacheId: string;
actionFootprint: string;
}
| undefined;
}): string {
const { preCacheTransform } = params;
// Parse the URL
const url = new URL(params.url);
// Extract pathname and remove leading slashes
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
// Optionally, add query parameters replacing special characters
if (url.search) {
fileName += url.search.replace(/[&=?]/g, "-");
}
// Replace any characters that are not valid in filenames
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
// Trim or pad the fileName to a specific length
fileName = fileName.substring(0, 50);
add_pre_cache_transform: {
if (preCacheTransform === undefined) {
break add_pre_cache_transform;
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
}
return fileName;
}

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node
import { getProjectRoot } from "./tools/getProjectRoot";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize";
@ -11,11 +11,14 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
import { getReactAppRootDirPath } from "./keycloakify/buildOptions/getReactAppRootDirPath";
(async () => {
console.log("Select a theme type");
const reactAppRootDirPath = process.cwd();
const { reactAppRootDirPath } = getReactAppRootDirPath({
"processArgv": process.argv.slice(2)
});
const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes]
@ -55,7 +58,7 @@ import { themeTypes, type ThemeType } from "./constants";
process.exit(-1);
}
await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
await writeFile(targetFilePath, await readFile(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();

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

@ -4,7 +4,10 @@ import { join as pathJoin } from "path";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { readResolvedViteConfig } from "./resolvedViteConfig";
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
import * as fs from "fs";
import { getCacheDirPath } from "./getCacheDirPath";
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = {
@ -28,36 +31,31 @@ export type BuildOptions = {
urlPathname: string | undefined;
assetsDirPath: string;
doBuildRetrocompatAccountTheme: boolean;
npmWorkspaceRootDirPath: string;
};
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
const { processArgv } = params;
const argv = parseArgv(processArgv);
const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
const reactAppRootDirPath = (() => {
const arg = argv["project"] ?? argv["p"];
const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
if (typeof arg !== "string") {
return process.cwd();
}
const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
return getAbsoluteAndInOsFormatPath({
"pathIsh": arg,
"cwd": process.cwd()
});
})();
if (resolvedViteConfig === undefined && fs.existsSync(pathJoin(reactAppRootDirPath, "vite.config.ts"))) {
throw new Error("Keycloakify's Vite plugin output not found");
}
const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
const { resolvedViteConfig } =
readResolvedViteConfig({
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
reactAppRootDirPath
}) ?? {};
const userProvidedBuildOptions = {
...userProvidedBuildOptionsFromPackageJson,
...resolvedViteConfig?.userProvidedBuildOptions
};
const themeNames = (() => {
if (parsedPackageJson.keycloakify?.themeName === undefined) {
if (userProvidedBuildOptions.themeName === undefined) {
return [
parsedPackageJson.name
.replace(/^@(.*)/, "$1")
@ -66,28 +64,22 @@ 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 { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
reactAppRootDirPath,
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack"
});
const reactAppBuildDirPath = (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
@ -98,18 +90,22 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
})();
const argv = parseArgv(processArgv);
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
return {
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
"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)
@ -119,12 +115,24 @@ 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,
"keycloakifyBuildDirPath": (() => {
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(
reactAppRootDirPath,
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
);
})(),
"publicDirPath": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
@ -143,19 +151,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
})(),
"cacheDirPath": pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
),
cacheDirPath,
"urlPathname": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
@ -191,6 +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

@ -0,0 +1,25 @@
import { join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
const cacheDirPath = pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
);
return { cacheDirPath };
}

View File

@ -1,33 +0,0 @@
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { join as pathJoin } from "path";
export function getKeycloakifyBuildDirPath(params: {
reactAppRootDirPath: string;
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
bundler: "vite" | "webpack";
}) {
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath, bundler } = params;
const keycloakifyBuildDirPath = (() => {
if (parsedPackageJson_keycloakify_keycloakifyBuildDirPath !== undefined) {
getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(
reactAppRootDirPath,
`${(() => {
switch (bundler) {
case "vite":
return "dist";
case "webpack":
return "build";
}
})()}_keycloak`
);
})();
return { keycloakifyBuildDirPath };
}

View File

@ -0,0 +1,49 @@
import * as child_process from "child_process";
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
let cache:
| {
reactAppRootDirPath: string;
npmWorkspaceRootDirPath: string;
}
| undefined = undefined;
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
use_cache: {
if (cache === undefined || cache.reactAppRootDirPath !== reactAppRootDirPath) {
break use_cache;
}
const { npmWorkspaceRootDirPath } = cache;
return { npmWorkspaceRootDirPath };
}
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
try {
child_process.execSync("npm config get", { cwd: cwd });
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep, "NPM workspace not found");
return callee(depth + 1);
}
throw error;
}
return cwd;
})(0);
cache = {
reactAppRootDirPath,
npmWorkspaceRootDirPath
};
return { npmWorkspaceRootDirPath };
}

View File

@ -0,0 +1,23 @@
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
export function getReactAppRootDirPath(params: { processArgv: string[] }) {
const { processArgv } = params;
const argv = parseArgv(processArgv);
const reactAppRootDirPath = (() => {
const arg = argv["project"] ?? argv["p"];
if (typeof arg !== "string") {
return process.cwd();
}
return getAbsoluteAndInOsFormatPath({
"pathIsh": arg,
"cwd": process.cwd()
});
})();
return { reactAppRootDirPath };
}

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,20 +5,22 @@ import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
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
});
{
@ -28,31 +30,18 @@ const zResolvedViteConfig = z.object({
assert<Equals<Got, Expected>>();
}
export function readResolvedViteConfig(params: {
reactAppRootDirPath: string;
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
}):
| {
resolvedViteConfig: ResolvedViteConfig;
}
| undefined {
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath } = params;
export function readResolvedViteConfig(params: { cacheDirPath: string }): {
resolvedViteConfig: ResolvedViteConfig | undefined;
} {
const { cacheDirPath } = params;
const viteConfigTsFilePath = pathJoin(reactAppRootDirPath, "vite.config.ts");
const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
if (!fs.existsSync(viteConfigTsFilePath)) {
return undefined;
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
return { "resolvedViteConfig": undefined };
}
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
reactAppRootDirPath,
parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
"bundler": "vite"
});
const resolvedViteConfig = (() => {
const resolvedViteConfigJsonFilePath = pathJoin(keycloakifyBuildDirPath, resolvedViteConfigJsonBasename);
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
throw new Error("Missing Keycloakify Vite plugin output.");
}

View File

@ -408,6 +408,14 @@
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["pageId"] = "${pageId}";
try {
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} catch(error) {
}
return out;
})()
@ -423,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 = "">
@ -455,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 -->
@ -480,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>
@ -650,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>
@ -660,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>
@ -678,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

@ -7,7 +7,7 @@ import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../constants";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
@ -105,7 +105,8 @@ export function generateFtlFilesCodeFactory(params: {
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName),
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",

View File

@ -30,7 +30,6 @@ export function generateStartKeycloakTestingContainer(params: { jarFilePath: str
Buffer.from(
[
"#!/usr/bin/env bash",
`# If you want to test with Keycloak version prior to 23 use the retrocompat-${pathBasename(jarFilePath)}`,
"",
`docker rm ${containerName} || true`,
"",

View File

@ -11,6 +11,7 @@ import { rmSync } from "../../tools/fs.rmSync";
type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
{

View File

@ -9,6 +9,7 @@ import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();

View File

@ -20,6 +20,7 @@ import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import { bringInAccountV1 } from "./bringInAccountV1";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
@ -33,6 +34,7 @@ export type BuildOptionsLike = {
urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
themeNames: string[];
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -77,6 +79,11 @@ export async function generateTheme(params: {
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: {
const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { "recursive": true, "force": true });
if (themeType === "account" && implementedThemeTypes.login) {
// NOTE: We prevend doing it twice, it has been done for the login theme.
@ -88,7 +95,7 @@ export async function generateTheme(params: {
"resources",
basenameOfTheKeycloakifyResourcesDir
),
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir)
destDirPath
});
break apply_replacers_and_move_to_theme_resources;
@ -96,7 +103,7 @@ export async function generateTheme(params: {
transformCodebase({
"srcDirPath": buildOptions.reactAppBuildDirPath,
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
destDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.

View File

@ -6,9 +6,10 @@ import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTe
import * as fs from "fs";
import { readBuildOptions } from "./buildOptions";
import { getLogger } from "../tools/logger";
import { assert } from "tsafe/assert";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants";
export async function main() {
const buildOptions = readBuildOptions({
@ -18,23 +19,15 @@ export async function main() {
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const keycloakifyDirPath = getProjectRoot();
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
for (const themeName of buildOptions.themeNames) {
await generateTheme({
themeName,
themeSrcDirPath,
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
buildOptions,
"keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
assert(typeof version === "string");
return version;
})()
"keycloakifySrcDirPath": pathJoin(getThisCodebaseRootDirPath(), "src"),
"keycloakifyVersion": readThisNpmProjectVersion(),
buildOptions
});
}
@ -44,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);
@ -67,14 +82,6 @@ export async function main() {
);
}
const containerKeycloakVersion = "23.0.0";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});
logger.log(
[
"",

View File

@ -29,20 +29,6 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
);
}
// d={NODE_ENV:"production",PUBLIC_URL:"/foo-bar",WDS_SOCKET_HOST
// d={NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST
// ->
// ... PUBLIC_URL:window.kcContext.url.resourcesPath+"/build" ...
fixedJsCode = fixedJsCode.replace(
new RegExp(
`NODE_ENV:"production",PUBLIC_URL:"${
buildOptions.urlPathname !== undefined ? replaceAll(buildOptions.urlPathname.slice(0, -1), "/", "\\/") : ""
}"`,
"g"
),
`NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}"`
);
// Example: "static/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);

View File

@ -1,301 +0,0 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, readFile, stat, writeFile, unlink } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { promisify } from "util";
import { transformCodebase } from "./transformCodebase";
import { unzip, zip } from "./unzip";
import { rm } from "../tools/fs.rm";
import * as child_process from "child_process";
const exec = promisify(execCallback);
function generateFileNameFromURL(params: {
url: string;
preCacheTransform:
| {
actionCacheId: string;
actionFootprint: string;
}
| undefined;
}): string {
const { preCacheTransform } = params;
// Parse the URL
const url = new URL(params.url);
// Extract pathname and remove leading slashes
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
// Optionally, add query parameters replacing special characters
if (url.search) {
fileName += url.search.replace(/[&=?]/g, "-");
}
// Replace any characters that are not valid in filenames
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
// Trim or pad the fileName to a specific length
fileName = fileName.substring(0, 50);
add_pre_cache_transform: {
if (preCacheTransform === undefined) {
break add_pre_cache_transform;
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
}
return fileName;
}
async function exists(path: string) {
try {
await stat(path);
return true;
} catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return false;
throw error;
}
}
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
function ensureSingleOrNone<T>(arg0: T | T[]) {
if (!Array.isArray(arg0)) return arg0;
if (arg0.length === 0) return undefined;
if (arg0.length === 1) return arg0[0];
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
}
type NPMConfig = Record<string, string | string[]>;
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
/**
* Get npm configuration as map
*/
async function getNmpConfig() {
return readNpmConfig().then(parseNpmConfig);
}
function readNpmConfig(): Promise<string> {
return (async function callee(depth: number): Promise<string> {
const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
let stdout: string;
try {
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep);
return callee(depth + 1);
}
throw error;
}
return stdout;
})(0);
}
function parseNpmConfig(stdout: string) {
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2) as [string, string])
.reduce(npmConfigReducer, {} as NPMConfig);
}
function maybeBoolean(arg0: string | undefined) {
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
}
function chunks<T>(arr: T[], size: number = 2) {
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
}
async function readCafile(cafile: string) {
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
}
/**
* Get proxy and ssl configuration from npm config files. Note that we don't care about
* proxy config in env vars, because make-fetch-happen will do that for us.
*
* @returns proxy configuration
*/
async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "ca" | "cert">> {
const cfg = await getNmpConfig();
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
const cert = cfg["cert"];
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
const cafile = ensureSingleOrNone(cfg["cafile"]);
if (typeof cafile !== "undefined" && cafile !== "null") ca.push(...(await readCafile(cafile)));
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
}
export async function downloadAndUnzip(
params: {
url: string;
destDirPath: string;
specificDirsToExtract?: string[];
preCacheTransform?: {
actionCacheId: string;
action: (params: { destDirPath: string }) => Promise<void>;
};
} & (
| {
doUseCache: true;
cacheDirPath: string;
}
| {
doUseCache: false;
}
)
) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
const zipFileBasename = generateFileNameFromURL({
url,
"preCacheTransform":
preCacheTransform === undefined
? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
}
});
const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
download_zip_and_transform: {
if (await exists(zipFilePath)) {
break download_zip_and_transform;
}
const opts = await getFetchOptions();
const { response, isFromRemoteCache } = await (async () => {
const response = await fetch(`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`, opts);
if (response.status === 200) {
return {
response,
"isFromRemoteCache": true
};
}
return {
"response": await fetch(url, opts),
"isFromRemoteCache": false
};
})();
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
* does not support node-fetch 3.x. So we stick around with this band-aid until
* octokit upgrades.
*/
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
if (isFromRemoteCache) {
break download_zip_and_transform;
}
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
break download_zip_and_transform;
}
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
try {
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
} catch (error) {
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
throw error;
}
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
upload_to_remot_cache_if_admin: {
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
if (githubToken === undefined) {
break upload_to_remot_cache_if_admin;
}
console.log("uploading to remote cache");
try {
child_process.execSync(`which putasset`);
} catch {
child_process.execSync(`npm install -g putasset`);
}
try {
child_process.execFileSync("putasset", [
"--owner",
"keycloakify",
"--repo",
"keycloakify",
"--tag",
"v0.0.1",
"--filename",
zipFilePath,
"--token",
githubToken
]);
} catch {
console.log("upload failed, asset probably already exists in remote cache");
}
}
}
await unzip(zipFilePath, extractDirPath);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
if (!rest.doUseCache) {
await rm(cacheDirPath, { "recursive": true });
} else {
await rm(extractDirPath, { "recursive": true });
}
}

View File

@ -0,0 +1,73 @@
import { exec as execCallback } from "child_process";
import { readFile } from "fs/promises";
import { type FetchOptions } from "make-fetch-happen";
import { promisify } from "util";
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
function ensureSingleOrNone<T>(arg0: T | T[]) {
if (!Array.isArray(arg0)) return arg0;
if (arg0.length === 0) return undefined;
if (arg0.length === 1) return arg0[0];
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
}
type NPMConfig = Record<string, string | string[]>;
/**
* Get npm configuration as map
*/
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
const { npmWorkspaceRootDirPath } = params;
const exec = promisify(execCallback);
const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2) as [string, string])
.reduce(npmConfigReducer, {} as NPMConfig);
}
export type ProxyFetchOptions = Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca">;
export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise<ProxyFetchOptions> {
const { npmWorkspaceRootDirPath } = params;
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
function maybeBoolean(arg0: string | undefined) {
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
}
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
const cert = cfg["cert"];
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
const cafile = ensureSingleOrNone(cfg["cafile"]);
if (typeof cafile !== "undefined" && cafile !== "null") {
ca.push(
...(await (async () => {
function chunks<T>(arr: T[], size: number = 2) {
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
})())
);
}
return { proxy, noProxy, strictSSL, cert, "ca": ca.length === 0 ? undefined : ca };
}

View File

@ -0,0 +1,11 @@
import * as fs from "fs/promises";
export async function existsAsync(path: string) {
try {
await fs.stat(path);
return true;
} catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return false;
throw error;
}
}

View File

@ -1,19 +1,19 @@
import * as fs from "fs";
import * as path from "path";
function getProjectRootRec(dirPath: string): string {
function getThisCodebaseRootDirPath_rec(dirPath: string): string {
if (fs.existsSync(path.join(dirPath, "package.json"))) {
return dirPath;
}
return getProjectRootRec(path.join(dirPath, ".."));
return getThisCodebaseRootDirPath_rec(path.join(dirPath, ".."));
}
let result: string | undefined = undefined;
export function getProjectRoot(): string {
export function getThisCodebaseRootDirPath(): string {
if (result !== undefined) {
return result;
}
return (result = getProjectRootRec(__dirname));
return (result = getThisCodebaseRootDirPath_rec(__dirname));
}

View File

@ -1,13 +1,15 @@
import { getProjectRoot } from "./getProjectRoot";
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
import { join as pathJoin } from "path";
import { constants } from "fs";
import { chmod, stat } from "fs/promises";
(async () => {
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json"));
const promises = Object.values<string>(bin).map(async scriptPath => {
const fullPath = pathJoin(getProjectRoot(), scriptPath);
const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);

View File

@ -1,6 +0,0 @@
export function pathJoin(...path: string[]): string {
return path
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
.join(typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/");
}

View File

@ -0,0 +1,12 @@
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
import { assert } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin } from "path";
export function readThisNpmProjectVersion(): string {
const version = JSON.parse(fs.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")).toString("utf8"))["version"];
assert(typeof version === "string");
return version;
}

44
src/lib/BASE_URL.ts Normal file
View File

@ -0,0 +1,44 @@
import { assert } from "tsafe/assert";
/**
* WARNING: Internal use only!!
* THIS DOES NOT WORK IN KEYCLOAK! It's only for resolving mock assets.
* This is just a way to know what's the base url that works
* both in webpack and vite.
* You can see this as a polyfill that return `import.meta.env.BASE_URL` when in Vite
* and when in Webpack returns the base url in the same format as vite does meaning
* "/" if hosted at root or "/foo/" when hosted under a subpath (always start and ends with a "/").
*/
export const BASE_URL = (() => {
vite: {
let BASE_URL: string;
try {
// @ts-expect-error
BASE_URL = import.meta.env.BASE_URL;
assert(typeof BASE_URL === "string");
} catch {
break vite;
}
return BASE_URL;
}
webpack: {
let BASE_URL: string;
try {
// @ts-expect-error
BASE_URL = process.env.PUBLIC_URL;
assert(typeof BASE_URL === "string");
} catch {
break webpack;
}
return BASE_URL === "" ? "/" : `${BASE_URL}/`;
}
throw new Error("Bundler not supported");
})();

1
src/lib/isStorybook.ts Normal file
View File

@ -0,0 +1 @@
export const isStorybook = typeof window === "object" && Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;

View File

@ -36,6 +36,10 @@ export declare namespace keycloak_js {
}
/**
* @deprecated: This will be removed in the next major version.
* If you use this, please copy paste the code into your project.
* Better yet migrate away from keycloak-js and use https://docs.oidc-spa.dev instead.
*
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
* The goal here is just to be able to inject search param in url before keycloak redirect.
* Our use case for it is to pass over the login screen the states of useGlobalState

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

@ -2,19 +2,19 @@ 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 { isStorybook } from "keycloakify/lib/isStorybook";
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 { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
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;
@ -31,7 +31,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
warn_that_mock_is_enbaled: {
if (isStorybook) {
break warn_that_mock_is_enbaled;
}
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
}
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
@ -136,6 +142,13 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
}
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
});
}
return { kcContext };
}
@ -147,8 +160,6 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
return { "kcContext": realKcContext as any };
}

View File

@ -1,10 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [
{
@ -100,9 +100,7 @@ const attributes: Attribute[] = [
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
@ -112,7 +110,7 @@ export const kcContextCommonMock: KcContext.Common = {
"url": {
"loginAction": "#",
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"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"
},
@ -238,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,10 +1,11 @@
import type { I18n } from "keycloakify/login/i18n";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { KcContext } from "keycloakify/account/kcContext";
export type PageProps<KcContext, I18nExtended extends I18n> = {
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
kcContext: NarowedKcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>;

View File

@ -1,232 +0,0 @@
{
"plugins": [
{
"name": "vite:build-metadata"
},
{
"name": "vite:watch-package-data"
},
{
"name": "vite:pre-alias"
},
{
"name": "alias"
},
{
"name": "vite:react-babel",
"enforce": "pre"
},
{
"name": "vite:react-refresh",
"enforce": "pre"
},
{
"name": "vite:modulepreload-polyfill"
},
{
"name": "vite:resolve"
},
{
"name": "vite:html-inline-proxy"
},
{
"name": "vite:css"
},
{
"name": "vite:esbuild"
},
{
"name": "vite:json"
},
{
"name": "vite:wasm-helper"
},
{
"name": "vite:worker"
},
{
"name": "vite:asset"
},
{
"name": "vite-plugin-commonjs"
},
{
"name": "keycloakify"
},
{
"name": "vite:wasm-fallback"
},
{
"name": "vite:define"
},
{
"name": "vite:css-post"
},
{
"name": "vite:build-html"
},
{
"name": "vite:worker-import-meta-url"
},
{
"name": "vite:asset-import-meta-url"
},
{
"name": "vite:force-systemjs-wrap-complete"
},
{
"name": "commonjs",
"version": "25.0.7"
},
{
"name": "vite:data-uri"
},
{
"name": "vite:dynamic-import-vars"
},
{
"name": "vite:import-glob"
},
{
"name": "vite:build-import-analysis"
},
{
"name": "vite:esbuild-transpile"
},
{
"name": "vite:terser"
},
{
"name": "vite:reporter"
},
{
"name": "vite:load-fallback"
}
],
"optimizeDeps": {
"disabled": "build",
"esbuildOptions": {
"preserveSymlinks": false,
"jsx": "automatic",
"plugins": [
{
"name": "vite-plugin-commonjs:pre-bundle"
}
]
},
"include": ["react", "react/jsx-dev-runtime", "react/jsx-runtime"]
},
"build": {
"target": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
"cssTarget": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
"outDir": "dist",
"assetsDir": "assets",
"assetsInlineLimit": 4096,
"cssCodeSplit": true,
"sourcemap": false,
"rollupOptions": {},
"minify": "esbuild",
"terserOptions": {},
"write": true,
"emptyOutDir": null,
"copyPublicDir": true,
"manifest": false,
"lib": false,
"ssr": false,
"ssrManifest": false,
"ssrEmitAssets": false,
"reportCompressedSize": true,
"chunkSizeWarningLimit": 500,
"watch": null,
"commonjsOptions": {
"include": [{}],
"extensions": [".js", ".cjs"]
},
"dynamicImportVarsOptions": {
"warnOnError": true,
"exclude": [{}]
},
"modulePreload": {
"polyfill": true
},
"cssMinify": true
},
"esbuild": {
"jsxDev": false,
"jsx": "automatic"
},
"resolve": {
"mainFields": ["browser", "module", "jsnext:main", "jsnext"],
"conditions": [],
"extensions": [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"],
"dedupe": ["react", "react-dom"],
"preserveSymlinks": false,
"alias": [
{
"find": {},
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/env.mjs"
},
{
"find": {},
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/client.mjs"
}
]
},
"configFile": "/Users/joseph/github/keycloakify-starter/vite.config.ts",
"configFileDependencies": ["/Users/joseph/github/keycloakify-starter/vite.config.ts"],
"inlineConfig": {
"optimizeDeps": {},
"build": {}
},
"root": "/Users/joseph/github/keycloakify-starter",
"base": "/",
"rawBase": "/",
"publicDir": "/Users/joseph/github/keycloakify-starter/public",
"cacheDir": "/Users/joseph/github/keycloakify-starter/node_modules/.vite",
"command": "build",
"mode": "production",
"ssr": {
"target": "node",
"optimizeDeps": {
"disabled": true,
"esbuildOptions": {
"preserveSymlinks": false
}
}
},
"isWorker": false,
"mainConfig": null,
"isProduction": true,
"css": {},
"server": {
"preTransformRequests": true,
"middlewareMode": false,
"fs": {
"strict": true,
"allow": ["/Users/joseph/github/keycloakify-starter"],
"deny": [".env", ".env.*", "*.{crt,pem}"],
"cachedChecks": false
}
},
"preview": {},
"envDir": "/Users/joseph/github/keycloakify-starter",
"env": {
"BASE_URL": "/",
"MODE": "production",
"DEV": false,
"PROD": true
},
"logger": {
"hasWarned": false
},
"packageCache": {},
"worker": {
"format": "iife",
"rollupOptions": {}
},
"appType": "spa",
"experimental": {
"importGlobRestoreExtension": false,
"hmrPartialAccept": false
}
}

View File

@ -1,28 +1,74 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import { readParsedPackageJson } from "../bin/keycloakify/buildOptions/parsedPackageJson";
import type { Plugin } from "vite";
import { assert } from "tsafe/assert";
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 { getKeycloakifyBuildDirPath } from "../bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath";
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
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(): Plugin {
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;
return {
"name": "keycloakify",
"apply": "build",
"configResolved": resolvedConfig => {
reactAppRootDirPath = resolvedConfig.root;
urlPathname = (() => {
let out = resolvedConfig.env.BASE_URL;
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.`,
`If this is a problem, please open an issue at https://github.com/keycloakify/keycloakify/issues/new`
].join("\n")
);
}
if (out === undefined) {
return undefined;
}
@ -40,26 +86,24 @@ export function keycloakify(): Plugin {
buildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": readParsedPackageJson({ reactAppRootDirPath }).keycloakify
?.keycloakifyBuildDirPath,
reactAppRootDirPath,
"bundler": "vite"
const { cacheDirPath } = getCacheDirPath({
reactAppRootDirPath
});
if (!fs.existsSync(keycloakifyBuildDirPath)) {
fs.mkdirSync(keycloakifyBuildDirPath);
if (!fs.existsSync(cacheDirPath)) {
fs.mkdirSync(cacheDirPath, { "recursive": true });
}
fs.writeFileSync(
pathJoin(keycloakifyBuildDirPath, resolvedViteConfigJsonBasename),
pathJoin(cacheDirPath, resolvedViteConfigJsonBasename),
Buffer.from(
JSON.stringify(
id<ResolvedViteConfig>({
"publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
"assetsDir": resolvedConfig.build.assetsDir,
"buildDir": resolvedConfig.build.outDir,
urlPathname
urlPathname,
userProvidedBuildOptions
}),
null,
2
@ -67,62 +111,82 @@ export function keycloakify(): Plugin {
"utf8"
)
);
await copyKeycloakResourcesToPublic({
"processArgv": ["--project", reactAppRootDirPath]
});
},
"transform": (code, id) => {
assert(reactAppRootDirPath !== undefined);
assert(command !== undefined);
assert(shouldGenerateSourcemap !== undefined);
let transformedCode: string | undefined = undefined;
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;
}
}
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;
}
}
const windowToken = isJavascriptFile ? "window" : "(window as any)";
if (transformedCode === undefined) {
transformedCode = code;
}
transformedCode = replaceAll(
transformedCode,
"import.meta.env.BASE_URL",
[
`(`,
`(${windowToken}.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development") ?`,
` "${urlPathname ?? "/"}" :`,
` \`\${${windowToken}.${nameOfTheGlobal}.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/\``,
`)`
].join("")
);
}
if (transformedCode === undefined) {
if (command !== "build") {
return;
}
assert(reactAppRootDirPath !== undefined);
{
const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
if (!isWithinSourceDirectory) {
return;
}
}
{
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()
};
},
"buildEnd": async () => {
"closeBundle": async () => {
assert(command !== undefined);
if (command !== "build") {
return;
}
assert(buildDirPath !== undefined);
await rm(pathJoin(buildDirPath, keycloak_resources), { "recursive": true, "force": true });
}
};
} satisfies Plugin;
return plugin as any;
}

View File

@ -361,40 +361,6 @@ describe("js replacer - webpack", () => {
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_webpack - 4", () => {
const jsCodeUntransformed = `d={NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST`;
const { fixedJsCode } = replaceImportsInJsCode_webpack({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/build/static",
"urlPathname": undefined
}
});
const fixedJsCodeExpected = `d={NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}",WDS_SOCKET_HOST`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_webpack - 5", () => {
const jsCodeUntransformed = `d={NODE_ENV:"production",PUBLIC_URL:"/foo-bar",WDS_SOCKET_HOST`;
const { fixedJsCode } = replaceImportsInJsCode_webpack({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/build/static",
"urlPathname": "/foo-bar/"
}
});
const fixedJsCodeExpected = `d={NODE_ENV:"production",PUBLIC_URL:window.${nameOfTheGlobal}.url.resourcesPath+"/${basenameOfTheKeycloakifyResourcesDir}",WDS_SOCKET_HOST`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
});
describe("css replacer", () => {

View File

@ -1,9 +1,16 @@
import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
import { downloadAndUnzip } from "keycloakify/bin/downloadAndUnzip";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "keycloakify/bin/tools/getThisCodebaseRootDirPath";
export async function setupSampleReactProject(destDirPath: string) {
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDirPath,
"doUseCache": false
"buildOptions": {
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
}
});
}

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"