Compare commits

..

53 Commits

Author SHA1 Message Date
5c5dce1422 Bump version 2023-12-18 13:19:54 +01:00
53585bf2f0 Merge pull request #476 from BlackVoid/feature/document-title
Fixes #473: Set document title using the "loginTitle" translation
2023-12-18 13:18:51 +01:00
116f88a503 Fixes #473: Set document title using the "loginTitle" translation 2023-12-18 11:56:37 +01:00
aaba8cd2c7 Bump version 2023-12-14 15:34:13 +01:00
b67aeb0d3a Fix tests #471 2023-12-14 15:33:57 +01:00
f620562d68 docs: update .all-contributorsrc [skip ci] 2023-12-14 15:09:10 +01:00
5231d0eaa1 docs: update README.md [skip ci] 2023-12-14 15:09:09 +01:00
cb470e3573 Handle CSS with multiple urls 2023-12-12 14:51:09 +00:00
0a0f90aa2e Bump version 2023-12-04 14:10:25 +01:00
635207d12c Generate a README alongside the jars to indicate why there's two jar file 2023-12-04 14:10:10 +01:00
5e4a829413 Bump version 2023-12-01 00:54:36 +01:00
b13b3fd92e Mention the nessesity to use retrocompat- with older Keycloak versions 2023-12-01 00:54:19 +01:00
564dc8e6f1 Bump version 2023-12-01 00:03:58 +01:00
6e4cced8c6 Rename original- to retrocompat- 2023-12-01 00:03:44 +01:00
29a4a5027c Bump version 2023-11-30 22:37:13 +01:00
ee327448b4 Delete original- jar 2023-11-30 22:36:58 +01:00
d078960c5c Bump version 2023-11-30 18:53:17 +01:00
2e8cd375fc Merge pull request #462 from BlackVoid/fix/client-attributes
Fixes #460: Fixes KcContext to contain attributes for client object
2023-11-30 18:52:26 +01:00
1f6751cb01 Fixes #460: Fixes KcContext to contain attributes for client object 2023-11-30 17:17:58 +01:00
3cca4e31cd Merge pull request #463 from keycloakify/all-contributors/add-BlackVoid
docs: add BlackVoid as a contributor for code
2023-11-30 17:10:29 +01:00
b93902800c docs: update .all-contributorsrc [skip ci] 2023-11-30 16:06:55 +00:00
70f6bb3fda docs: update README.md [skip ci] 2023-11-30 16:06:54 +00:00
c075cb6311 Merge pull request #461 from BlackVoid/fix/template
Fixes #459: Use Template from props
2023-11-30 17:03:36 +01:00
d7db85b062 Fixes #459: Use Template from props 2023-11-30 16:29:53 +01:00
b442e7d958 Merge pull request #457 from keycloakify/all-contributors/add-xgp
docs: add xgp as a contributor for code
2023-11-28 13:06:04 +01:00
a495ae637f docs: update .all-contributorsrc [skip ci] 2023-11-28 12:05:49 +00:00
94748a96a9 docs: update README.md [skip ci] 2023-11-28 12:05:48 +00:00
7657429054 Release v9 2023-11-28 12:53:37 +01:00
2ff6dbf975 Update README 2023-11-28 12:53:10 +01:00
4f34628c14 Merge pull request #414 from keycloakify/v9
v9: Support Keycloak 22 and decoupling account / login builtin resources
2023-11-28 12:18:07 +01:00
6ff2111cee #389 https://github.com/p2-inc/keycloak-account-v1/issues/3 2023-11-28 12:17:16 +01:00
85957980f6 update CI 2023-11-26 16:24:57 +01:00
a6dcfe2c87 Release candidate 2023-11-26 16:14:25 +01:00
c32d590fbb #389 https://github.com/xgp/keycloak-account-v1/issues/3 2023-11-26 16:10:34 +01:00
ab41462f71 Actually resolve conflict with main 2023-11-26 15:07:27 +01:00
951f16b1a5 Merge pull request #456 from keycloakify/v9_tmp
Rebase v9 to main
2023-11-26 14:46:28 +01:00
b5818888bb Merge branch 'v9' into v9_tmp 2023-11-26 14:45:23 +01:00
e06ef01f72 Merge branch 'main' into v9 2023-09-22 15:52:23 +02:00
7de54a2cc4 Fix tests 2023-09-04 03:26:30 +02:00
c788b8cc82 Release candidate 2023-09-04 02:49:58 +02:00
cb8db1a541 Fix build 2023-09-04 02:49:32 +02:00
8a7a551c3b Fix mock path error in account 2023-09-04 02:38:19 +02:00
84d180b810 Fix bug with asset paths 2023-09-04 02:34:10 +02:00
de261a27ca Do not display that the jar have been created if we don't create it. 2023-09-04 02:29:16 +02:00
28288a8f7b Build retrocompatible account theme 2023-09-04 02:16:55 +02:00
cd8548fc32 Remove extraThemeNames option in favor of extending themeName to accept array 2023-09-04 01:19:21 +02:00
37dbd49589 Rename extraThemeNames to themeVariantNames 2023-09-04 00:53:57 +02:00
5af8d67b62 Refactor and update docker script 2023-09-04 00:25:36 +02:00
72e6309c4a Fix warning 2023-09-03 23:32:21 +02:00
18f0f3cce1 Refactor build option managment 2023-09-03 23:26:34 +02:00
8c3e9ff192 Remove inhouse bundler, we actually need Maven to build now 2023-09-03 21:10:20 +02:00
21d6d27435 Rename build option, update readme 2023-09-03 21:02:51 +02:00
39ff7913d6 https://github.com/xgp/keycloak-account-v1/issues/3 2023-09-03 07:14:57 +02:00
50 changed files with 760 additions and 1000 deletions

View File

