Compare commits

..

78 Commits

Author SHA1 Message Date
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
037cd150de Release candidate 2024-02-08 14:10:10 +01:00
ae0b059217 Fix a couple of bug in donwnload-builtin-keycloak-theme 2024-02-08 14:09:55 +01:00
8255ce1158 Release candidate 2024-02-08 00:56:55 +01:00
5bf905723c Feature -p or --project option for ease of use in monorepo setups #449 2024-02-08 00:56:33 +01:00
3e336f4937 Release candidate 2024-02-08 00:12:50 +01:00
cd1cc37916 Explicitely exclude a number of node deps from Keycloak commons for bundle size 2024-02-08 00:12:10 +01:00
4ad7183d7e Prevent crashing when github is not up to date yet 2024-02-07 21:22:58 +01:00
e1b52e7439 Implement remote caching mechanism to prevent full download of the Keycloak sources 2024-02-07 21:17:09 +01:00
dca8c9f9d7 Merge main, fix runtime error in scripts, fix clean build 2024-02-07 20:01:26 +01:00
7e4eba6376 Update README, add poll 2024-02-07 10:29:46 +01:00
f642a56eaa Release candidate 2024-02-06 08:00:07 +01:00
c091089830 Apply #502 from main 2024-02-06 07:59:21 +01:00
18900d20e1 Update All contributors 2024-02-06 07:56:06 +01:00
6c622b1580 Remove TODO comment 2024-02-06 07:55:32 +01:00
4290cd23b2 Remove what seems to be dead code (not used in the starter nor in onyxia, besite, looks fishy) '//TODO: Write a test case for this' for ref and .chunk.css", 2024-02-06 07:29:48 +01:00
5076c1e93f Unit test passing 2024-02-06 07:28:03 +01:00
884b701fc6 Removing keycloak-resources dir from dist dir after build 2024-02-05 09:26:54 +01:00
73a8ec0295 Building version 2024-02-05 08:52:58 +01:00
a29b6097a4 Reintroduce doBuildRetrocompatAccountTheme (for now) 2024-02-04 10:25:48 +01:00
a9231e2ed8 Bump version 2024-02-04 06:31:10 +01:00
5f4669a7a6 Merge pull request #502 from giorgoslytos/fix/login-otp-radio-inputs 2024-02-03 10:53:58 +01:00
75c54df109 Fix some errors in base account v1 theme 2024-02-03 08:30:06 +01:00
2a07f7151d Merge pull request #503 from keycloakify/all-contributors/add-giorgoslytos
docs: add giorgoslytos as a contributor for code
2024-02-03 08:13:16 +01:00
b6ecff2dd3 docs: update .all-contributorsrc [skip ci] 2024-02-03 07:11:46 +00:00
83df27ec99 docs: update README.md [skip ci] 2024-02-03 07:11:45 +00:00
ca255985c0 fix: radio inputs on login-otp page 2024-02-02 14:35:14 +02:00
82f34c38f6 Bump version 2024-02-01 06:22:54 +01:00
694b4c8027 Reintroduce doBuildRetrocompatAccountTheme (for now) and fix multiple things in the account default theme 2024-02-01 06:22:33 +01:00
bd25621b2c Remove dead code 2024-01-31 21:56:46 +01:00
fde34be270 Compiling build 2024-01-30 07:10:53 +01:00
7c7ce159fe Complete build option 2024-01-30 06:55:26 +01:00
5a57bb59e5 Refactor 2024-01-30 06:38:26 +01:00
cd278f4ab5 Refactor 2024-01-30 06:37:49 +01:00
8b24e23721 refactor 2024-01-30 06:04:05 +01:00
22fa1411bf Moving on 2024-01-30 05:54:36 +01:00
2799a52d0c Implement router for js remplacer depending of the bundle (vite or webpack) 2024-01-30 01:24:44 +01:00
4c2e01a7a8 Done with implementation of webpack js replacer test case for support of non root base 2024-01-30 00:56:47 +01:00
7267d2ef38 Add a test case for the code that enable the import to work in webpack if base isn't / 2024-01-30 00:37:30 +01:00
1eb6b154f7 Rearenge test case 2024-01-30 00:15:08 +01:00
f55d61bf0b Rename test case file 2024-01-30 00:10:59 +01:00
5b350274bd Fundation 2024-01-30 00:06:17 +01:00
77 changed files with 2382 additions and 1418 deletions

View File