@ -186,6 +186,33 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "xgp",
"name": "Garth",
"avatar_url": "https://avatars.githubusercontent.com/u/244253?v=4",
"profile": "https://github.com/xgp",
"contributions": [
"code"
]
},
{
"login": "BlackVoid",
"name": "Felix Gustavsson",
"avatar_url": "https://avatars.githubusercontent.com/u/673720?v=4",
"profile": "https://github.com/BlackVoid",
"contributions": [
"code"
]
},
{
"login": "msiemens",
"name": "Markus Siemens",
"avatar_url": "https://avatars.githubusercontent.com/u/1873922?v=4",
"profile": "https://m-siemens.de/",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@ -3,7 +3,9 @@ on:
push: push:
branches: branches:
- main - main
- v8 - v5
- v6
- v7
pull_request: pull_request:
branches: branches:
- main - main
@ -130,7 +132,10 @@ jobs:
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets" echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
false false
fi fi
EXTRA_ARGS="--tag v8" EXTRA_ARGS=""
if [ "$IS_PRE_RELEASE" = "true" ]; then
EXTRA_ARGS="--tag next"
fi
npm publish $EXTRA_ARGS npm publish $EXTRA_ARGS
env: env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

View File

@ -36,24 +36,12 @@
<p align="center"> <p align="center">
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i> <i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png"> <br/>
<br/>
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
</p> </p>
> Whether or not React is your preferred framework, Keycloakify 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)!
> offers a solid option for building Keycloak themes.
> It's not just a convenient way to create a Keycloak theme
> when using React; it's a well-regarded solution that many
> developers appreciate.
> 📣 🛑 Account themes generated by Keycloakify are not currently compatible with Keycloak 22.
> We are working on a solution. [Follow progress](https://github.com/keycloakify/keycloakify/issues/389).
> **Login and email themes are not affected**.
> UPDATE: [The PR](https://github.com/keycloak/keycloak/pull/22317) that should future proof Keycloakify account themes has been
> merged into Keycloak! 🥳 Credit to @xgp. We are now waiting for a new Keycloak release to be published.
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
You can update your Keycloak, your Keycloakify generated theme won't break. (Well except for Keycloak 22's Account theme obviously but this was hopefully a one time debacle)
To understand the basis of my confidence in this, you can [visit this discussion thread where I've explained in detail](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791).
## Sponsor 👼 ## Sponsor 👼
@ -119,6 +107,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xgp"><img src="https://avatars.githubusercontent.com/u/244253?v=4?s=100" width="100px;" alt="Garth"/><br /><sub><b>Garth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=xgp" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BlackVoid"><img src="https://avatars.githubusercontent.com/u/673720?v=4?s=100" width="100px;" alt="Felix Gustavsson"/><br /><sub><b>Felix Gustavsson</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=BlackVoid" title="Code">💻</a></td>
<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>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -130,6 +123,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights # Changelog highlights
## v9.0
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
### Breaking changes
Very few. Check them out [here](https://docs.keycloakify.dev/migration-guides/v8-greater-than-v9).
## 8.0 ## 8.0
- Much smaller .jar size. 70.2 MB -> 7.8 MB. - Much smaller .jar size. 70.2 MB -> 7.8 MB.
@ -138,58 +139,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
### Breaking changes ### Breaking changes
There are very few breaking changes in this major version. There are very few breaking changes in this major version. [Check them out](https://docs.keycloakify.dev/migration-guides/v7-greater-than-v8).
- The [`--external-assets` build option has been removed](https://docs.keycloakify.dev/v/v7/build-options#external-assets-deprecated) it was a performance optimization that is no longer relevant now that
we have lazy loading.
- `kcContext.usernameEditDisabled` is now `kcContext.usernameHidden`, the type was lying, it has been updated to reflect what's actually on the `kcContext` at runtime.
If you want to see in detail what should be updated [see issue](https://github.com/keycloakify/keycloakify/pull/399), or you can search and replace `usernameEditDisabled` -> `usernameHidden` it'll do the trick.
- The `usePrepareTemplate` prototype has been changed, you can search and replace:
`src/keycloak-theme/login/Template.tsx`
```ts
url,
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
```
by
```ts
"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`
],
```
and
`src/keycloak-theme/account/Template.css`
```ts
url,
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
"styles": ["css/account.css"],
```
by
```ts
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
],
```
## 7.15 ## 7.15
@ -211,7 +161,7 @@ by
## 7.12 ## 7.12
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme. - You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.
There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.extrathemenames). There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.themeVariantNames).
## 7.9 ## 7.9

View File

@ -1,73 +0,0 @@
{
"allOf": [
{
"$ref": "https://json.schemastore.org/package.json"
},
{
"$ref": "keycloakifyPackageJsonSchema"
}
],
"$ref": "#/definitions/keycloakifyPackageJsonSchema",
"definitions": {
"keycloakifyPackageJsonSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"homepage": {
"type": "string"
},
"keycloakify": {
"type": "object",
"properties": {
"extraPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraThemeProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"areAppAndKeycloakServerSharingSameDomain": {
"type": "boolean"
},
"artifactId": {
"type": "string"
},
"groupId": {
"type": "string"
},
"bundler": {
"type": "string",
"enum": ["mvn", "keycloakify", "none"]
},
"keycloakVersionDefaultAssets": {
"type": "string"
},
"reactAppBuildDirPath": {
"type": "string"
},
"keycloakifyBuildDirPath": {
"type": "string"
},
"themeName": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": ["name", "version"],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "8.4.5", "version": "9.1.6",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,7 +13,7 @@
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/", "build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts", "generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js", "grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl", "copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
"test": "yarn test:types && vitest run", "test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter", "test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit", "test:types": "tsc -p test/tsconfig.json --noEmit",
@ -112,7 +112,7 @@
"@babel/parser": "^7.22.7", "@babel/parser": "^7.22.7",
"@babel/types": "^7.22.5", "@babel/types": "^7.22.5",
"@octokit/rest": "^18.12.0", "@octokit/rest": "^18.12.0",
"cheerio": "1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2", "cli-select": "^1.1.2",
"evt": "^2.4.18", "evt": "^2.4.18",
"make-fetch-happen": "^11.0.3", "make-fetch-happen": "^11.0.3",

View File

@ -24,9 +24,11 @@ async function main() {
fs.rmSync(tmpDirPath, { "recursive": true, "force": true }); fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
"projectDirPath": getProjectRoot(),
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
"buildOptions": {
"cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
}
}); });
type Dictionary = { [idiomId: string]: string }; type Dictionary = { [idiomId: string]: string };

View File

@ -1,6 +1,7 @@
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl"; import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import { type ThemeType } from "keycloakify/bin/constants";
export type KcContext = KcContext.Password | KcContext.Account; export type KcContext = KcContext.Password | KcContext.Account;

View File

@ -3,9 +3,8 @@ import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow"; import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow"; import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin"; import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks"; import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: { export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
@ -89,11 +88,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any }; return { "kcContext": undefined as any };
} }
{ realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
return { "kcContext": realKcContext as any }; return { "kcContext": realKcContext as any };
} }

View File

@ -1,19 +1,21 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath"; import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin"; import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/"; const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
export const kcContextCommonMock: KcContext.Common = { export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0", "themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0", "keycloakifyVersion": "0.0.0",
"themeType": "account", "themeType": "account",
"themeName": "my-theme-name", "themeName": "my-theme-name",
"url": { "url": {
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir), resourcesPath,
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir), "resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"resourceUrl": "#", "resourceUrl": "#",
"accountUrl": "#", "accountUrl": "#",
"applicationsUrl": "#", "applicationsUrl": "#",

9
src/bin/constants.ts Normal file
View File

@ -0,0 +1,9 @@
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const themeTypes = ["login", "account"] as const;
export const retrocompatPostfix = "_retrocompat";
export const accountV1 = "account-v1";
export type ThemeType = (typeof themeTypes)[number];

View File

@ -2,38 +2,39 @@
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources"; import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
import { readBuildOptions } from "./keycloakify/BuildOptions"; import { readBuildOptions } from "./keycloakify/BuildOptions";
import { themeTypes } from "./keycloakify/generateFtl"; import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
import * as fs from "fs"; import * as fs from "fs";
(async () => { (async () => {
const projectDirPath = process.cwd(); const reactAppRootDirPath = process.cwd();
const buildOptions = readBuildOptions({ const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2), reactAppRootDirPath,
"projectDirPath": process.cwd() "processArgv": process.argv.slice(2)
}); });
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir); const reservedDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
if (fs.existsSync(keycloakDirInPublicDir)) {
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
return;
}
for (const themeType of themeTypes) { for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({ await downloadKeycloakStaticResources({
projectDirPath, "keycloakVersion": (() => {
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets, switch (themeType) {
"themeType": themeType, case "login":
"themeDirPath": keycloakDirInPublicDir, return buildOptions.loginThemeResourcesFromKeycloakVersion;
"usedResources": undefined case "account":
return lastKeycloakVersionWithAccountV1;
}
})(),
themeType,
"themeDirPath": reservedDirPath,
"usedResources": undefined,
buildOptions
}); });
} }
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"), pathJoin(reservedDirPath, "README.txt"),
Buffer.from( Buffer.from(
// prettier-ignore // prettier-ignore
[ [
@ -43,7 +44,7 @@ import * as fs from "fs";
) )
); );
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8")); fs.writeFileSync(pathJoin(buildOptions.publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`); console.log(`${pathRelative(reactAppRootDirPath, reservedDirPath)} directory created.`);
})(); })();

View File

@ -4,15 +4,23 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion"; import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger"; import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions"; import { readBuildOptions } from "./keycloakify/BuildOptions";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: string; keycloakVersion: string; destDirPath: string }) { export type BuildOptionsLike = {
const { projectDirPath, keycloakVersion, destDirPath } = params; cacheDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({ await downloadAndUnzip({
"doUseCache": true, "doUseCache": true,
projectDirPath, "cacheDirPath": buildOptions.cacheDirPath,
destDirPath, destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`, "url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`), "specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
@ -74,7 +82,7 @@ export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: str
async function main() { async function main() {
const buildOptions = readBuildOptions({ const buildOptions = readBuildOptions({
"projectDirPath": process.cwd(), "reactAppRootDirPath": process.cwd(),
"processArgv": process.argv.slice(2) "processArgv": process.argv.slice(2)
}); });
@ -86,9 +94,9 @@ async function main() {
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`); logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
"projectDirPath": process.cwd(),
keycloakVersion, keycloakVersion,
destDirPath destDirPath,
buildOptions
}); });
} }

View File

@ -2,14 +2,7 @@
import { getProjectRoot } from "./tools/getProjectRoot"; import { getProjectRoot } from "./tools/getProjectRoot";
import cliSelect from "cli-select"; import cliSelect from "cli-select";
import { import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
loginThemePageIds,
accountThemePageIds,
type LoginThemePageId,
type AccountThemePageId,
themeTypes,
type ThemeType
} from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize"; import { capitalize } from "tsafe/capitalize";
import { readFile, writeFile } from "fs/promises"; import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs"; import { existsSync } from "fs";
@ -17,10 +10,13 @@ import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getSrcDirPath"; import { getThemeSrcDirPath } from "./getSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
(async () => { (async () => {
console.log("Select a theme type"); console.log("Select a theme type");
const reactAppRootDirPath = process.cwd();
const { value: themeType } = await cliSelect<ThemeType>({ const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes] "values": [...themeTypes]
}).catch(() => { }).catch(() => {
@ -49,7 +45,7 @@ import { getThemeSrcDirPath } from "./getSrcDirPath";
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx"); const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
const { themeSrcDirPath } = getThemeSrcDirPath({ "projectDirPath": process.cwd() }); const { themeSrcDirPath } = getThemeSrcDirPath({ reactAppRootDirPath });
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename); const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);

View File

@ -2,15 +2,15 @@ import * as fs from "fs";
import { exclude } from "tsafe"; import { exclude } from "tsafe";
import { crawl } from "./tools/crawl"; import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { themeTypes } from "./keycloakify/generateFtl"; import { themeTypes } from "./constants";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"]; const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */ /** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { projectDirPath: string }) { export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
const { projectDirPath } = params; const { reactAppRootDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src"); const srcDirPath = pathJoin(reactAppRootDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" }) const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
.map(fileRelativePath => { .map(fileRelativePath => {

View File

@ -10,17 +10,17 @@ import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getSrcDirPath"; import { getThemeSrcDirPath } from "./getSrcDirPath";
export async function main() { export async function main() {
const projectDirPath = process.cwd(); const reactAppRootDirPath = process.cwd();
const { isSilent } = readBuildOptions({ const buildOptions = readBuildOptions({
projectDirPath, reactAppRootDirPath,
"processArgv": process.argv.slice(2) "processArgv": process.argv.slice(2)
}); });
const logger = getLogger({ isSilent }); const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({ const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath reactAppRootDirPath
}); });
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
@ -36,9 +36,9 @@ export async function main() {
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme"); const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
projectDirPath,
keycloakVersion, keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath "destDirPath": builtinKeycloakThemeTmpDirPath,
buildOptions
}); });
transformCodebase({ transformCodebase({

View File

@ -1,34 +1,34 @@
import { assert } from "tsafe/assert";
import { id } from "tsafe/id";
import { parse as urlParse } from "url"; import { parse as urlParse } from "url";
import { typeGuard } from "tsafe/typeGuard"; import { getParsedPackageJson } from "./parsedPackageJson";
import { symToStr } from "tsafe/symToStr"; import { join as pathJoin } from "path";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import { join as pathJoin, sep as pathSep } from "path";
import parseArgv from "minimist"; import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
/** Consolidated build option gathered form CLI arguments and config in package.json */ /** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = { export type BuildOptions = {
isSilent: boolean; isSilent: boolean;
themeVersion: string; themeVersion: string;
themeName: string; themeNames: string[];
extraThemeNames: string[];
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
groupId: string; groupId: string;
artifactId: string; artifactId: string;
bundler: Bundler; doCreateJar: boolean;
keycloakVersionDefaultAssets: string; loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
/** Directory of your built react project. Defaults to {cwd}/build */ /** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */ /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string; 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 /** 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/" */ * In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined; urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
}; };
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions { export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params; const { reactAppRootDirPath, processArgv } = params;
const { isSilentCliParamProvided } = (() => { const { isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv); const argv = parseArgv(processArgv);
@ -38,35 +38,36 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
}; };
})(); })();
const parsedPackageJson = getParsedPackageJson({ projectDirPath }); const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath });
const { name, keycloakify = {}, version, homepage } = parsedPackageJson; const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {}; const { extraThemeProperties, groupId, artifactId, doCreateJar, loginThemeResourcesFromKeycloakVersion } = keycloakify ?? {};
const themeName = const themeNames = (() => {
keycloakify.themeName ?? if (keycloakify.themeName === undefined) {
name return [
.replace(/^@(.*)/, "$1") name
.split("/") .replace(/^@(.*)/, "$1")
.join("-"); .split("/")
.join("-")
];
}
if (typeof keycloakify.themeName === "string") {
return [keycloakify.themeName];
}
return keycloakify.themeName;
})();
return { return {
themeName, reactAppRootDirPath,
extraThemeNames, themeNames,
"bundler": (() => { "doCreateJar": doCreateJar ?? true,
const { KEYCLOAKIFY_BUNDLER } = process.env; "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeNames[0]}-keycloak-theme`,
assert(
typeGuard<Bundler | undefined>(KEYCLOAKIFY_BUNDLER, [undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
);
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
"groupId": (() => { "groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`; const fallbackGroupId = `${themeNames[0]}.keycloak`;
return ( return (
process.env.KEYCLOAKIFY_GROUP_ID ?? process.env.KEYCLOAKIFY_GROUP_ID ??
@ -83,41 +84,58 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0", "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
extraThemeProperties, extraThemeProperties,
"isSilent": isSilentCliParamProvided, "isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3", "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": (() => { "reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {}; const { reactAppBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath === undefined) { if (reactAppBuildDirPath !== undefined) {
return pathJoin(projectDirPath, "build"); return getAbsoluteAndInOsFormatPath({
"pathIsh": reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
} }
if (pathSep === "\\") { return pathJoin(reactAppRootDirPath, "build");
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
}
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, reactAppBuildDirPath);
}
return reactAppBuildDirPath;
})(), })(),
"keycloakifyBuildDirPath": (() => { "keycloakifyBuildDirPath": (() => {
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {}; const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath === undefined) { if (keycloakifyBuildDirPath !== undefined) {
return pathJoin(projectDirPath, "build_keycloak"); return getAbsoluteAndInOsFormatPath({
"pathIsh": keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
} }
if (pathSep === "\\") { return pathJoin(reactAppRootDirPath, "build_keycloak");
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
}
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
}
return keycloakifyBuildDirPath;
})(), })(),
"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": (() => { "urlPathname": (() => {
const { homepage } = parsedPackageJson; const { homepage } = parsedPackageJson;
@ -133,6 +151,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
const out = url.pathname.replace(/([^/])$/, "$1/"); const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out; return out === "/" ? undefined : out;
})() })(),
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
}; };
} }

View File

@ -408,14 +408,6 @@
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer"; out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["pageId"] = "${pageId}"; out["pageId"] = "${pageId}";
try {
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} catch(error) {
}
return out; return out;
})() })()
@ -431,7 +423,7 @@
<#if isHash> <#if isHash>
<#if path?size gt 10> <#if path?size gt 10>
<#return "ABORT: Too many recursive calls, path: " + path?join(".")> <#return "ABORT: Too many recursive calls">
</#if> </#if>
<#local keys = ""> <#local keys = "">
@ -463,10 +455,9 @@
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) --> <#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 --> <#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/issues/357 --> <#-- https://github.com/keycloakify/keycloakify/issues/357 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" && key == "loginAction" &&
are_same_path(path, ["url"]) && are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) && ["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink()) !(auth?has_content && auth.showTryAnotherWayLink())
) || ( ) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 --> <#-- https://github.com/keycloakify/keycloakify/issues/362 -->
@ -488,37 +479,25 @@
are_same_path(path, ["realm"]) && are_same_path(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key) !["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || ( ) || (
"smtpConfig" == key && "applications.ftl" == pageId &&
are_same_path(path, ["realm"]) are_same_path(path, ["applications", "applications", "*", "client", "realm"])
) || ( ) || (
"applications.ftl" == pageId && "applications.ftl" == pageId &&
is_subpath(path, ["applications", "applications"]) && "masterAdminClient" == key
(
key == "realm" ||
key == "container"
)
) || (
are_same_path(path, ["user"]) &&
key == "delegateForUpdate"
) )
> >
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]> <#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue> <#continue>
</#if> </#if>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 --> <#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#if (
["register.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
key == "attemptedUsername" && are_same_path(path, ["auth"])
)>
<#attempt> <#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 --> <#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue> <#continue>
</#if> </#if>
<#recover> <#recover>
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]>
</#attempt> </#attempt>
</#if> </#if>
@ -671,9 +650,9 @@
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object"> <#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function> </#function>
<#function is_subpath path searchedPath> <#function are_same_path path searchedPath>
<#if path?size < searchedPath?size> <#if path?size != searchedPath?size>
<#return false> <#return false>
</#if> </#if>
@ -681,14 +660,8 @@
<#list path as property> <#list path as property>
<#if i == searchedPath?size >
<#continue>
</#if>
<#local searchedProperty=searchedPath[i]> <#local searchedProperty=searchedPath[i]>
<#local i+= 1>
<#if searchedProperty?is_string && searchedProperty == "*"> <#if searchedProperty?is_string && searchedProperty == "*">
<#continue> <#continue>
</#if> </#if>
@ -705,13 +678,11 @@
<#return false> <#return false>
</#if> </#if>
<#local i+= 1>
</#list> </#list>
<#return true> <#return true>
</#function> </#function>
<#function are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function>
</script> </script>

View File

@ -8,13 +8,9 @@ import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName"; import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions"; import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { ThemeType } from "../../constants";
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];
export type BuildOptionsLike = { export type BuildOptionsLike = {
themeName: string;
themeVersion: string; themeVersion: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -22,6 +18,7 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>(); assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string; indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled. //NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>; cssGlobalsToDefine: Record<string, string>;
@ -30,7 +27,7 @@ export function generateFtlFilesCodeFactory(params: {
themeType: ThemeType; themeType: ThemeType;
fieldNames: string[]; fieldNames: string[];
}) { }) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params; const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
@ -104,7 +101,7 @@ export function generateFtlFilesCodeFactory(params: {
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion) .replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion) .replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType) .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", buildOptions.themeName), .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [ "<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>", "<#if scripts??>",
" <#list scripts as script>", " <#list scripts as script>",

View File

@ -1,84 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId: string;
themeVersion: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
implementedThemeTypes
} = 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>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -0,0 +1,87 @@
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 { resources_common, lastKeycloakVersionWithAccountV1, accountV1 } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
cacheDirPath: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
"destDirPath": builtinKeycloakThemeTmpDirPath,
"keycloakVersion": lastKeycloakVersionWithAccountV1,
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1, "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"
];
for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
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 });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
Buffer.from(
[
"accountResourceProvider=account-v1",
"",
"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(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
"kcButtonClass=btn",
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
"kcButtonPrimaryClass=btn-primary",
"kcButtonDefaultClass=btn-default",
"# classes defining size of the button",
"kcButtonLargeClass=btn-lg",
""
].join("\n"),
"utf8"
)
);
}