@ -43,6 +43,10 @@
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 👼
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.
@ -114,6 +118,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giorgoslytos"><img src="https://avatars.githubusercontent.com/u/50946162?v=4?s=100" width="100px;" alt="giorgoslytos"/><br /><sub><b>giorgoslytos</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=giorgoslytos" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@ -125,17 +130,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
## 9.2
Remove the `<your theme name>_retrocompat` option in the account theme dropdown and the `doBuildRetrocompatAccountTheme` build option.
`<your theme name>_retrocompat` was useless because `<rout theme name>` works fine even on older Keycloak version.
What you have to remember is that:
- If you have no account theme: You can use the `retrocompat_*.jar` it will work on any Keycloak version.
- If you have an Account theme: Use `retrocompat_*.jar` only if your Keycloak is in a version prior to 23.
Keycloak 22 is not supported (Only login themes works in Keycloak 22).
## 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.3.0-rc.0",
"version": "9.4.0-rc.15",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -14,7 +14,7 @@
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
"test": "echo 'yarn test:types && vitest run'",
"test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
@ -119,7 +119,6 @@
"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",

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,15 +19,22 @@ 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 });
fs.mkdirSync(tmpDirPath);
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
"cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
}
});
@ -68,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 => {
@ -88,6 +94,7 @@ async function main() {
Buffer.from(
[
generatedFileHeader,
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
@ -106,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

@ -1,10 +1,9 @@
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?: {
@ -27,7 +26,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);
@ -88,8 +93,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,5 +1,5 @@
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
import { nameOfTheGlobal } from "keycloakify/bin/constants";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
@ -7,5 +7,5 @@ export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [Kc
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

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": "#",

View File

@ -1,8 +1,12 @@
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 = "vite.json";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const accountV1 = "account-v1";
export const retrocompatPostfix = "_retrocompat";
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];

View File

@ -1,20 +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 { 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 reactAppRootDirPath = process.cwd();
export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
const { processArgv } = params;
const buildOptions = readBuildOptions({
reactAppRootDirPath,
"processArgv": process.argv.slice(2)
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({
@ -27,14 +51,13 @@ import * as fs from "fs";
}
})(),
themeType,
"themeDirPath": reservedDirPath,
"usedResources": undefined,
"themeDirPath": destDirPath,
buildOptions
});
}
fs.writeFileSync(
pathJoin(reservedDirPath, "README.txt"),
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
@ -44,7 +67,46 @@ import * as fs from "fs";
)
);
fs.writeFileSync(pathJoin(buildOptions.publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8"));
fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(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,16 +1,19 @@
#!/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 } from "./keycloakify/BuildOptions";
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -19,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 }) => {
@ -48,43 +50,188 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
});
}
install_and_move_to_common_resources_generated_in_keycloak_v2: {
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
remove_keycloak_v2: {
const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
if (!fs.existsSync(accountV2DirSrcDirPath)) {
break install_and_move_to_common_resources_generated_in_keycloak_v2;
if (!fs.existsSync(keycloakV2DirPath)) {
break remove_keycloak_v2;
}
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
rmSync(keycloakV2DirPath, { "recursive": true });
}
if (packageManager === "pnpm") {
try {
child_process.execSync(`which pnpm`);
} catch {
console.log(`Installing pnpm globally`);
child_process.execSync(`npm install -g pnpm`);
// Note, this is an optimization for reducing the size of the jar
remove_unused_node_modules: {
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
if (!fs.existsSync(nodeModuleDirPath)) {
break remove_unused_node_modules;
}
const toDeletePerfixes = [
"angular",
"bootstrap",
"rcue",
"font-awesome",
"ng-file-upload",
pathJoin("patternfly", "dist", "sass"),
pathJoin("patternfly", "dist", "less"),
pathJoin("patternfly", "dist", "js"),
"d3",
pathJoin("jquery", "src"),
"c3",
"core-js",
"eonasdan-bootstrap-datetimepicker",
"moment",
"react",
"patternfly-bootstrap-treeview",
"popper.js",
"tippy.js",
"jquery-match-height",
"google-code-prettify",
"patternfly-bootstrap-combobox",
"focus-trap",
"tabbable",
"scheduler",
"@types",
"datatables.net",
"datatables.net-colreorder",
"tslib",
"prop-types",
"file-selector",
"datatables.net-colreorder-bs",
"object-assign",
"warning",
"js-tokens",
"loose-envify",
"prop-types-extra",
"attr-accept",
"datatables.net-select",
"drmonty-datatables-colvis",
"datatables.net-bs",
pathJoin("@patternfly", "react"),
pathJoin("@patternfly", "patternfly", "docs")
];
transformCodebase({
"srcDirPath": nodeModuleDirPath,
"destDirPath": nodeModuleDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
return undefined;
}
if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
if (
!fileRelativePath.endsWith(".woff2") &&
!fileRelativePath.endsWith(".woff") &&
!fileRelativePath.endsWith(".ttf")
) {
return undefined;
}
}
return { "modifiedSourceCode": sourceCode };
}
});
}
// Just like node_modules
remove_unused_lib: {
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
if (!fs.existsSync(libDirPath)) {
break remove_unused_lib;
}
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace", "pficon"];
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
transformCodebase({
"srcDirPath": libDirPath,
"destDirPath": libDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
return undefined;
}
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
return { "modifiedSourceCode": sourceCode };
}
});
}
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
.replace(`${packageManager} run check-types`, "true")
.replace(`${packageManager} run babel`, "true");
last_account_v1_transformations: {
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
break last_account_v1_transformations;
}
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
{
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
fs.writeFileSync(
accountCssFilePath,
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
);
}
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
{
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
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 toKeepPrefixes = [
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
pathJoin("patternfly", "dist", "css", fileBasename)
),
pathJoin("patternfly", "dist", "fonts")
];
transformCodebase({
"srcDirPath": nodeModulesDirPath,
"destDirPath": nodeModulesDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
});
}
}
}
}
@ -93,7 +240,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
async function main() {
const buildOptions = readBuildOptions({
"reactAppRootDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});

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";
@ -9,13 +9,16 @@ import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getSrcDirPath";
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

@ -4,23 +4,21 @@ import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme"
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { readBuildOptions } from "./keycloakify/buildOptions";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getSrcDirPath";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import { rmSync } from "./tools/fs.rmSync";
export async function main() {
const reactAppRootDirPath = process.cwd();
const buildOptions = readBuildOptions({
reactAppRootDirPath,
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({
reactAppRootDirPath
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
@ -54,7 +52,7 @@ export async function main() {
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {

View File

@ -1,165 +0,0 @@
import { parse as urlParse } from "url";
import { getParsedPackageJson } from "./parsedPackageJson";
import { join as pathJoin } from "path";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import * as fs from "fs";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = {
isSilent: boolean;
themeVersion: string;
themeNames: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
doCreateJar: boolean;
loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
publicDirPath: string;
cacheDirPath: string;
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
};
export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {
const { reactAppRootDirPath, processArgv } = params;
const { isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv);
return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
};
})();
const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath });
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraThemeProperties, groupId, artifactId, doCreateJar, loginThemeResourcesFromKeycloakVersion } = keycloakify ?? {};
const themeNames = (() => {
if (keycloakify.themeName === undefined) {
return [
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-")
];
}
if (typeof keycloakify.themeName === "string") {
return [keycloakify.themeName];
}
return keycloakify.themeName;
})();
return {
reactAppRootDirPath,
themeNames,
"doCreateJar": doCreateJar ?? true,
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeNames[0]}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"loginThemeResourcesFromKeycloakVersion": loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
"publicDirPath": (() => {
let { PUBLIC_DIR_PATH } = process.env;
if (PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": PUBLIC_DIR_PATH,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "public");
})(),
"reactAppBuildDirPath": (() => {
const { reactAppBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
for (const name of ["build", "dist"]) {
const out = pathJoin(reactAppRootDirPath, name);
if (!fs.existsSync(out)) {
continue;
}
return out;
}
throw new Error("Please use the reactAppBuildDirPath option to specify the build directory of your react app");
})(),
"keycloakifyBuildDirPath": (() => {
const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build_keycloak");
})(),
"cacheDirPath": pathJoin(
(() => {
let { XDG_CACHE_HOME } = process.env;
if (XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
),
"urlPathname": (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (url === undefined) {
return undefined;
}
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
}

View File

@ -0,0 +1,185 @@
import { parse as urlParse } from "url";
import { readParsedPackageJson } from "./parsedPackageJson";
import { join as pathJoin } from "path";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { readResolvedViteConfig } from "./resolvedViteConfig";
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 = {
bundler: "vite" | "webpack";
isSilent: boolean;
themeVersion: string;
themeNames: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
doCreateJar: boolean;
loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
publicDirPath: string;
cacheDirPath: string;
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
assetsDirPath: string;
doBuildRetrocompatAccountTheme: boolean;
npmWorkspaceRootDirPath: string;
};
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
const { processArgv } = params;
const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
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 themeNames = (() => {
if (parsedPackageJson.keycloakify?.themeName === undefined) {
return [
parsedPackageJson.name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-")
];
}
if (typeof parsedPackageJson.keycloakify.themeName === "string") {
return [parsedPackageJson.keycloakify.themeName];
}
return parsedPackageJson.keycloakify.themeName;
})();
const reactAppBuildDirPath = (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build");
}
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,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
parsedPackageJson.keycloakify?.groupId ??
(parsedPackageJson.homepage === undefined
? fallbackGroupId
: urlParse(parsedPackageJson.homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.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",
reactAppRootDirPath,
reactAppBuildDirPath,
"keycloakifyBuildDirPath": (() => {
if (parsedPackageJson.keycloakify?.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`;
})(),
"publicDirPath": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
if (process.env.PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.PUBLIC_DIR_PATH,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "public");
}
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
})(),
cacheDirPath,
"urlPathname": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (url === undefined) {
return undefined;
}
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
}
return resolvedViteConfig.urlPathname;
})(),
"assetsDirPath": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
return pathJoin(reactAppBuildDirPath, "static");
}
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
})(),
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.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

@ -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

@ -0,0 +1 @@
export * from "./buildOptions";

View File

@ -2,7 +2,7 @@ import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { pathJoin } from "../tools/pathJoin";
import { join as pathJoin } from "path";
export type ParsedPackageJson = {
name: string;
@ -10,7 +10,6 @@ export type ParsedPackageJson = {
homepage?: string;
keycloakify?: {
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
@ -22,14 +21,13 @@ export type ParsedPackageJson = {
};
};
export const zParsedPackageJson = z.object({
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
@ -44,8 +42,8 @@ export const zParsedPackageJson = z.object({
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
export function getParsedPackageJson(params: { reactAppRootDirPath: string }) {
let parsedPackageJson: undefined | ParsedPackageJson;
export function readParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;

View File

@ -0,0 +1,71 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
};
const zResolvedViteConfig = z.object({
"buildDir": z.string(),
"publicDir": z.string(),
"assetsDir": z.string(),
"urlPathname": z.string().optional()
});
{
type Got = ReturnType<(typeof zResolvedViteConfig)["parse"]>;
type Expected = OptionalIfCanBeUndefined<ResolvedViteConfig>;
assert<Equals<Got, Expected>>();
}
export function readResolvedViteConfig(params: { cacheDirPath: string }): {
resolvedViteConfig: ResolvedViteConfig | undefined;
} {
const { cacheDirPath } = params;
const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
return { "resolvedViteConfig": undefined };
}
const resolvedViteConfig = (() => {
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
throw new Error("Missing Keycloakify Vite plugin output.");
}
let out: ResolvedViteConfig;
try {
out = JSON.parse(fs.readFileSync(resolvedViteConfigJsonFilePath).toString("utf8"));
} catch {
throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
}
try {
const zodParseReturn = zResolvedViteConfig.parse(out);
// So that objectKeys from tsafe return the expected result no matter what.
Object.keys(zodParseReturn)
.filter(key => !(key in out))
.forEach(key => {
delete (out as any)[key];
});
} catch {
throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
}
return out;
})();
return { resolvedViteConfig };
}

View File

@ -1 +0,0 @@
export const ftlValuesGlobalName = "kcContext";

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;
})()

View File

@ -1,18 +1,20 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import type { ThemeType } from "../../constants";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
themeVersion: string;
urlPathname: string | undefined;
reactAppBuildDirPath: string;
assetsDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -20,7 +22,6 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
@ -37,7 +38,7 @@ export function generateFtlFilesCodeFactory(params: {
assert(jsCode !== null);
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode, "bundler": "vite" });
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
$(element).text(fixedJsCode);
});
@ -70,7 +71,10 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
href.replace(
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
);
})
);
@ -101,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>",
@ -114,7 +119,7 @@ export function generateFtlFilesCodeFactory(params: {
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
` window.${nameOfTheGlobal}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]

View File

@ -1,132 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { type ThemeType, accountV1 } from "../../constants";
import { bringInAccountV1 } from "./bringInAccountV1";
export type BuildOptionsLike = {
groupId: string;
artifactId: string;
themeVersion: string;
cacheDirPath: string;
keycloakifyBuildDirPath: string;
themeNames: string[];
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export async function generateJavaStackFiles(params: {
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): Promise<{
jarFilePath: string;
}> {
const { implementedThemeTypes, buildOptions } = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${buildOptions.groupId}</groupId>`,
` <artifactId>${buildOptions.artifactId}</artifactId>`,
` <version>${buildOptions.themeVersion}</version>`,
` <name>${buildOptions.artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
` <build>`,
` <plugins>`,
` <plugin>`,
` <groupId>org.apache.maven.plugins</groupId>`,
` <artifactId>maven-shade-plugin</artifactId>`,
` <version>3.5.1</version>`,
` <executions>`,
` <execution>`,
` <phase>package</phase>`,
` <goals>`,
` <goal>shade</goal>`,
` </goals>`,
` </execution>`,
` </executions>`,
` </plugin>`,
` </plugins>`,
` </build>`,
` <dependencies>`,
` <dependency>`,
` <groupId>io.phasetwo.keycloak</groupId>`,
` <artifactId>keycloak-account-v1</artifactId>`,
` <version>0.1</version>`,
` </dependency>`,
` </dependencies>`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
if (implementedThemeTypes.account) {
await bringInAccountV1({ buildOptions });
}
{
const themeManifestFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [
...(!implementedThemeTypes.account
? []
: [
{
"name": accountV1,
"types": ["account"]
}
]),
...buildOptions.themeNames
.map(themeName => [
{
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}
])
.flat()
]
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`)
};
}

View File

@ -1 +0,0 @@
export * from "./generateJavaStackFiles";

View File

@ -0,0 +1,70 @@
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./buildOptions";
type BuildOptionsLike = {
groupId: string;
artifactId: string;
themeVersion: string;
keycloakifyBuildDirPath: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generatePom(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${buildOptions.groupId}</groupId>`,
` <artifactId>${buildOptions.artifactId}</artifactId>`,
` <version>${buildOptions.themeVersion}</version>`,
` <name>${buildOptions.artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
` <build>`,
` <plugins>`,
` <plugin>`,
` <groupId>org.apache.maven.plugins</groupId>`,
` <artifactId>maven-shade-plugin</artifactId>`,
` <version>3.5.1</version>`,
` <executions>`,
` <execution>`,
` <phase>package</phase>`,
` <goals>`,
` <goal>shade</goal>`,
` </goals>`,
` </execution>`,
` </executions>`,
` </plugin>`,
` </plugins>`,
` </build>`,
` <dependencies>`,
` <dependency>`,
` <groupId>io.phasetwo.keycloak</groupId>`,
` <artifactId>keycloak-account-v1</artifactId>`,
` <version>0.1</version>`,
` </dependency>`,
` </dependencies>`,
`</project>`
].join("\n");
return { pomFileCode };
})();
return { pomFileCode };
}

View File

@ -2,7 +2,7 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
import type { BuildOptions } from "./buildOptions";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
@ -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

@ -1,15 +1,17 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1 } from "../../constants";
import type { BuildOptions } from "../buildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
{
@ -29,37 +31,24 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1, "account");
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
"destDirPath": accountV1DirPath
});
const commonResourceFilePaths = [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css"
];
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
"destDirPath": pathJoin(accountV1DirPath, "resources")
});
for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
});
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
}
const resourceFilePaths = ["css/account.css"];
for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath);
}
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
@ -69,7 +58,15 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"",
"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",
"",
"styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources_common/${path}`)].join(" "),
"styles=" +
[
"css/account.css",
"img/icon-sidebar-active.png",
"img/logo.png",
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
)
].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",

View File

@ -1,57 +1,27 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { join as pathJoin } from "path";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { resources_common, type ThemeType } from "../../constants";
import { BuildOptions } from "../BuildOptions";
import { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import * as crypto from "crypto";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadKeycloakStaticResources(
// prettier-ignore
params: {
themeType: ThemeType;
themeDirPath: string;
keycloakVersion: string;
usedResources: {
resourcesCommonFilePaths: string[];
} | undefined;
buildOptions: BuildOptionsLike;
}
) {
export async function downloadKeycloakStaticResources(params: {
themeType: ThemeType;
themeDirPath: string;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
}) {
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
// NOTE: Hack for 427
const usedResources = (() => {
const { usedResources } = params;
if (usedResources === undefined) {
return undefined;
}
assert(usedResources !== undefined);
return {
"resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
{
const splitArg = "/dist/";
if (filePath.includes(splitArg)) {
return filePath.split(splitArg)[0] + splitArg;
}
}
return pathDirname(filePath);
})
};
})();
const tmpDirPath = pathJoin(
themeDirPath,
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
@ -72,18 +42,8 @@ export async function downloadKeycloakStaticResources(
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(resourcesPath, resources_common),
"transformSourceCode":
usedResources === undefined
? undefined
: ({ fileRelativePath, sourceCode }) => {
if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
"destDirPath": pathJoin(resourcesPath, resources_common)
});
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
rmSync(tmpDirPath, { "recursive": true, "force": true });
}

View File

@ -1,27 +1,39 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { join as pathJoin, basename as pathBasename, resolve as pathResolve, dirname as pathDirname } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, accountV1 } from "../../constants";
import {
type ThemeType,
lastKeycloakVersionWithAccountV1,
keycloak_resources,
retrocompatPostfix,
accountV1ThemeName,
basenameOfTheKeycloakifyResourcesDir
} from "../../constants";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
import { bringInAccountV1 } from "./bringInAccountV1";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined;
themeVersion: string;
loginThemeResourcesFromKeycloakVersion: string;
urlPathname: string | undefined;
keycloakifyBuildDirPath: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
themeNames: string[];
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -35,34 +47,60 @@ export async function generateTheme(params: {
}): Promise<void> {
const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType);
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email"; isRetrocompat?: true }) => {
const { themeType, isRetrocompat = false } = params;
return pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
`${themeName}${isRetrocompat ? retrocompatPostfix : ""}`,
themeType
);
};
let allCssGlobalsToDefine: Record<string, string> = {};
const cssGlobalsToDefine: Record<string, string> = {};
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
"login": false,
"account": false,
"email": false
};
for (const themeType of themeTypes) {
for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
implementedThemeTypes[themeType] = true;
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0;
apply_replacers_and_move_to_theme_resources: {
if (themeType === "account" && implementedThemeTypes.login) {
// NOTE: We prevend doing it twice, it has been done for the login theme.
if (!isFirstPass) {
break copy_app_resources_to_theme_path;
transformCodebase({
"srcDirPath": pathJoin(
getThemeTypeDirPath({
"themeType": "login"
}),
"resources",
basenameOfTheKeycloakifyResourcesDir
),
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir)
});
break apply_replacers_and_move_to_theme_resources;
}
transformCodebase({
"destDirPath": pathJoin(themeTypeDirPath, "resources", "build"),
"srcDirPath": buildOptions.reactAppBuildDirPath,
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
"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.
if (
isInside({
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
@ -73,28 +111,21 @@ export async function generateTheme(params: {
}
if (/\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
register_css_variables: {
if (!isFirstPass) {
break register_css_variables;
}
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
}
Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
cssGlobalsToDefine[key] = value;
});
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
const { fixedJsCode } = replaceImportsInJsCode({
"jsCode": sourceCode.toString("utf8"),
"bundler": "vite"
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
@ -105,22 +136,19 @@ export async function generateTheme(params: {
});
}
const generateFtlFilesCode =
generateFtlFilesCode_glob !== undefined
? generateFtlFilesCode_glob
: generateFtlFilesCodeFactory({
themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
}).generateFtlFilesCode;
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
cssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
});
[
...(() => {
@ -167,11 +195,6 @@ export async function generateTheme(params: {
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
"usedResources": readStaticResourcesUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
}),
buildOptions
});
@ -182,7 +205,7 @@ export async function generateTheme(params: {
`parent=${(() => {
switch (themeType) {
case "account":
return accountV1;
return accountV1ThemeName;
case "login":
return "keycloak";
}
@ -193,6 +216,25 @@ export async function generateTheme(params: {
"utf8"
)
);
if (themeType === "account" && buildOptions.doBuildRetrocompatAccountTheme) {
transformCodebase({
"srcDirPath": themeTypeDirPath,
"destDirPath": getThemeTypeDirPath({ themeType, "isRetrocompat": true }),
"transformSourceCode": ({ filePath, sourceCode }) => {
if (pathBasename(filePath) === "theme.properties") {
return {
"modifiedSourceCode": Buffer.from(
sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
)
};
}
return { "modifiedSourceCode": sourceCode };
}
});
}
}
email: {
@ -202,9 +244,82 @@ export async function generateTheme(params: {
break email;
}
implementedThemeTypes.email = true;
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
});
}
const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] };
buildOptions.themeNames.forEach(themeName =>
parsedKeycloakThemeJson.themes.push({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
})
);
account_specific_extra_work: {
if (!implementedThemeTypes.account) {
break account_specific_extra_work;
}
await bringInAccountV1({ buildOptions });
parsedKeycloakThemeJson.themes.push({
"name": accountV1ThemeName,
"types": ["account"]
});
add_retrocompat_account_theme: {
if (!buildOptions.doBuildRetrocompatAccountTheme) {
break add_retrocompat_account_theme;
}
transformCodebase({
"srcDirPath": getThemeTypeDirPath({ "themeType": "account" }),
"destDirPath": getThemeTypeDirPath({ "themeType": "account", "isRetrocompat": true }),
"transformSourceCode": ({ filePath, sourceCode }) => {
if (pathBasename(filePath) === "theme.properties") {
return {
"modifiedSourceCode": Buffer.from(
sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
)
};
}
return { "modifiedSourceCode": sourceCode };
}
});
buildOptions.themeNames.forEach(themeName =>
parsedKeycloakThemeJson.themes.push({
"name": `${themeName}${retrocompatPostfix}`,
"types": ["account"]
})
);
}
}
{
const keycloakThemeJsonFilePath = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"META-INF",
"keycloak-themes.json"
);
try {
fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath));
} catch {}
fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8"));
}
}

View File

@ -1,76 +0,0 @@
import { crawl } from "../../tools/crawl";
import { join as pathJoin, sep as pathSep } from "path";
import * as fs from "fs";
import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
resourcesCommonFilePaths: string[];
} {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const resourcesCommonFilePaths = new Set<string>();
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) {
continue;
}
const wrap = readPaths({ rawSourceFile });
wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
}
}
return {
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths)
};
}
/** Exported for testing purpose */
export function readPaths(params: { rawSourceFile: string }): {
resourcesCommonFilePaths: string[];
} {
const { rawSourceFile } = params;
const resourcesCommonFilePaths = new Set<string>();
{
const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g");
const matches = [...rawSourceFile.matchAll(regexp)];
for (const match of matches) {
const filePath = match[1];
resourcesCommonFilePaths.add(filePath);
}
}
{
const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
const matches = [...rawSourceFile.matchAll(regexp)];
for (const match of matches) {
const filePath = match[1];
resourcesCommonFilePaths.add(filePath);
}
}
const normalizePath = (filePath: string) => {
filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
filePath = filePath.replace(/\//g, pathSep);
return filePath;
};
return {
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath)
};
}

View File

@ -1,66 +1,42 @@
import { generateTheme } from "./generateTheme";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { generatePom } from "./generatePom";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, dirname as pathDirname, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { readBuildOptions } from "./buildOptions";
import { getLogger } from "../tools/logger";
import { assert } from "tsafe/assert";
import { getThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
import { objectKeys } from "tsafe/objectKeys";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
export async function main() {
const reactAppRootDirPath = process.cwd();
const buildOptions = readBuildOptions({
reactAppRootDirPath,
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const keycloakifyDirPath = getProjectRoot();
const { themeSrcDirPath } = getThemeSrcDirPath({ reactAppRootDirPath });
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
});
}
const { jarFilePath } = await generateJavaStackFiles({
"implementedThemeTypes": (() => {
const implementedThemeTypes = {
"login": false,
"account": false,
"email": false
};
{
const { pomFileCode } = generatePom({ buildOptions });
for (const themeType of objectKeys(implementedThemeTypes)) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
implementedThemeTypes[themeType] = true;
}
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
return implementedThemeTypes;
})(),
buildOptions
});
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
if (buildOptions.doCreateJar) {
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
@ -83,7 +59,7 @@ export async function main() {
);
}
const containerKeycloakVersion = "23.0.0";
const containerKeycloakVersion = "23.0.6";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
@ -97,47 +73,18 @@ export async function main() {
...(!buildOptions.doCreateJar
? []
: [
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
""
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(
buildOptions.reactAppRootDirPath,
jarFilePath
)} 🚀`
]),
//TODO: Restore when we find a good Helm chart for Keycloak.
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
"",
"value.yaml: ",
" extraInitContainers: |",
" - name: realm-ext-provider",
" image: curlimages/curl",
" imagePullPolicy: IfNotPresent",
" command:",
" - sh",
" args:",
" - -c",
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
" volumeMounts:",
" - name: extensions",
" mountPath: /extensions",
" ",
" extraVolumeMounts: |",
" - name: extensions",
" mountPath: /opt/keycloak/providers",
" extraEnv: |",
" - name: KEYCLOAK_USER",
" value: admin",
" - name: KEYCLOAK_PASSWORD",
" value: xxxxxxxxx",
" - name: JAVA_OPTS",
" value: -Dkeycloak.profile=preview",
"",
"",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ .${pathSep}${pathRelative(
reactAppRootDirPath,
buildOptions.reactAppRootDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`,
"",
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",

View File

@ -1,65 +0,0 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; bundler: "vite" | "webpack" }): { fixedJsCode: string } {
const { jsCode } = params;
const { fixedJsCode } = (() => {
switch (params.bundler) {
case "vite":
return replaceImportsFromStaticInJsCode_vite({ jsCode });
case "webpack":
return replaceImportsFromStaticInJsCode_webpack({ jsCode });
}
})();
return { fixedJsCode };
}
export function replaceImportsFromStaticInJsCode_vite(params: { jsCode: string }): { fixedJsCode: string } {
const { jsCode } = params;
const fixedJsCode = jsCode.replace(
/\.viteFileDeps = \[(.*)\]/g,
(...args) => `.viteFileDeps = [${args[1]}].map(viteFileDep => window.kcContext.url.resourcesPath.substring(1) + "/build/" + viteFileDep)`
);
return { fixedJsCode };
}
export function replaceImportsFromStaticInJsCode_webpack(params: { jsCode: string }): { fixedJsCode: string } {
const { jsCode } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
return `
${n}[(function(){
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function() {}
});
}
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
.replace(/\s+/g, " ")
.trim();
}
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
//TODO: Write a test case for this
.replace(
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
(...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
);
return { fixedJsCode };
}

View File

@ -1,6 +1,7 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@ -45,7 +46,7 @@ export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Rec
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/"
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
].join(" ")
)

View File

@ -1,5 +1,6 @@
import type { BuildOptions } from "../BuildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@ -16,7 +17,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => `url(\${url.resourcesPath}/build/${group})`
(...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };

View File

@ -0,0 +1 @@
export * from "./replaceImportsInJsCode";

View File

@ -0,0 +1,66 @@
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../buildOptions";
import { replaceImportsInJsCode_vite } from "./vite";
import { replaceImportsInJsCode_webpack } from "./webpack";
import * as fs from "fs";
export type BuildOptionsLike = {
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
bundler: "vite" | "webpack";
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }) {
const { jsCode, buildOptions } = params;
const { fixedJsCode } = (() => {
switch (buildOptions.bundler) {
case "vite":
return replaceImportsInJsCode_vite({
jsCode,
buildOptions,
"basenameOfAssetsFiles": readAssetsDirSync({
"assetsDirPath": params.buildOptions.assetsDirPath
})
});
case "webpack":
return replaceImportsInJsCode_webpack({
jsCode,
buildOptions
});
}
})();
return { fixedJsCode };
}
const { readAssetsDirSync } = (() => {
let cache:
| {
assetsDirPath: string;
basenameOfAssetsFiles: string[];
}
| undefined = undefined;
function readAssetsDirSync(params: { assetsDirPath: string }): string[] {
const { assetsDirPath } = params;
if (cache !== undefined && cache.assetsDirPath === assetsDirPath) {
return cache.basenameOfAssetsFiles;
}
const basenameOfAssetsFiles = fs.readdirSync(assetsDirPath);
cache = {
assetsDirPath,
basenameOfAssetsFiles
};
return basenameOfAssetsFiles;
}
return { readAssetsDirSync };
})();

View File

@ -0,0 +1,85 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../buildOptions";
import * as nodePath from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
export type BuildOptionsLike = {
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode_vite(params: {
jsCode: string;
buildOptions: BuildOptionsLike;
basenameOfAssetsFiles: string[];
systemType?: "posix" | "win32";
}): {
fixedJsCode: string;
} {
const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
let fixedJsCode = jsCode;
replace_base_javacript_import: {
if (buildOptions.urlPathname === undefined) {
break replace_base_javacript_import;
}
// Optimization
if (!jsCode.includes(buildOptions.urlPathname)) {
break replace_base_javacript_import;
}
// Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
fixedJsCode = fixedJsCode.replace(
new RegExp(
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
"g"
),
(...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
);
}
replace_javascript_relatives_import_paths: {
// Example: "assets/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
}
return out;
})();
// Optimization
if (!jsCode.includes(staticDir)) {
break replace_javascript_relatives_import_paths;
}
basenameOfAssetsFiles
.map(basenameOfAssetsFile => `${staticDir}${basenameOfAssetsFile}`)
.forEach(relativePathOfAssetFile => {
fixedJsCode = replaceAll(
fixedJsCode,
`"${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
);
fixedJsCode = replaceAll(
fixedJsCode,
`"${buildOptions.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
);
});
}
return { fixedJsCode };
}

View File

@ -0,0 +1,76 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../buildOptions";
import * as nodePath from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
export type BuildOptionsLike = {
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike; systemType?: "posix" | "win32" }): {
fixedJsCode: string;
} {
const { jsCode, buildOptions, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
let fixedJsCode = jsCode;
if (buildOptions.urlPathname !== undefined) {
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
fixedJsCode = fixedJsCode.replace(
new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
(...[, assignTo]) => `,${assignTo}="/",`
);
}
// Example: "static/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
}
return out;
})();
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
return `
${n}[(function(){
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", {
get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; },
set: function() {}
});
}
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
.replace(/\s+/g, " ")
.trim();
}
];
fixedJsCode = fixedJsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(
new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g"),
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
);
return { fixedJsCode };
}

View File

@ -1,6 +1,7 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
export async function promptKeycloakVersion() {
const { getLatestsSemVersionedTag } = (() => {
@ -22,10 +23,10 @@ export async function promptKeycloakVersion() {
const tags = [
...(await getLatestsSemVersionedTag({
"count": 10,
"doIgnoreBeta": true,
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
lastKeycloakVersionWithAccountV1,
"11.0.3"
];

View File

@ -1,73 +0,0 @@
export type NpmModuleVersion = {
major: number;
minor: number;
patch: number;
betaPreRelease?: number;
};
export namespace NpmModuleVersion {
export function parse(versionStr: string): NpmModuleVersion {
const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
if (!match) {
throw new Error(`${versionStr} is not a valid NPM version`);
}
return {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": parseInt(match[3]),
...(() => {
const str = match[4];
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
})()
};
}
export function stringify(v: NpmModuleVersion) {
return `${v.major}.${v.minor}.${v.patch}${v.betaPreRelease === undefined ? "" : `-beta.${v.betaPreRelease}`}`;
}
/**
*
* v1 < v2 => -1
* v1 === v2 => 0
* v1 > v2 => 1
*
*/
export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
const noUndefined = (n: number | undefined) => n ?? Infinity;
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
}
}
return 0;
}
/*
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0-beta.4")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("4.0.0")) === -1 )
*/
export function bumpType(params: { versionBehindStr: string; versionAheadStr: string }): "major" | "minor" | "patch" | "betaPreRelease" | "same" {
const versionAhead = parse(params.versionAheadStr);
const versionBehind = parse(params.versionBehindStr);
if (compare(versionBehind, versionAhead) === 1) {
throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
}
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (versionBehind[level] !== versionAhead[level]) {
return level;
}
}
return "same";
}
}

View File

@ -0,0 +1,12 @@
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
[Key in keyof T]: undefined extends T[Key] ? Key : never;
}[keyof T];
/**
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
* is
* { p1?: string | undefined; p2: string }
*/
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };

99
src/bin/tools/SemVer.ts Normal file
View File

@ -0,0 +1,99 @@
export type SemVer = {
major: number;
minor: number;
patch: number;
rc?: number;
parsedFrom: string;
};
export namespace SemVer {
const bumpTypes = ["major", "minor", "patch", "rc", "no bump"] as const;
export type BumpType = (typeof bumpTypes)[number];
export function parse(versionStr: string): SemVer {
const match = versionStr.match(/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/);
if (!match) {
throw new Error(`${versionStr} is not a valid semantic version`);
}
const semVer: Omit<SemVer, "parsedFrom"> = {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": (() => {
const str = match[3];
return str === undefined ? 0 : parseInt(str);
})(),
...(() => {
const str = match[4];
return str === undefined ? {} : { "rc": parseInt(str) };
})()
};
const initialStr = stringify(semVer);
Object.defineProperty(semVer, "parsedFrom", {
"enumerable": true,
"get": function () {
const currentStr = stringify(this);
if (currentStr !== initialStr) {
throw new Error(`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`);
}
return versionStr;
}
});
return semVer as any;
}
export function stringify(v: Omit<SemVer, "parsedFrom">): string {
return `${v.major}.${v.minor}.${v.patch}${v.rc === undefined ? "" : `-rc.${v.rc}`}`;
}
/**
*
* v1 < v2 => -1
* v1 === v2 => 0
* v1 > v2 => 1
*
*/
export function compare(v1: SemVer, v2: SemVer): -1 | 0 | 1 {
const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
const noUndefined = (n: number | undefined) => n ?? Infinity;
for (const level of ["major", "minor", "patch", "rc"] as const) {
if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
}
}
return 0;
}
/*
console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0")) === -1 )
console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0-rc.4")) === -1 )
console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 )
*/
export function bumpType(params: { versionBehind: string | SemVer; versionAhead: string | SemVer }): BumpType | "no bump" {
const versionAhead = typeof params.versionAhead === "string" ? parse(params.versionAhead) : params.versionAhead;
const versionBehind = typeof params.versionBehind === "string" ? parse(params.versionBehind) : params.versionBehind;
if (compare(versionBehind, versionAhead) === 1) {
throw new Error(`Version regression ${stringify(versionBehind)} -> ${stringify(versionAhead)}`);
}
for (const level of ["major", "minor", "patch", "rc"] as const) {
if (versionBehind[level] !== versionAhead[level]) {
return level;
}
}
return "no bump";
}
}

View File

@ -0,0 +1,30 @@
export function replaceAll(string: string, searchValue: string | RegExp, replaceValue: string): string {
if ((string as any).replaceAll !== undefined) {
return (string as any).replaceAll(searchValue, replaceValue);
}
// If the searchValue is a string
if (typeof searchValue === "string") {
// Escape special characters in the string to be used in a regex
var escapedSearchValue = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
var regex = new RegExp(escapedSearchValue, "g");
return string.replace(regex, replaceValue);
}
// If the searchValue is a global RegExp, use it directly
if (searchValue instanceof RegExp && searchValue.global) {
return string.replace(searchValue, replaceValue);
}
// If the searchValue is a non-global RegExp, throw an error
if (searchValue instanceof RegExp) {
throw new TypeError("replaceAll must be called with a global RegExp");
}
// Convert searchValue to string if it's not a string or RegExp
var searchString = String(searchValue);
var regexFromString = new RegExp(searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
return string.replace(regexFromString, replaceValue);
}

View File

@ -1,17 +1,17 @@
import * as fs from "fs";
import * as path from "path";
import { join as pathJoin, relative as pathRelative } from "path";
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
const crawlRec = (dirPath: string, filePaths: string[]) => {
for (const basename of fs.readdirSync(dirPath)) {
const fileOrDirPath = pathJoin(dirPath, basename);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
if (fs.lstatSync(fileOrDirPath).isDirectory()) {
crawlRec(fileOrDirPath, filePaths);
continue;
}
paths.push(file_path);
filePaths.push(fileOrDirPath);
}
};
@ -27,6 +27,6 @@ export function crawl(params: { dirPath: string; returnedPathsType: "absolute" |
case "absolute":
return filePaths;
case "relative to dirPath":
return filePaths.map(filePath => path.relative(dirPath, filePath));
return filePaths.map(filePath => pathRelative(dirPath, filePath));
}
}

View File

@ -1,232 +0,0 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import { promisify } from "util";
import { transformCodebase } from "./transformCodebase";
import { unzip, zip } from "./unzip";
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}`);
if (!(await exists(zipFilePath))) {
const opts = await getFetchOptions();
const response = await fetch(url, opts);
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 (specificDirsToExtract !== undefined || preCacheTransform !== undefined) {
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
}
}
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;
}
}

43
src/bin/tools/fs.rm.ts Normal file
View File

@ -0,0 +1,43 @@
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { SemVer } from "./SemVer";
/**
* Polyfill of fs.rm(dirPath, { "recursive": true })
* For older version of Node
*/
export async function rm(dirPath: string, options: { recursive: true; force?: true }) {
if (SemVer.compare(SemVer.parse(process.version), SemVer.parse("14.14.0")) > 0) {
return fs.rm(dirPath, options);
}
const { force = true } = options;
if (force && !(await checkDirExists(dirPath))) {
return;
}
const removeDir_rec = async (dirPath: string) =>
Promise.all(
(await fs.readdir(dirPath)).map(async basename => {
const fileOrDirpath = pathJoin(dirPath, basename);
if ((await fs.lstat(fileOrDirpath)).isDirectory()) {
await removeDir_rec(fileOrDirpath);
} else {
await fs.unlink(fileOrDirpath);
}
})
);
await removeDir_rec(dirPath);
}
async function checkDirExists(dirPath: string) {
try {
await fs.access(dirPath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}

View File

@ -0,0 +1,34 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { SemVer } from "./SemVer";
/**
* Polyfill of fs.rmSync(dirPath, { "recursive": true })
* For older version of Node
*/
export function rmSync(dirPath: string, options: { recursive: true; force?: true }) {
if (SemVer.compare(SemVer.parse(process.version), SemVer.parse("14.14.0")) > 0) {
fs.rmSync(dirPath, options);
return;
}
const { force = true } = options;
if (force && !fs.existsSync(dirPath)) {
return;
}
const removeDir_rec = (dirPath: string) =>
fs.readdirSync(dirPath).forEach(basename => {
const fileOrDirpath = pathJoin(dirPath, basename);
if (fs.lstatSync(fileOrDirpath).isDirectory()) {
removeDir_rec(fileOrDirpath);
return;
} else {
fs.unlinkSync(fileOrDirpath);
}
});
removeDir_rec(dirPath);
}

View File

@ -7,6 +7,8 @@ export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: str
pathOut = pathOut.replace(/\//g, pathSep);
pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut;
if (!pathIsAbsolute(pathOut)) {
pathOut = pathJoin(cwd, pathOut);
}

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,39 +1,39 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
import { NpmModuleVersion } from "../NpmModuleVersion";
import { SemVer } from "../SemVer";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; doIgnoreBeta: boolean; count: number }): Promise<
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; count: number }): Promise<
{
tag: string;
version: NpmModuleVersion;
version: SemVer;
}[]
> {
const { owner, repo, doIgnoreBeta, count } = params;
const { owner, repo, count } = params;
const semVersionedTags: { tag: string; version: NpmModuleVersion }[] = [];
const semVersionedTags: { tag: string; version: SemVer }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
let version: NpmModuleVersion;
let version: SemVer;
try {
version = NpmModuleVersion.parse(tag.replace(/^[vV]?/, ""));
version = SemVer.parse(tag.replace(/^[vV]?/, ""));
} catch {
continue;
}
if (doIgnoreBeta && version.betaPreRelease !== undefined) {
if (version.rc !== undefined) {
continue;
}
semVersionedTags.push({ tag, version });
}
return semVersionedTags.sort(({ version: vX }, { version: vY }) => NpmModuleVersion.compare(vY, vX)).slice(0, count);
return semVersionedTags.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX)).slice(0, count);
}
return { getLatestsSemVersionedTag };

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;
}

View File

@ -1,7 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
import { id } from "tsafe/id";
import { rmSync } from "../tools/fs.rmSync";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
| {
@ -10,18 +10,37 @@ type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; file
}
| undefined;
/** Apply a transformation function to every file of directory */
/**
* Apply a transformation function to every file of directory
* If source and destination are the same this function can be used to apply the transformation in place
* like filtering out some files or modifying them.
* */
export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) {
const {
srcDirPath,
destDirPath,
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
"modifiedSourceCode": sourceCode
}))
} = params;
const { srcDirPath, transformSourceCode } = params;
const isTargetSameAsSource = path.relative(srcDirPath, params.destDirPath) === "";
const destDirPath = isTargetSameAsSource ? path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs") : params.destDirPath;
fs.mkdirSync(destDirPath, {
"recursive": true
});
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, fileRelativePath);
const destFilePath = path.join(destDirPath, fileRelativePath);
// NOTE: Optimization, if we don't need to transform the file, just copy
// it using the lower level implementation.
if (transformSourceCode === undefined) {
fs.mkdirSync(path.dirname(destFilePath), {
"recursive": true
});
fs.copyFileSync(filePath, destFilePath);
continue;
}
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
@ -33,15 +52,18 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
continue;
}
fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
fs.mkdirSync(path.dirname(destFilePath), {
"recursive": true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(
path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
modifiedSourceCode
);
fs.writeFileSync(path.join(path.dirname(destFilePath), newFileName ?? path.basename(destFilePath)), modifiedSourceCode);
}
if (isTargetSameAsSource) {
rmSync(srcDirPath, { "recursive": true });
fs.renameSync(destDirPath, srcDirPath);
}
}

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");
})();

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

@ -0,0 +1,3 @@
import { BASE_URL } from "./BASE_URL";
export const isStorybook = BASE_URL.startsWith(".");

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

@ -2,14 +2,13 @@ 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>>[];
@ -31,7 +30,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);
@ -147,8 +152,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,11 +1,11 @@
import type { KcContext } from "./KcContext";
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
import { nameOfTheGlobal } from "keycloakify/bin/constants";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

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"
},