View File

@ -0,0 +1,140 @@
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, retrocompatPostfix, 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)
},
...(!implementedThemeTypes.account
? []
: [
{
"name": `${themeName}${retrocompatPostfix}`,
"types": ["account"]
}
])
])
.flat()
]
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`)
};
}

View File

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

View File

@ -1,53 +1,60 @@
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = { export type BuildOptionsLike = {
themeName: string; keycloakifyBuildDirPath: string;
extraThemeNames: string[];
}; };
assert<BuildOptions extends BuildOptionsLike ? true : false>(); {
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh"; generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container"; const containerName = "keycloak-testing-container";
/** Files for being able to run a hot reload keycloak container */ /** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: { export function generateStartKeycloakTestingContainer(params: { jarFilePath: string; keycloakVersion: string; buildOptions: BuildOptionsLike }) {
keycloakVersion: string; const { jarFilePath, keycloakVersion, buildOptions } = params;
keycloakThemeBuildingDirPath: string;
buildOptions: BuildOptionsLike; const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
}) { const themeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, themeRelativeDirPath);
const {
keycloakThemeBuildingDirPath,
keycloakVersion,
buildOptions: { themeName, extraThemeNames }
} = params;
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename), pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from( Buffer.from(
[ [
"#!/usr/bin/env bash", "#!/usr/bin/env bash",
`# If you want to test with Keycloak version prior to 23 use the retrocompat-${pathBasename(jarFilePath)}`,
"", "",
`docker rm ${containerName} || true`, `docker rm ${containerName} || true`,
"", "",
`cd "${keycloakThemeBuildingDirPath.replace(/\\/g, "/")}"`, `cd "${buildOptions.keycloakifyBuildDirPath}"`,
"", "",
"docker run \\", "docker run \\",
" -p 8080:8080 \\", " -p 8080:8080 \\",
` --name ${containerName} \\`, ` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\", " -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\", " -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
...[themeName, ...extraThemeNames].map( ` -v "${pathJoin(
themeName => "$(pwd)",
` -v "${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace( pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
/\\/g, )}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
"/" ...fs
)}":"/opt/keycloak/themes/${themeName}":rw \\` .readdirSync(themeDirPath)
), .filter(name => fs.lstatSync(pathJoin(themeDirPath, name)).isDirectory())
.map(
themeName =>
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(
/\\/g,
"/"
)}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`, ` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev --features=declarative-user-profile`, ` start-dev --features=declarative-user-profile`,
"" ""

View File

@ -1,29 +1,31 @@
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "../generateFtl";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { import { resources_common, type ThemeType } from "../../constants";
resourcesCommonDirPathRelativeToPublicDir, import { BuildOptions } from "../BuildOptions";
resourcesDirPathRelativeToPublicDir,
basenameOfKeycloakDirInPublicDir
} from "../../mockTestingResourcesPath";
import * as crypto from "crypto";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import * as crypto from "crypto";
export type BuildOptionsLike = {
cacheDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadKeycloakStaticResources( export async function downloadKeycloakStaticResources(
// prettier-ignore // prettier-ignore
params: { params: {
projectDirPath: string;
themeType: ThemeType; themeType: ThemeType;
themeDirPath: string; themeDirPath: string;
keycloakVersion: string; keycloakVersion: string;
usedResources: { usedResources: {
resourcesCommonFilePaths: string[]; resourcesCommonFilePaths: string[];
} | undefined } | undefined;
buildOptions: BuildOptionsLike;
} }
) { ) {
const { projectDirPath, themeType, themeDirPath, keycloakVersion } = params; const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
// NOTE: Hack for 427 // NOTE: Hack for 427
const usedResources = (() => { const usedResources = (() => {
@ -52,24 +54,25 @@ export async function downloadKeycloakStaticResources(
const tmpDirPath = pathJoin( const tmpDirPath = pathJoin(
themeDirPath, themeDirPath,
"..",
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}` `tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
); );
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
projectDirPath,
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
buildOptions
}); });
const resourcesPath = pathJoin(themeDirPath, themeType, "resources");
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"), "srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir)) "destDirPath": resourcesPath
}); });
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"), "srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)), "destDirPath": pathJoin(resourcesPath, resources_common),
"transformSourceCode": "transformSourceCode":
usedResources === undefined usedResources === undefined
? undefined ? undefined

View File

@ -1,4 +1,4 @@
import type { ThemeType } from "../generateFtl"; import type { ThemeType } from "../../constants";
import { crawl } from "../../tools/crawl"; import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";

View File

@ -1,13 +1,13 @@
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin, basename as pathBasename, resolve as pathResolve } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode"; import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl"; import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath"; import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, retrocompatPostfix, accountV1 } from "../../constants";
import { isInside } from "../../tools/isInside"; import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions"; import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources"; import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage"; import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames"; import { readExtraPagesNames } from "./readExtraPageNames";
@ -15,36 +15,39 @@ import { generateMessageProperties } from "./generateMessageProperties";
import { readStaticResourcesUsage } from "./readStaticResourcesUsage"; import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
export type BuildOptionsLike = { export type BuildOptionsLike = {
themeName: string;
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
themeVersion: string; themeVersion: string;
keycloakVersionDefaultAssets: string; loginThemeResourcesFromKeycloakVersion: string;
urlPathname: string | undefined; urlPathname: string | undefined;
keycloakifyBuildDirPath: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
doBuildRetrocompatAccountTheme: boolean;
}; };
assert<BuildOptions extends BuildOptionsLike ? true : false>(); assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: { export async function generateTheme(params: {
projectDirPath: string; themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
themeSrcDirPath: string; themeSrcDirPath: string;
keycloakifySrcDirPath: string; keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
keycloakifyVersion: string; keycloakifyVersion: string;
}): Promise<void> { }): Promise<void> {
const { const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
projectDirPath,
reactAppBuildDirPath,
keycloakThemeBuildingDirPath,
themeSrcDirPath,
keycloakifySrcDirPath,
buildOptions,
keycloakifyVersion
} = params;
const getThemeDirPath = (themeType: ThemeType | "email") => const getThemeTypeDirPath = (params: { themeType: ThemeType | "email"; isRetrocompat?: true }) => {
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType); const { themeType, isRetrocompat = false } = params;
return pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
`${themeName}${isRetrocompat ? retrocompatPostfix : ""}`,
themeType
);
};
let allCssGlobalsToDefine: Record<string, string> = {}; let allCssGlobalsToDefine: Record<string, string> = {};
@ -55,7 +58,7 @@ export async function generateTheme(params: {
continue; continue;
} }
const themeDirPath = getThemeDirPath(themeType); const themeTypeDirPath = getThemeTypeDirPath({ themeType });
copy_app_resources_to_theme_path: { copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0; const isFirstPass = themeType.indexOf(themeType) === 0;
@ -65,13 +68,13 @@ export async function generateTheme(params: {
} }
transformCodebase({ transformCodebase({
"destDirPath": pathJoin(themeDirPath, "resources", "build"), "destDirPath": pathJoin(themeTypeDirPath, "resources", "build"),
"srcDirPath": reactAppBuildDirPath, "srcDirPath": buildOptions.reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => { "transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/ //NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if ( if (
isInside({ isInside({
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir), "dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
filePath filePath
}) })
) { ) {
@ -114,7 +117,8 @@ export async function generateTheme(params: {
generateFtlFilesCode_glob !== undefined generateFtlFilesCode_glob !== undefined
? generateFtlFilesCode_glob ? generateFtlFilesCode_glob
: generateFtlFilesCodeFactory({ : generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"), themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine, "cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions, buildOptions,
keycloakifyVersion, keycloakifyVersion,
@ -142,75 +146,77 @@ export async function generateTheme(params: {
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true }); fs.mkdirSync(themeTypeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8")); fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
}); });
generateMessageProperties({ generateMessageProperties({
themeSrcDirPath, themeSrcDirPath,
themeType themeType
}).forEach(({ languageTag, propertiesFileSource }) => { }).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeDirPath, "messages"); const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
fs.mkdirSync(pathJoin(themeDirPath, "messages"), { "recursive": true }); fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true });
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`); const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8")); fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
}); });
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
break copy_keycloak_resources_to_public;
}
await downloadKeycloakStaticResources({
projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir,
themeType,
"usedResources": undefined
});
if (themeType !== themeTypes[0]) {
break copy_keycloak_resources_to_public;
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
}
await downloadKeycloakStaticResources({ await downloadKeycloakStaticResources({
projectDirPath, "keycloakVersion": (() => {
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets, switch (themeType) {
themeDirPath, case "account":
return lastKeycloakVersionWithAccountV1;
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
}
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType, themeType,
"usedResources": readStaticResourcesUsage({ "usedResources": readStaticResourcesUsage({
keycloakifySrcDirPath, keycloakifySrcDirPath,
themeSrcDirPath, themeSrcDirPath,
themeType themeType
}) }),
buildOptions
}); });
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"), pathJoin(themeTypeDirPath, "theme.properties"),
Buffer.from([`parent=keycloak`, ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8") Buffer.from(
[
`parent=${(() => {
switch (themeType) {
case "account":
return accountV1;
case "login":
return "keycloak";
}
assert<Equals<typeof themeType, never>>(false);
})()}`,
...(buildOptions.extraThemeProperties ?? [])
].join("\n\n"),
"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=${accountV1}`, "parent=keycloak"), "utf8")
};
}
return { "modifiedSourceCode": sourceCode };
}
});
}
} }
email: { email: {
@ -222,7 +228,7 @@ export async function generateTheme(params: {
transformCodebase({ transformCodebase({
"srcDirPath": emailThemeSrcDirPath, "srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email") "destDirPath": getThemeTypeDirPath({ "themeType": "email" })
}); });
} }
} }

View File

@ -1,9 +1,10 @@
import { crawl } from "../../tools/crawl"; import { crawl } from "../../tools/crawl";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../generateFtl"; import { accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates"; import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import type { ThemeType } from "../../constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] { export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params; const { themeSrcDirPath, themeType } = params;

View File

@ -2,7 +2,7 @@ import { crawl } from "../../tools/crawl";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates"; import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import * as fs from "fs"; import * as fs from "fs";
import type { ThemeType } from "../generateFtl"; import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */ /** Assumes the theme type exists */
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] { export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {

View File

@ -1,7 +1,7 @@
import { crawl } from "../../tools/crawl"; import { crawl } from "../../tools/crawl";
import { join as pathJoin, sep as pathSep } from "path"; import { join as pathJoin, sep as pathSep } from "path";
import * as fs from "fs"; import * as fs from "fs";
import type { ThemeType } from "../generateFtl"; import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */ /** Assumes the theme type exists */
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): { export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {

View File

@ -1,23 +1,21 @@
import { generateTheme } from "./generateTheme"; import { generateTheme } from "./generateTheme";
import { generateJavaStackFiles } from "./generateJavaStackFiles"; import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path"; 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 * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer"; import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs"; import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions"; import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger"; import { getLogger } from "../tools/logger";
import jar from "../tools/jar";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Equals } from "tsafe";
import { getThemeSrcDirPath } from "../getSrcDirPath"; import { getThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot"; import { getProjectRoot } from "../tools/getProjectRoot";
import { objectKeys } from "tsafe/objectKeys"; import { objectKeys } from "tsafe/objectKeys";
export async function main() { export async function main() {
const projectDirPath = process.cwd(); const reactAppRootDirPath = process.cwd();
const buildOptions = readBuildOptions({ const buildOptions = readBuildOptions({
projectDirPath, reactAppRootDirPath,
"processArgv": process.argv.slice(2) "processArgv": process.argv.slice(2)
}); });
@ -26,19 +24,14 @@ export async function main() {
const keycloakifyDirPath = getProjectRoot(); const keycloakifyDirPath = getProjectRoot();
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath }); const { themeSrcDirPath } = getThemeSrcDirPath({ reactAppRootDirPath });
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) { for (const themeName of buildOptions.themeNames) {
await generateTheme({ await generateTheme({
projectDirPath, themeName,
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
themeSrcDirPath, themeSrcDirPath,
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"), "keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath, buildOptions,
"buildOptions": {
...buildOptions,
"themeName": themeName
},
"keycloakifyVersion": (() => { "keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"]; const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
@ -49,8 +42,7 @@ export async function main() {
}); });
} }
const { jarFilePath } = generateJavaStackFiles({ const { jarFilePath } = await generateJavaStackFiles({
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
"implementedThemeTypes": (() => { "implementedThemeTypes": (() => {
const implementedThemeTypes = { const implementedThemeTypes = {
"login": false, "login": false,
@ -70,43 +62,45 @@ export async function main() {
buildOptions buildOptions
}); });
switch (buildOptions.bundler) { if (buildOptions.doCreateJar) {
case "none": child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
logger.log("😱 Skipping bundling step, there will be no jar");
break; const jarDirPath = pathDirname(jarFilePath);
case "keycloakify": const retrocompatJarFilePath = pathJoin(jarDirPath, "retrocompat-" + pathBasename(jarFilePath));
logger.log("🫶 Let keycloakify do its thang");
await jar({ fs.renameSync(pathJoin(jarDirPath, "original-" + pathBasename(jarFilePath)), retrocompatJarFilePath);
"rootPath": buildOptions.keycloakifyBuildDirPath,
"version": buildOptions.themeVersion, fs.writeFileSync(
"groupId": buildOptions.groupId, pathJoin(jarDirPath, "README.md"),
"artifactId": buildOptions.artifactId, Buffer.from(
"targetPath": jarFilePath [
}); `- The ${jarFilePath} is to be used in Keycloak 23 and up. `,
break; `- The ${retrocompatJarFilePath} is to be used in Keycloak 21 and below.`,
case "mvn": ` Note that Keycloak 22 is only supported for login and email theme but not for account themes. `
logger.log("🫙 Run maven to deliver a jar"); ].join("\n"),
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakifyBuildDirPath }); "utf8"
break; )
default: );
assert<Equals<typeof buildOptions.bundler, never>>(false);
} }
// We want, however, to test in a container running the latest Keycloak version const containerKeycloakVersion = "23.0.0";
const containerKeycloakVersion = "21.1.2";
generateStartKeycloakTestingContainer({ generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
"keycloakVersion": containerKeycloakVersion, "keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions buildOptions
}); });
logger.log( logger.log(
[ [
"", "",
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(projectDirPath, jarFilePath)} 🚀`, ...(!buildOptions.doCreateJar
`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(reactAppRootDirPath, jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
""
]),
//TODO: Restore when we find a good Helm chart for Keycloak. //TODO: Restore when we find a good Helm chart for Keycloak.
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:", //"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
"", "",
@ -139,7 +133,7 @@ export async function main() {
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`, `To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"", "",
`👉 $ .${pathSep}${pathRelative( `👉 $ .${pathSep}${pathRelative(
projectDirPath, reactAppRootDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename) pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`, )} 👈`,
"", "",
@ -149,15 +143,15 @@ export async function main() {
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈", "- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
`- Create a realm: Master -> AddRealm -> Name: myrealm`, `- Create a realm: Master -> AddRealm -> Name: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`, `- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeName}`, `- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeNames[0]}`,
` Clients -> account -> Login theme: ${buildOptions.themeName}`, ` Clients -> account -> Login theme: ${buildOptions.themeNames[0]}`,
`- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeName} (option will appear only if you have ran npx initialize-email-theme)`, `- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeNames[0]} (option will appear only if you have ran npx initialize-email-theme)`,
`- Create a client Clients -> Create -> Client ID: myclient`, `- Create a client Clients -> Create -> Client ID: myclient`,
` Root URL: https://www.keycloak.org/app/`, ` Root URL: https://www.keycloak.org/app/`,
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`, ` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`, ` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
` Web origins: *`, ` Web origins: *`,
` Login Theme: ${buildOptions.themeName}`, ` Login Theme: ${buildOptions.themeNames[0]}`,
` Save (button at the bottom of the page)`, ` Save (button at the bottom of the page)`,
``, ``,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`, `- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,

View File

@ -4,8 +4,6 @@ import type { Equals } from "tsafe";
import { z } from "zod"; import { z } from "zod";
import { pathJoin } from "../tools/pathJoin"; import { pathJoin } from "../tools/pathJoin";
export const bundlers = ["mvn", "keycloakify", "none"] as const;
export type Bundler = (typeof bundlers)[number];
export type ParsedPackageJson = { export type ParsedPackageJson = {
name: string; name: string;
version?: string; version?: string;
@ -15,12 +13,12 @@ export type ParsedPackageJson = {
areAppAndKeycloakServerSharingSameDomain?: boolean; areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string; artifactId?: string;
groupId?: string; groupId?: string;
bundler?: Bundler; doCreateJar?: boolean;
keycloakVersionDefaultAssets?: string; loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string; reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
themeName?: string; themeName?: string | string[];
extraThemeNames?: string[]; doBuildRetrocompatAccountTheme?: boolean;
}; };
}; };
@ -34,12 +32,12 @@ export const zParsedPackageJson = z.object({
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(), "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(), "artifactId": z.string().optional(),
"groupId": z.string().optional(), "groupId": z.string().optional(),
"bundler": z.enum(bundlers).optional(), "doCreateJar": z.boolean().optional(),
"keycloakVersionDefaultAssets": z.string().optional(), "loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(), "reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(), "keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.string().optional(), "themeName": z.union([z.string(), z.array(z.string())]).optional(),
"extraThemeNames": z.array(z.string()).optional() "doBuildRetrocompatAccountTheme": z.boolean().optional()
}) })
.optional() .optional()
}); });
@ -47,11 +45,11 @@ export const zParsedPackageJson = z.object({
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>(); assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>; let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
export function getParsedPackageJson(params: { projectDirPath: string }) { export function getParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { projectDirPath } = params; const { reactAppRootDirPath } = params;
if (parsedPackageJson) { if (parsedPackageJson) {
return parsedPackageJson; return parsedPackageJson;
} }
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8"))); parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
return parsedPackageJson; return parsedPackageJson;
} }

View File

@ -16,7 +16,7 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
const cssGlobalsToDefine: Record<string, string> = {}; const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach( new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match) match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
); );

View File

@ -1,5 +0,0 @@
import { pathJoin } from "./tools/pathJoin";
export const basenameOfKeycloakDirInPublicDir = "keycloak-resources";
export const resourcesDirPathRelativeToPublicDir = pathJoin(basenameOfKeycloakDirInPublicDir, "resources");
export const resourcesCommonDirPathRelativeToPublicDir = pathJoin(resourcesDirPathRelativeToPublicDir, "resources_common");

View File

@ -162,7 +162,7 @@ export async function downloadAndUnzip(
} & ( } & (
| { | {
doUseCache: true; doUseCache: true;
projectDirPath: string; cacheDirPath: string;
} }
| { | {
doUseCache: false; doUseCache: false;
@ -182,11 +182,10 @@ export async function downloadAndUnzip(
} }
}); });
const cacheRoot = !rest.doUseCache const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
? `tmp_${Math.random().toString().slice(2, 12)}`
: pathJoin(process.env.XDG_CACHE_HOME ?? pathJoin(rest.projectDirPath, "node_modules", ".cache"), "keycloakify"); const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
const zipFilePath = pathJoin(cacheRoot, `${zipFileBasename}.zip`); const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
const extractDirPath = pathJoin(cacheRoot, `tmp_unzip_${zipFileBasename}`);
if (!(await exists(zipFilePath))) { if (!(await exists(zipFilePath))) {
const opts = await getFetchOptions(); const opts = await getFetchOptions();
@ -226,7 +225,7 @@ export async function downloadAndUnzip(
}); });
if (!rest.doUseCache) { if (!rest.doUseCache) {
await rm(cacheRoot, { "recursive": true }); await rm(cacheDirPath, { "recursive": true });
} else { } else {
await rm(extractDirPath, { "recursive": true }); await rm(extractDirPath, { "recursive": true });
} }

View File

@ -0,0 +1,15 @@
import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin } from "path";
export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: string }): string {
const { pathIsh, cwd } = params;
let pathOut = pathIsh;
pathOut = pathOut.replace(/\//g, pathSep);
if (!pathIsAbsolute(pathOut)) {
pathOut = pathJoin(cwd, pathOut);
}
return pathOut;
}

View File

@ -1,99 +0,0 @@
import { dirname, relative, sep, join } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
import { ZipFile } from "yazl";
import { mkdir } from "fs/promises";
import trimIndent from "./trimIndent";
export type ZipEntry = { zipPath: string } & ({ fsPath: string } | { buffer: Buffer });
export type ZipEntryGenerator = AsyncGenerator<ZipEntry, void, unknown>;
type CommonJarArgs = {
groupId: string;
artifactId: string;
version: string;
};
export type JarStreamArgs = CommonJarArgs & {
asyncPathGeneratorFn(): ZipEntryGenerator;
};
export type JarArgs = CommonJarArgs & {
targetPath: string;
rootPath: string;
};
export async function jarStream({ groupId, artifactId, version, asyncPathGeneratorFn }: JarStreamArgs) {
const manifestPath = "META-INF/MANIFEST.MF";
const manifestData = Buffer.from(trimIndent`
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Keycloakify
Built-By: unknown
Build-Jdk: 19.0.0
`);
const pomPropsPath = `META-INF/maven/${groupId}/${artifactId}/pom.properties`;
const pomPropsData = Buffer.from(trimIndent`
# Generated by keycloakify
# ${new Date()}
artifactId=${artifactId}
groupId=${groupId}
version=${version}
`);
const zipFile = new ZipFile();
for await (const entry of asyncPathGeneratorFn()) {
if ("buffer" in entry) {
zipFile.addBuffer(entry.buffer, entry.zipPath);
} else if ("fsPath" in entry) {
if (entry.fsPath.endsWith(sep)) {
zipFile.addEmptyDirectory(entry.zipPath);
} else {
zipFile.addFile(entry.fsPath, entry.zipPath);
}
}
}
zipFile.addBuffer(manifestData, manifestPath);
zipFile.addBuffer(pomPropsData, pomPropsPath);
zipFile.end();
return zipFile;
}
/**
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
* the contents of the pom.properties file which is going to be added to the archive.
* The root directory is expectedto have a conventional maven/gradle folder structure with a
* single `pom.xml` file at the root and a `src/main/resources` directory containing all
* application resources.
*/
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
await mkdir(dirname(targetPath), { recursive: true });
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
const resourcesPath = join(rootPath, "src", "main", "resources");
for await (const fsPath of walk(resourcesPath)) {
const zipPath = relative(resourcesPath, fsPath).split(sep).join("/");
yield { fsPath, zipPath };
}
yield {
fsPath: join(rootPath, "pom.xml"),
zipPath: `META-INF/maven/${groupId}/${artifactId}/pom.xml`
};
};
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });
await new Promise<void>(async (resolve, reject) => {
zipFile.outputStream
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
.on("close", () => resolve())
.on("error", e => reject(e));
});
}