View File

@ -94,7 +94,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
//css classes for the OTP Login Form
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select",
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select col-xs-12",
"kcSelectOTPListItemClass": "card-pf-body card-pf-top-element",
"kcAuthenticatorOtpCircleClass": "fa fa-mobile card-pf-icon-circle",
"kcSelectOTPItemHeadingClass": "card-pf-title text-center",

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
@ -18,105 +16,77 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { msg, msgStr } = i18n;
useEffect(() => {
let isCleanedUp = false;
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
});
(async () => {
await prLoaded;
if (isCleanedUp) {
return;
}
evaluateInlineScript();
})();
return () => {
isCleanedUp = true;
remove();
};
}, []);
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("doLogIn")}>
<form id="kc-otp-login-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && (
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcInputWrapperClass")}>
{otpLogin.userOtpCredentials.map(otpCredential => (
<div key={otpCredential.id} className={getClassName("kcSelectOTPListClass")}>
<input type="hidden" value="${otpCredential.id}" />
<div className={getClassName("kcSelectOTPListItemClass")}>
<span className={getClassName("kcAuthenticatorOtpCircleClass")} />
<h2 className={getClassName("kcSelectOTPItemHeadingClass")}>{otpCredential.userLabel}</h2>
<>
<style>
{`
input[type="radio"]:checked~label.kcSelectOTPListClass{
border: 2px solid #39a5dc;
}`}
</style>
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("doLogIn")}>
<form id="kc-otp-login-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && (
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcInputWrapperClass")}>
{otpLogin.userOtpCredentials.map((otpCredential, index) => (
<div key={otpCredential.id}>
<input
id={`kc-otp-credential-${index}`}
name="selectedCredentialId"
type="radio"
value={otpCredential.id}
style={{ display: "none" }}
/>
<label
htmlFor={`kc-otp-credential-${index}`}
key={otpCredential.id}
className={getClassName("kcSelectOTPListClass")}
>
<div className={getClassName("kcSelectOTPListItemClass")}>
<span className={getClassName("kcAuthenticatorOtpCircleClass")} />
<h2 className={getClassName("kcSelectOTPItemHeadingClass")}>{otpCredential.userLabel}</h2>
</div>
</label>
</div>
</div>
))}
))}
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="otp" className={getClassName("kcLabelClass")}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input id="otp" name="otp" autoComplete="off" type="text" className={getClassName("kcInputClass")} autoFocus />
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="otp" className={getClassName("kcLabelClass")}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input id="otp" name="otp" autoComplete="off" type="text" className={getClassName("kcInputClass")} autoFocus />
</div>
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")} />
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")} />
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
</form>
</Template>
</form>
</Template>
</>
);
}
declare const $: any;
function evaluateInlineScript() {
$(document).ready(function () {
// Card Single Select
$(".card-pf-view-single-select").click(function (this: any) {
if ($(this).hasClass("active")) {
$(this).removeClass("active");
$(this).children().removeAttr("name");
} else {
$(".card-pf-view-single-select").removeClass("active");
$(".card-pf-view-single-select").children().removeAttr("name");
$(this).addClass("active");
$(this).children().attr("name", "selectedCredentialId");
}
});
var defaultCred = $(".card-pf-view-single-select")[0];
if (defaultCred) {
defaultCred.click();
}
});
}

View File

@ -2,11 +2,17 @@
"extends": "../../tsproject.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES5",
"target": "ES2019",
"esModuleInterop": true,
"lib": ["es2015", "ES2019.Object"],
"lib": ["es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"],
"outDir": "../../dist/vite-plugin",
"rootDir": ".",
// https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
"skipLibCheck": true
}
},
"references": [
{
"path": "../bin"
}
]
}

View File

@ -1,31 +1,151 @@
// index.ts
import type { Plugin, ResolvedConfig } from "vite";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import type { Plugin } from "vite";
import * as fs from "fs";
import { resolvedViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, keycloak_resources } from "../bin/constants";
import type { ResolvedViteConfig } from "../bin/keycloakify/buildOptions/resolvedViteConfig";
import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
import { copyKeycloakResourcesToPublic } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
console.log("Hello world!");
export function keycloakify() {
let reactAppRootDirPath: string | undefined = undefined;
let urlPathname: string | undefined = undefined;
let buildDirPath: string | undefined = undefined;
let command: "build" | "serve" | undefined = undefined;
export function keycloakify(): Plugin {
let config: ResolvedConfig;
const plugin = {
"name": "keycloakify" as const,
"configResolved": async resolvedConfig => {
command = resolvedConfig.command;
return {
"name": "keycloakify",
reactAppRootDirPath = resolvedConfig.root;
urlPathname = (() => {
let out = resolvedConfig.env.BASE_URL;
"configResolved": resolvedConfig => {
// Store the resolved config
config = resolvedConfig;
if (out.startsWith(".") && command === "build") {
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")
);
}
console.log("========> configResolved", config);
if (out === undefined) {
return undefined;
}
fs.writeFileSync("/Users/joseph/github/keycloakify-starter/log.txt", Buffer.from("Hello World", "utf8"));
if (!out.startsWith("/")) {
out = "/" + out;
}
if (!out.endsWith("/")) {
out += "/";
}
return out;
})();
buildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
const { cacheDirPath } = getCacheDirPath({
reactAppRootDirPath
});
if (!fs.existsSync(cacheDirPath)) {
fs.mkdirSync(cacheDirPath, { "recursive": true });
}
fs.writeFileSync(
pathJoin(cacheDirPath, resolvedViteConfigJsonBasename),
Buffer.from(
JSON.stringify(
id<ResolvedViteConfig>({
"publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
"assetsDir": resolvedConfig.build.assetsDir,
"buildDir": resolvedConfig.build.outDir,
urlPathname
}),
null,
2
),
"utf8"
)
);
await copyKeycloakResourcesToPublic({
"processArgv": ["--project", reactAppRootDirPath]
});
},
"transform": (code, id) => {
assert(command !== undefined);
"buildStart": () => {
console.log("Public Directory:", config.publicDir); // Path to the public directory
console.log("Dist Directory:", config.build.outDir); // Path to the dist directory
console.log("Assets Directory:", config.build.assetsDir); // Path to the assets directory within outDir
if (command !== "build") {
return;
}
assert(reactAppRootDirPath !== 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;
}
}
if (transformedCode === undefined) {
transformedCode = code;
}
transformedCode = replaceAll(
transformedCode,
"import.meta.env.BASE_URL",
[
`(`,
`(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`,
`"${urlPathname ?? "/"}":`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
`)`
].join("")
);
}
if (transformedCode === undefined) {
return;
}
return {
"code": transformedCode
};
},
"closeBundle": async () => {
assert(command !== undefined);
if (command !== "build") {
return;
}
assert(buildDirPath !== undefined);
await rm(pathJoin(buildDirPath, keycloak_resources), { "recursive": true, "force": true });
}
} satisfies Plugin;
// ... other hooks
};
return plugin as any;
}