View File

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

View File

@ -1,19 +0,0 @@
import { readdir } from "fs/promises";
import { resolve, sep } from "path";
/**
* Asynchronously and recursively walk a directory tree, yielding every file and directory
* found. Directory paths will _always_ end with a path separator.
*
* @param root the starting directory
* @returns AsyncGenerator
*/
export default async function* walk(root: string): AsyncGenerator<string, void, unknown> {
for (const entry of await readdir(root, { withFileTypes: true })) {
const absolutePath = resolve(root, entry.name);
if (entry.isDirectory()) {
yield absolutePath.endsWith(sep) ? absolutePath : absolutePath + sep;
yield* walk(absolutePath);
} else yield absolutePath.endsWith(sep) ? absolutePath.substring(0, absolutePath.length - 1) : absolutePath;
}
}

View File

@ -45,6 +45,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
return null; return null;
} }
document.title = i18n.msgStr("loginTitle", kcContext.realm.displayName);
return ( return (
<div className={getClassName("kcLoginClass")}> <div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={getClassName("kcHeaderClass")}> <div id="kc-header" className={getClassName("kcHeaderClass")}>

View File

@ -1,4 +1,5 @@
import type { LoginThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl"; import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { type ThemeType } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n"; import type { MessageKey } from "../i18n/i18n";
@ -81,6 +82,7 @@ export declare namespace KcContext {
clientId: string; clientId: string;
name?: string; name?: string;
description?: string; description?: string;
attributes: Record<string, string>;
}; };
isAppInitiatedAction: boolean; isAppInitiatedAction: boolean;
messagesPerField: { messagesPerField: {

View File

@ -8,9 +8,8 @@ import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow"; import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow"; import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin"; import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: { export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[]; mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
@ -148,11 +147,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any }; return { "kcContext": undefined as any };
} }
{ realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
return { "kcContext": realKcContext as any }; return { "kcContext": realKcContext as any };
} }

View File

@ -1,13 +1,11 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext"; import type { KcContext, Attribute } from "./KcContext";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath"; import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin"; import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl"; import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const attributes: Attribute[] = [ const attributes: Attribute[] = [
{ {
"validators": { "validators": {
@ -102,6 +100,10 @@ const attributes: Attribute[] = [
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any; 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");
export const kcContextCommonMock: KcContext.Common = { export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0", "themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0", "keycloakifyVersion": "0.0.0",
@ -109,8 +111,8 @@ export const kcContextCommonMock: KcContext.Common = {
"themeName": "my-theme-name", "themeName": "my-theme-name",
"url": { "url": {
"loginAction": "#", "loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir), resourcesPath,
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir), "resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg", "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" "loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
}, },
@ -232,7 +234,8 @@ export const kcContextCommonMock: KcContext.Common = {
"showTryAnotherWayLink": false "showTryAnotherWayLink": false
}, },
"client": { "client": {
"clientId": "myApp" "clientId": "myApp",
"attributes": {}
}, },
"scripts": [], "scripts": [],
"isAppInitiatedAction": false "isAppInitiatedAction": false
@ -312,7 +315,8 @@ export const kcContextMocks = [
"actionUri": "#", "actionUri": "#",
"client": { "client": {
"clientId": "myApp", "clientId": "myApp",
"baseUrl": "#" "baseUrl": "#",
"attributes": {}
} }
}), }),
id<KcContext.Error>({ id<KcContext.Error>({
@ -320,7 +324,8 @@ export const kcContextMocks = [
"pageId": "error.ftl", "pageId": "error.ftl",
"client": { "client": {
"clientId": "myApp", "clientId": "myApp",
"baseUrl": "#" "baseUrl": "#",
"attributes": {}
}, },
"message": { "message": {
"type": "error", "type": "error",
@ -494,7 +499,8 @@ export const kcContextMocks = [
}, },
"client": { "client": {
"clientId": "myApp", "clientId": "myApp",
"baseUrl": "#" "baseUrl": "#",
"attributes": {}
}, },
"logoutConfirm": { "code": "123", skipLink: false } "logoutConfirm": { "code": "123", skipLink: false }
}), }),

View File

@ -1,12 +1,11 @@
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import Template from "../Template";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { KcContext } from "../kcContext"; import { KcContext } from "../kcContext";
import { useGetClassName } from "../lib/useGetClassName"; import { useGetClassName } from "../lib/useGetClassName";
import { PageProps } from "./PageProps"; import { PageProps } from "./PageProps";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) { export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url } = kcContext; const { url } = kcContext;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;

View File

@ -2,11 +2,10 @@ import { clsx } from "keycloakify/tools/clsx";
import { PageProps } from "./PageProps"; import { PageProps } from "./PageProps";
import { KcContext } from "../kcContext"; import { KcContext } from "../kcContext";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import Template from "../Template";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName"; import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) { export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, oauth, client } = kcContext; const { url, oauth, client } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n; const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;

View File

@ -1,131 +0,0 @@
import jar, { jarStream, type ZipEntryGenerator } from "keycloakify/bin/tools/jar";
import { fromBuffer, Entry, ZipFile } from "yauzl";
import { it, describe, assert, afterAll } from "vitest";
import { Readable } from "stream";
import { tmpdir } from "os";
import { mkdtemp, cp, mkdir, rm, writeFile } from "fs/promises";
import path from "path";
import { createReadStream } from "fs";
import walk from "keycloakify/bin/tools/walk";
type AsyncIterable<T> = {
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
};
async function arrayFromAsync<T>(asyncIterable: AsyncIterable<T>) {
const chunks: T[] = [];
for await (const chunk of asyncIterable) chunks.push(chunk);
return chunks;
}
async function readToBuffer(stream: NodeJS.ReadableStream) {
return Buffer.concat(await arrayFromAsync(stream as AsyncIterable<Buffer>));
}
function unzipBuffer(buffer: Buffer) {
return new Promise<ZipFile>((resolve, reject) =>
fromBuffer(buffer, { lazyEntries: true }, (err, zipFile) => {
if (err !== null) {
reject(err);
} else {
resolve(zipFile);
}
})
);
}
function readEntry(zipFile: ZipFile, entry: Entry): Promise<Readable> {
return new Promise<Readable>((resolve, reject) => {
zipFile.openReadStream(entry, (err, stream) => {
if (err !== null) {
reject(err);
} else {
resolve(stream);
}
});
});
}
function readAll(zipFile: ZipFile): Promise<Map<string, Buffer>> {
return new Promise<Map<string, Buffer>>((resolve, reject) => {
const entries1: Map<string, Buffer> = new Map();
zipFile.on("entry", async (entry: Entry) => {
const stream = await readEntry(zipFile, entry);
const buffer = await readToBuffer(stream);
entries1.set(entry.fileName, buffer);
zipFile.readEntry();
});
zipFile.on("end", () => resolve(entries1));
zipFile.on("error", e => reject(e));
zipFile.readEntry();
});
}
describe("jar", () => {
const coords = { artifactId: "someArtifactId", groupId: "someGroupId", version: "1.2.3" };
const tmpDirs: string[] = [];
afterAll(async () => {
await Promise.all(tmpDirs.map(dir => rm(dir, { force: true, recursive: true })));
});
it("creates jar artifacts without error", async () => {
async function* mockFiles(): ZipEntryGenerator {
yield { zipPath: "foo", buffer: Buffer.from("foo") };
}
const zipped = await jarStream({ ...coords, asyncPathGeneratorFn: mockFiles });
const buffered = await readToBuffer(zipped.outputStream);
const unzipped = await unzipBuffer(buffered);
const entries = await readAll(unzipped);
assert.equal(entries.size, 3);
assert.isOk(entries.has("foo"));
assert.isOk(entries.has("META-INF/MANIFEST.MF"));
assert.isOk(entries.has("META-INF/maven/someGroupId/someArtifactId/pom.properties"));
assert.equal("foo", entries.get("foo")?.toString("utf-8"));
const manifest = entries.get("META-INF/MANIFEST.MF")?.toString("utf-8");
const pomProperties = entries.get("META-INF/maven/someGroupId/someArtifactId/pom.properties")?.toString("utf-8");
assert.isOk(manifest?.includes("Created-By: Keycloakify"));
assert.isOk(pomProperties?.includes("1.2.3"));
assert.isOk(pomProperties?.includes("someGroupId"));
assert.isOk(pomProperties?.includes("someArtifactId"));
});
it("creates a jar from _real_ files without error", async () => {
const tmp = await mkdtemp(path.join(tmpdir(), "kc-jar-test-"));
tmpDirs.push(tmp);
const rootPath = path.join(tmp, "root");
const resourcesPath = path.join(tmp, "root", "src", "main", "resources");
const targetPath = path.join(tmp, "jar.jar");
await mkdir(resourcesPath, { recursive: true });
await writeFile(path.join(rootPath, "pom.xml"), "foo", "utf-8");
await cp(path.dirname(__dirname), resourcesPath, { recursive: true });
await jar({ ...coords, rootPath, targetPath });
const buffered = await readToBuffer(createReadStream(targetPath));
const unzipped = await unzipBuffer(buffered);
const entries = await readAll(unzipped);
const zipPaths = Array.from(entries.keys());
assert.isOk(entries.has("META-INF/MANIFEST.MF"));
assert.isOk(entries.has("META-INF/maven/someGroupId/someArtifactId/pom.properties"));
assert.isOk(entries.has("META-INF/maven/someGroupId/someArtifactId/pom.xml"));
for await (const fsPath of walk(resourcesPath)) {
if (!fsPath.endsWith(path.sep)) {
const rel = path.relative(resourcesPath, fsPath).replace(path.sep === "/" ? /\//g : /\\/g, "/");
assert.isOk(zipPaths.includes(rel), `missing '${rel}' (${rel}, '${zipPaths.join("', '")}')`);
}
}
});
});

View File

@ -67,9 +67,6 @@ describe("Ensure it's able to extract used Keycloak resources", () => {
` `
}); });
console.log(paths);
console.log(expectedPaths);
expect(same(paths, expectedPaths)).toBe(true); expect(same(paths, expectedPaths)).toBe(true);
}); });

View File