View File

@ -1,104 +0,0 @@
import { readPaths } from "keycloakify/bin/keycloakify/generateTheme/readStaticResourcesUsage";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest";
describe("Ensure it's able to extract used Keycloak resources", () => {
const expectedPaths = {
"resourcesCommonFilePaths": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css",
"node_modules/jquery/dist/jquery.min.js"
]
};
it("works with coding style n°1", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
\`\${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css\`,
\`\${
url.resourcesCommonPath
}/node_modules/patternfly/dist/css/patternfly-additions.min.css\`,
\`\${resourcesCommonPath }/lib/zocial/zocial.css\`,
\`\${url.resourcesPath}/css/login.css\`
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": \`\${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js\`
});
`
});
expect(same(paths, expectedPaths)).toBe(true);
});
it("works with coding style n°2", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
url.resourcesCommonPath + "/node_modules/patternfly/dist/css/patternfly.min.css",
url.resourcesCommonPath + '/node_modules/patternfly/dist/css/patternfly-additions.min.css',
url.resourcesCommonPath
+ "/lib/zocial/zocial.css",
url.resourcesPath +
'/css/login.css'
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": kcContext.url.resourcesCommonPath + "/node_modules/jquery/dist/jquery.min.js\"
});
`
});
expect(same(paths, expectedPaths)).toBe(true);
});
it("works with coding style n°3", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
path.join(resourcesCommonPath,"/node_modules/patternfly/dist/css/patternfly.min.css"),
path.join(url.resourcesCommonPath, '/node_modules/patternfly/dist/css/patternfly-additions.min.css'),
path.join(url.resourcesCommonPath,
"/lib/zocial/zocial.css"),
pathJoin(
url.resourcesPath,
'css/login.css'
)
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": path.join(kcContext.url.resourcesCommonPath, "/node_modules/jquery/dist/jquery.min.js")
});
`
});
expect(same(paths, expectedPaths)).toBe(true);
});
});