@ -124,7 +124,7 @@ describe("bin/css-transforms", () => {
} }
.my-div2 { .my-div2 {
background: url(/logo192.png) no-repeat center center; background: url(/logo192.png) repeat center center;
} }
.my-div { .my-div {
@ -135,11 +135,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = ` const fixedCssCodeExpected = `
.my-div { .my-div {
background: var(--url1f9ef5a892c104c); background: var(--urla882a969fd39473) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: var(--url1f9ef5a892c104c); background: var(--urla882a969fd39473) repeat center center;
} }
.my-div { .my-div {
@ -150,7 +150,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = { const cssGlobalsToDefineExpected = {
"url1f9ef5a892c104c": "url(/logo192.png) no-repeat center center", "urla882a969fd39473": "url(/logo192.png)",
"urldd75cab58377c19": "url(/static/media/something.svg)" "urldd75cab58377c19": "url(/static/media/something.svg)"
}; };
@ -165,7 +165,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = ` const cssCodeToPrependInHeadExpected = `
:root { :root {
--url1f9ef5a892c104c: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center; --urla882a969fd39473: url(\${url.resourcesPath}/build/logo192.png);
--urldd75cab58377c19: url(\${url.resourcesPath}/build/static/media/something.svg); --urldd75cab58377c19: url(\${url.resourcesPath}/build/static/media/something.svg);
} }
`; `;
@ -191,11 +191,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = ` const fixedCssCodeExpected = `
.my-div { .my-div {
background: var(--urlf8277cddaa2be78); background: var(--url749a3139386b2c8) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: var(--urlf8277cddaa2be78); background: var(--url749a3139386b2c8) no-repeat center center;
} }
.my-div { .my-div {
@ -206,7 +206,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = { const cssGlobalsToDefineExpected = {
"urlf8277cddaa2be78": "url(/x/y/z/logo192.png) no-repeat center center", "url749a3139386b2c8": "url(/x/y/z/logo192.png)",
"url8bdc0887b97ac9a": "url(/x/y/z/static/media/something.svg)" "url8bdc0887b97ac9a": "url(/x/y/z/static/media/something.svg)"
}; };
@ -221,7 +221,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = ` const cssCodeToPrependInHeadExpected = `
:root { :root {
--urlf8277cddaa2be78: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center; --url749a3139386b2c8: url(\${url.resourcesPath}/build/logo192.png);
--url8bdc0887b97ac9a: url(\${url.resourcesPath}/build/static/media/something.svg); --url8bdc0887b97ac9a: url(\${url.resourcesPath}/build/static/media/something.svg);
} }
`; `;

View File

@ -24,7 +24,7 @@ vi.mock("keycloakify/bin/keycloakify/parsed-package-json", async () => ({
vi.mock("keycloakify/bin/promptKeycloakVersion", async () => ({ vi.mock("keycloakify/bin/promptKeycloakVersion", async () => ({
...((await vi.importActual("keycloakify/bin/promptKeycloakVersion")) as Record<string, unknown>), ...((await vi.importActual("keycloakify/bin/promptKeycloakVersion")) as Record<string, unknown>),
promptKeycloakVersion: () => ({ keycloakVersion: "11.0.3" }) promptKeycloakVersion: () => ({ "keycloakVersion": "11.0.3" })
})); }));
const nativeCwd = process.cwd; const nativeCwd = process.cwd;
@ -52,19 +52,25 @@ describe("Sample Project", () => {
await setupSampleReactProject(sampleReactProjectDirPath); await setupSampleReactProject(sampleReactProjectDirPath);
await initializeEmailTheme(); await initializeEmailTheme();
const projectDirPath = process.cwd(); const reactAppRootDirPath = process.cwd();
const destDirPath = pathJoin( const destDirPath = pathJoin(
readBuildOptions({ readBuildOptions({
"processArgv": ["--silent"], "processArgv": ["--silent"],
projectDirPath reactAppRootDirPath
}).keycloakifyBuildDirPath, }).keycloakifyBuildDirPath,
"src", "src",
"main", "main",
"resources", "resources",
"theme" "theme"
); );
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath }); await downloadBuiltinKeycloakTheme({
destDirPath,
"keycloakVersion": "11.0.3",
"buildOptions": {
"cacheDirPath": pathJoin(reactAppRootDirPath, "node_modules", ".cache", "keycloakify")
}
});
}, },
{ timeout: 90000 } { timeout: 90000 }
); );
@ -80,19 +86,25 @@ describe("Sample Project", () => {
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input")); await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
await initializeEmailTheme(); await initializeEmailTheme();
const projectDirPath = process.cwd(); const reactAppRootDirPath = process.cwd();
const destDirPath = pathJoin( const destDirPath = pathJoin(
readBuildOptions({ readBuildOptions({
"processArgv": ["--silent"], "processArgv": ["--silent"],
projectDirPath reactAppRootDirPath
}).keycloakifyBuildDirPath, }).keycloakifyBuildDirPath,
"src", "src",
"main", "main",
"resources", "resources",
"theme" "theme"
); );
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath }); await downloadBuiltinKeycloakTheme({
destDirPath,
"keycloakVersion": "11.0.3",
buildOptions: {
"cacheDirPath": pathJoin(reactAppRootDirPath, "node_modules", ".cache", "keycloakify")
}
});
}, },
{ timeout: 90000 } { timeout: 90000 }
); );

View File

@ -1,65 +0,0 @@
import trimIndent from "keycloakify/bin/tools/trimIndent";
import { it, describe, assert } from "vitest";
describe("trimIndent", () => {
it("does not change a left-aligned string as expected", () => {
const txt = trimIndent`lorem
ipsum`;
assert.equal(txt, ["lorem", "ipsum"].join("\n"));
});
it("removes leading and trailing empty lines from a left-aligned string", () => {
const txt = trimIndent`
lorem
ipsum
`;
assert.equal(txt, ["lorem", "ipsum"].join("\n"));
});
it("removes indent from an aligned string", () => {
const txt = trimIndent`
lorem
ipsum
`;
assert.equal(txt, ["lorem", "ipsum"].join("\n"));
});
it("removes indent from unaligned string", () => {
const txt = trimIndent`
lorem
ipsum
`;
assert.equal(txt, ["lorem", " ipsum"].join("\n"));
});
it("removes only first and last empty line", () => {
const txt = trimIndent`
lorem
ipsum
`;
assert.equal(txt, ["", "lorem", "ipsum", ""].join("\n"));
});
it("interpolates non-strings", () => {
const d = new Date();
const txt = trimIndent`
lorem
${d}
ipsum`;
assert.equal(txt, ["lorem", String(d), "ipsum"].join("\n"));
});
it("inderpolates preserving new-lines in the interpolated bits", () => {
const a = ["ipsum", "dolor", "sit"].join("\n");
const txt = trimIndent`
lorem
${a}
amet
`;
assert.equal(txt, ["lorem", "ipsum", "dolor", "sit", "amet"].join("\n"));
});
});

112
yarn.lock
View File

@ -4559,29 +4559,30 @@ check-error@^1.0.2:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
cheerio-select-tmp@^0.1.0: cheerio-select@^2.1.0:
version "0.1.1" version "2.1.0"
resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
dependencies: dependencies:
css-select "^3.1.2" boolbase "^1.0.0"
css-what "^4.0.0" css-select "^5.1.0"
domelementtype "^2.1.0" css-what "^6.1.0"
domhandler "^4.0.0" domelementtype "^2.3.0"
domutils "^2.4.4" domhandler "^5.0.3"
domutils "^3.0.1"
cheerio@1.0.0-rc.5: cheerio@^1.0.0-rc.5:
version "1.0.0-rc.5" version "1.0.0-rc.12"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
dependencies: dependencies:
cheerio-select-tmp "^0.1.0" cheerio-select "^2.1.0"
dom-serializer "~1.2.0" dom-serializer "^2.0.0"
domhandler "^4.0.0" domhandler "^5.0.3"
entities "~2.1.0" domutils "^3.0.1"
htmlparser2 "^6.0.0" htmlparser2 "^8.0.1"
parse5 "^6.0.0" parse5 "^7.0.0"
parse5-htmlparser2-tree-adapter "^6.0.0" parse5-htmlparser2-tree-adapter "^7.0.0"
chokidar@^2.1.8: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
@ -5163,17 +5164,6 @@ css-loader@^5.0.1:
schema-utils "^3.0.0" schema-utils "^3.0.0"
semver "^7.3.5" semver "^7.3.5"
css-select@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
dependencies:
boolbase "^1.0.0"
css-what "^4.0.0"
domhandler "^4.0.0"
domutils "^2.4.3"
nth-check "^2.0.0"
css-select@^4.1.3: css-select@^4.1.3:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
@ -5185,12 +5175,18 @@ css-select@^4.1.3:
domutils "^2.8.0" domutils "^2.8.0"
nth-check "^2.0.1" nth-check "^2.0.1"
css-what@^4.0.0: css-select@^5.1.0:
version "4.0.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
dependencies:
boolbase "^1.0.0"
css-what "^6.1.0"
domhandler "^5.0.2"
domutils "^3.0.1"
nth-check "^2.0.1"
css-what@^6.0.1: css-what@^6.0.1, css-what@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
@ -5475,15 +5471,6 @@ dom-serializer@^2.0.0:
domhandler "^5.0.2" domhandler "^5.0.2"
entities "^4.2.0" entities "^4.2.0"
dom-serializer@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
entities "^2.0.0"
dom-walk@^0.1.0: dom-walk@^0.1.0:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@ -5494,7 +5481,7 @@ domain-browser@^1.1.1:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
domelementtype@^2.0.1, domelementtype@^2.1.0, domelementtype@^2.2.0, domelementtype@^2.3.0: domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
@ -5513,7 +5500,7 @@ domhandler@^5.0, domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
dependencies: dependencies:
domelementtype "^2.3.0" domelementtype "^2.3.0"
domutils@^2.4.3, domutils@^2.4.4, domutils@^2.5.2, domutils@^2.8.0: domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@ -5671,11 +5658,6 @@ entities@^4.2.0, entities@^4.4.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
err-code@^2.0.2: err-code@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
@ -7005,7 +6987,7 @@ html-webpack-plugin@^5.0.0:
pretty-error "^4.0.0" pretty-error "^4.0.0"
tapable "^2.0.0" tapable "^2.0.0"
htmlparser2@^6.0.0, htmlparser2@^6.1.0: htmlparser2@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
@ -7015,7 +6997,7 @@ htmlparser2@^6.0.0, htmlparser2@^6.1.0:
domutils "^2.5.2" domutils "^2.5.2"
entities "^2.0.0" entities "^2.0.0"
htmlparser2@^8.0: htmlparser2@^8.0, htmlparser2@^8.0.1:
version "8.0.2" version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
@ -8841,7 +8823,7 @@ npmlog@^5.0.1:
gauge "^3.0.0" gauge "^3.0.0"
set-blocking "^2.0.0" set-blocking "^2.0.0"
nth-check@^2.0.0, nth-check@^2.0.1: nth-check@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
@ -9194,18 +9176,26 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0" json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6" lines-and-columns "^1.1.6"
parse5-htmlparser2-tree-adapter@^6.0.0: parse5-htmlparser2-tree-adapter@^7.0.0:
version "6.0.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
dependencies: dependencies:
parse5 "^6.0.1" domhandler "^5.0.2"
parse5 "^7.0.0"
parse5@^6.0.0, parse5@^6.0.1: parse5@^6.0.0:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
dependencies:
entities "^4.4.0"
parseurl@~1.3.2, parseurl@~1.3.3: parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"