View File

@ -1,15 +1,57 @@
import { replaceImportsFromStaticInJsCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite";
import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack";
import { generateCssCodeToDefineGlobals, replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest";
import { isSameCode } from "../tools/isSameCode";
import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal } from "keycloakify/bin/constants";
describe("bin/js-transforms", () => {
// Vite
{
describe("js replacer - vite", () => {
it("replaceImportsInJsCode_vite - 1", () => {
const before = `Uv="modulepreload",`;
const after = `,Wc={},`;
const jsCodeUntransformed = `${before}Hv=function(e){return"/foo-bar-baz/"+e}${after}`;
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": [],
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
"urlPathname": "/foo-bar-baz/"
}
});
const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_vite - 2", () => {
const before = `Uv="modulepreload",`;
const after = `,Wc={},`;
const jsCodeUntransformed = `${before}Hv=function(e){return"/foo/bar/baz/"+e}${after}`;
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": [],
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
"urlPathname": "/foo/bar/baz/"
}
});
const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_vite - 3", () => {
const jsCodeUntransformed = `
S="/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
@ -17,64 +59,198 @@ describe("bin/js-transforms", () => {
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
it("Correctly replace import path in Vite dist/static/xxx.js files", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"bundler": "vite"
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": undefined
},
systemType
});
const fixedJsCodeExpected = `
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"].map(viteFileDep => window.kcContext.url.resourcesPath.substring(1) + "/build/" + viteFileDep)
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
}
}
});
// Webpack
{
it("replaceImportsInJsCode_vite - 4", () => {
const jsCodeUntransformed = `
function f() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
S="/foo/bar/keycloakify-logo-mqjydaoZ.png",H=(()=>{
__webpack_require__.u=function(e){return"static/js/" + e + "." + {
147: "6c5cee76",
787: "8da10fcf",
922: "be170a73"
} [e] + ".chunk.js"
}
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["foo/bar/Login-dJpPRzM4.js", "foo/bar/index-XwzrZ5Gu.js"]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
t.miniCssF=function(e){return"static/css/"+e+"."+{
164:"dcfd7749",
908:"67c9ed2c"
}[e]+".chunk.css"
}
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
it("Correctly replace import path in Webpack build/static/js/xxx.js files", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/foo/bar"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\foo\\bar"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"bundler": "webpack"
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": undefined
},
systemType
});
const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/index-XwzrZ5Gu.js")
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}
});
it("replaceImportsInJsCode_vite - 5", () => {
const jsCodeUntransformed = `
S="/foo-bar-baz/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": "/foo-bar-baz/"
},
systemType
});
const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}
});
});
describe("js replacer - webpack", () => {
it("replaceImportsInJsCode_webpack - 1", () => {
const jsCodeUntransformed = `
function f() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
__webpack_require__.u=function(e){return"static/js/" + e + "." + {
147: "6c5cee76",
787: "8da10fcf",
922: "be170a73"
} [e] + ".chunk.js"
}
t.miniCssF=function(e){return"static/css/"+e+"."+{
164:"dcfd7749",
908:"67c9ed2c"
}[e]+".chunk.css"
}
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
const { fixedJsCode } = replaceImportsInJsCode_webpack({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/build/static",
"urlPathname": undefined
}
});
const fixedJsCodeExpected = `
function f() {
return window.kcContext.url.resourcesPath + "/build/static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
@ -143,12 +319,51 @@ describe("bin/js-transforms", () => {
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_webpack - 2", () => {
const before = `"__esModule",{value:!0})}`;
const after = `function(){if("undefined"`;
const jsCodeUntransformed = `${before},n.p="/foo-bar/",${after}`;
const { fixedJsCode } = replaceImportsInJsCode_webpack({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/build/static",
"urlPathname": "/foo-bar/"
}
});
}
const fixedJsCodeExpected = `${before},n.p="/",${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_webpack - 3", () => {
const before = `"__esModule",{value:!0})}`;
const after = `function(){if("undefined"`;
const jsCodeUntransformed = `${before},n.p="/foo/bar/",${after}`;
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 = `${before},n.p="/",${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
});
describe("bin/css-transforms", () => {
describe("css replacer", () => {
it("transforms absolute urls to css globals properly with no urlPathname", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
"cssCode": `
@ -263,7 +478,7 @@ describe("bin/css-transforms", () => {
});
});
describe("bin/css-inline-transforms", () => {
describe("inline css replacer", () => {
describe("no url pathName", () => {
const cssCode = `
@font-face {

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

@ -4,20 +4,17 @@
"target": "es5",
"lib": ["es2015", "DOM", "ES2019.Object"],
"esModuleInterop": true,
"declaration": true,
"outDir": "../dist_test",
"sourceMap": true,
"newLine": "LF",
"noUnusedLocals": true,
"noUnusedParameters": true,
"incremental": false,
"strict": true,
"downlevelIteration": true,
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true,
"paths": {
"keycloakify/*": ["../src/*"]
}
},
// https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
"skipLibCheck": true
},
"include": ["../src", "."]
}

View File

@ -1,11 +1,10 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
import path from "path";
import { defineConfig } from "vitest/config";
import { resolve as pathResolve } from "path";
export default defineConfig({
"test": {
"alias": {
"keycloakify": path.resolve(__dirname, "./src")
"keycloakify": pathResolve(__dirname, "./src")
},
"watchExclude": ["**/node_modules/**", "**/dist/**", "**/sample_react_project/**"]
}