Compare commits
409 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
a147084458 | |||
b25e171412 | |||
60aaa03202 | |||
3392ab8385 | |||
f172b94467 | |||
ca549fe8d8 | |||
0b6f56a774 | |||
e5bcff12cb | |||
2754900f7a | |||
54f43d3331 | |||
4900200c06 | |||
6d82a74db4 | |||
2d7f21b021 | |||
4292c0c642 | |||
9dca515a42 | |||
b577cd9829 | |||
704682cbbe | |||
858f0d77c0 | |||
31ef6063f2 | |||
f3bd81c55b | |||
24bb4902c2 | |||
ca7821cfad | |||
a73b25580e | |||
82e179730e | |||
b5d5002061 | |||
2ab2c9e05e | |||
b1e9ba3ac6 | |||
5822ed0185 | |||
17b295788d | |||
6cd5b958c7 | |||
df92cc5f73 | |||
03106cdee3 | |||
c4638daf1b | |||
e2f5eb79ad | |||
b6c8e9bca0 | |||
573839019e | |||
815bf10ae0 | |||
7c257d97a7 | |||
59f8814660 | |||
1a6993099f | |||
f62ded3c8e | |||
4eca6366cc | |||
51a45b355d | |||
e5765cb902 | |||
6e922d2033 | |||
5d1695ada8 | |||
6e3ce29067 | |||
2b9bbc4cef | |||
9557145f72 | |||
249877b9c5 | |||
ff2321fde5 | |||
1edd6e4193 | |||
c7d47f128e | |||
14cb07efb2 | |||
a51724208c | |||
050e2b2b99 | |||
3706f15f7e | |||
bdde9162d9 | |||
99b4933536 | |||
c5caf7e0da | |||
bcc5308cfb | |||
9fb902db5c | |||
7461e38034 | |||
dccd85a151 | |||
910604fdad | |||
508cb9158e | |||
915c500d32 | |||
60bd6621c8 | |||
b5f6262763 | |||
2b8c4422de | |||
a686432c65 | |||
449e625877 | |||
1ac07dafde | |||
3878e28b56 | |||
cf6bc8666b | |||
f76063eb40 | |||
ed52c5824d | |||
9333400322 | |||
3689cfcc0d | |||
b73eceb535 | |||
5dc3453fc9 | |||
cef1139a4b | |||
ac96959947 | |||
4d73d877ba | |||
9f1186302e | |||
319dcc0d15 | |||
e99fdb8561 | |||
f37a342a63 | |||
09a039894d | |||
3efbb1a9fd | |||
920ee62ee3 | |||
1ace44fe31 | |||
a60f05415b | |||
42c9d39e02 | |||
a8186f1ed9 | |||
c2ff515a17 | |||
960c3ba558 | |||
454a9cd01c | |||
7d42ce1c87 | |||
57f6f980cf | |||
8cba3aae2c | |||
01b32f78ed | |||
b6066dfd5f | |||
3ad554ed59 | |||
6aacc6361b | |||
638e4e6410 | |||
aa9b7cccc7 | |||
41739c8528 | |||
89b32dc7fc | |||
44aec23251 | |||
12fd6160c5 | |||
8819abc418 | |||
96b627095c | |||
239f98aa9c | |||
f5d0511662 | |||
75582d2a26 | |||
dba004f924 | |||
5423a07c47 | |||
aba725372e | |||
a61aa9dd5d | |||
74349b20ce | |||
09ab9a1c8f | |||
abfe5789a3 | |||
67ebac496d | |||
60a2bf173b | |||
4e03f07864 | |||
aef1709d7f | |||
2f590f7be2 | |||
d5fa6ca89a | |||
8eaaffb25a | |||
28c5e2bab2 | |||
e212039f2c | |||
99b0b67f77 | |||
6ec9ba3c01 | |||
d7960a7dcf | |||
2a6e9af9c9 | |||
327e4d1f90 | |||
fffadd7b9e | |||
aaaf0d2e77 | |||
9f9a9b8c90 | |||
1f6edb3c0c | |||
142efb4f99 | |||
532655d2d5 | |||
287edabd90 | |||
7aaedbe2ce | |||
4cae1c673c | |||
8e01d836a9 | |||
f6dc8f0741 | |||
3a976d08d2 | |||
50e83b1eb5 | |||
61fbbb0b09 | |||
9e70e5c12e | |||
69d9b64468 | |||
0620d29880 | |||
b52dc74d9b | |||
a46aef2e7e | |||
736806a53d | |||
f1475e5cdf | |||
d04724c70a | |||
bacaadc16d | |||
c51dd235f0 | |||
92f2c9857e | |||
3998cc7f8b | |||
c126d080bc | |||
bc05f1714d | |||
e98becb94b | |||
250b94c8b5 | |||
47f03f6833 | |||
6e7ae48f78 | |||
526dbcc0e7 | |||
1abc5a5643 | |||
c81c350136 | |||
f90dc8bc7e | |||
072e22d072 | |||
59807c1bb0 | |||
7c19e1f1f7 | |||
3b9f915f57 | |||
d85cc530d4 | |||
2bb27c7642 | |||
e90e003204 | |||
b1e58e1add | |||
0fd836314a | |||
0bc3f08cc1 | |||
a78af5080a | |||
074e465284 | |||
bc8165d0ae | |||
ba8561d75a | |||
b2d381ba4b | |||
d39353d332 | |||
ee916af48e | |||
da1dc0309b | |||
30f4e7d833 | |||
cf3a86fb9b | |||
e1633f43f4 | |||
5b64cfc23c | |||
19709cf085 | |||
b8bb6c4f02 | |||
b7a543f8cb | |||
04b4e19563 | |||
ffb27fc66d | |||
8b5f7eefda | |||
c750bf4ee8 | |||
aa74019ef6 | |||
9be6d9f75f | |||
81ebb9b552 | |||
5e13b8c41f | |||
dd1ed948ec | |||
8b93f701cf | |||
2f0084de5b | |||
2ef9828625 | |||
89db8983a7 | |||
287dd9bd31 | |||
9a92054c1a | |||
4189036213 | |||
2c0a427ba5 | |||
77b488d624 | |||
5249e05746 | |||
1e7a0dd7a6 | |||
fd67f2402a | |||
60a65ede2f | |||
1fa659ce61 | |||
0ab903dbc7 | |||
70b0a04793 | |||
c0df9aa939 | |||
60a1886942 | |||
1ebf97871b | |||
72e321aa32 | |||
b0f602b565 | |||
84c774503d | |||
9bbc7cc651 | |||
458083fb6d | |||
8dcfc840b4 | |||
9d06a3a6ad | |||
86cd08b954 | |||
144c3cc082 | |||
802cef41a6 | |||
e128e8f0a9 | |||
8a25b93ab2 | |||
7a040935e9 | |||
2015882688 | |||
379301eb9d | |||
5d86b05cdb | |||
73c99d3157 | |||
acba197c94 | |||
2441d8ed8a | |||
9c123f37c8 | |||
b48dbd99cf | |||
25c8599d8f | |||
3453a17c15 | |||
6e95dacd3a | |||
a286e252e9 | |||
a8997e92c3 | |||
89137153a0 | |||
e3382de8e0 | |||
1a48681591 | |||
8f006f0009 | |||
77e32aad2a | |||
8d365dae53 | |||
01fb89674c | |||
e3144adc61 | |||
c9fb0ca6ae | |||
82d7e1371e | |||
e1341dfdba | |||
7f917311d8 | |||
2bfb856f07 | |||
702f52f1c9 | |||
7ba8649940 | |||
485ca28a29 | |||
33460afaf2 | |||
2421ac2c11 | |||
f0cdb0b80b | |||
2af953927e | |||
dcb9fbd0f7 | |||
5bc1f6479d | |||
f3e4bca468 | |||
54645f5cff | |||
a7f3e00821 | |||
108c281b0c | |||
58892cbb56 | |||
dae1053ca8 | |||
83a9778c30 | |||
c52157bfb9 | |||
62bf846d5f | |||
148f7fa316 | |||
f488327885 | |||
593b929254 | |||
9218e97315 | |||
beb0e8bd77 | |||
cace66e9f8 | |||
ef850c71fd | |||
aa8dc1919f | |||
c7c9b19853 | |||
68c26e0f5b | |||
6bcdf286ef | |||
d9345396e8 | |||
4c423900d4 | |||
504419b26d | |||
6e058eafed | |||
08fc9d8631 | |||
e8a11991a0 | |||
3e6d679838 | |||
4dad859c4d | |||
ef9c933ca8 | |||
0461190a67 | |||
06b3211b08 | |||
2033a9ce0c | |||
fca18d9209 | |||
4f99088449 | |||
b1da684008 | |||
89fb6de2d5 | |||
b665bae3bb | |||
0b5a7544ca | |||
183826ca0d | |||
e507aace6b | |||
43c93ef0b4 | |||
093e51e092 | |||
17e1655eaf | |||
6b570f2b9a | |||
f239d105a7 | |||
776d8378e3 | |||
dd770cd7c6 | |||
4b3de54e18 | |||
5741cd1b2b | |||
b780d7136e | |||
3c28a05746 | |||
57ac5badba | |||
e873eb5123 | |||
c1a63edd71 | |||
37a060c4db | |||
157e4ac485 | |||
ba4d9675a8 | |||
e011fb094c | |||
f55a934939 | |||
96a88fe865 | |||
6cdb83d730 | |||
95f06df45d | |||
ec52b357d5 | |||
d84546cd7d | |||
4eee4156da | |||
70f475d13e | |||
3a50a61b12 | |||
a217f617d8 | |||
fdfcd78f02 | |||
56d6d8001a | |||
c3ee8e10e6 | |||
2f42732deb | |||
956b8260e7 | |||
b7954f87e0 | |||
540ce55dc2 | |||
d71a2c98d1 | |||
cb9cec676d | |||
9f2755bc7f | |||
fbe5a1f477 | |||
338642094d | |||
a3270d10f0 | |||
4c5924556a | |||
99a9b62c6c | |||
1497672a4e | |||
01161fd8ef | |||
68f5ee42e6 | |||
53955a0713 | |||
2271fd43b8 | |||
6a44cfb876 | |||
37c90d53e0 | |||
9a5ac5f13f | |||
6603852355 | |||
5670a71e6b | |||
332b1f74d9 | |||
c28caaa495 | |||
74fed835e8 | |||
6bb7f7dc16 | |||
84bb2338d1 | |||
caa42538a1 | |||
f5b9a8de55 | |||
dd33f554da | |||
7e84d0b108 | |||
9e1217fbf0 | |||
26d3c7f9e0 | |||
76542e6859 | |||
3c4bbf8aa7 | |||
ccc5ac6a1f | |||
b34f86d2f0 | |||
ee5f73519a | |||
22e7ff1424 | |||
7a89888d11 | |||
64fe15cf8c | |||
336813646f | |||
53a18c462a | |||
b893eee086 | |||
792020dd18 | |||
0c11ba05af | |||
2cf82f510e | |||
7c0cbe3a31 | |||
08e659cf01 | |||
97c3f4fa5f | |||
06a24d35cb | |||
9b6d1a957f | |||
189bd4697a | |||
d52252cd55 | |||
303bbc8431 | |||
727dc471c2 | |||
45b5c21ab5 | |||
adddce7764 | |||
b35a9f8f61 | |||
34b46a9280 | |||
383a9953e2 | |||
94bc7127fa | |||
289f0efd24 | |||
3f15586dae |
21
.github/workflows/ci.yaml
vendored
21
.github/workflows/ci.yaml
vendored
@ -16,8 +16,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- name: If this step fails run 'yarn format' then commit again.
|
||||
run: yarn format:check
|
||||
- name: If this step fails run 'npm run format' then commit again.
|
||||
run: npm run _format --list-different
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: test_lint
|
||||
@ -32,13 +32,12 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
#- run: yarn test:keycloakify-starter
|
||||
- run: npm run build
|
||||
- run: npm run test
|
||||
|
||||
storybook:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
#if: github.event_name == 'push'
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -46,11 +45,11 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build-storybook -o ./build_storybook
|
||||
- run: npm run build-storybook
|
||||
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>"
|
||||
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./storybook-static -u "github-actions-bot <actions@github.com>"
|
||||
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
@ -88,7 +87,7 @@ jobs:
|
||||
needs:
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
@ -112,8 +111,8 @@ jobs:
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx -y -p denoify@1.6.10 enable_short_npm_import_path
|
||||
- run: npm run build
|
||||
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
|
||||
env:
|
||||
DRY_RUN: "0"
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -48,8 +48,8 @@ jspm_packages
|
||||
|
||||
.idea
|
||||
|
||||
/src/login/i18n/baseMessages/
|
||||
/src/account/i18n/baseMessages/
|
||||
/src/login/i18n/messages_defaultSet/
|
||||
/src/account/i18n/messages_defaultSet/
|
||||
|
||||
# VS Code devcontainers
|
||||
.devcontainer
|
||||
|
@ -6,8 +6,8 @@ node_modules/
|
||||
/src/tools/types/
|
||||
/build_keycloak/
|
||||
/.vscode/
|
||||
/src/login/i18n/baseMessages/
|
||||
/src/account/i18n/baseMessages/
|
||||
/src/login/i18n/messages_defaultSet/
|
||||
/src/account/i18n/messages_defaultSet/
|
||||
/dist_test
|
||||
/sample_react_project/
|
||||
/sample_custom_react_project/
|
||||
|
@ -1,11 +1,24 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"printWidth": 90,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "preserve",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
"arrowParens": "avoid",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "useUserProfileForm.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ export function DocsContainer({ children, context }) {
|
||||
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
<BaseContainer
|
||||
context={{
|
||||
@ -64,11 +63,6 @@ export function CanvasContainer({ children }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
17
.storybook/preview-head.html
Normal file
17
.storybook/preview-head.html
Normal file
@ -0,0 +1,17 @@
|
||||
<style>
|
||||
body.sb-show-main.sb-main-padded {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body:not(.kcBodyClass) {
|
||||
background-color: #393939;
|
||||
}
|
||||
|
||||
body.sb-show-preparing-docs > .sb-wrapper {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body .sb-preparing-story {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
@ -116,10 +116,45 @@ const { getHardCodedWeight } = (() => {
|
||||
const orderedPagesPrefix = [
|
||||
"Introduction",
|
||||
"login/login.ftl",
|
||||
"login/register-user-profile.ftl",
|
||||
"login/register.ftl",
|
||||
"login/terms.ftl",
|
||||
"login/error.ftl",
|
||||
"login/code.ftl",
|
||||
"login/delete-account-confirm.ftl",
|
||||
"login/delete-credential.ftl",
|
||||
"login/frontchannel-logout.ftl",
|
||||
"login/idp-review-user-profile.ftl",
|
||||
"login/info.ftl",
|
||||
"login/login-config-totp.ftl",
|
||||
"login/login-idp-link-confirm.ftl",
|
||||
"login/login-idp-link-email.ftl",
|
||||
"login/login-oauth-grant.ftl",
|
||||
"login/login-otp.ftl",
|
||||
"login/login-page-expired.ftl",
|
||||
"login/login-password.ftl",
|
||||
"login/login-reset-otp.ftl",
|
||||
"login/login-reset-password.ftl",
|
||||
"login/login-update-password.ftl",
|
||||
"login/login-update-profile.ftl",
|
||||
"login/login-username.ftl",
|
||||
"login/login-verify-email.ftl",
|
||||
"login/login-x509-info.ftl",
|
||||
"login/logout-confirm.ftl",
|
||||
"login/saml-post-form.ftl",
|
||||
"login/select-authenticator.ftl",
|
||||
"login/update-email.ftl",
|
||||
"login/webauthn-authenticate.ftl",
|
||||
"login/webauthn-error.ftl",
|
||||
"login/webauthn-register.ftl",
|
||||
"login/login-oauth2-device-verify-user-code.ftl",
|
||||
"login/login-recovery-authn-code-config.ftl",
|
||||
"login/login-recovery-authn-code-input.ftl",
|
||||
"account/account.ftl",
|
||||
"account/password.ftl",
|
||||
"account/federatedIdentity.ftl",
|
||||
"account/log.ftl",
|
||||
"account/sessions.ftl",
|
||||
"account/totp.ftl",
|
||||
];
|
||||
|
||||
function getHardCodedWeight(kind) {
|
||||
|
49
.storybook/static/terms/en.md
Normal file
49
.storybook/static/terms/en.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Overview
|
||||
|
||||
This Terms of Service document outlines the rules and regulations for the use of **Example Company's** Services.
|
||||
|
||||
## Acceptance of Terms
|
||||
|
||||
By accessing and using our services, you acknowledge that you have read, understood, and agree to be bound by these terms. If you do not accept these terms, you are not authorized to use our services.
|
||||
|
||||
## Description of Service
|
||||
|
||||
**Example Service** (hereinafter referred to as "the Service") is a web-based solution offered by **Example Company** (hereinafter referred to as "the Company"). Our service provides users with access to [documentation](https://example.com/docs) and support for managing their projects effectively.
|
||||
|
||||
## Modifications to the Terms of Service
|
||||
|
||||
The Company reserves the right to modify these terms at any time. Such modifications will be effective immediately upon posting the updated terms on our website. Your continued use of the Service after any such changes shall constitute your consent to such changes.
|
||||
|
||||
## Account Registration
|
||||
|
||||
You may be required to register with the Service to access certain features. When registering, you agree to provide accurate, current, and complete information about yourself as requested.
|
||||
|
||||
## User Responsibilities
|
||||
|
||||
- **Data Security**: Users are responsible for safeguarding their login credentials and should not disclose their passwords to any third party.
|
||||
- **Acceptable Use**: Users are expected to use the Service in a responsible manner that does not infringe upon the rights of others.
|
||||
- **Content Ownership**: Users retain all rights to the content they upload to the Service but grant the Company a license to use and distribute this content as part of the Service.
|
||||
|
||||
## Intellectual Property
|
||||
|
||||
All intellectual property rights related to the Service and its original content, features, and functionality are owned by the Company.
|
||||
|
||||
## Termination
|
||||
|
||||
The Company may terminate or suspend access to our Service immediately, without prior notice or liability, for any reason whatsoever, including, without limitation, breach of these Terms.
|
||||
|
||||
## Governing Law
|
||||
|
||||
These Terms shall be governed and construed in accordance with the laws of [Your Country], without regard to its conflict of law provisions.
|
||||
|
||||
## Contact Information
|
||||
|
||||
For any questions about these Terms, please contact us at [support@example.com](mailto:support@example.com) or visit our [FAQ page](https://example.com/faq).
|
||||
|
||||
## Changes to Terms of Service
|
||||
|
||||
We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect.
|
||||
|
||||
## Effective Date
|
||||
|
||||
These terms are effective as of **[Insert Date]**.
|
49
.storybook/static/terms/es.md
Normal file
49
.storybook/static/terms/es.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Resumen
|
||||
|
||||
Este documento de Términos de Servicio detalla las reglas y regulaciones para el uso de los servicios de **Empresa Ejemplo**.
|
||||
|
||||
## Aceptación de Términos
|
||||
|
||||
Al acceder y utilizar nuestros servicios, usted reconoce que ha leído, entendido y acepta estar vinculado por estos términos. Si no acepta estos términos, no está autorizado para usar nuestros servicios.
|
||||
|
||||
## Descripción del Servicio
|
||||
|
||||
**Servicio Ejemplo** (en adelante denominado "el Servicio") es una solución basada en la web ofrecida por **Empresa Ejemplo** (en adelante denominada "la Empresa"). Nuestro servicio proporciona a los usuarios acceso a [documentación](https://ejemplo.com/docs) y soporte para gestionar sus proyectos de manera efectiva.
|
||||
|
||||
## Modificaciones a los Términos de Servicio
|
||||
|
||||
La Empresa se reserva el derecho de modificar estos términos en cualquier momento. Dichas modificaciones entrarán en vigor inmediatamente después de la publicación de los términos actualizados en nuestro sitio web. Su uso continuado del Servicio después de tales cambios constituirá su consentimiento a dichos cambios.
|
||||
|
||||
## Registro de Cuenta
|
||||
|
||||
Puede ser necesario que se registre en el Servicio para acceder a ciertas características. Al registrarse, usted acepta proporcionar información precisa, actual y completa sobre sí mismo como se solicita.
|
||||
|
||||
## Responsabilidades del Usuario
|
||||
|
||||
- **Seguridad de Datos**: Los usuarios son responsables de salvaguardar sus credenciales de inicio de sesión y no deben divulgar sus contraseñas a terceros.
|
||||
- **Uso Aceptable**: Se espera que los usuarios utilicen el Servicio de manera responsable que no infrinja los derechos de otros.
|
||||
- **Propiedad del Contenido**: Los usuarios retienen todos los derechos sobre el contenido que cargan en el Servicio, pero otorgan a la Empresa una licencia para usar y distribuir este contenido como parte del Servicio.
|
||||
|
||||
## Propiedad Intelectual
|
||||
|
||||
Todos los derechos de propiedad intelectual relacionados con el Servicio y su contenido original, características y funcionalidad son propiedad de la Empresa.
|
||||
|
||||
## Terminación
|
||||
|
||||
La Empresa puede terminar o suspender su acceso a nuestro Servicio de inmediato, sin previo aviso ni responsabilidad, por cualquier motivo, incluido, entre otros, una violación de estos Términos.
|
||||
|
||||
## Ley Aplicable
|
||||
|
||||
Estos Términos se regirán e interpretarán de acuerdo con las leyes de [Su País], sin tener en cuenta sus disposiciones de conflicto de leyes.
|
||||
|
||||
## Información de Contacto
|
||||
|
||||
Para cualquier pregunta sobre estos Términos, contáctenos en [support@ejemplo.com](mailto:support@ejemplo.com) o visite nuestra [página de FAQ](https://ejemplo.com/faq).
|
||||
|
||||
## Cambios a los Términos de Servicio
|
||||
|
||||
Nos reservamos el derecho, a nuestra única discreción, de modificar o reemplazar estos Términos en cualquier momento. Si una revisión es material, proporcionaremos al menos 30 días de aviso antes de que los nuevos términos entren en vigor.
|
||||
|
||||
## Fecha de Efectividad
|
||||
|
||||
Estos términos son efectivos a partir del **[Insertar Fecha]**.
|
49
.storybook/static/terms/fr.md
Normal file
49
.storybook/static/terms/fr.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document des Conditions Générales d'Utilisation détaille les règles et réglementations pour l'utilisation des services de **l'Entreprise Exemple**.
|
||||
|
||||
## Acceptation des Conditions
|
||||
|
||||
En accédant et en utilisant nos services, vous reconnaissez avoir lu, compris et accepté d'être lié par ces conditions. Si vous n'acceptez pas ces termes, vous n'êtes pas autorisé à utiliser nos services.
|
||||
|
||||
## Description du Service
|
||||
|
||||
**Service Exemple** (ci-après dénommé "le Service") est une solution basée sur le web offerte par **l'Entreprise Exemple** (ci-après dénommée "l'Entreprise"). Notre service offre aux utilisateurs un accès à la [documentation](https://exemple.com/docs) et un support pour gérer efficacement leurs projets.
|
||||
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
L'Entreprise se réserve le droit de modifier ces conditions à tout moment. De telles modifications entreront en vigueur immédiatement après la publication des termes mis à jour sur notre site web. Votre utilisation continue du Service après de tels changements constitue votre consentement à ces modifications.
|
||||
|
||||
## Inscription au Compte
|
||||
|
||||
Vous devrez peut-être vous inscrire au Service pour accéder à certaines fonctionnalités. Lors de l'inscription, vous acceptez de fournir des informations précises, actuelles et complètes vous concernant, comme demandé.
|
||||
|
||||
## Responsabilités des Utilisateurs
|
||||
|
||||
- **Sécurité des Données** : Les utilisateurs sont responsables de la sauvegarde de leurs identifiants de connexion et ne doivent divulguer leurs mots de passe à aucun tiers.
|
||||
- **Utilisation Acceptable** : Les utilisateurs sont censés utiliser le Service de manière responsable qui ne porte pas atteinte aux droits d'autrui.
|
||||
- **Propriété du Contenu** : Les utilisateurs conservent tous les droits sur le contenu qu'ils téléchargent sur le Service mais accordent à l'Entreprise une licence pour utiliser et distribuer ce contenu dans le cadre du Service.
|
||||
|
||||
## Propriété Intellectuelle
|
||||
|
||||
Tous les droits de propriété intellectuelle relatifs au Service et à son contenu original, fonctionnalités et fonctionnement sont détenus par l'Entreprise.
|
||||
|
||||
## Résiliation
|
||||
|
||||
L'Entreprise peut résilier ou suspendre votre accès à notre Service immédiatement, sans préavis ni responsabilité, pour quelque raison que ce soit, y compris, sans limitation, en cas de violation de ces Conditions.
|
||||
|
||||
## Loi Applicable
|
||||
|
||||
Ces Conditions seront régies et interprétées conformément aux lois de [Votre Pays], sans égard à ses dispositions de conflit de lois.
|
||||
|
||||
## Informations de Contact
|
||||
|
||||
Pour toute question concernant ces Conditions, veuillez nous contacter à [support@exemple.com](mailto:support@exemple.com) ou visitez notre [page FAQ](https://exemple.com/faq).
|
||||
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
Nous nous réservons le droit, à notre seule discrétion, de modifier ou de remplacer ces Conditions à tout moment. Si une révision est importante, nous vous fournirons un préavis d'au moins 30 jours avant que les nouveaux termes prennent effet.
|
||||
|
||||
## Date d'Effet
|
||||
|
||||
Ces conditions sont effectives à partir du **[Insérer la Date]**.
|
14
README.md
14
README.md
@ -17,9 +17,12 @@
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/kYFZG7fQmn">
|
||||
<img src="https://img.shields.io/discord/1097708346976505977"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
Check out our discord server!<br/>
|
||||
<a href="https://discord.gg/mJdYJSdcm4">
|
||||
<img src="https://dcbadge.limes.pink/api/server/kYFZG7fQmn"/>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.keycloakify.dev">Home</a>
|
||||
-
|
||||
@ -38,10 +41,9 @@
|
||||
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
|
||||
</p>
|
||||
|
||||
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)!
|
||||
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, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
|
||||
> NOTE: Keycloak 24 introduces [important changes](https://www.keycloak.org/docs/latest/upgrading/index.html#changes-to-freemarker-templates-to-render-pages-based-on-the-user-profile-and-realm).
|
||||
> We're actively working on incorporating them into Keycloakify. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
||||
> NOTE: Keycloakify 10 is still in realase-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
||||
|
||||
## Sponsor
|
||||
|
||||
|
56
package.json
56
package.json
@ -1,25 +1,23 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.13",
|
||||
"version": "10.0.0-rc.112",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "ts-node --skipProject scripts/generate-i18n-messages.ts && patch-package",
|
||||
"build": "ts-node --skipProject scripts/build.ts",
|
||||
"watch": "chokidar './src/**/*' -c 'yarn build'",
|
||||
"prepare": "tsx scripts/generate-i18n-messages.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"storybook": "tsx scripts/start-storybook.ts",
|
||||
"link-in-starter": "tsx scripts/link-in-starter.ts",
|
||||
"test": "yarn test:types && vitest run",
|
||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||
"format": "yarn _format --write",
|
||||
"format:check": "yarn _format --list-different",
|
||||
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
|
||||
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts",
|
||||
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/main.js copy-keycloak-resources-to-public",
|
||||
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
|
||||
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
|
||||
"link-in-app": "tsx scripts/link-in-app.ts",
|
||||
"build-storybook": "tsx scripts/build-storybook.ts",
|
||||
"dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
|
||||
},
|
||||
"bin": {
|
||||
"keycloakify": "dist/bin/main.js"
|
||||
@ -37,37 +35,33 @@
|
||||
"author": "u/garronej",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src/",
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/constants.d.ts",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"dist/bin/shared/*.d.ts",
|
||||
"dist/bin/shared/*.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
"dist/vite-plugin/index.ts"
|
||||
"dist/vite-plugin/vite-plugin.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"bluehats",
|
||||
"keycloak",
|
||||
"react",
|
||||
"theme",
|
||||
"FreeMarker",
|
||||
"ftl",
|
||||
"login",
|
||||
"register"
|
||||
"register",
|
||||
"account",
|
||||
"bluehats"
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"evt": "^2.5.7",
|
||||
"minimal-polyfills": "^2.2.3",
|
||||
"react-markdown": "^5.0.3",
|
||||
"tsafe": "^1.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -93,7 +87,6 @@
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@types/yazl": "^2.4.5",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@ -104,24 +97,23 @@
|
||||
"lint-staged": "^11.0.0",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"powerhooks": "^1.0.10",
|
||||
"prettier": "^2.3.0",
|
||||
"prettier": "^3.2.5",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recast": "^0.23.3",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"run-exclusive": "^2.2.19",
|
||||
"storybook-dark-mode": "^1.1.2",
|
||||
"termost": "^0.12.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"termost": "^v0.12.1",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tss-react": "^4.9.10",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^4.9.1-beta",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^0.29.8",
|
||||
"vitest": "^1.6.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1",
|
||||
"zod": "^3.17.10"
|
||||
"zod": "^3.17.10",
|
||||
"evt": "^2.5.7",
|
||||
"tsx": "^4.15.5"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
19
scripts/build-storybook.ts
Normal file
19
scripts/build-storybook.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as child_process from "child_process";
|
||||
import { join } from "path";
|
||||
|
||||
run("yarn build");
|
||||
|
||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
||||
env: {
|
||||
...process.env,
|
||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
||||
}
|
||||
});
|
||||
|
||||
run("npx build-storybook");
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
135
scripts/build.ts
135
scripts/build.ts
@ -3,12 +3,20 @@ import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "../src/bin/tools/transformCodebase";
|
||||
import chalk from "chalk";
|
||||
|
||||
console.log(chalk.cyan("Building Keycloakify..."));
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
|
||||
fs.renameSync(join("dist", "bin", "main.original.js"), join("dist", "bin", "main.js"));
|
||||
fs.renameSync(
|
||||
join("dist", "bin", "main.original.js"),
|
||||
join("dist", "bin", "main.js")
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (/[0-9]\.index.js/.test(fileBasename)) {
|
||||
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
|
||||
fs.rmSync(join("dist", "bin", fileBasename));
|
||||
}
|
||||
});
|
||||
@ -16,65 +24,140 @@ if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
|
||||
|
||||
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
|
||||
|
||||
fs.cpSync(join("dist", "bin", "main.js"), join("dist", "bin", "main.original.js"));
|
||||
if (
|
||||
!fs
|
||||
.readFileSync(join("dist", "bin", "main.js"))
|
||||
.toString("utf8")
|
||||
.includes("__nccwpck_require__")
|
||||
) {
|
||||
fs.cpSync(join("dist", "bin", "main.js"), join("dist", "bin", "main.original.js"));
|
||||
}
|
||||
|
||||
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": join("dist", "ncc_out"),
|
||||
"destDirPath": join("dist", "bin"),
|
||||
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
destDirPath: join("dist", "bin"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath === "index.js") {
|
||||
return {
|
||||
"newFileName": "main.js",
|
||||
"modifiedSourceCode": sourceCode
|
||||
newFileName: "main.js",
|
||||
modifiedSourceCode: sourceCode
|
||||
};
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
patchDeprecatedBufferApiUsage(join("dist", "bin", "main.js"));
|
||||
{
|
||||
let hasBeenPatched = false;
|
||||
|
||||
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage(
|
||||
join("dist", "bin", fileBasename)
|
||||
);
|
||||
|
||||
if (hasBeenPatched_i) {
|
||||
hasBeenPatched = true;
|
||||
}
|
||||
});
|
||||
|
||||
assert(hasBeenPatched);
|
||||
}
|
||||
|
||||
fs.chmodSync(
|
||||
join("dist", "bin", "main.js"),
|
||||
fs.statSync(join("dist", "bin", "main.js")).mode | fs.constants.S_IXUSR | fs.constants.S_IXGRP | fs.constants.S_IXOTH
|
||||
fs.statSync(join("dist", "bin", "main.js")).mode |
|
||||
fs.constants.S_IXUSR |
|
||||
fs.constants.S_IXGRP |
|
||||
fs.constants.S_IXOTH
|
||||
);
|
||||
|
||||
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
|
||||
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
|
||||
|
||||
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
|
||||
fs.renameSync(join("dist", "vite-plugin", "index.original.js"), join("dist", "vite-plugin", "index.js"));
|
||||
fs.renameSync(
|
||||
join("dist", "vite-plugin", "index.original.js"),
|
||||
join("dist", "vite-plugin", "index.js")
|
||||
);
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
|
||||
|
||||
fs.cpSync(join("dist", "vite-plugin", "index.js"), join("dist", "vite-plugin", "index.original.js"));
|
||||
if (
|
||||
!fs
|
||||
.readFileSync(join("dist", "vite-plugin", "index.js"))
|
||||
.toString("utf8")
|
||||
.includes("__nccwpck_require__")
|
||||
) {
|
||||
fs.cpSync(
|
||||
join("dist", "vite-plugin", "index.js"),
|
||||
join("dist", "vite-plugin", "index.original.js")
|
||||
);
|
||||
}
|
||||
|
||||
run(`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join("dist", "ncc_out")}`);
|
||||
run(
|
||||
`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
|
||||
"dist",
|
||||
"ncc_out"
|
||||
)}`
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => {
|
||||
assert(!fileBasename.endsWith(".index.js"));
|
||||
assert(!fileBasename.endsWith(".node"));
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": join("dist", "ncc_out"),
|
||||
"destDirPath": join("dist", "vite-plugin"),
|
||||
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
destDirPath: join("dist", "vite-plugin"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
assert(fileRelativePath === "index.js");
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
patchDeprecatedBufferApiUsage(join("dist", "vite-plugin", "index.js"));
|
||||
{
|
||||
const { hasBeenPatched } = patchDeprecatedBufferApiUsage(
|
||||
join("dist", "vite-plugin", "index.js")
|
||||
);
|
||||
|
||||
assert(hasBeenPatched);
|
||||
}
|
||||
|
||||
fs.rmSync(join("dist", "src"), { recursive: true, force: true });
|
||||
|
||||
fs.cpSync("src", join("dist", "src"), { recursive: true });
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("stories"),
|
||||
destDirPath: join("dist", "stories"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (!fileRelativePath.endsWith(".stories.tsx")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
|
||||
|
||||
function run(command: string) {
|
||||
console.log(`$ ${command}`);
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { "stdio": "inherit" });
|
||||
child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
@ -85,7 +168,9 @@ function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
||||
);
|
||||
|
||||
assert(after !== before);
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||
|
||||
const hasBeenPatched = after !== before;
|
||||
|
||||
return { hasBeenPatched };
|
||||
}
|
||||
|
92
scripts/dump-keycloak-realm.ts
Normal file
92
scripts/dump-keycloak-realm.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { CONTAINER_NAME } from "../src/bin/shared/constants";
|
||||
import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import chalk from "chalk";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
(async () => {
|
||||
{
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn(
|
||||
"docker",
|
||||
[
|
||||
...["exec", CONTAINER_NAME],
|
||||
...["/opt/keycloak/bin/kc.sh", "export"],
|
||||
...["--dir", "/tmp"],
|
||||
...["--realm", "myrealm"],
|
||||
...["--users", "realm_file"]
|
||||
],
|
||||
{ shell: true }
|
||||
);
|
||||
|
||||
let output = "";
|
||||
|
||||
const onExit = (code: number | null) => {
|
||||
dCompleted.reject(new Error(`Exited with code ${code}`));
|
||||
};
|
||||
|
||||
child.on("exit", onExit);
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
const outputStr = data.toString("utf8");
|
||||
|
||||
if (outputStr.includes("Export finished successfully")) {
|
||||
child.removeListener("exit", onExit);
|
||||
|
||||
child.kill();
|
||||
|
||||
dCompleted.resolve();
|
||||
}
|
||||
|
||||
output += outputStr;
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
||||
|
||||
try {
|
||||
await dCompleted.pr;
|
||||
} catch (error) {
|
||||
assert(is<Error>(error));
|
||||
|
||||
console.log(chalk.red(error.message));
|
||||
|
||||
console.log(output);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
.split(":")[1]
|
||||
).major;
|
||||
|
||||
const targetFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
)
|
||||
);
|
||||
|
||||
run(`docker cp ${CONTAINER_NAME}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
||||
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
})();
|
||||
|
||||
function run(command: string) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
return child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
@ -1,10 +1,18 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { same } from "evt/tools/inDepth";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../src/bin/shared/downloadBuiltinKeycloakTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { rmSync } from "../src/bin/tools/fs.rmSync";
|
||||
import { deepAssign } from "../src/tools/deepAssign";
|
||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||
|
||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
// update the version array for generating for newer version.
|
||||
@ -17,20 +25,18 @@ async function main() {
|
||||
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
fs.mkdirSync(tmpDirPath);
|
||||
|
||||
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
"buildOptions": {
|
||||
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
|
||||
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
|
||||
buildContext: {
|
||||
cacheDirPath: pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"node_modules",
|
||||
".cache",
|
||||
"keycloakify"
|
||||
),
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: thisCodebaseRootDirPath
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@ -39,12 +45,14 @@ async function main() {
|
||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
||||
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
|
||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
|
||||
crawl({
|
||||
"dirPath": baseThemeDirPath,
|
||||
"returnedPathsType": "relative to dirPath"
|
||||
dirPath: baseThemeDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
}).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
@ -55,35 +63,73 @@ async function main() {
|
||||
const [, typeOfPage, language] = match;
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
||||
([key, value]: any) => [key, value.replace(/''/g, "'")]
|
||||
Object.entries(
|
||||
propertiesParser.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||
.toString("utf8")
|
||||
) as Record<string, string>
|
||||
)
|
||||
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
||||
.map(([key, value]) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value
|
||||
])
|
||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true });
|
||||
|
||||
Object.keys(record).forEach(themeType => {
|
||||
const recordForPageType = record[themeType];
|
||||
|
||||
if (themeType !== "login" && themeType !== "account") {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
|
||||
const recordForThemeType = record[themeType];
|
||||
|
||||
const languages = Object.keys(recordForPageType);
|
||||
const languages = Object.keys(recordForThemeType);
|
||||
|
||||
const keycloakifyExtraMessages = (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return keycloakifyExtraMessages_login;
|
||||
case "account":
|
||||
return keycloakifyExtraMessages_account;
|
||||
}
|
||||
assert(false);
|
||||
})();
|
||||
|
||||
assert(
|
||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||
takeIntoAccountArraysOrdering: false
|
||||
})
|
||||
);
|
||||
|
||||
deepAssign({
|
||||
target: recordForThemeType,
|
||||
source: keycloakifyExtraMessages
|
||||
});
|
||||
|
||||
const messagesDirPath = pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"messages_defaultSet"
|
||||
);
|
||||
|
||||
const generatedFileHeader = [
|
||||
`//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
|
||||
`//This code was automatically generated by running ${pathRelative(
|
||||
thisCodebaseRootDirPath,
|
||||
__filename
|
||||
)}`,
|
||||
"//PLEASE DO NOT EDIT MANUALLY"
|
||||
].join("\n");
|
||||
|
||||
languages.forEach(language => {
|
||||
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
|
||||
const filePath = pathJoin(messagesDirPath, `${language}.ts`);
|
||||
|
||||
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
|
||||
fs.mkdirSync(pathDirname(filePath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
@ -92,7 +138,11 @@ async function main() {
|
||||
generatedFileHeader,
|
||||
"",
|
||||
"/* spell-checker: disable */",
|
||||
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||
`const messages= ${JSON.stringify(
|
||||
recordForThemeType[language],
|
||||
null,
|
||||
2
|
||||
)};`,
|
||||
"",
|
||||
"export default messages;",
|
||||
"/* spell-checker: enable */"
|
||||
@ -105,23 +155,26 @@ async function main() {
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(baseMessagesDirPath, "index.ts"),
|
||||
pathJoin(messagesDirPath, "index.ts"),
|
||||
Buffer.from(
|
||||
[
|
||||
generatedFileHeader,
|
||||
`import * as en from "./en";`,
|
||||
"",
|
||||
"export async function getMessages(currentLanguageTag: string) {",
|
||||
" const { default: messages } = await (() => {",
|
||||
"export async function fetchMessages_defaultSet(currentLanguageTag: string) {",
|
||||
" const { default: messages_defaultSet } = await (() => {",
|
||||
" switch (currentLanguageTag) {",
|
||||
` case "en": return en;`,
|
||||
...languages
|
||||
.filter(language => language !== "en")
|
||||
.map(language => ` case "${language}": return import("./${language}");`),
|
||||
.map(
|
||||
language =>
|
||||
` case "${language}": return import("./${language}");`
|
||||
),
|
||||
' default: return { "default": {} };',
|
||||
" }",
|
||||
" })();",
|
||||
" return messages;",
|
||||
" return messages_defaultSet;",
|
||||
"}"
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
@ -130,6 +183,491 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const keycloakifyExtraMessages_login: Record<
|
||||
| "en"
|
||||
| "ar"
|
||||
| "ca"
|
||||
| "cs"
|
||||
| "da"
|
||||
| "de"
|
||||
| "el"
|
||||
| "es"
|
||||
| "fa"
|
||||
| "fi"
|
||||
| "fr"
|
||||
| "hu"
|
||||
| "it"
|
||||
| "ja"
|
||||
| "lt"
|
||||
| "lv"
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
| "sv"
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "zh-CN",
|
||||
Record<
|
||||
| "shouldBeEqual"
|
||||
| "shouldBeDifferent"
|
||||
| "shouldMatchPattern"
|
||||
| "mustBeAnInteger"
|
||||
| "notAValidOption"
|
||||
| "selectAnOption"
|
||||
| "remove"
|
||||
| "addValue"
|
||||
| "languages",
|
||||
string
|
||||
>
|
||||
> = {
|
||||
en: {
|
||||
shouldBeEqual: "{0} should be equal to {1}",
|
||||
shouldBeDifferent: "{0} should be different to {1}",
|
||||
shouldMatchPattern: "Pattern should match: `/{0}/`",
|
||||
mustBeAnInteger: "Must be an integer",
|
||||
notAValidOption: "Not a valid option",
|
||||
selectAnOption: "Select an option",
|
||||
remove: "Remove",
|
||||
addValue: "Add value",
|
||||
languages: "Languages"
|
||||
},
|
||||
/* spell-checker: disable */
|
||||
ar: {
|
||||
shouldBeEqual: "{0} يجب أن يكون مساويًا لـ {1}",
|
||||
shouldBeDifferent: "{0} يجب أن يكون مختلفًا عن {1}",
|
||||
shouldMatchPattern: "`/يجب أن يطابق النمط: `/{0}/",
|
||||
mustBeAnInteger: "يجب أن يكون عددًا صحيحًا",
|
||||
notAValidOption: "ليس خيارًا صالحًا",
|
||||
selectAnOption: "اختر خيارًا",
|
||||
remove: "إزالة",
|
||||
addValue: "أضف قيمة",
|
||||
languages: "اللغات"
|
||||
},
|
||||
ca: {
|
||||
shouldBeEqual: "{0} hauria de ser igual a {1}",
|
||||
shouldBeDifferent: "{0} hauria de ser diferent de {1}",
|
||||
shouldMatchPattern: "El patró hauria de coincidir: `/{0}/`",
|
||||
mustBeAnInteger: "Ha de ser un enter",
|
||||
notAValidOption: "No és una opció vàlida",
|
||||
selectAnOption: "Selecciona una opció",
|
||||
remove: "Elimina",
|
||||
addValue: "Afegeix valor",
|
||||
languages: "Idiomes"
|
||||
},
|
||||
cs: {
|
||||
shouldBeEqual: "{0} by měl být roven {1}",
|
||||
shouldBeDifferent: "{0} by měl být odlišný od {1}",
|
||||
shouldMatchPattern: "Vzor by měl odpovídat: `/{0}/`",
|
||||
mustBeAnInteger: "Musí být celé číslo",
|
||||
notAValidOption: "Není platná možnost",
|
||||
selectAnOption: "Vyberte možnost",
|
||||
remove: "Odstranit",
|
||||
addValue: "Přidat hodnotu",
|
||||
languages: "Jazyky"
|
||||
},
|
||||
da: {
|
||||
shouldBeEqual: "{0} bør være lig med {1}",
|
||||
shouldBeDifferent: "{0} bør være forskellig fra {1}",
|
||||
shouldMatchPattern: "Mønsteret bør matche: `/{0}/`",
|
||||
mustBeAnInteger: "Skal være et heltal",
|
||||
notAValidOption: "Ikke en gyldig mulighed",
|
||||
selectAnOption: "Vælg en mulighed",
|
||||
remove: "Fjern",
|
||||
addValue: "Tilføj værdi",
|
||||
languages: "Sprog"
|
||||
},
|
||||
de: {
|
||||
shouldBeEqual: "{0} sollte gleich {1} sein",
|
||||
shouldBeDifferent: "{0} sollte sich von {1} unterscheiden",
|
||||
shouldMatchPattern: "Muster sollte übereinstimmen: `/{0}/`",
|
||||
mustBeAnInteger: "Muss eine ganze Zahl sein",
|
||||
notAValidOption: "Keine gültige Option",
|
||||
selectAnOption: "Wählen Sie eine Option",
|
||||
remove: "Entfernen",
|
||||
addValue: "Wert hinzufügen",
|
||||
languages: "Sprachen"
|
||||
},
|
||||
el: {
|
||||
shouldBeEqual: "Το {0} πρέπει να είναι ίσο με {1}",
|
||||
shouldBeDifferent: "Το {0} πρέπει να διαφέρει από το {1}",
|
||||
shouldMatchPattern: "Το πρότυπο πρέπει να ταιριάζει: `/{0}/`",
|
||||
mustBeAnInteger: "Πρέπει να είναι ακέραιος",
|
||||
notAValidOption: "Δεν είναι μια έγκυρη επιλογή",
|
||||
selectAnOption: "Επιλέξτε μια επιλογή",
|
||||
remove: "Αφαίρεση",
|
||||
addValue: "Προσθήκη τιμής",
|
||||
languages: "Γλώσσες"
|
||||
},
|
||||
es: {
|
||||
shouldBeEqual: "{0} debería ser igual a {1}",
|
||||
shouldBeDifferent: "{0} debería ser diferente a {1}",
|
||||
shouldMatchPattern: "El patrón debería coincidir: `/{0}/`",
|
||||
mustBeAnInteger: "Debe ser un número entero",
|
||||
notAValidOption: "No es una opción válida",
|
||||
selectAnOption: "Selecciona una opción",
|
||||
remove: "Eliminar",
|
||||
addValue: "Añadir valor",
|
||||
languages: "Idiomas"
|
||||
},
|
||||
fa: {
|
||||
shouldBeEqual: "{0} باید برابر باشد با {1}",
|
||||
shouldBeDifferent: "{0} باید متفاوت باشد از {1}",
|
||||
shouldMatchPattern: "الگو باید مطابقت داشته باشد: `/{0}/`",
|
||||
mustBeAnInteger: "باید یک عدد صحیح باشد",
|
||||
notAValidOption: "یک گزینه معتبر نیست",
|
||||
selectAnOption: "یک گزینه انتخاب کنید",
|
||||
remove: "حذف",
|
||||
addValue: "افزودن مقدار",
|
||||
languages: "زبانها"
|
||||
},
|
||||
fi: {
|
||||
shouldBeEqual: "{0} pitäisi olla yhtä suuri kuin {1}",
|
||||
shouldBeDifferent: "{0} pitäisi olla erilainen kuin {1}",
|
||||
shouldMatchPattern: "Mallin tulisi vastata: `/{0}/`",
|
||||
mustBeAnInteger: "On oltava kokonaisluku",
|
||||
notAValidOption: "Ei ole kelvollinen vaihtoehto",
|
||||
selectAnOption: "Valitse vaihtoehto",
|
||||
remove: "Poista",
|
||||
addValue: "Lisää arvo",
|
||||
languages: "Kielet"
|
||||
},
|
||||
fr: {
|
||||
shouldBeEqual: "{0} devrait être égal à {1}",
|
||||
shouldBeDifferent: "{0} devrait être différent de {1}",
|
||||
shouldMatchPattern: "Le motif devrait correspondre: `/{0}/`",
|
||||
mustBeAnInteger: "Doit être un entier",
|
||||
notAValidOption: "Pas une option valide",
|
||||
selectAnOption: "Sélectionnez une option",
|
||||
remove: "Supprimer",
|
||||
addValue: "Ajouter une valeur",
|
||||
languages: "Langues"
|
||||
},
|
||||
hu: {
|
||||
shouldBeEqual: "{0} egyenlő kell legyen {1}-vel",
|
||||
shouldBeDifferent: "{0} különbözőnek kell lennie, mint {1}",
|
||||
shouldMatchPattern: "A mintának egyeznie kell: `/{0}/`",
|
||||
mustBeAnInteger: "Egész számnak kell lennie",
|
||||
notAValidOption: "Nem érvényes opció",
|
||||
selectAnOption: "Válasszon egy lehetőséget",
|
||||
remove: "Eltávolítás",
|
||||
addValue: "Érték hozzáadása",
|
||||
languages: "Nyelvek"
|
||||
},
|
||||
it: {
|
||||
shouldBeEqual: "{0} dovrebbe essere uguale a {1}",
|
||||
shouldBeDifferent: "{0} dovrebbe essere diverso da {1}",
|
||||
shouldMatchPattern: "Il modello dovrebbe corrispondere: `/{0}/`",
|
||||
mustBeAnInteger: "Deve essere un numero intero",
|
||||
notAValidOption: "Non è un'opzione valida",
|
||||
selectAnOption: "Seleziona un'opzione",
|
||||
remove: "Rimuovi",
|
||||
addValue: "Aggiungi valore",
|
||||
languages: "Lingue"
|
||||
},
|
||||
ja: {
|
||||
shouldBeEqual: "{0} は {1} と等しい必要があります",
|
||||
shouldBeDifferent: "{0} は {1} と異なる必要があります",
|
||||
shouldMatchPattern: "パターンは一致する必要があります: `/{0}/`",
|
||||
mustBeAnInteger: "整数である必要があります",
|
||||
notAValidOption: "有効なオプションではありません",
|
||||
selectAnOption: "オプションを選択",
|
||||
remove: "削除",
|
||||
addValue: "値を追加",
|
||||
languages: "言語"
|
||||
},
|
||||
lt: {
|
||||
shouldBeEqual: "{0} turėtų būti lygus {1}",
|
||||
shouldBeDifferent: "{0} turėtų skirtis nuo {1}",
|
||||
shouldMatchPattern: "Šablonas turėtų atitikti: `/{0}/`",
|
||||
mustBeAnInteger: "Turi būti sveikasis skaičius",
|
||||
notAValidOption: "Netinkama parinktis",
|
||||
selectAnOption: "Pasirinkite parinktį",
|
||||
remove: "Pašalinti",
|
||||
addValue: "Pridėti reikšmę",
|
||||
languages: "Kalbos"
|
||||
},
|
||||
lv: {
|
||||
shouldBeEqual: "{0} jābūt vienādam ar {1}",
|
||||
shouldBeDifferent: "{0} jābūt atšķirīgam no {1}",
|
||||
shouldMatchPattern: "Mustrim jāsakrīt: `/{0}/`",
|
||||
mustBeAnInteger: "Jābūt veselam skaitlim",
|
||||
notAValidOption: "Nav derīga opcija",
|
||||
selectAnOption: "Izvēlieties opciju",
|
||||
remove: "Noņemt",
|
||||
addValue: "Pievienot vērtību",
|
||||
languages: "Valodas"
|
||||
},
|
||||
nl: {
|
||||
shouldBeEqual: "{0} moet gelijk zijn aan {1}",
|
||||
shouldBeDifferent: "{0} moet verschillen van {1}",
|
||||
shouldMatchPattern: "Patroon moet overeenkomen: `/{0}/`",
|
||||
mustBeAnInteger: "Moet een geheel getal zijn",
|
||||
notAValidOption: "Geen geldige optie",
|
||||
selectAnOption: "Selecteer een optie",
|
||||
remove: "Verwijderen",
|
||||
addValue: "Waarde toevoegen",
|
||||
languages: "Talen"
|
||||
},
|
||||
no: {
|
||||
shouldBeEqual: "{0} skal være lik {1}",
|
||||
shouldBeDifferent: "{0} skal være forskjellig fra {1}",
|
||||
shouldMatchPattern: "Mønsteret skal matche: `/{0}/`",
|
||||
mustBeAnInteger: "Må være et heltall",
|
||||
notAValidOption: "Ikke et gyldig alternativ",
|
||||
selectAnOption: "Velg et alternativ",
|
||||
remove: "Fjern",
|
||||
addValue: "Legg til verdi",
|
||||
languages: "Språk"
|
||||
},
|
||||
pl: {
|
||||
shouldBeEqual: "{0} powinno być równe {1}",
|
||||
shouldBeDifferent: "{0} powinno być różne od {1}",
|
||||
shouldMatchPattern: "Wzór pow inien pasować: `/{0}/`",
|
||||
mustBeAnInteger: "Musi być liczbą całkowitą",
|
||||
notAValidOption: "Nieprawidłowa opcja",
|
||||
selectAnOption: "Wybierz opcję",
|
||||
remove: "Usuń",
|
||||
addValue: "Dodaj wartość",
|
||||
languages: "Języki"
|
||||
},
|
||||
"pt-BR": {
|
||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||
mustBeAnInteger: "Deve ser um número inteiro",
|
||||
notAValidOption: "Não é uma opção válida",
|
||||
selectAnOption: "Selecione uma opção",
|
||||
remove: "Remover",
|
||||
addValue: "Adicionar valor",
|
||||
languages: "Idiomas"
|
||||
},
|
||||
ru: {
|
||||
shouldBeEqual: "{0} должно быть равно {1}",
|
||||
shouldBeDifferent: "{0} должно отличаться от {1}",
|
||||
shouldMatchPattern: "Шаблон должен соответствовать: `/{0}/`",
|
||||
mustBeAnInteger: "Должно быть целым числом",
|
||||
notAValidOption: "Недопустимый вариант",
|
||||
selectAnOption: "Выберите вариант",
|
||||
remove: "Удалить",
|
||||
addValue: "Добавить значение",
|
||||
languages: "Языки"
|
||||
},
|
||||
sk: {
|
||||
shouldBeEqual: "{0} by mal byť rovnaký ako {1}",
|
||||
shouldBeDifferent: "{0} by mal byť odlišný od {1}",
|
||||
shouldMatchPattern: "Vzor by mal zodpovedať: `/{0}/`",
|
||||
mustBeAnInteger: "Musí byť celé číslo",
|
||||
notAValidOption: "Nie je platná možnosť",
|
||||
selectAnOption: "Vyberte možnosť",
|
||||
remove: "Odstrániť",
|
||||
addValue: "Pridať hodnotu",
|
||||
languages: "Jazyky"
|
||||
},
|
||||
sv: {
|
||||
shouldBeEqual: "{0} bör vara lika med {1}",
|
||||
shouldBeDifferent: "{0} bör vara annorlunda än {1}",
|
||||
shouldMatchPattern: "Mönstret bör matcha: `/{0}/`",
|
||||
mustBeAnInteger: "Måste vara ett heltal",
|
||||
notAValidOption: "Inte ett giltigt alternativ",
|
||||
selectAnOption: "Välj ett alternativ",
|
||||
remove: "Ta bort",
|
||||
addValue: "Lägg till värde",
|
||||
languages: "Språk"
|
||||
},
|
||||
th: {
|
||||
shouldBeEqual: "{0} ควรเท่ากับ {1}",
|
||||
shouldBeDifferent: "{0} ควรแตกต่างจาก {1}",
|
||||
shouldMatchPattern: "รูปแบบควรตรงกับ: `/{0}/`",
|
||||
mustBeAnInteger: "ต้องเป็นจำนวนเต็ม",
|
||||
notAValidOption: "ไม่ใช่ตัวเลือกที่ถูกต้อง",
|
||||
selectAnOption: "เลือกตัวเลือก",
|
||||
remove: "ลบ",
|
||||
addValue: "เพิ่มค่า",
|
||||
languages: "ภาษา"
|
||||
},
|
||||
tr: {
|
||||
shouldBeEqual: "{0} {1} eşit olmalıdır",
|
||||
shouldBeDifferent: "{0} {1} farklı olmalıdır",
|
||||
shouldMatchPattern: "Desen eşleşmelidir: `/{0}/`",
|
||||
mustBeAnInteger: "Tam sayı olmalıdır",
|
||||
notAValidOption: "Geçerli bir seçenek değil",
|
||||
selectAnOption: "Bir seçenek seçin",
|
||||
remove: "Kaldır",
|
||||
addValue: "Değer ekle",
|
||||
languages: "Diller"
|
||||
},
|
||||
uk: {
|
||||
shouldBeEqual: "{0} повинно бути рівним {1}",
|
||||
shouldBeDifferent: "{0} повинно відрізнятися від {1}",
|
||||
shouldMatchPattern: "Шаблон повинен відповідати: `/{0}/`",
|
||||
mustBeAnInteger: "Повинно бути цілим числом",
|
||||
notAValidOption: "Не є дійсною опцією",
|
||||
selectAnOption: "Виберіть опцію",
|
||||
remove: "Видалити",
|
||||
addValue: "Додати значення",
|
||||
languages: "Мови"
|
||||
},
|
||||
"zh-CN": {
|
||||
shouldBeEqual: "{0} 应该等于 {1}",
|
||||
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||
shouldMatchPattern: "模式应匹配: `/{0}/`",
|
||||
mustBeAnInteger: "必须是整数",
|
||||
notAValidOption: "不是有效选项",
|
||||
selectAnOption: "选择一个选项",
|
||||
remove: "移除",
|
||||
addValue: "添加值",
|
||||
languages: "语言"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
||||
|
||||
const keycloakifyExtraMessages_account: Record<
|
||||
| "en"
|
||||
| "ar"
|
||||
| "ca"
|
||||
| "cs"
|
||||
| "da"
|
||||
| "de"
|
||||
| "el"
|
||||
| "es"
|
||||
| "fa"
|
||||
| "fi"
|
||||
| "fr"
|
||||
| "hu"
|
||||
| "it"
|
||||
| "ja"
|
||||
| "lt"
|
||||
| "lv"
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
| "sv"
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "zh-CN",
|
||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||
> = {
|
||||
en: {
|
||||
newPasswordSameAsOld: "New password must be different from the old one",
|
||||
passwordConfirmNotMatch: "Password confirmation does not match"
|
||||
},
|
||||
/* spell-checker: disable */
|
||||
ar: {
|
||||
newPasswordSameAsOld: "يجب أن تكون كلمة المرور الجديدة مختلفة عن القديمة",
|
||||
passwordConfirmNotMatch: "تأكيد كلمة المرور لا يتطابق"
|
||||
},
|
||||
ca: {
|
||||
newPasswordSameAsOld: "La nova contrasenya ha de ser diferent de l'anterior",
|
||||
passwordConfirmNotMatch: "La confirmació de la contrasenya no coincideix"
|
||||
},
|
||||
cs: {
|
||||
newPasswordSameAsOld: "Nové heslo musí být odlišné od starého",
|
||||
passwordConfirmNotMatch: "Potvrzení hesla se neshoduje"
|
||||
},
|
||||
da: {
|
||||
newPasswordSameAsOld: "Det nye kodeord skal være forskelligt fra det gamle",
|
||||
passwordConfirmNotMatch: "Adgangskodebekræftelse matcher ikke"
|
||||
},
|
||||
de: {
|
||||
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
||||
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
||||
},
|
||||
el: {
|
||||
newPasswordSameAsOld: "Ο νέος κωδικός πρόσβασης πρέπει να διαφέρει από τον παλιό",
|
||||
passwordConfirmNotMatch: "Η επιβεβαίωση του κωδικού πρόσβασης δεν ταιριάζει"
|
||||
},
|
||||
es: {
|
||||
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
||||
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
||||
},
|
||||
fa: {
|
||||
newPasswordSameAsOld: "رمز عبور جدید باید با رمز عبور قبلی متفاوت باشد",
|
||||
passwordConfirmNotMatch: "تأیید رمز عبور مطابقت ندارد"
|
||||
},
|
||||
fi: {
|
||||
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
||||
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
||||
},
|
||||
fr: {
|
||||
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
|
||||
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
|
||||
},
|
||||
hu: {
|
||||
newPasswordSameAsOld: "Az új jelszónak különböznie kell az előzőtől",
|
||||
passwordConfirmNotMatch: "A jelszó megerősítése nem egyezik"
|
||||
},
|
||||
it: {
|
||||
newPasswordSameAsOld:
|
||||
"La nuova password deve essere diversa da quella precedente",
|
||||
passwordConfirmNotMatch: "La conferma della password non corrisponde"
|
||||
},
|
||||
ja: {
|
||||
newPasswordSameAsOld: "新しいパスワードは古いパスワードと異なる必要があります",
|
||||
passwordConfirmNotMatch: "パスワード確認が一致しません"
|
||||
},
|
||||
lt: {
|
||||
newPasswordSameAsOld: "Naujas slaptažodis turi skirtis nuo seno",
|
||||
passwordConfirmNotMatch: "Slaptažodžio patvirtinimas neatitinka"
|
||||
},
|
||||
lv: {
|
||||
newPasswordSameAsOld: "Jaunajam parolam jābūt atšķirīgam no vecā",
|
||||
passwordConfirmNotMatch: "Paroles apstiprināšana neatbilst"
|
||||
},
|
||||
nl: {
|
||||
newPasswordSameAsOld: "Het nieuwe wachtwoord moet verschillend zijn van het oude",
|
||||
passwordConfirmNotMatch: "Wachtwoordbevestiging komt niet overeen"
|
||||
},
|
||||
no: {
|
||||
newPasswordSameAsOld: "Det nye passordet må være forskjellig fra det gamle",
|
||||
passwordConfirmNotMatch: "Passordbekreftelsen stemmer ikke"
|
||||
},
|
||||
pl: {
|
||||
newPasswordSameAsOld: "Nowe hasło musi być inne niż stare",
|
||||
passwordConfirmNotMatch: "Potwierdzenie hasła nie pasuje"
|
||||
},
|
||||
"pt-BR": {
|
||||
newPasswordSameAsOld: "A nova senha deve ser diferente da antiga",
|
||||
passwordConfirmNotMatch: "A confirmação da senha não corresponde"
|
||||
},
|
||||
ru: {
|
||||
newPasswordSameAsOld: "Новый пароль должен отличаться от старого",
|
||||
passwordConfirmNotMatch: "Подтверждение пароля не совпадает"
|
||||
},
|
||||
sk: {
|
||||
newPasswordSameAsOld: "Nové heslo musí byť odlišné od starého",
|
||||
passwordConfirmNotMatch: "Potvrdenie hesla sa nezhoduje"
|
||||
},
|
||||
sv: {
|
||||
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
||||
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
||||
},
|
||||
th: {
|
||||
newPasswordSameAsOld: "รหัสผ่านใหม่ต้องต่างจากรหัสผ่านเดิม",
|
||||
passwordConfirmNotMatch: "การยืนยันรหัสผ่านไม่ตรงกัน"
|
||||
},
|
||||
tr: {
|
||||
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
||||
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
||||
},
|
||||
uk: {
|
||||
newPasswordSameAsOld: "Новий пароль повинен відрізнятися від старого",
|
||||
passwordConfirmNotMatch: "Підтвердження пароля не співпадає"
|
||||
},
|
||||
"zh-CN": {
|
||||
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
||||
passwordConfirmNotMatch: "密码确认不匹配"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ import { chmod, stat } from "fs/promises";
|
||||
const promises = Object.values<string>(bin).map(async scriptPath => {
|
||||
const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
|
||||
const oldMode = (await stat(fullPath)).mode;
|
||||
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||
const newMode =
|
||||
oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||
await chmod(fullPath, newMode);
|
||||
});
|
||||
|
||||
|
@ -2,82 +2,77 @@ import { execSync } from "child_process";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||
|
||||
// For example [ "@emotion" ] it's more convenient than
|
||||
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||
// in singletonDependencies
|
||||
const namespaceSingletonDependencies: string[] = [];
|
||||
|
||||
const rootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const commonThirdPartyDeps = [
|
||||
...namespaceSingletonDependencies
|
||||
.map(namespaceModuleName =>
|
||||
fs
|
||||
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||
)
|
||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||
...singletonDependencies
|
||||
];
|
||||
|
||||
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||
fs.writeFileSync(
|
||||
pathJoin(rootDirPath, "dist", "package.json"),
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
(() => {
|
||||
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
|
||||
{
|
||||
let modifiedPackageJsonContent = fs
|
||||
.readFileSync(pathJoin(rootDirPath, "package.json"))
|
||||
.toString("utf8");
|
||||
|
||||
return {
|
||||
...packageJsonParsed,
|
||||
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||
"exports": !("exports" in packageJsonParsed)
|
||||
? undefined
|
||||
: Object.fromEntries(
|
||||
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
|
||||
key,
|
||||
(value as string).replace(/^\.\/dist\//, "./")
|
||||
])
|
||||
)
|
||||
};
|
||||
})(),
|
||||
null,
|
||||
2
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
modifiedPackageJsonContent = (() => {
|
||||
const o = JSON.parse(modifiedPackageJsonContent);
|
||||
|
||||
const destSrcDirPath = pathJoin(rootDirPath, "dist", "src");
|
||||
delete o.files;
|
||||
|
||||
fs.rmSync(destSrcDirPath, { "recursive": true, "force": true });
|
||||
return JSON.stringify(o, null, 2);
|
||||
})();
|
||||
|
||||
fs.cpSync(pathJoin(rootDirPath, "src"), destSrcDirPath, { "recursive": true });
|
||||
modifiedPackageJsonContent = modifiedPackageJsonContent
|
||||
.replace(/"dist\//g, '"')
|
||||
.replace(/"\.\/dist\//g, '"./')
|
||||
.replace(/"!dist\//g, '"!')
|
||||
.replace(/"!\.\/dist\//g, '"!./');
|
||||
|
||||
const commonThirdPartyDeps = (() => {
|
||||
// For example [ "@emotion" ] it's more convenient than
|
||||
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||
// in singletonDependencies
|
||||
const namespaceSingletonDependencies: string[] = [];
|
||||
|
||||
return [
|
||||
...namespaceSingletonDependencies
|
||||
.map(namespaceModuleName =>
|
||||
fs
|
||||
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||
)
|
||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||
...singletonDependencies
|
||||
];
|
||||
})();
|
||||
fs.writeFileSync(
|
||||
pathJoin(rootDirPath, "dist", "package.json"),
|
||||
Buffer.from(modifiedPackageJsonContent, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||
|
||||
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
|
||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||
fs.mkdirSync(yarnGlobalDirPath);
|
||||
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
const { targetModuleName, cwd } = params;
|
||||
|
||||
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
|
||||
const cmd = [
|
||||
"yarn",
|
||||
"link",
|
||||
...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])
|
||||
].join(" ");
|
||||
|
||||
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
"env": {
|
||||
env: {
|
||||
...process.env,
|
||||
"HOME": yarnGlobalDirPath
|
||||
...(os.platform() === "win32"
|
||||
? { USERPROFILE: yarnGlobalDirPath }
|
||||
: { HOME: yarnGlobalDirPath })
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -93,7 +88,9 @@ const testAppPaths = (() => {
|
||||
return testAppPath;
|
||||
}
|
||||
|
||||
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
|
||||
console.warn(
|
||||
`Skipping ${testAppName} since it cant be found here: ${testAppPath}`
|
||||
);
|
||||
|
||||
return undefined;
|
||||
})
|
||||
@ -105,7 +102,7 @@ if (testAppPaths.length === 0) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
||||
|
||||
console.log("=== Linking common dependencies ===");
|
||||
|
||||
@ -118,29 +115,37 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||
|
||||
const localInstallPath = pathJoin(
|
||||
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
||||
...[
|
||||
rootDirPath,
|
||||
"node_modules",
|
||||
...(commonThirdPartyDep.startsWith("@")
|
||||
? commonThirdPartyDep.split("/")
|
||||
: [commonThirdPartyDep])
|
||||
]
|
||||
);
|
||||
|
||||
execYarnLink({ "cwd": localInstallPath });
|
||||
execYarnLink({ cwd: localInstallPath });
|
||||
});
|
||||
|
||||
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": commonThirdPartyDep
|
||||
cwd: testAppPath,
|
||||
targetModuleName: commonThirdPartyDep
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log("=== Linking in house dependencies ===");
|
||||
|
||||
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
|
||||
execYarnLink({ cwd: pathJoin(rootDirPath, "dist") });
|
||||
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
|
||||
cwd: testAppPath,
|
||||
targetModuleName: JSON.parse(
|
||||
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
|
||||
)["name"]
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -1,24 +1,30 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
|
||||
fs.rmSync("node_modules", { "recursive": true, "force": true });
|
||||
fs.rmSync("dist", { "recursive": true, "force": true });
|
||||
fs.rmSync(".yarn_home", { "recursive": true, "force": true });
|
||||
fs.rmSync("node_modules", { recursive: true, force: true });
|
||||
fs.rmSync("dist", { recursive: true, force: true });
|
||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
|
||||
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), { "recursive": true, "force": true });
|
||||
const starterName = "keycloakify-starter";
|
||||
|
||||
run("yarn install", { "cwd": join("..", "keycloakify-starter") });
|
||||
fs.rmSync(join("..", starterName, "node_modules"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
|
||||
run("yarn install", { cwd: join("..", starterName) });
|
||||
|
||||
run(`npx chokidar '${join("src", "**", "*")}' -c 'yarn build'`);
|
||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
function run(command: string, options?: { cwd: string }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { "stdio": "inherit", ...options });
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
33
scripts/start-storybook.ts
Normal file
33
scripts/start-storybook.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
|
||||
run("yarn build");
|
||||
|
||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
||||
env: {
|
||||
...process.env,
|
||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", process.exit.bind(process));
|
||||
}
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
40
scripts/startRebuildOnSrcChange.ts
Normal file
40
scripts/startRebuildOnSrcChange.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as child_process from "child_process";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import chokidar from "chokidar";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function startRebuildOnSrcChange() {
|
||||
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
|
||||
|
||||
const runYarnBuild = runExclusive.build(async () => {
|
||||
console.log(chalk.green("Running `yarn build`"));
|
||||
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn("yarn", ["build"], { shell: true });
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", () => dCompleted.resolve());
|
||||
|
||||
await dCompleted.pr;
|
||||
|
||||
console.log("\n\n");
|
||||
});
|
||||
|
||||
console.log(chalk.green("Watching for changes in src/"));
|
||||
|
||||
chokidar
|
||||
.watch(["src", "stories"], { ignoreInitial: true })
|
||||
.on("all", async (event, path) => {
|
||||
console.log(chalk.bold(`${event}: ${path}`));
|
||||
|
||||
await waitForDebounce();
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "keycloakify/bin/shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
* This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
|
||||
* This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects.
|
||||
* This works both in your main app and in your Keycloak theme.
|
||||
*/
|
||||
export const PUBLIC_URL = (() => {
|
||||
const kcContext = (window as any)[nameOfTheGlobal];
|
||||
const kcContext = (window as any).kcContext;
|
||||
|
||||
if (kcContext === undefined || process.env.NODE_ENV === "development") {
|
||||
assert(
|
||||
@ -17,5 +17,5 @@ export const PUBLIC_URL = (() => {
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
|
||||
return `${kcContext.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}`;
|
||||
return `${kcContext.url.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}`;
|
||||
})();
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { I18n } from "keycloakify/account/i18n";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import FederatedIdentity from "./pages/FederatedIdentity";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "keycloakify/account/KcContext";
|
||||
import { I18n } from "keycloakify/account/i18n";
|
||||
|
||||
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||
@ -11,8 +10,9 @@ const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
|
||||
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
|
||||
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
|
||||
const Log = lazy(() => import("keycloakify/account/pages/Log"));
|
||||
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
|
||||
|
||||
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||
export default function DefaultPage(props: PageProps<KcContext, I18n>) {
|
||||
const { kcContext, ...rest } = props;
|
||||
|
||||
return (
|
@ -1,6 +1,24 @@
|
||||
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { ValueOf } from "keycloakify/tools/ValueOf";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type ExtendKcContext<
|
||||
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtensionPerPage extends Record<string, Record<string, unknown>>
|
||||
> = ValueOf<{
|
||||
[PageId in keyof KcContextExtensionPerPage | KcContext["pageId"]]: Extract<
|
||||
KcContext,
|
||||
{ pageId: PageId }
|
||||
> extends never
|
||||
? KcContext.Common &
|
||||
KcContextExtension & {
|
||||
pageId: PageId;
|
||||
} & KcContextExtensionPerPage[PageId]
|
||||
: Extract<KcContext, { pageId: PageId }> &
|
||||
KcContextExtension &
|
||||
KcContextExtensionPerPage[PageId];
|
||||
}>;
|
||||
|
||||
export type KcContext =
|
||||
| KcContext.Password
|
||||
@ -68,7 +86,10 @@ export declare namespace KcContext {
|
||||
* @param text to return
|
||||
* @return text if message exists for given field, else undefined
|
||||
*/
|
||||
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
|
||||
printIfExists: <T extends string>(
|
||||
fieldName: string,
|
||||
text: T
|
||||
) => T | undefined;
|
||||
/**
|
||||
* Check if exists error message for given fields
|
||||
*
|
||||
@ -97,7 +118,10 @@ export declare namespace KcContext {
|
||||
lastName?: string;
|
||||
username?: string;
|
||||
};
|
||||
properties: Record<string, string | undefined>;
|
||||
properties: {};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
export type Password = Common & {
|
||||
@ -145,6 +169,7 @@ export declare namespace KcContext {
|
||||
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
|
||||
digits: number;
|
||||
lookAheadWindow: number;
|
||||
getAlgorithmKey: () => string;
|
||||
} & (
|
||||
| {
|
||||
type: "totp";
|
69
src/account/KcContext/getKcContextMock.ts
Normal file
69
src/account/KcContext/getKcContextMock.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
|
||||
import type { AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export function createGetKcContextMock<
|
||||
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtensionPerPage extends Record<`${string}.ftl`, Record<string, unknown>>
|
||||
>(params: {
|
||||
kcContextExtension: KcContextExtension;
|
||||
kcContextExtensionPerPage: KcContextExtensionPerPage;
|
||||
overrides?: DeepPartial<KcContextExtension & KcContextBase.Common>;
|
||||
overridesPerPage?: {
|
||||
[PageId in AccountThemePageId | keyof KcContextExtensionPerPage]?: DeepPartial<
|
||||
Extract<
|
||||
ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>,
|
||||
{ pageId: PageId }
|
||||
>
|
||||
>;
|
||||
};
|
||||
}) {
|
||||
const {
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage,
|
||||
overrides: overrides_global,
|
||||
overridesPerPage: overridesPerPage_global
|
||||
} = params;
|
||||
|
||||
type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
||||
|
||||
function getKcContextMock<
|
||||
PageId extends AccountThemePageId | keyof KcContextExtensionPerPage
|
||||
>(params: {
|
||||
pageId: PageId;
|
||||
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||
}): Extract<KcContext, { pageId: PageId }> {
|
||||
const { pageId, overrides } = params;
|
||||
|
||||
const kcContextMock = structuredCloneButFunctions(
|
||||
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
|
||||
...kcContextCommonMock,
|
||||
pageId
|
||||
}
|
||||
);
|
||||
|
||||
[
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage[pageId],
|
||||
overrides_global,
|
||||
overridesPerPage_global?.[pageId],
|
||||
overrides
|
||||
]
|
||||
.filter(exclude(undefined))
|
||||
.forEach(overrides =>
|
||||
deepAssign({
|
||||
target: kcContextMock,
|
||||
source: overrides
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
return kcContextMock;
|
||||
}
|
||||
|
||||
return { getKcContextMock };
|
||||
}
|
2
src/account/KcContext/index.ts
Normal file
2
src/account/KcContext/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type { ExtendKcContext, KcContext } from "./KcContext";
|
||||
export { createGetKcContextMock } from "./getKcContextMock";
|
193
src/account/KcContext/kcContextMocks.ts
Normal file
193
src/account/KcContext/kcContextMocks.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { RESOURCES_COMMON, KEYCLOAK_RESOURCES } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
|
||||
const resourcesPath = `${BASE_URL}${KEYCLOAK_RESOURCES}/account/resources`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
themeVersion: "0.0.0",
|
||||
keycloakifyVersion: "0.0.0",
|
||||
themeType: "account",
|
||||
themeName: "my-theme-name",
|
||||
url: {
|
||||
resourcesPath,
|
||||
resourcesCommonPath: `${resourcesPath}/${RESOURCES_COMMON}`,
|
||||
resourceUrl: "#",
|
||||
accountUrl: "#",
|
||||
applicationsUrl: "#",
|
||||
logoutUrl: "#",
|
||||
getLogoutUrl: () => "#",
|
||||
logUrl: "#",
|
||||
passwordUrl: "#",
|
||||
sessionsUrl: "#",
|
||||
socialUrl: "#",
|
||||
totpUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
internationalizationEnabled: true,
|
||||
userManagedAccessAllowed: true
|
||||
},
|
||||
messagesPerField: {
|
||||
printIfExists: () => {
|
||||
return undefined;
|
||||
},
|
||||
existsError: () => false,
|
||||
get: key => `Fake error for ${key}`,
|
||||
exists: () => false
|
||||
},
|
||||
locale: {
|
||||
supported: [
|
||||
/* spell-checker: disable */
|
||||
["de", "Deutsch"],
|
||||
["no", "Norsk"],
|
||||
["ru", "Русский"],
|
||||
["sv", "Svenska"],
|
||||
["pt-BR", "Português (Brasil)"],
|
||||
["lt", "Lietuvių"],
|
||||
["en", "English"],
|
||||
["it", "Italiano"],
|
||||
["fr", "Français"],
|
||||
["zh-CN", "中文简体"],
|
||||
["es", "Español"],
|
||||
["cs", "Čeština"],
|
||||
["ja", "日本語"],
|
||||
["sk", "Slovenčina"],
|
||||
["pl", "Polski"],
|
||||
["ca", "Català"],
|
||||
["nl", "Nederlands"],
|
||||
["tr", "Türkçe"]
|
||||
/* spell-checker: enable */
|
||||
].map(
|
||||
([languageTag, label]) =>
|
||||
({
|
||||
languageTag,
|
||||
label,
|
||||
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
|
||||
}) as const
|
||||
),
|
||||
currentLanguageTag: "en"
|
||||
},
|
||||
features: {
|
||||
authorization: true,
|
||||
identityFederation: true,
|
||||
log: true,
|
||||
passwordUpdateSupported: true
|
||||
},
|
||||
referrer: undefined,
|
||||
account: {
|
||||
firstName: "john",
|
||||
lastName: "doe",
|
||||
email: "john.doe@code.gouv.fr",
|
||||
username: "doe_j"
|
||||
},
|
||||
properties: {},
|
||||
"x-keycloakify": {
|
||||
messages: {}
|
||||
}
|
||||
};
|
||||
|
||||
export const kcContextMocks: KcContext[] = [
|
||||
id<KcContext.Password>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "password.ftl",
|
||||
password: {
|
||||
passwordSet: true
|
||||
},
|
||||
stateChecker: "state checker"
|
||||
}),
|
||||
id<KcContext.Account>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "account.ftl",
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
referrerURI: "#",
|
||||
accountUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
registrationEmailAsUsername: true,
|
||||
editUsernameAllowed: true
|
||||
},
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Sessions>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "sessions.ftl",
|
||||
sessions: {
|
||||
sessions: [
|
||||
{
|
||||
ipAddress: "127.0.0.1",
|
||||
started: new Date().toString(),
|
||||
lastAccess: new Date().toString(),
|
||||
expires: new Date().toString(),
|
||||
clients: ["Chrome", "Firefox"],
|
||||
id: "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
}
|
||||
]
|
||||
},
|
||||
stateChecker: "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
}),
|
||||
id<KcContext.Totp>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "totp.ftl",
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
qrUrl: "#",
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
|
||||
manualUrl: "#",
|
||||
totpSecret: "G4nsI8lQagRMUchH8jEG",
|
||||
otpCredentials: [],
|
||||
supportedApplications: [
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppGoogleName"
|
||||
],
|
||||
policy: {
|
||||
algorithm: "HmacSHA1",
|
||||
digits: 6,
|
||||
lookAheadWindow: 1,
|
||||
type: "totp",
|
||||
period: 30,
|
||||
getAlgorithmKey: () => "SHA1"
|
||||
}
|
||||
},
|
||||
mode: "qr",
|
||||
isAppInitiatedAction: false,
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Log>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "log.ftl",
|
||||
log: {
|
||||
events: [
|
||||
{
|
||||
date: "2/21/2024, 1:28:39 PM",
|
||||
event: "login",
|
||||
ipAddress: "172.17.0.1",
|
||||
client: "security-admin-console",
|
||||
details: [{ key: "openid-connect", value: "admin" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContext.FederatedIdentity>({
|
||||
...kcContextCommonMock,
|
||||
stateChecker: "",
|
||||
pageId: "federatedIdentity.ftl",
|
||||
federatedIdentity: {
|
||||
identities: [
|
||||
{
|
||||
providerId: "keycloak-oidc",
|
||||
displayName: "keycloak-oidc",
|
||||
userName: "John",
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
removeLinkPossible: true
|
||||
}
|
||||
})
|
||||
];
|
@ -1,21 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import { createUseInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
|
||||
const { useInsertLinkTags } = createUseInsertLinkTags();
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
@ -24,13 +22,13 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
"qualifiedName": "html",
|
||||
"className": getClassName("kcHtmlClass")
|
||||
qualifiedName: "html",
|
||||
className: kcClsx("kcHtmlClass")
|
||||
});
|
||||
|
||||
useSetClassName({
|
||||
"qualifiedName": "body",
|
||||
"className": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
qualifiedName: "body",
|
||||
className: clsx("admin-console", "user", kcClsx("kcBodyClass"))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,7 +44,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
"hrefs": !doUseDefaultCss
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
@ -74,14 +73,13 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<li>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href={getChangeLocalUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
|
||||
<a href={getChangeLocaleUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -147,7 +145,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className="pficon pficon-ok"></span>}
|
||||
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
|
||||
<span className="kc-feedback-text">{message.summary}</span>
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
|
||||
export type TemplateProps<KcContext, I18n> = {
|
||||
kcContext: KcContext;
|
||||
i18n: I18nExtended;
|
||||
i18n: I18n;
|
||||
doUseDefaultCss: boolean;
|
||||
active: string;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
children: ReactNode;
|
||||
|
||||
active: string;
|
||||
};
|
||||
|
||||
export type ClassKey =
|
||||
|
6
src/account/i18n/GenericI18n.tsx
Normal file
6
src/account/i18n/GenericI18n.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import type { GenericI18n_noJsx } from "./i18n";
|
||||
|
||||
export type GenericI18n<MessageKey extends string> = GenericI18n_noJsx<MessageKey> & {
|
||||
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||
};
|
@ -1,26 +1,24 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import fallbackMessages from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { KcContext } from "../kcContext/KcContext";
|
||||
import { Markdown } from "keycloakify/tools/Markdown";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
import messages_defaultSet_fallbackLanguage from "./messages_defaultSet/en";
|
||||
import { fetchMessages_defaultSet } from "./messages_defaultSet";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export type KcContextLike = {
|
||||
locale?: {
|
||||
currentLanguageTag: string;
|
||||
supported: { languageTag: string; url: string; label: string }[];
|
||||
};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||
|
||||
export type GenericI18n<MessageKey extends string> = {
|
||||
export type GenericI18n_noJsx<MessageKey extends string> = {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
@ -31,7 +29,7 @@ export type GenericI18n<MessageKey extends string> = {
|
||||
* Redirect to this url to change the language.
|
||||
* After reload currentLanguageTag === newLanguageTag
|
||||
*/
|
||||
getChangeLocalUrl: (newLanguageTag: string) => string;
|
||||
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
||||
/**
|
||||
* e.g. "en" => "English", "fr" => "Français", ...
|
||||
*
|
||||
@ -40,111 +38,165 @@ export type GenericI18n<MessageKey extends string> = {
|
||||
* */
|
||||
labelBySupportedLanguageTag: Record<string, string>;
|
||||
/**
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
*
|
||||
* msg("access-denied") === <span>Access denied</span>
|
||||
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
|
||||
*/
|
||||
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||
/**
|
||||
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
|
||||
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
* {
|
||||
* en: {
|
||||
* "access-denied": "Access denied",
|
||||
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
||||
* "bar": "Bar {0}"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* msgStr("access-denied") === "Access denied"
|
||||
* msgStr("not-a-message-key") Throws an error
|
||||
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
||||
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
||||
* The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html.
|
||||
*/
|
||||
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
||||
/**
|
||||
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
||||
* in the Keycloak admin for example.
|
||||
*
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
||||
*/
|
||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||
/**
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||
* {
|
||||
* en: {
|
||||
* "access-denied": "Access denied",
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
||||
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
||||
*/
|
||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||
|
||||
/**
|
||||
* Initially the messages are in english (fallback language).
|
||||
* The translations in the current language are being fetched dynamically.
|
||||
* This property is true while the translations are being fetched.
|
||||
*/
|
||||
isFetchingTranslations: boolean;
|
||||
};
|
||||
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
||||
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
||||
}) {
|
||||
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
||||
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>;
|
||||
|
||||
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
||||
|
||||
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
||||
|
||||
function getI18n(params: { kcContext: KcContextLike }): Result {
|
||||
const { kcContext } = params;
|
||||
|
||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||
use_cache: {
|
||||
const cachedResult = cachedResultByKcContext.get(kcContext);
|
||||
|
||||
const refHasStartedFetching = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
if (cachedResult === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||
|
||||
setI18n({
|
||||
...createI18nTranslationFunctions({
|
||||
"fallbackMessages": {
|
||||
...fallbackMessages,
|
||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||
} as any,
|
||||
"messages": {
|
||||
...(await getMessages(currentLanguageTag)),
|
||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||
...(extraMessages[currentLanguageTag] ?? {})
|
||||
} as any
|
||||
}),
|
||||
currentLanguageTag,
|
||||
"getChangeLocalUrl": newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
|
||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||
|
||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||
|
||||
return targetSupportedLocale.url;
|
||||
},
|
||||
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||
)
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return i18n ?? null;
|
||||
}
|
||||
|
||||
return { useI18n };
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
fallbackMessages: Record<MessageKey, string>;
|
||||
messages: Record<MessageKey, string>;
|
||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const { fallbackMessages, messages } = params;
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
||||
const { key, args, doRenderMarkdown } = props;
|
||||
|
||||
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||
|
||||
if (messageOrUndefined === undefined) {
|
||||
return undefined;
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const message = messageOrUndefined;
|
||||
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG,
|
||||
getChangeLocaleUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
|
||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||
|
||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||
|
||||
return targetSupportedLocale.url;
|
||||
},
|
||||
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
|
||||
};
|
||||
|
||||
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey_themeDefined>({
|
||||
messages_themeDefined:
|
||||
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
||||
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG] ??
|
||||
(() => {
|
||||
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
||||
if (firstLanguageTag === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return messagesByLanguageTag_themeDefined[firstLanguageTag];
|
||||
})(),
|
||||
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
||||
});
|
||||
|
||||
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === FALLBACK_LANGUAGE_TAG;
|
||||
|
||||
const result: Result = {
|
||||
i18n: {
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({
|
||||
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
||||
}),
|
||||
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
||||
},
|
||||
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
||||
? undefined
|
||||
: (async () => {
|
||||
const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag);
|
||||
|
||||
const i18n_currentLanguage: I18n = {
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
||||
isFetchingTranslations: false
|
||||
};
|
||||
|
||||
// NOTE: This promise.resolve is just because without it we TypeScript
|
||||
// gives a Variable 'result' is used before being assigned. error
|
||||
await Promise.resolve().then(() => {
|
||||
result.i18n = i18n_currentLanguage;
|
||||
result.prI18n_currentLanguage = undefined;
|
||||
});
|
||||
|
||||
return i18n_currentLanguage;
|
||||
})()
|
||||
};
|
||||
|
||||
cachedResultByKcContext.set(kcContext, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return { getI18n };
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
||||
messages_themeDefined: Record<MessageKey_themeDefined, string> | undefined;
|
||||
messages_fromKcServer: Record<string, string>;
|
||||
}) {
|
||||
const { messages_themeDefined, messages_fromKcServer } = params;
|
||||
|
||||
function createI18nTranslationFunctions(params: {
|
||||
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
||||
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>, "msgStr" | "advancedMsgStr"> {
|
||||
const { messages_defaultSet_currentLanguage } = params;
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
||||
const { key, args } = props;
|
||||
|
||||
const message =
|
||||
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
||||
id<Record<string, string | undefined> | undefined>(messages_themeDefined)?.[key] ??
|
||||
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
||||
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
||||
|
||||
if (message === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const messageWithArgsInjectedIfAny = (() => {
|
||||
const startIndex = message
|
||||
.match(/{[0-9]+}/g)
|
||||
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||
@ -163,68 +215,36 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
||||
);
|
||||
});
|
||||
|
||||
return messageWithArgsInjected;
|
||||
})();
|
||||
}
|
||||
|
||||
return doRenderMarkdown ? (
|
||||
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||
{messageWithArgsInjectedIfAny}
|
||||
</Markdown>
|
||||
) : (
|
||||
messageWithArgsInjectedIfAny
|
||||
);
|
||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string {
|
||||
const { key, args } = props;
|
||||
|
||||
const match = key.match(/^\$\{(.+)\}$/);
|
||||
|
||||
if (match === null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return resolveMsg({ key: match[1], args }) ?? key;
|
||||
}
|
||||
|
||||
return {
|
||||
msgStr: (key, ...args) => {
|
||||
const resolvedMessage = resolveMsg({ key, args });
|
||||
assert(resolvedMessage !== undefined, `Message with key "${key}" not found`);
|
||||
return resolvedMessage;
|
||||
},
|
||||
advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args })
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
||||
const { key, args, doRenderMarkdown } = props;
|
||||
|
||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||
|
||||
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||
|
||||
const out = resolveMsg({
|
||||
"key": keyUnwrappedFromCurlyBraces,
|
||||
args,
|
||||
doRenderMarkdown
|
||||
});
|
||||
|
||||
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||
}
|
||||
|
||||
return {
|
||||
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
||||
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
||||
};
|
||||
return { createI18nTranslationFunctions };
|
||||
}
|
||||
|
||||
const keycloakifyExtraMessages = {
|
||||
"en": {
|
||||
"shouldBeEqual": "{0} should be equal to {1}",
|
||||
"shouldBeDifferent": "{0} should be different to {1}",
|
||||
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||
"mustBeAnInteger": "Must be an integer",
|
||||
"notAValidOption": "Not a valid option",
|
||||
"newPasswordSameAsOld": "New password must be different from the old one",
|
||||
"passwordConfirmNotMatch": "Password confirmation does not match"
|
||||
},
|
||||
"fr": {
|
||||
/* spell-checker: disable */
|
||||
"shouldBeEqual": "{0} doit être égal à {1}",
|
||||
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||
"mustBeAnInteger": "Doit être un nombre entier",
|
||||
"notAValidOption": "N'est pas une option valide",
|
||||
|
||||
"logoutConfirmTitle": "Déconnexion",
|
||||
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||
"doLogout": "Se déconnecter",
|
||||
"newPasswordSameAsOld": "Le nouveau mot de passe doit être différent de l'ancien",
|
||||
"passwordConfirmNotMatch": "La confirmation du mot de passe ne correspond pas"
|
||||
/* spell-checker: enable */
|
||||
}
|
||||
};
|
||||
|
@ -1 +1,5 @@
|
||||
export type { I18n } from "./i18n";
|
||||
import type { GenericI18n } from "./GenericI18n";
|
||||
import type { MessageKey_defaultSet, KcContextLike } from "./i18n";
|
||||
export type { MessageKey_defaultSet, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey_defaultSet>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
|
95
src/account/i18n/useI18n.tsx
Normal file
95
src/account/i18n/useI18n.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { createGetI18n, type GenericI18n_noJsx, type KcContextLike, type MessageKey_defaultSet } from "./i18n";
|
||||
import { GenericI18n } from "./GenericI18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export function createUseI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
||||
}) {
|
||||
type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined;
|
||||
|
||||
type I18n = GenericI18n<MessageKey>;
|
||||
|
||||
const { withJsx } = (() => {
|
||||
const cache = new WeakMap<GenericI18n_noJsx<MessageKey>, GenericI18n<MessageKey>>();
|
||||
|
||||
function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element {
|
||||
const { htmlString, msgKey } = params;
|
||||
return (
|
||||
<div
|
||||
data-kc-msg={msgKey}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: htmlString
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function withJsx(i18n_noJsx: GenericI18n_noJsx<MessageKey>): I18n {
|
||||
use_cache: {
|
||||
const i18n = cache.get(i18n_noJsx);
|
||||
|
||||
if (i18n === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
||||
|
||||
const i18n: I18n = {
|
||||
...i18n_noJsx,
|
||||
msg: (msgKey, ...args) => renderHtmlString({ htmlString: i18n_noJsx.msgStr(msgKey, ...args), msgKey }),
|
||||
advancedMsg: (msgKey, ...args) => renderHtmlString({ htmlString: i18n_noJsx.advancedMsgStr(msgKey, ...args), msgKey })
|
||||
};
|
||||
|
||||
cache.set(i18n_noJsx, i18n);
|
||||
|
||||
return i18n;
|
||||
}
|
||||
|
||||
return { withJsx };
|
||||
})();
|
||||
|
||||
add_style: {
|
||||
const attributeName = "data-kc-i18n";
|
||||
|
||||
// Check if already exists in head
|
||||
if (document.querySelector(`style[${attributeName}]`) !== null) {
|
||||
break add_style;
|
||||
}
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.attributes.setNamedItem(document.createAttribute(attributeName));
|
||||
(styleElement.textContent = `[data-kc-msg] { display: inline-block; }`), document.head.prepend(styleElement);
|
||||
}
|
||||
|
||||
const { getI18n } = createGetI18n(messagesByLanguageTag);
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
||||
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
|
||||
|
||||
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(withJsx(i18n));
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
|
||||
prI18n_currentLanguage?.then(i18n => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
setI18n_toReturn(withJsx(i18n));
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { i18n: i18n_toReturn };
|
||||
}
|
||||
|
||||
return { useI18n, ofTypeI18n: Reflect<I18n>() };
|
||||
}
|
@ -1,10 +1,3 @@
|
||||
import Fallback from "keycloakify/account/Fallback";
|
||||
|
||||
export default Fallback;
|
||||
|
||||
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
|
||||
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
|
||||
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||
|
||||
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
export type { ExtendKcContext } from "keycloakify/account/KcContext";
|
||||
export type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
export { createUseI18n } from "keycloakify/account/i18n";
|
||||
|
@ -1,108 +0,0 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { isStorybook } from "keycloakify/lib/isStorybook";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||
|
||||
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
mockProperties?: Record<string, string>;
|
||||
}) {
|
||||
const { mockData, mockProperties } = params ?? {};
|
||||
|
||||
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||
mockPageId?: PageId;
|
||||
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||
}): {
|
||||
kcContext: PageId extends undefined
|
||||
? ExtendKcContext<KcContextExtension> | undefined
|
||||
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
|
||||
} {
|
||||
const { mockPageId, storyPartialKcContext } = params ?? {};
|
||||
|
||||
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
warn_that_mock_is_enbaled: {
|
||||
if (isStorybook) {
|
||||
break warn_that_mock_is_enbaled;
|
||||
}
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
}
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
const partialKcContextCustomMock = (() => {
|
||||
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||
|
||||
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
if (mockDataPick !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": mockDataPick
|
||||
});
|
||||
}
|
||||
|
||||
if (storyPartialKcContext !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": storyPartialKcContext
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(out).length === 0 ? undefined : out;
|
||||
})();
|
||||
|
||||
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
|
||||
console.warn(
|
||||
[
|
||||
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||
`Please check the documentation of the getKcContext function`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
const kcContext: any = {};
|
||||
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||
});
|
||||
|
||||
if (partialKcContextCustomMock !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": partialKcContextCustomMock
|
||||
});
|
||||
}
|
||||
|
||||
if (mockProperties !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext.properties,
|
||||
"source": mockProperties
|
||||
});
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
if (realKcContext === undefined) {
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
if (realKcContext.themeType !== "account") {
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
return { "kcContext": realKcContext as any };
|
||||
}
|
||||
|
||||
return { getKcContext };
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { createGetKcContext } from "./createGetKcContext";
|
||||
|
||||
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
|
||||
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
|
||||
*/
|
||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||
const { mockPageId, mockData } = params ?? {};
|
||||
|
||||
const { getKcContext } = createGetKcContext({
|
||||
mockData
|
||||
});
|
||||
|
||||
const { kcContext } = getKcContext({ mockPageId });
|
||||
|
||||
return { kcContext };
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||
? KcContext
|
||||
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
|
||||
|
||||
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
|
||||
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
|
||||
}
|
@ -1 +0,0 @@
|
||||
export type { KcContext } from "./KcContext";
|
@ -1,260 +0,0 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import { resources_common, keycloak_resources } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
|
||||
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "account",
|
||||
"themeName": "my-theme-name",
|
||||
"url": {
|
||||
resourcesPath,
|
||||
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
|
||||
"resourceUrl": "#",
|
||||
"accountUrl": "#",
|
||||
"applicationsUrl": "#",
|
||||
"logoutUrl": "#",
|
||||
"getLogoutUrl": () => "#",
|
||||
"logUrl": "#",
|
||||
"passwordUrl": "#",
|
||||
"sessionsUrl": "#",
|
||||
"socialUrl": "#",
|
||||
"totpUrl": "#"
|
||||
},
|
||||
"realm": {
|
||||
"internationalizationEnabled": true,
|
||||
"userManagedAccessAllowed": true
|
||||
},
|
||||
"messagesPerField": {
|
||||
"printIfExists": () => {
|
||||
return undefined;
|
||||
},
|
||||
"existsError": () => false,
|
||||
"get": key => `Fake error for ${key}`,
|
||||
"exists": () => false
|
||||
},
|
||||
"locale": {
|
||||
"supported": [
|
||||
/* spell-checker: disable */
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||
"label": "Deutsch",
|
||||
"languageTag": "de"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||
"label": "Norsk",
|
||||
"languageTag": "no"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||
"label": "Русский",
|
||||
"languageTag": "ru"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||
"label": "Svenska",
|
||||
"languageTag": "sv"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||
"label": "Português (Brasil)",
|
||||
"languageTag": "pt-BR"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||
"label": "Lietuvių",
|
||||
"languageTag": "lt"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||
"label": "English",
|
||||
"languageTag": "en"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||
"label": "Italiano",
|
||||
"languageTag": "it"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||
"label": "Français",
|
||||
"languageTag": "fr"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||
"label": "中文简体",
|
||||
"languageTag": "zh-CN"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||
"label": "Español",
|
||||
"languageTag": "es"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||
"label": "Čeština",
|
||||
"languageTag": "cs"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||
"label": "日本語",
|
||||
"languageTag": "ja"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||
"label": "Slovenčina",
|
||||
"languageTag": "sk"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||
"label": "Polski",
|
||||
"languageTag": "pl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||
"label": "Català",
|
||||
"languageTag": "ca"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||
"label": "Nederlands",
|
||||
"languageTag": "nl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||
"label": "Türkçe",
|
||||
"languageTag": "tr"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
],
|
||||
"currentLanguageTag": "en"
|
||||
},
|
||||
"features": {
|
||||
"authorization": true,
|
||||
"identityFederation": true,
|
||||
"log": true,
|
||||
"passwordUpdateSupported": true
|
||||
},
|
||||
"referrer": undefined,
|
||||
"account": {
|
||||
"firstName": "john",
|
||||
"lastName": "doe",
|
||||
"email": "john.doe@code.gouv.fr",
|
||||
"username": "doe_j"
|
||||
},
|
||||
"properties": {
|
||||
"parent": "account-v1",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"accountResourceProvider": "account-v1",
|
||||
"styles":
|
||||
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
"kcButtonClass": "btn",
|
||||
"kcButtonDefaultClass": "btn-default"
|
||||
}
|
||||
};
|
||||
|
||||
export const kcContextMocks: KcContext[] = [
|
||||
id<KcContext.Password>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "password.ftl",
|
||||
"password": {
|
||||
"passwordSet": true
|
||||
},
|
||||
"stateChecker": "state checker"
|
||||
}),
|
||||
id<KcContext.Account>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "account.ftl",
|
||||
"url": {
|
||||
...kcContextCommonMock.url,
|
||||
"referrerURI": "#",
|
||||
"accountUrl": "#"
|
||||
},
|
||||
"realm": {
|
||||
...kcContextCommonMock.realm,
|
||||
"registrationEmailAsUsername": true,
|
||||
"editUsernameAllowed": true
|
||||
},
|
||||
"stateChecker": ""
|
||||
}),
|
||||
id<KcContext.Sessions>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "sessions.ftl",
|
||||
"sessions": {
|
||||
"sessions": [
|
||||
{
|
||||
"ipAddress": "127.0.0.1",
|
||||
"started": new Date().toString(),
|
||||
"lastAccess": new Date().toString(),
|
||||
"expires": new Date().toString(),
|
||||
"clients": ["Chrome", "Firefox"],
|
||||
"id": "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stateChecker": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
}),
|
||||
id<KcContext.Totp>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "totp.ftl",
|
||||
"totp": {
|
||||
"enabled": true,
|
||||
"totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
"qrUrl": "#",
|
||||
"totpSecretQrCode":
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
|
||||
"manualUrl": "#",
|
||||
"totpSecret": "G4nsI8lQagRMUchH8jEG",
|
||||
"otpCredentials": [],
|
||||
"supportedApplications": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
"policy": {
|
||||
"algorithm": "HmacSHA1",
|
||||
"digits": 6,
|
||||
"lookAheadWindow": 1,
|
||||
"type": "totp",
|
||||
"period": 30
|
||||
}
|
||||
},
|
||||
"mode": "qr",
|
||||
"isAppInitiatedAction": false,
|
||||
"stateChecker": ""
|
||||
}),
|
||||
id<KcContext.Log>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "log.ftl",
|
||||
"log": {
|
||||
"events": [
|
||||
{
|
||||
"date": "2/21/2024, 1:28:39 PM",
|
||||
"event": "login",
|
||||
"ipAddress": "172.17.0.1",
|
||||
"client": "security-admin-console",
|
||||
"details": [{ key: "openid-connect", value: "admin" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContext.FederatedIdentity>({
|
||||
...kcContextCommonMock,
|
||||
"stateChecker": "",
|
||||
"pageId": "federatedIdentity.ftl",
|
||||
"federatedIdentity": {
|
||||
"identities": [
|
||||
{
|
||||
"providerId": "keycloak-oidc",
|
||||
"displayName": "keycloak-oidc",
|
||||
"userName": "John",
|
||||
"connected": true
|
||||
}
|
||||
],
|
||||
"removeLinkPossible": true
|
||||
}
|
||||
})
|
||||
];
|
25
src/account/lib/kcClsx.ts
Normal file
25
src/account/lib/kcClsx.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||
defaultClasses: {
|
||||
kcHtmlClass: undefined,
|
||||
kcBodyClass: undefined,
|
||||
kcButtonClass: "btn",
|
||||
kcContentWrapperClass: "row",
|
||||
kcButtonPrimaryClass: "btn-primary",
|
||||
kcButtonLargeClass: "btn-lg",
|
||||
kcButtonDefaultClass: "btn-default",
|
||||
kcFormClass: "form-horizontal",
|
||||
kcFormGroupClass: "form-group",
|
||||
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
kcLabelClass: "control-label",
|
||||
kcInputClass: "form-control",
|
||||
kcInputErrorMessageClass:
|
||||
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
}
|
||||
});
|
||||
|
||||
export type { ClassKey };
|
||||
|
||||
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
@ -1,20 +0,0 @@
|
||||
import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcHtmlClass": undefined,
|
||||
"kcBodyClass": undefined,
|
||||
"kcButtonClass": "btn",
|
||||
"kcContentWrapperClass": "row",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"kcButtonDefaultClass": "btn-default",
|
||||
"kcFormClass": "form-horizontal",
|
||||
"kcFormGroupClass": "form-group",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcLabelClass": "control-label",
|
||||
"kcInputClass": "form-control",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
}
|
||||
});
|
@ -1,18 +1,20 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const classes = {
|
||||
...props.classes,
|
||||
kcBodyClass: clsx(props.classes?.kcBodyClass, "user")
|
||||
};
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
|
||||
}
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
|
||||
@ -102,11 +104,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
@ -114,11 +112,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonDefaultClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Cancel"
|
||||
>
|
||||
|
@ -1,17 +1,12 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
function isArrayWithEmptyObject(variable: any): boolean {
|
||||
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
||||
}
|
||||
|
||||
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
@ -67,7 +62,6 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
||||
{index < application.realmRolesAvailable.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}
|
||||
{application.resourceRolesAvailable &&
|
||||
Object.keys(application.resourceRolesAvailable).map(resource => (
|
||||
<span key={resource}>
|
||||
@ -118,7 +112,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
||||
application.additionalGrants.length > 0 ? (
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(getClassName("kcButtonPrimaryClass"), getClassName("kcButtonClass"))}
|
||||
className={kcClsx("kcButtonPrimaryClass", "kcButtonClass")}
|
||||
id={`revoke-${application.client.clientId}`}
|
||||
name="clientId"
|
||||
value={application.client.id}
|
||||
@ -136,3 +130,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
function isArrayWithEmptyObject(variable: any): boolean {
|
||||
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PageProps } from "keycloakify/account";
|
||||
import { I18n } from "keycloakify/account/i18n";
|
||||
import { KcContext } from "keycloakify/account/kcContext";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type { Key } from "react";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Key } from "react";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
|
||||
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
@ -18,7 +18,7 @@ export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.f
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("accountLogHtmlTitle")}</h2>
|
||||
</div>
|
||||
|
@ -1,12 +1,10 @@
|
||||
import type { I18n } from "keycloakify/account/i18n";
|
||||
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
|
||||
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import type { KcContext } from "keycloakify/account/kcContext";
|
||||
|
||||
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
|
||||
export type PageProps<NarrowedKcContext, I18n> = {
|
||||
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||
kcContext: NarowedKcContext;
|
||||
i18n: I18nExtended;
|
||||
kcContext: NarrowedKcContext;
|
||||
i18n: I18n;
|
||||
doUseDefaultCss: boolean;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
};
|
||||
|
@ -1,19 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const classes = {
|
||||
...props.classes,
|
||||
kcBodyClass: clsx(props.classes?.kcBodyClass, "password")
|
||||
};
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||
}
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, password, account, stateChecker } = kcContext;
|
||||
@ -57,18 +59,18 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
{...{
|
||||
kcContext: {
|
||||
...kcContext,
|
||||
"message": (() => {
|
||||
message: (() => {
|
||||
if (newPasswordError !== "") {
|
||||
return {
|
||||
"type": "error",
|
||||
"summary": newPasswordError
|
||||
type: "error",
|
||||
summary: newPasswordError
|
||||
};
|
||||
}
|
||||
|
||||
if (newPasswordConfirmError !== "") {
|
||||
return {
|
||||
"type": "error",
|
||||
"summary": newPasswordConfirmError
|
||||
type: "error",
|
||||
summary: newPasswordConfirmError
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,7 +100,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
value={account.username ?? ""}
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
style={{ "display": "none" }}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{password.passwordSet && (
|
||||
@ -192,11 +194,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
<button
|
||||
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
@ -17,7 +16,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
|
||||
const { msg } = i18n;
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("sessionsHtmlTitle")}</h2>
|
||||
</div>
|
||||
@ -56,7 +55,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
|
||||
|
||||
<form action={url.sessionsUrl} method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
|
||||
<button id="logout-all-sessions" type="submit" className={kcClsx("kcButtonDefaultClass", "kcButtonClass")}>
|
||||
{msg("doLogOutAllSessions")}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -1,27 +1,20 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { MessageKey } from "keycloakify/account/i18n/i18n";
|
||||
|
||||
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
|
||||
"HmacSHA1": "SHA1",
|
||||
"HmacSHA256": "SHA256",
|
||||
"HmacSHA512": "SHA512"
|
||||
};
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
|
||||
@ -78,11 +71,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
<li>
|
||||
<p>{msg("totpStep1")}</p>
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.supportedApplications?.map(app => (
|
||||
<li key={app}>{msg(app as MessageKey)}</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul id="kc-totp-supported-apps">{totp.supportedApplications?.map(app => <li key={app}>{advancedMsg(app)}</li>)}</ul>
|
||||
</li>
|
||||
|
||||
{mode && mode == "manual" ? (
|
||||
@ -105,7 +94,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
|
||||
</li>
|
||||
<li id="kc-totp-algorithm">
|
||||
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
|
||||
{msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
|
||||
</li>
|
||||
<li id="kc-totp-digits">
|
||||
{msg("totpDigits")}: {totp.policy.digits}
|
||||
@ -145,9 +134,9 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<form action={url.totpUrl} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||
<form action={url.totpUrl} className={kcClsx("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="totp" className="control-label">
|
||||
{msg("authenticatorCode")}
|
||||
@ -160,23 +149,28 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
id="totp"
|
||||
name="totp"
|
||||
autoComplete="off"
|
||||
className={getClassName("kcInputClass")}
|
||||
className={kcClsx("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("totp")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
|
||||
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
|
||||
{msg("totpDeviceName")}
|
||||
</label>
|
||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
@ -187,37 +181,33 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
id="userLabel"
|
||||
name="userLabel"
|
||||
autoComplete="off"
|
||||
className={getClassName("kcInputClass")}
|
||||
className={kcClsx("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-label"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("userLabel")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(getClassName("kcFormGroupClass"), "text-right")}>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div id="kc-form-buttons" className={clsx(kcClsx("kcFormGroupClass"), "text-right")}>
|
||||
<div className={kcClsx("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSave")}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonDefaultClass"),
|
||||
getClassName("kcButtonLargeClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
|
||||
id="cancelTOTPBtn"
|
||||
name="submitAction"
|
||||
value="Cancel"
|
||||
|
105
src/bin/add-story.ts
Normal file
105
src/bin/add-story.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
values: [...THEME_TYPES]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
|
||||
console.log(chalk.cyan("Select the page you want to create a Storybook for:"));
|
||||
|
||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [...LOGIN_THEME_PAGE_IDS];
|
||||
case "account":
|
||||
return [...ACCOUNT_THEME_PAGE_IDS];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${pageId}`);
|
||||
|
||||
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
|
||||
/ftl$/,
|
||||
"stories.tsx"
|
||||
);
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"pages",
|
||||
componentBasename
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(`${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"stories",
|
||||
themeType,
|
||||
"pages",
|
||||
componentBasename
|
||||
)
|
||||
)
|
||||
.toString("utf8")
|
||||
.replace('import React from "react";\n', "")
|
||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
[
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} copy pasted from the Keycloakify source code into your project`,
|
||||
`You can start storybook with ${chalk.bold("npm run storybook")}`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,16 +1,13 @@
|
||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
await copyKeycloakResourcesToPublic({
|
||||
"buildOptions": {
|
||||
...buildOptions,
|
||||
"publicDirPath": buildOptions.reactAppRootDirPath
|
||||
}
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import { downloadBuiltinKeycloakTheme } from "./shared/downloadBuiltinKeycloakTheme";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log("Select the Keycloak version from which you want to download the builtins theme:");
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
"startingFromMajor": undefined,
|
||||
"cacheDirPath": buildOptions.cacheDirPath
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
|
||||
|
||||
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
destDirPath,
|
||||
buildOptions
|
||||
});
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
loginThemePageIds,
|
||||
accountThemePageIds,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
themeTypes,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log("Theme type:");
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
"values": [...themeTypes]
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log("Select the page you want to customize:");
|
||||
|
||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||
"values": (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [...loginThemePageIds];
|
||||
case "account":
|
||||
return [...accountThemePageIds];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
||||
|
||||
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", componentPageBasename);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { "recursive": true });
|
||||
}
|
||||
}
|
||||
|
||||
const componentPageContent = fs
|
||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", componentPageBasename))
|
||||
.toString("utf8");
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
|
||||
|
||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||
|
||||
console.log(
|
||||
[
|
||||
``,
|
||||
`\`${pathJoin(".", pathRelative(process.cwd(), targetFilePath))}\` copy pasted from the Keycloakify source code into your project.`,
|
||||
``,
|
||||
`You now need to update your page router:`,
|
||||
``,
|
||||
`\`${pathJoin(".", pathRelative(process.cwd(), themeSrcDirPath), themeType, "KcApp.tsx")}\`:`,
|
||||
"```",
|
||||
`// ...`,
|
||||
``,
|
||||
`+const ${componentPageBasename.replace(/.tsx$/, "")} = lazy(() => import("./pages/${componentPageBasename}"));`,
|
||||
``,
|
||||
` export default function KcApp(props: { kcContext: KcContext; }) {`,
|
||||
``,
|
||||
` // ...`,
|
||||
``,
|
||||
` return (`,
|
||||
` <Suspense>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.pageId) {`,
|
||||
` // ...`,
|
||||
` case "${pageId}": return (`,
|
||||
`+ <Login`,
|
||||
`+ {...{ kcContext, i18n, classes }}`,
|
||||
`+ Template={Template}`,
|
||||
...(!componentPageContent.includes(userProfileFormFieldComponentName)
|
||||
? []
|
||||
: [`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`]),
|
||||
`+ doUseDefaultCss={true}`,
|
||||
`+ />`,
|
||||
`+ );`,
|
||||
` default: return <Fallback /* .. */ />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
` }`,
|
||||
"```"
|
||||
].join("\n")
|
||||
);
|
||||
}
|
256
src/bin/eject-page.ts
Normal file
256
src/bin/eject-page.ts
Normal file
@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
values: [...THEME_TYPES]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
|
||||
console.log(chalk.cyan("Select the page you want to customize:"));
|
||||
|
||||
const templateValue = "Template.tsx (Layout common to every page)";
|
||||
const userProfileFormFieldsValue =
|
||||
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
||||
|
||||
const { value: pageIdOrComponent } = await cliSelect<
|
||||
| LoginThemePageId
|
||||
| AccountThemePageId
|
||||
| typeof templateValue
|
||||
| typeof userProfileFormFieldsValue
|
||||
>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [
|
||||
templateValue,
|
||||
userProfileFormFieldsValue,
|
||||
...LOGIN_THEME_PAGE_IDS
|
||||
];
|
||||
case "account":
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${pageIdOrComponent}`);
|
||||
|
||||
const componentBasename = (() => {
|
||||
if (pageIdOrComponent === templateValue) {
|
||||
return "Template.tsx";
|
||||
}
|
||||
|
||||
if (pageIdOrComponent === userProfileFormFieldsValue) {
|
||||
return "UserProfileFormFields.tsx";
|
||||
}
|
||||
|
||||
return capitalize(kebabCaseToCamelCase(pageIdOrComponent)).replace(/ftl$/, "tsx");
|
||||
})();
|
||||
|
||||
const pagesOrDot = (() => {
|
||||
if (
|
||||
pageIdOrComponent === templateValue ||
|
||||
pageIdOrComponent === userProfileFormFieldsValue
|
||||
) {
|
||||
return ".";
|
||||
}
|
||||
|
||||
return "pages";
|
||||
})();
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
pagesOrDot,
|
||||
componentBasename
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(
|
||||
`${pageIdOrComponent} is already ejected, ${pathRelative(
|
||||
process.cwd(),
|
||||
targetFilePath
|
||||
)} already exists`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
pagesOrDot,
|
||||
componentBasename
|
||||
)
|
||||
)
|
||||
.toString("utf8");
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} copy pasted from the Keycloakify source code into your project`
|
||||
);
|
||||
|
||||
edit_KcApp: {
|
||||
if (
|
||||
pageIdOrComponent !== templateValue &&
|
||||
pageIdOrComponent !== userProfileFormFieldsValue
|
||||
) {
|
||||
break edit_KcApp;
|
||||
}
|
||||
|
||||
const kcAppTsxPath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"KcPage.tsx"
|
||||
);
|
||||
|
||||
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
|
||||
|
||||
const modifiedKcAppTsxCode = (() => {
|
||||
switch (pageIdOrComponent) {
|
||||
case templateValue:
|
||||
return kcAppTsxCode.replace(
|
||||
`keycloakify/${themeType}/Template`,
|
||||
"./Template"
|
||||
);
|
||||
case userProfileFormFieldsValue:
|
||||
return kcAppTsxCode.replace(
|
||||
`keycloakify/login/UserProfileFormFields`,
|
||||
"./UserProfileFormFields"
|
||||
);
|
||||
}
|
||||
assert<Equals<typeof pageIdOrComponent, never>>(false);
|
||||
})();
|
||||
|
||||
if (kcAppTsxCode === modifiedKcAppTsxCode) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
"Unable to automatically update KcPage.tsx, please update it manually"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(kcAppTsxPath, Buffer.from(modifiedKcAppTsxCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), kcAppTsxPath))
|
||||
)} Updated`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||
|
||||
const componentName = componentBasename.replace(/.tsx$/, "");
|
||||
|
||||
console.log(
|
||||
[
|
||||
``,
|
||||
`You now need to update your page router:`,
|
||||
``,
|
||||
`${chalk.bold(
|
||||
pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), buildContext.themeSrcDirPath),
|
||||
themeType,
|
||||
"KcPage.tsx"
|
||||
)
|
||||
)}:`,
|
||||
chalk.grey("```"),
|
||||
`// ...`,
|
||||
``,
|
||||
chalk.green(
|
||||
`+const ${componentName} = lazy(() => import("./pages/${componentName}"));`
|
||||
),
|
||||
...[
|
||||
``,
|
||||
` export default function KcPage(props: { kcContext: KcContext; }) {`,
|
||||
``,
|
||||
` // ...`,
|
||||
``,
|
||||
` return (`,
|
||||
` <Suspense>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.pageId) {`,
|
||||
` // ...`,
|
||||
`+ case "${pageIdOrComponent}": return (`,
|
||||
`+ <${componentName}`,
|
||||
`+ {...{ kcContext, i18n, classes }}`,
|
||||
`+ Template={Template}`,
|
||||
`+ doUseDefaultCss={true}`,
|
||||
...(!componentCode.includes(userProfileFormFieldComponentName)
|
||||
? []
|
||||
: [
|
||||
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`,
|
||||
`+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}`
|
||||
]),
|
||||
`+ />`,
|
||||
`+ );`,
|
||||
` default: return <Fallback /* .. */ />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
` }`
|
||||
].map(line => {
|
||||
if (line.startsWith("+")) {
|
||||
return chalk.green(line);
|
||||
}
|
||||
if (line.startsWith("-")) {
|
||||
return chalk.red(line);
|
||||
}
|
||||
return chalk.grey(line);
|
||||
}),
|
||||
chalk.grey("```")
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,26 +1,25 @@
|
||||
import { downloadBuiltinKeycloakTheme } from "./shared/downloadBuiltinKeycloakTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||
import { rmSync } from "./tools/fs.rmSync";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
|
||||
});
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||
console.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
|
||||
console.warn(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
@ -29,33 +28,38 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
"startingFromMajor": 17,
|
||||
"cacheDirPath": buildOptions.cacheDirPath
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
cacheDirPath: buildContext.cacheDirPath
|
||||
});
|
||||
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "initialize-email-theme_tmp");
|
||||
|
||||
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
buildOptions
|
||||
buildContext
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||
"destDirPath": emailThemeSrcDirPath
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "base", "email"),
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
{
|
||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||
|
||||
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||
fs.writeFileSync(
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`,
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`The \`${pathJoin(".", pathRelative(process.cwd(), emailThemeSrcDirPath))}\` directory have been created.`);
|
||||
console.log(
|
||||
`The \`${pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
||||
)}\` directory have been created.`
|
||||
);
|
||||
console.log("You can delete any file you don't modify.");
|
||||
|
||||
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
|
||||
}
|
||||
|
@ -1,88 +1,144 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { accountV1ThemeName } from "../../shared/constants";
|
||||
import { generatePom, BuildOptionsLike as BuildOptionsLike_generatePom } from "./generatePom";
|
||||
import { ACCOUNT_V1_THEME_NAME } from "../../shared/constants";
|
||||
import {
|
||||
generatePom,
|
||||
BuildContextLike as BuildContextLike_generatePom
|
||||
} from "./generatePom";
|
||||
import { readFileSync } from "fs";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { getMetaInfKeycloakThemesJsonPath } from "../../shared/metaInfKeycloakThemes";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike_generatePom & {
|
||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||
keycloakifyBuildDirPath: string;
|
||||
themeNames: string[];
|
||||
artifactId: string;
|
||||
themeVersion: string;
|
||||
cacheDirPath: string;
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function buildJar(params: {
|
||||
jarFileBasename: string;
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
buildOptions: BuildOptionsLike;
|
||||
resourcesDirPath: string;
|
||||
doesImplementAccountV1Theme: boolean;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { jarFileBasename, keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
|
||||
const {
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
resourcesDirPath,
|
||||
doesImplementAccountV1Theme,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.cacheDirPath, jarFileBasename.replace(".jar", ""));
|
||||
const keycloakifyBuildCacheDirPath = pathJoin(
|
||||
buildContext.cacheDirPath,
|
||||
"maven",
|
||||
jarFileBasename.replace(".jar", "")
|
||||
);
|
||||
|
||||
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true, "force": true });
|
||||
const tmpResourcesDirPath = pathJoin(
|
||||
keycloakifyBuildCacheDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources"
|
||||
);
|
||||
|
||||
{
|
||||
const keycloakThemesJsonFilePath = getMetaInfKeycloakThemesJsonPath({ "keycloakifyBuildDirPath": "" });
|
||||
rmSync(tmpResourcesDirPath, { recursive: true, force: true });
|
||||
|
||||
const themePropertiesFilePathSet = new Set(
|
||||
...buildOptions.themeNames.map(themeName => pathJoin("src", "main", "resources", "theme", themeName, "account", "theme.properties"))
|
||||
);
|
||||
transformCodebase({
|
||||
srcDirPath: resourcesDirPath,
|
||||
destDirPath: tmpResourcesDirPath,
|
||||
transformSourceCode:
|
||||
!doesImplementAccountV1Theme || keycloakAccountV1Version !== null
|
||||
? undefined
|
||||
: (params: {
|
||||
fileRelativePath: string;
|
||||
sourceCode: Buffer;
|
||||
}): { modifiedSourceCode: Buffer } | undefined => {
|
||||
const { fileRelativePath, sourceCode } = params;
|
||||
|
||||
const accountV1RelativeDirPath = pathJoin("src", "main", "resources", "theme", accountV1ThemeName);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
"destDirPath": keycloakifyBuildTmpDirPath,
|
||||
"transformSourceCode":
|
||||
keycloakAccountV1Version !== null
|
||||
? undefined
|
||||
: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath === keycloakThemesJsonFilePath) {
|
||||
const keycloakThemesJsonParsed = JSON.parse(sourceCode.toString("utf8")) as {
|
||||
themes: { name: string; types: string[] }[];
|
||||
};
|
||||
|
||||
keycloakThemesJsonParsed.themes = keycloakThemesJsonParsed.themes.filter(({ name }) => name !== accountV1ThemeName);
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(JSON.stringify(keycloakThemesJsonParsed, null, 2), "utf8") };
|
||||
}
|
||||
|
||||
if (isInside({ "dirPath": "target", "filePath": fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isInside({ "dirPath": accountV1RelativeDirPath, "filePath": fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (themePropertiesFilePathSet.has(fileRelativePath)) {
|
||||
return {
|
||||
"modifiedSourceCode": Buffer.from(
|
||||
sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin("theme", ACCOUNT_V1_THEME_NAME),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
if (
|
||||
fileRelativePath ===
|
||||
pathJoin("theme", themeName, "account", "theme.properties")
|
||||
) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(
|
||||
`parent=${ACCOUNT_V1_THEME_NAME}`,
|
||||
"parent=keycloak"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(
|
||||
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||
);
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
remove_account_v1_in_meta_inf: {
|
||||
if (!doesImplementAccountV1Theme) {
|
||||
// NOTE: We do not have account v1 anyway
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath: tmpResourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
|
||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
||||
({ name }) => name !== ACCOUNT_V1_THEME_NAME
|
||||
);
|
||||
|
||||
return metaInfKeycloakTheme;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
route_legacy_pages: {
|
||||
if (!buildContext.recordIsImplementedByThemeType.login) {
|
||||
break route_legacy_pages;
|
||||
}
|
||||
|
||||
// NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create
|
||||
// the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak
|
||||
// 24 in version 0.4 and up, we can safely break the route for legacy pages.
|
||||
@ -97,17 +153,24 @@ export async function buildJar(params: {
|
||||
}
|
||||
})();
|
||||
|
||||
// TODO: Remove this optimization, it's a bit hacky.
|
||||
if (doBreak) {
|
||||
break route_legacy_pages;
|
||||
}
|
||||
|
||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
||||
buildOptions.themeNames.map(themeName => {
|
||||
const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources", "theme", themeName, "login", pageId);
|
||||
buildContext.themeNames.map(themeName => {
|
||||
const ftlFilePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"theme",
|
||||
themeName,
|
||||
"login",
|
||||
pageId
|
||||
);
|
||||
|
||||
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
||||
|
||||
const realPageId = (() => {
|
||||
const ftlFileBasename = (() => {
|
||||
switch (pageId) {
|
||||
case "register.ftl":
|
||||
return "register-user-profile.ftl";
|
||||
@ -118,53 +181,65 @@ export async function buildJar(params: {
|
||||
})();
|
||||
|
||||
const modifiedFtlFileContent = ftlFileContent.replace(
|
||||
`out["pageId"] = "\${pageId}";`,
|
||||
`out["pageId"] = "${pageId}"; out["realPageId"] = "${realPageId}";`
|
||||
`"ftlTemplateFileName": "${pageId}"`,
|
||||
`"ftlTemplateFileName": "${ftlFileBasename}"`
|
||||
);
|
||||
|
||||
assert(modifiedFtlFileContent !== ftlFileContent);
|
||||
|
||||
fs.writeFile(pathJoin(pathDirname(ftlFilePath), realPageId), Buffer.from(modifiedFtlFileContent, "utf8"));
|
||||
fs.writeFile(
|
||||
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
|
||||
Buffer.from(modifiedFtlFileContent, "utf8")
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const { pomFileCode } = generatePom({
|
||||
buildOptions,
|
||||
buildContext,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
});
|
||||
|
||||
await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
|
||||
await fs.writeFile(
|
||||
pathJoin(keycloakifyBuildCacheDirPath, "pom.xml"),
|
||||
Buffer.from(pomFileCode, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec("mvn clean install", { "cwd": keycloakifyBuildTmpDirPath }, error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
child_process.exec(
|
||||
`mvn install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await fs.rename(
|
||||
pathJoin(keycloakifyBuildTmpDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`),
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)
|
||||
pathJoin(
|
||||
keycloakifyBuildCacheDirPath,
|
||||
"target",
|
||||
`${buildContext.artifactId}-${buildContext.themeVersion}.jar`
|
||||
),
|
||||
pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename)
|
||||
);
|
||||
|
||||
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true });
|
||||
}
|
||||
|
@ -1,33 +1,39 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { keycloakAccountV1Versions, keycloakThemeAdditionalInfoExtensionVersions } from "./extensionVersions";
|
||||
import {
|
||||
keycloakAccountV1Versions,
|
||||
keycloakThemeAdditionalInfoExtensionVersions
|
||||
} from "./extensionVersions";
|
||||
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
|
||||
import { buildJar, BuildOptionsLike as BuildOptionsLike_buildJar } from "./buildJar";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { getJarFileBasename } from "../../shared/getJarFileBasename";
|
||||
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { accountV1ThemeName } from "../../shared/constants";
|
||||
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike_buildJar & {
|
||||
export type BuildContextLike = BuildContextLike_buildJar & {
|
||||
projectDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
jarTargets: BuildContext["jarTargets"];
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function buildJars(params: { buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
const { buildOptions } = params;
|
||||
export async function buildJars(params: {
|
||||
resourcesDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
|
||||
}).themes.some(({ name }) => name === accountV1ThemeName);
|
||||
const doesImplementAccountV1Theme =
|
||||
buildContext.recordIsImplementedByThemeType.account &&
|
||||
!buildContext.doUseAccountV3;
|
||||
|
||||
await Promise.all(
|
||||
keycloakAccountV1Versions
|
||||
.map(keycloakAccountV1Version =>
|
||||
keycloakThemeAdditionalInfoExtensionVersions
|
||||
.map(keycloakThemeAdditionalInfoExtensionVersion => {
|
||||
keycloakThemeAdditionalInfoExtensionVersions.map(
|
||||
keycloakThemeAdditionalInfoExtensionVersion => {
|
||||
const keycloakVersionRange = getKeycloakVersionRangeForJar({
|
||||
doesImplementAccountTheme,
|
||||
doesImplementAccountV1Theme,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
});
|
||||
@ -36,25 +42,27 @@ export async function buildJars(params: { buildOptions: BuildOptionsLike }): Pro
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange };
|
||||
})
|
||||
.filter(exclude(undefined))
|
||||
.map(({ keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange }) => {
|
||||
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
|
||||
const jarTarget = buildContext.jarTargets.find(
|
||||
jarTarget =>
|
||||
jarTarget.keycloakVersionRange === keycloakVersionRange
|
||||
);
|
||||
|
||||
return {
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
jarFileBasename
|
||||
};
|
||||
})
|
||||
.map(({ keycloakThemeAdditionalInfoExtensionVersion, jarFileBasename }) =>
|
||||
buildJar({
|
||||
if (jarTarget === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { jarFileBasename } = jarTarget;
|
||||
|
||||
return buildJar({
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildOptions
|
||||
})
|
||||
)
|
||||
resourcesDirPath,
|
||||
doesImplementAccountV1Theme,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
// NOTE: v0.5 is a dummy version.
|
||||
export const keycloakAccountV1Versions = [null, "0.3", "0.4"] as const;
|
||||
export const keycloakAccountV1Versions = [null, "0.3", "0.4", "0.6"] as const;
|
||||
|
||||
/**
|
||||
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1
|
||||
@ -13,4 +13,5 @@ export const keycloakThemeAdditionalInfoExtensionVersions = [null, "1.1.5"] as c
|
||||
* https://central.sonatype.com/artifact/dev.jcputney/keycloak-theme-additional-info-extension
|
||||
* https://github.com/jcputney/keycloak-theme-additional-info-extension
|
||||
* */
|
||||
export type KeycloakThemeAdditionalInfoExtensionVersion = (typeof keycloakThemeAdditionalInfoExtensionVersions)[number];
|
||||
export type KeycloakThemeAdditionalInfoExtensionVersion =
|
||||
(typeof keycloakThemeAdditionalInfoExtensionVersions)[number];
|
||||
|
@ -1,21 +1,28 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
export type BuildContextLike = {
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
themeVersion: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generatePom(params: {
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
buildOptions: BuildOptionsLike;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
|
||||
const {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
const { pomFileCode } = (function generatePomFileCode(): {
|
||||
pomFileCode: string;
|
||||
@ -26,16 +33,17 @@ export function generatePom(params: {
|
||||
` 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>`,
|
||||
` <groupId>${buildContext.groupId}</groupId>`,
|
||||
` <artifactId>${buildContext.artifactId}</artifactId>`,
|
||||
` <version>${buildContext.themeVersion}</version>`,
|
||||
` <name>${buildContext.artifactId}</name>`,
|
||||
` <description />`,
|
||||
` <packaging>jar</packaging>`,
|
||||
` <properties>`,
|
||||
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
|
||||
` </properties>`,
|
||||
...(keycloakAccountV1Version !== null && keycloakThemeAdditionalInfoExtensionVersion !== null
|
||||
...(keycloakAccountV1Version !== null ||
|
||||
keycloakThemeAdditionalInfoExtensionVersion !== null
|
||||
? [
|
||||
` <build>`,
|
||||
` <plugins>`,
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
|
||||
|
||||
export function getKeycloakVersionRangeForJar(params: {
|
||||
doesImplementAccountTheme: boolean;
|
||||
doesImplementAccountV1Theme: boolean;
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
}): KeycloakVersionRange | undefined {
|
||||
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, doesImplementAccountTheme } = params;
|
||||
const {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
doesImplementAccountV1Theme
|
||||
} = params;
|
||||
|
||||
if (doesImplementAccountTheme) {
|
||||
if (doesImplementAccountV1Theme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
switch (keycloakAccountV1Version) {
|
||||
case null:
|
||||
@ -19,7 +26,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return undefined;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
case "0.3":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
@ -27,19 +36,36 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return "23" as const;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
case "0.4":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
return undefined;
|
||||
case "1.1.5":
|
||||
return "24-and-above" as const;
|
||||
return "24" as const;
|
||||
}
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
case "0.6":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
return undefined;
|
||||
case "1.1.5":
|
||||
return "25-and-above" as const;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
}
|
||||
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme | undefined>>();
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountV1Theme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
@ -47,17 +73,23 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
return "21-and-below";
|
||||
case "1.1.5":
|
||||
return "22-and-above";
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
|
||||
false
|
||||
);
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme | undefined>>();
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountV1Theme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
}
|
||||
|
@ -1,521 +0,0 @@
|
||||
<script>const _=
|
||||
(()=>{
|
||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["messagesPerField"]= {
|
||||
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
|
||||
<#attempt>
|
||||
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
|
||||
<#list profile.attributes as attribute>
|
||||
<#if fieldNames?seq_contains(attribute.name)>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#assign fieldNames += [attribute.name]>
|
||||
</#list>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
"printIfExists": function (fieldName, text) {
|
||||
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
|
||||
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
|
||||
<#else>
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
|
||||
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
|
||||
<#assign doExistErrorOnUsernameOrPassword = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
|
||||
<#recover>
|
||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||
</#attempt>
|
||||
<#if doExistErrorOnUsernameOrPassword>
|
||||
return text;
|
||||
<#else>
|
||||
<#assign doExistMessageForField = "">
|
||||
<#attempt>
|
||||
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
|
||||
<#recover>
|
||||
<#assign doExistMessageForField = true>
|
||||
</#attempt>
|
||||
return <#if doExistMessageForField>text<#else>undefined</#if>;
|
||||
</#if>
|
||||
<#else>
|
||||
<#assign doExistMessageForField = "">
|
||||
<#attempt>
|
||||
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
|
||||
<#recover>
|
||||
<#assign doExistMessageForField = true>
|
||||
</#attempt>
|
||||
return <#if doExistMessageForField>text<#else>undefined</#if>;
|
||||
</#if>
|
||||
}
|
||||
</#list>
|
||||
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
|
||||
</#if>
|
||||
},
|
||||
"existsError": function (){
|
||||
function existsError_singleFieldName(fieldName) {
|
||||
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
|
||||
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
|
||||
<#else>
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
|
||||
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
|
||||
<#assign doExistErrorOnUsernameOrPassword = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
|
||||
<#recover>
|
||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||
</#attempt>
|
||||
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
|
||||
<#else>
|
||||
<#assign doExistErrorMessageForField = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
|
||||
<#recover>
|
||||
<#assign doExistErrorMessageForField = true>
|
||||
</#attempt>
|
||||
return <#if doExistErrorMessageForField>true<#else>false</#if>;
|
||||
</#if>
|
||||
}
|
||||
</#list>
|
||||
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
|
||||
</#if>
|
||||
}
|
||||
for( let i = 0; i < arguments.length; i++ ){
|
||||
if( existsError_singleFieldName(arguments[i]) ){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
"get": function (fieldName) {
|
||||
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
|
||||
throw new Error("You're not supposed to use messagesPerField.get in this page");
|
||||
<#else>
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
|
||||
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
|
||||
<#assign doExistErrorOnUsernameOrPassword = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
|
||||
<#recover>
|
||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||
</#attempt>
|
||||
<#if doExistErrorOnUsernameOrPassword>
|
||||
<#attempt>
|
||||
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
|
||||
<#recover>
|
||||
return "Invalid username or password.";
|
||||
</#attempt>
|
||||
<#else>
|
||||
<#attempt>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
<#recover>
|
||||
return "";
|
||||
</#attempt>
|
||||
</#if>
|
||||
<#else>
|
||||
<#attempt>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
<#recover>
|
||||
return "invalid field";
|
||||
</#attempt>
|
||||
</#if>
|
||||
}
|
||||
</#list>
|
||||
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
|
||||
</#if>
|
||||
},
|
||||
"exists": function (fieldName) {
|
||||
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
|
||||
throw new Error("You're not supposed to use messagesPerField.exists in this page");
|
||||
<#else>
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
|
||||
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
|
||||
<#assign doExistErrorOnUsernameOrPassword = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
|
||||
<#recover>
|
||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||
</#attempt>
|
||||
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
|
||||
<#else>
|
||||
<#assign doExistErrorMessageForField = "">
|
||||
<#attempt>
|
||||
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
|
||||
<#recover>
|
||||
<#assign doExistErrorMessageForField = true>
|
||||
</#attempt>
|
||||
return <#if doExistErrorMessageForField>true<#else>false</#if>;
|
||||
</#if>
|
||||
}
|
||||
</#list>
|
||||
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
|
||||
</#if>
|
||||
},
|
||||
"getFirstError": function () {
|
||||
for( let i = 0; i < arguments.length; i++ ){
|
||||
const fieldName = arguments[i];
|
||||
if( out.messagesPerField.existsError(fieldName) ){
|
||||
return out.messagesPerField.get(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
|
||||
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
|
||||
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
|
||||
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
|
||||
out["pageId"] = "${pageId}";
|
||||
|
||||
try {
|
||||
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
|
||||
} catch(error) { }
|
||||
|
||||
return out;
|
||||
|
||||
})();
|
||||
<#function ftl_object_to_js_code_declaring_an_object object path>
|
||||
|
||||
<#local isHash = "">
|
||||
<#attempt>
|
||||
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
|
||||
</#attempt>
|
||||
|
||||
<#if isHash>
|
||||
|
||||
<#if path?size gt 10>
|
||||
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
|
||||
</#if>
|
||||
|
||||
<#local keys = "">
|
||||
|
||||
<#attempt>
|
||||
<#local keys = object?keys>
|
||||
<#recover>
|
||||
<#return "ABORT: We can't list keys on this object">
|
||||
</#attempt>
|
||||
|
||||
<#local out_seq = []>
|
||||
|
||||
<#list keys as key>
|
||||
|
||||
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if
|
||||
(
|
||||
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
|
||||
are_same_path(path, ["url"])
|
||||
) || (
|
||||
key == "updateProfileCtx" &&
|
||||
are_same_path(path, [])
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
are_same_path(path, ["url"]) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
["secretData", "value"]?seq_contains(key) &&
|
||||
are_same_path(path, [ "totp", "otpCredentials", "*" ])
|
||||
) || (
|
||||
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
|
||||
are_same_path(path, ["brokerContext"]) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
) || (
|
||||
key == "identityProviderBrokerCtx" &&
|
||||
are_same_path(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
) || (
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||
are_same_path(path, ["realm"])
|
||||
) || (
|
||||
"error.ftl" == pageId &&
|
||||
are_same_path(path, ["realm"]) &&
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
is_subpath(path, ["applications", "applications"]) &&
|
||||
(
|
||||
key == "realm" ||
|
||||
key == "container"
|
||||
)
|
||||
) || (
|
||||
are_same_path(path, ["user"]) &&
|
||||
key == "delegateForUpdate"
|
||||
) || (
|
||||
<#-- Security audit forwarded by Garth (Gmail) -->
|
||||
are_same_path(path, ["client", "attributes"]) &&
|
||||
key == "saml.signing.private.key"
|
||||
) || (
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
|
||||
are_same_path(path, ["login"]) &&
|
||||
key == "password"
|
||||
) || (
|
||||
<#-- Remove realmAttributes added by https://github.com/jcputney/keycloak-theme-additional-info-extension for peace of mind. -->
|
||||
are_same_path(path, []) &&
|
||||
key == "realmAttributes"
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
key == "attemptedUsername" && are_same_path(path, ["auth"])
|
||||
)>
|
||||
<#attempt>
|
||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]>
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
<#if !object[key]??>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local propertyValue = "">
|
||||
|
||||
<#attempt>
|
||||
<#local propertyValue = object[key]>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
|
||||
|
||||
<#if rec_out?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local isMethod = "">
|
||||
<#attempt>
|
||||
<#local isMethod = object?is_method>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it'sa method.">
|
||||
</#attempt>
|
||||
|
||||
<#if isMethod>
|
||||
|
||||
<#if are_same_path(path, ["auth", "showUsername"])>
|
||||
<#attempt>
|
||||
<#return auth.showUsername()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showUsername()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if are_same_path(path, ["auth", "showResetCredentials"])>
|
||||
<#attempt>
|
||||
<#return auth.showResetCredentials()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
|
||||
<#attempt>
|
||||
<#return auth.showTryAnotherWayLink()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if are_same_path(path, ["url", "getLogoutUrl"])>
|
||||
<#local returnValue = "">
|
||||
<#attempt>
|
||||
<#local returnValue = url.getLogoutUrl()>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate url.getLogoutUrl()">
|
||||
</#attempt>
|
||||
<#return 'function(){ return "' + returnValue + '"; }'>
|
||||
</#if>
|
||||
|
||||
<#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])>
|
||||
<#local returnValue = "">
|
||||
<#attempt>
|
||||
<#local returnValue = totp.policy.getAlgorithmKey()>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
|
||||
</#attempt>
|
||||
<#return 'function(){ return "' + returnValue + '"; }'>
|
||||
</#if>
|
||||
|
||||
<#return "ABORT: It's a method">
|
||||
</#if>
|
||||
|
||||
<#local isBoolean = "">
|
||||
<#attempt>
|
||||
<#local isBoolean = object?is_boolean>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a boolean">
|
||||
</#attempt>
|
||||
|
||||
<#if isBoolean>
|
||||
<#return object?c>
|
||||
</#if>
|
||||
|
||||
<#local isEnumerable = "">
|
||||
<#attempt>
|
||||
<#local isEnumerable = object?is_enumerable>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's an enumerable">
|
||||
</#attempt>
|
||||
|
||||
|
||||
<#if isEnumerable>
|
||||
|
||||
<#local out_seq = []>
|
||||
|
||||
<#local i = 0>
|
||||
|
||||
<#list object as array_item>
|
||||
|
||||
<#if !array_item??>
|
||||
<#local out_seq += ["null,"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
|
||||
|
||||
<#local i = i + 1>
|
||||
|
||||
<#if rec_out?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local out_seq += [rec_out + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local isDate = "">
|
||||
<#attempt>
|
||||
<#local isDate = object?is_date_like>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a date">
|
||||
</#attempt>
|
||||
|
||||
<#if isDate>
|
||||
<#return '"' + object?datetime?iso_utc + '"'>
|
||||
</#if>
|
||||
|
||||
<#local isNumber = "">
|
||||
<#attempt>
|
||||
<#local isNumber = object?is_number>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a number">
|
||||
</#attempt>
|
||||
|
||||
<#if isNumber>
|
||||
<#return object?c>
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
<#return '"' + object?js_string + '"'>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non number, non enumerable object">
|
||||
|
||||
</#function>
|
||||
<#function is_subpath path searchedPath>
|
||||
|
||||
<#if path?size < searchedPath?size>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#local i=0>
|
||||
|
||||
<#list path as property>
|
||||
|
||||
<#if i == searchedPath?size >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local searchedProperty=searchedPath[i]>
|
||||
|
||||
<#local i+= 1>
|
||||
|
||||
<#if searchedProperty?is_string && searchedProperty == "*">
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_string && !property?is_string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_number && !property?is_number>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?string != property?string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return true>
|
||||
|
||||
</#function>
|
||||
|
||||
<#function are_same_path path searchedPath>
|
||||
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
|
||||
</#function>
|
||||
</script>
|
@ -1,34 +1,48 @@
|
||||
import cheerio from "cheerio";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
||||
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||
import {
|
||||
replaceImportsInJsCode,
|
||||
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
||||
} from "../replacers/replaceImportsInJsCode";
|
||||
import {
|
||||
replaceImportsInCssCode,
|
||||
BuildContextLike as BuildContextLike_replaceImportsInCssCode
|
||||
} from "../replacers/replaceImportsInCssCode";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../shared/constants";
|
||||
import {
|
||||
type ThemeType,
|
||||
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
|
||||
RESOURCES_COMMON
|
||||
} from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
themeVersion: string;
|
||||
urlPathname: string | undefined;
|
||||
reactAppBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
||||
BuildContextLike_replaceImportsInCssCode & {
|
||||
urlPathname: string | undefined;
|
||||
themeVersion: string;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generateFtlFilesCodeFactory(params: {
|
||||
themeName: string;
|
||||
indexHtmlCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
buildOptions: BuildOptionsLike;
|
||||
buildContext: BuildContextLike;
|
||||
keycloakifyVersion: string;
|
||||
themeType: ThemeType;
|
||||
fieldNames: string[];
|
||||
}) {
|
||||
const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
|
||||
const {
|
||||
themeName,
|
||||
indexHtmlCode,
|
||||
buildContext,
|
||||
keycloakifyVersion,
|
||||
themeType,
|
||||
fieldNames
|
||||
} = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
@ -38,7 +52,10 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
assert(jsCode !== null);
|
||||
|
||||
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
|
||||
const { fixedJsCode } = replaceImportsInJsCode({
|
||||
jsCode,
|
||||
buildContext
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
});
|
||||
@ -48,9 +65,10 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
assert(cssCode !== null);
|
||||
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
const { fixedCssCode } = replaceImportsInCssCode({
|
||||
cssCode,
|
||||
buildOptions
|
||||
cssFileRelativeDirPath: undefined,
|
||||
buildContext
|
||||
});
|
||||
|
||||
$(element).text(fixedCssCode);
|
||||
@ -72,46 +90,46 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
$(element).attr(
|
||||
attrName,
|
||||
href.replace(
|
||||
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
|
||||
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
new RegExp(
|
||||
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
||||
),
|
||||
`\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/`
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
if (Object.keys(cssGlobalsToDefine).length !== 0) {
|
||||
$("head").prepend(
|
||||
[
|
||||
"",
|
||||
"<style>",
|
||||
generateCssCodeToDefineGlobals({
|
||||
cssGlobalsToDefine,
|
||||
buildOptions
|
||||
}).cssCodeToPrependInHead,
|
||||
"</style>",
|
||||
""
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const ftlObjectToJsCodeDeclaringAnObject = fs
|
||||
const kcContextDeclarationTemplateFtl = fs
|
||||
.readFileSync(
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src", "bin", "keycloakify", "generateFtl", "ftl_object_to_js_code_declaring_an_object.ftl")
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"keycloakify",
|
||||
"generateFtl",
|
||||
"kcContextDeclarationTemplate.ftl"
|
||||
)
|
||||
)
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
|
||||
.replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))
|
||||
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common);
|
||||
.replace("{{themeType}}", themeType)
|
||||
.replace("{{themeName}}", themeName)
|
||||
.replace("{{keycloakifyVersion}}", keycloakifyVersion)
|
||||
.replace("{{themeVersion}}", buildContext.themeVersion)
|
||||
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
|
||||
.replace("{{RESOURCES_COMMON}}", RESOURCES_COMMON)
|
||||
.replace(
|
||||
"{{userDefinedExclusions}}",
|
||||
buildContext.kcContextExclusionsFtlCode ?? ""
|
||||
);
|
||||
|
||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||
|
||||
$("head").prepend(`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`);
|
||||
$("head").prepend(
|
||||
`<script>\n${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}\n</script>`
|
||||
);
|
||||
|
||||
// Remove part of the document marked as ignored.
|
||||
{
|
||||
@ -119,7 +137,9 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
startTags.each((...[, startTag]) => {
|
||||
const $startTag = $(startTag);
|
||||
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
|
||||
const $endTag = $startTag
|
||||
.nextAll('meta[name="keycloakify-ignore-end"]')
|
||||
.first();
|
||||
|
||||
if ($endTag.length) {
|
||||
let currentNode = $startTag.next();
|
||||
@ -146,9 +166,14 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
let ftlCode = $.html();
|
||||
|
||||
Object.entries({
|
||||
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]: ftlObjectToJsCodeDeclaringAnObject,
|
||||
"PAGE_ID_xIgLsPgGId9D8e": pageId
|
||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
|
||||
kcContextDeclarationTemplateFtl,
|
||||
"{{pageId}}": pageId,
|
||||
"{{ftlTemplateFileName}}": pageId
|
||||
}).map(
|
||||
([searchValue, replaceValue]) =>
|
||||
(ftlCode = ftlCode.replace(searchValue, replaceValue))
|
||||
);
|
||||
|
||||
return { ftlCode };
|
||||
}
|
||||
|
655
src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
Normal file
655
src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
Normal file
@ -0,0 +1,655 @@
|
||||
<#assign xKeycloakify={
|
||||
"messages": {},
|
||||
"pageId": "{{pageId}}",
|
||||
"ftlTemplateFileName": "{{ftlTemplateFileName}}",
|
||||
"themeType": "{{themeType}}",
|
||||
"themeName": "{{themeName}}",
|
||||
"keycloakifyVersion": "{{keycloakifyVersion}}",
|
||||
"themeVersion": "{{themeVersion}}",
|
||||
"resourcesPath": ""
|
||||
}>
|
||||
|
||||
<#if url?? && url?is_hash && url.resourcesPath?? && url.resourcesPath?is_string>
|
||||
<#assign xKeycloakify = xKeycloakify + { "resourcesPath": url.resourcesPath }>
|
||||
</#if>
|
||||
<#if resourceUrl?? && resourceUrl?is_string>
|
||||
<#assign xKeycloakify = xKeycloakify + { "resourcesPath": resourceUrl }>
|
||||
</#if>
|
||||
|
||||
const kcContext = ${toJsDeclarationString(.data_model, [])?no_esc};
|
||||
kcContext.keycloakifyVersion = "${xKeycloakify.keycloakifyVersion}";
|
||||
kcContext.themeVersion = "${xKeycloakify.themeVersion}";
|
||||
kcContext.themeType = "${xKeycloakify.themeType}";
|
||||
kcContext.themeName = "${xKeycloakify.themeName}";
|
||||
kcContext.pageId = "${xKeycloakify.pageId}";
|
||||
kcContext.ftlTemplateFileName = "${xKeycloakify.ftlTemplateFileName}";
|
||||
|
||||
<@addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages />
|
||||
|
||||
kcContext["x-keycloakify"] = {};
|
||||
|
||||
kcContext["x-keycloakify"].resourcesPath = "${xKeycloakify.resourcesPath}";
|
||||
|
||||
{
|
||||
var messages = {};
|
||||
<#list xKeycloakify.messages as key, resolvedMsg>
|
||||
messages["${key}"] = decodeHtmlEntities("${resolvedMsg?js_string}");
|
||||
</#list>
|
||||
kcContext["x-keycloakify"].messages = messages;
|
||||
}
|
||||
|
||||
if(
|
||||
kcContext.url instanceof Object &&
|
||||
typeof kcContext.url.resourcesPath === "string"
|
||||
){
|
||||
kcContext.url.resourcesCommonPath = kcContext.url.resourcesPath + "/{{RESOURCES_COMMON}}";
|
||||
}
|
||||
|
||||
if( kcContext.messagesPerField ){
|
||||
var existsError_singleFieldName = kcContext.messagesPerField.existsError;
|
||||
kcContext.messagesPerField.existsError = function (){
|
||||
for( let i = 0; i < arguments.length; i++ ){
|
||||
if( existsError_singleFieldName(arguments[i]) ){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
kcContext.messagesPerField.exists = function (fieldName) {
|
||||
return kcContext.messagesPerField.get(fieldName) !== "";
|
||||
};
|
||||
kcContext.messagesPerField.printIfExists = function (fieldName, text) {
|
||||
return kcContext.messagesPerField.exists(fieldName) ? text : undefined;
|
||||
};
|
||||
kcContext.messagesPerField.getFirstError = function () {
|
||||
for( let i = 0; i < arguments.length; i++ ){
|
||||
const fieldName = arguments[i];
|
||||
if( kcContext.messagesPerField.existsError(fieldName) ){
|
||||
return kcContext.messagesPerField.get(fieldName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
attributes_to_attributesByName: {
|
||||
if( !kcContext.profile ){
|
||||
break attributes_to_attributesByName;
|
||||
}
|
||||
if( !kcContext.profile.attributes ){
|
||||
break attributes_to_attributesByName;
|
||||
}
|
||||
var attributes = kcContext.profile.attributes;
|
||||
delete kcContext.profile.attributes;
|
||||
kcContext.profile.attributesByName = {};
|
||||
attributes.forEach(function(attribute){
|
||||
kcContext.profile.attributesByName[attribute.name] = attribute;
|
||||
});
|
||||
}
|
||||
window.kcContext = kcContext;
|
||||
function decodeHtmlEntities(htmlStr){
|
||||
var element = decodeHtmlEntities.element;
|
||||
if (!element) {
|
||||
element = document.createElement("textarea");
|
||||
decodeHtmlEntities.element = element;
|
||||
}
|
||||
element.innerHTML = htmlStr;
|
||||
return element.value;
|
||||
}
|
||||
|
||||
<#function toJsDeclarationString object path>
|
||||
<#local isHash = -1>
|
||||
<#attempt>
|
||||
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't evaluate if " + path?join(".") + " is a hash">
|
||||
</#attempt>
|
||||
|
||||
<#if isHash>
|
||||
<#if path?size gt 10>
|
||||
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
|
||||
</#if>
|
||||
<#local keys = -1>
|
||||
|
||||
<#attempt>
|
||||
<#local keys = object?keys>
|
||||
<#recover>
|
||||
<#return "ABORT: We can't list keys on object">
|
||||
</#attempt>
|
||||
|
||||
<#local outSeq = []>
|
||||
|
||||
<#list keys as key>
|
||||
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if (
|
||||
areSamePath(path, ["url"]) &&
|
||||
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key)
|
||||
) || (
|
||||
key == "updateProfileCtx" &&
|
||||
areSamePath(path, [])
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
areSamePath(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(xKeycloakify.pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
["secretData", "value"]?seq_contains(key) &&
|
||||
areSamePath(path, [ "totp", "otpCredentials", "*" ])
|
||||
) || (
|
||||
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
|
||||
areSamePath(path, ["brokerContext"]) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
|
||||
) || (
|
||||
key == "identityProviderBrokerCtx" &&
|
||||
areSamePath(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
|
||||
) || (
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||
areSamePath(path, ["realm"])
|
||||
) || (
|
||||
xKeycloakify.pageId == "error.ftl" &&
|
||||
areSamePath(path, ["realm"]) &&
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
xKeycloakify.pageId == "applications.ftl" &&
|
||||
(
|
||||
key == "realm" ||
|
||||
key == "container"
|
||||
) &&
|
||||
isSubpath(path, ["applications", "applications"])
|
||||
) || (
|
||||
key == "delegateForUpdate" &&
|
||||
areSamePath(path, ["user"])
|
||||
) || (
|
||||
<#-- Security audit forwarded by Garth (Gmail) -->
|
||||
key == "saml.signing.private.key" &&
|
||||
areSamePath(path, ["client", "attributes"])
|
||||
) || (
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
|
||||
key == "password" &&
|
||||
areSamePath(path, ["login"])
|
||||
) || (
|
||||
<#-- Remove realmAttributes added by https://github.com/jcputney/keycloak-theme-additional-info-extension for peace of mind. -->
|
||||
key == "realmAttributes" &&
|
||||
areSamePath(path, [])
|
||||
) || (
|
||||
<#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
|
||||
key == "attributesByName" &&
|
||||
areSamePath(path, ["profile"])
|
||||
) || (
|
||||
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
|
||||
(key == "attributes" || key == "attributesByName") &&
|
||||
areSamePath(path, ["register"])
|
||||
) || (
|
||||
areSamePath(path, ["properties"]) &&
|
||||
(
|
||||
key?starts_with("kc") ||
|
||||
key == "locales" ||
|
||||
key == "import" ||
|
||||
key == "parent" ||
|
||||
key == "meta" ||
|
||||
key == "stylesCommon" ||
|
||||
key == "styles" ||
|
||||
key == "accountResourceProvider"
|
||||
)
|
||||
) || (
|
||||
key == "execution" &&
|
||||
areSamePath(path, [])
|
||||
) || (
|
||||
key == "entity" &&
|
||||
areSamePath(path, ["user"])
|
||||
) || (
|
||||
key == "attributes" &&
|
||||
areSamePath(path, ["realm"])
|
||||
)
|
||||
>
|
||||
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
key == "attemptedUsername" &&
|
||||
areSamePath(path, ["auth"]) &&
|
||||
[
|
||||
"register.ftl", "terms.ftl", "info.ftl", "login.ftl",
|
||||
"login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"
|
||||
]?seq_contains(xKeycloakify.pageId)
|
||||
)>
|
||||
<#attempt>
|
||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local outSeq += ["/*Accessing attemptedUsername throwed an exception */"]>
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
{{userDefinedExclusions}}
|
||||
|
||||
<#attempt>
|
||||
<#if !object[key]??>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local outSeq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local propertyValue = -1>
|
||||
|
||||
<#attempt>
|
||||
<#local propertyValue = object[key]>
|
||||
<#recover>
|
||||
<#local outSeq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local recOut = toJsDeclarationString(propertyValue, path + [ key ])>
|
||||
|
||||
<#if recOut?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = recOut?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local outSeq += ["/*" + key + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local outSeq += ['"' + key + '": ' + recOut + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["{"] + outSeq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local isMethod = -1>
|
||||
<#attempt>
|
||||
<#local isMethod = object?is_method>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it'sa method.">
|
||||
</#attempt>
|
||||
|
||||
<#if isMethod>
|
||||
|
||||
<#if areSamePath(path, ["auth", "showUsername"])>
|
||||
<#attempt>
|
||||
<#return auth.showUsername()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showUsername()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["auth", "showResetCredentials"])>
|
||||
<#attempt>
|
||||
<#return auth.showResetCredentials()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["auth", "showTryAnotherWayLink"])>
|
||||
<#attempt>
|
||||
<#return auth.showTryAnotherWayLink()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["url", "getLogoutUrl"])>
|
||||
<#local returnValue = -1>
|
||||
<#attempt>
|
||||
<#local returnValue = url.getLogoutUrl()>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate url.getLogoutUrl()">
|
||||
</#attempt>
|
||||
<#return 'function(){ return "' + returnValue + '"; }'>
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["totp", "policy", "getAlgorithmKey"])>
|
||||
<#local returnValue = "error">
|
||||
<#if mode?? && mode = "manual">
|
||||
<#attempt>
|
||||
<#local returnValue = totp.policy.getAlgorithmKey()>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
<#return 'function(){ return "' + returnValue + '"; }'>
|
||||
</#if>
|
||||
|
||||
<#assign fieldNames = [{{fieldNames}}]>
|
||||
<#if profile?? && profile.attributes??>
|
||||
<#list profile.attributes as attribute>
|
||||
<#if fieldNames?seq_contains(attribute.name)>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#assign fieldNames += [attribute.name]>
|
||||
</#list>
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["messagesPerField", "get"])>
|
||||
|
||||
<#local jsFunctionCode = "function (fieldName) { ">
|
||||
|
||||
<#list fieldNames as fieldName>
|
||||
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
|
||||
<#if xKeycloakify.pageId == "login.ftl" >
|
||||
|
||||
<#if fieldName == "username">
|
||||
|
||||
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
|
||||
|
||||
<#if messagesPerField.exists('username') || messagesPerField.exists('password')>
|
||||
<#local jsFunctionCode += "return kcContext.message && kcContext.message.summary ? kcContext.message.summary : 'error'; ">
|
||||
<#else>
|
||||
<#local jsFunctionCode += "return ''; ">
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "} ">
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if fieldName == "password">
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "'){ ">
|
||||
|
||||
<#if messagesPerField.exists('${fieldName}')>
|
||||
<#local jsFunctionCode += 'return decodeHtmlEntities("' + messagesPerField.get('${fieldName}')?js_string + '"); '>
|
||||
<#else>
|
||||
<#local jsFunctionCode += "return ''; ">
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "} ">
|
||||
|
||||
</#list>
|
||||
|
||||
<#local jsFunctionCode += "}">
|
||||
|
||||
<#return jsFunctionCode>
|
||||
|
||||
</#if>
|
||||
|
||||
<#if areSamePath(path, ["messagesPerField", "existsError"])>
|
||||
|
||||
<#local jsFunctionCode = "function (fieldName) { ">
|
||||
|
||||
<#list fieldNames as fieldName>
|
||||
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
|
||||
<#if xKeycloakify.pageId == "login.ftl" >
|
||||
<#if fieldName == "username">
|
||||
|
||||
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
|
||||
|
||||
<#if messagesPerField.existsError('username') || messagesPerField.existsError('password')>
|
||||
<#local jsFunctionCode += "return true; ">
|
||||
<#else>
|
||||
<#local jsFunctionCode += "return false; ">
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "} ">
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if fieldName == "password">
|
||||
<#continue>
|
||||
</#if>
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "' ){ ">
|
||||
|
||||
<#if messagesPerField.existsError('${fieldName}')>
|
||||
<#local jsFunctionCode += 'return true; '>
|
||||
<#else>
|
||||
<#local jsFunctionCode += "return false; ">
|
||||
</#if>
|
||||
|
||||
<#local jsFunctionCode += "}">
|
||||
|
||||
</#list>
|
||||
|
||||
<#local jsFunctionCode += "}">
|
||||
|
||||
<#return jsFunctionCode>
|
||||
|
||||
</#if>
|
||||
|
||||
<#if xKeycloakify.themeType == "account" && areSamePath(path, ["realm", "isInternationalizationEnabled"])>
|
||||
<#attempt>
|
||||
<#return realm.isInternationalizationEnabled()?c>
|
||||
<#recover>
|
||||
<#return "ABORT: Couldn't evaluate realm.isInternationalizationEnabled()">
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
<#return "ABORT: It's a method">
|
||||
</#if>
|
||||
|
||||
<#local isBoolean = -1>
|
||||
<#attempt>
|
||||
<#local isBoolean = object?is_boolean>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a boolean">
|
||||
</#attempt>
|
||||
|
||||
<#if isBoolean>
|
||||
<#return object?c>
|
||||
</#if>
|
||||
|
||||
<#local isEnumerable = -1>
|
||||
<#attempt>
|
||||
<#local isEnumerable = object?is_enumerable>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's an enumerable">
|
||||
</#attempt>
|
||||
|
||||
|
||||
<#if isEnumerable>
|
||||
|
||||
<#local outSeq = []>
|
||||
|
||||
<#local i = 0>
|
||||
|
||||
<#list object as array_item>
|
||||
|
||||
<#if !array_item??>
|
||||
<#local outSeq += ["null,"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local recOut = toJsDeclarationString(array_item, path + [ i ])>
|
||||
|
||||
<#local i = i + 1>
|
||||
|
||||
<#if recOut?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = recOut?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local outSeq += ["/*" + i?string + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local outSeq += [recOut + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["["] + outSeq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local isDate = -1>
|
||||
<#attempt>
|
||||
<#local isDate = object?is_date_like>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a date">
|
||||
</#attempt>
|
||||
|
||||
<#if isDate>
|
||||
<#return '"' + object?datetime?iso_utc + '"'>
|
||||
</#if>
|
||||
|
||||
<#local isNumber = -1>
|
||||
<#attempt>
|
||||
<#local isNumber = object?is_number>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a number">
|
||||
</#attempt>
|
||||
|
||||
<#if isNumber>
|
||||
<#return object?c>
|
||||
</#if>
|
||||
|
||||
<#local isString = -1>
|
||||
<#attempt>
|
||||
<#local isString = object?is_string>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a string">
|
||||
</#attempt>
|
||||
|
||||
<#if isString>
|
||||
<@addToXKeycloakifyMessagesIfMessageKey str=object />
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
<#return '"' + object?js_string + '"'>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non number, non enumerable object">
|
||||
|
||||
</#function>
|
||||
<#function isSubpath path searchedPath>
|
||||
|
||||
<#if path?size < searchedPath?size>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#local i=0>
|
||||
|
||||
<#list path as property>
|
||||
|
||||
<#if i == searchedPath?size >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local searchedProperty=searchedPath[i]>
|
||||
|
||||
<#local i+= 1>
|
||||
|
||||
<#if searchedProperty?is_string && searchedProperty == "*">
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_string && !property?is_string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_number && !property?is_number>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?string != property?string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return true>
|
||||
|
||||
</#function>
|
||||
|
||||
<#function areSamePath path searchedPath>
|
||||
<#return path?size == searchedPath?size && isSubpath(path, searchedPath)>
|
||||
</#function>
|
||||
|
||||
<#macro addToXKeycloakifyMessagesIfMessageKey str>
|
||||
<#if !msg?? || !msg?is_method>
|
||||
<#return>
|
||||
</#if>
|
||||
<#if (str?length > 200)>
|
||||
<#return>
|
||||
</#if>
|
||||
<#local key=removeBrackets(str)>
|
||||
<#if key?length==0>
|
||||
<#return>
|
||||
</#if>
|
||||
<#if !(key?matches(r"^[a-zA-Z0-9-_.]*$"))>
|
||||
<#return>
|
||||
</#if>
|
||||
<#local resolvedMsg=msg(key)>
|
||||
<#if resolvedMsg==key>
|
||||
<#return>
|
||||
</#if>
|
||||
<#local messages=xKeycloakify.messages>
|
||||
<#local messages = messages + { key: resolvedMsg }>
|
||||
<#assign xKeycloakify = xKeycloakify + { "messages": messages }>
|
||||
</#macro>
|
||||
|
||||
<#function removeBrackets str>
|
||||
<#if str?starts_with("${") && str?ends_with("}")>
|
||||
<#return str[2..(str?length-2)]>
|
||||
<#else>
|
||||
<#return str>
|
||||
</#if>
|
||||
</#function>
|
||||
|
||||
<#macro addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages>
|
||||
<#if profile?? && profile?is_hash && profile.attributes?? && profile.attributes?is_enumerable>
|
||||
<#list profile.attributes as attribute>
|
||||
<#if !(
|
||||
attribute.annotations?? && attribute.annotations?is_hash &&
|
||||
attribute.annotations.inputOptionLabelsI18nPrefix?? && attribute.annotations.inputOptionLabelsI18nPrefix?is_string
|
||||
)>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#local prefix=attribute.annotations.inputOptionLabelsI18nPrefix>
|
||||
<#if !(
|
||||
attribute.validators?? && attribute.validators?is_hash &&
|
||||
attribute.validators.options?? && attribute.validators.options?is_hash &&
|
||||
attribute.validators.options.options?? && attribute.validators.options.options?is_enumerable
|
||||
)>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#list attribute.validators.options.options as option>
|
||||
<#if !option?is_string>
|
||||
<#continue>
|
||||
</#if>
|
||||
<@addToXKeycloakifyMessagesIfMessageKey str="${prefix}.${option}" />
|
||||
</#list>
|
||||
</#list>
|
||||
</#if>
|
||||
<#if xKeycloakify.pageId == "terms.ftl" || termsAcceptanceRequired?? && termsAcceptanceRequired>
|
||||
<@addToXKeycloakifyMessagesIfMessageKey str="termsText" />
|
||||
</#if>
|
||||
<#if requiredActions?? && requiredActions?is_enumerable>
|
||||
<#list requiredActions as requiredAction>
|
||||
<#if !requiredAction?is_string>
|
||||
<#continue>
|
||||
</#if>
|
||||
<@addToXKeycloakifyMessagesIfMessageKey str="requiredAction.${requiredAction}" />
|
||||
</#list>
|
||||
</#if>
|
||||
</#macro>
|
89
src/bin/keycloakify/generateResources/bringInAccountV1.ts
Normal file
89
src/bin/keycloakify/generateResources/bringInAccountV1.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import {
|
||||
RESOURCES_COMMON,
|
||||
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
|
||||
ACCOUNT_V1_THEME_NAME
|
||||
} from "../../shared/constants";
|
||||
import {
|
||||
downloadKeycloakDefaultTheme,
|
||||
BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
|
||||
} from "../../shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme;
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function bringInAccountV1(params: {
|
||||
resourcesDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion: LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const accountV1DirPath = pathJoin(
|
||||
resourcesDirPath,
|
||||
"theme",
|
||||
ACCOUNT_V1_THEME_NAME,
|
||||
"account"
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "base", "account"),
|
||||
destDirPath: accountV1DirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "account", "resources"),
|
||||
destDirPath: pathJoin(accountV1DirPath, "resources")
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(accountV1DirPath, "resources", RESOURCES_COMMON)
|
||||
});
|
||||
|
||||
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=" +
|
||||
[
|
||||
"css/account.css",
|
||||
"img/icon-sidebar-active.png",
|
||||
"img/logo.png",
|
||||
...[
|
||||
"patternfly.min.css",
|
||||
"patternfly-additions.min.css",
|
||||
"patternfly-additions.min.css"
|
||||
].map(
|
||||
fileBasename =>
|
||||
`${RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}`
|
||||
)
|
||||
].join(" "),
|
||||
"",
|
||||
"##### css classes for form buttons",
|
||||
"# main class used for all buttons",
|
||||
"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"
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
import { type ThemeType, FALLBACK_LANGUAGE_TAG } from "../../shared/constants";
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import * as recast from "recast";
|
||||
import * as babelParser from "@babel/parser";
|
||||
import babelGenerate from "@babel/generator";
|
||||
import * as babelTypes from "@babel/types";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"messages_defaultSet"
|
||||
);
|
||||
|
||||
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(baseMessagesDirPath)
|
||||
.filter(baseName => baseName !== "index.ts")
|
||||
.map(basename => ({
|
||||
languageTag: basename.replace(/\.ts$/, ""),
|
||||
filePath: pathJoin(baseMessagesDirPath, basename)
|
||||
}))
|
||||
.map(({ languageTag, filePath }) => {
|
||||
const lines = fs
|
||||
.readFileSync(filePath)
|
||||
.toString("utf8")
|
||||
.split(/\r?\n/);
|
||||
|
||||
let messagesJson = "{";
|
||||
|
||||
let isInDeclaration = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!isInDeclaration) {
|
||||
if (line.startsWith("const messages")) {
|
||||
isInDeclaration = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("}")) {
|
||||
messagesJson += "}";
|
||||
break;
|
||||
}
|
||||
|
||||
messagesJson += line;
|
||||
}
|
||||
|
||||
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
||||
|
||||
return [languageTag, messages];
|
||||
})
|
||||
);
|
||||
|
||||
const { i18nTsFilePath } = (() => {
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
|
||||
);
|
||||
|
||||
const i18nTsFilePath: string | undefined = files[0];
|
||||
|
||||
return { i18nTsFilePath };
|
||||
})();
|
||||
|
||||
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
|
||||
(() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"]
|
||||
}),
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(root, {
|
||||
visitCallExpression: function (path) {
|
||||
if (
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
) {
|
||||
messageBundleDeclarationTsCode = babelGenerate(
|
||||
path.node.arguments[0] as any
|
||||
).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
assert(messageBundleDeclarationTsCode !== undefined);
|
||||
|
||||
let messageBundle: {
|
||||
[languageTag: string]: Record<string, string>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(
|
||||
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
|
||||
);
|
||||
} catch {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: Make sure the messageBundle your provided as argument of createUseI18n can be statically evaluated.",
|
||||
"This is important because we need to put your i18n messages in messages_*.properties files",
|
||||
"or they won't be available server side.",
|
||||
"\n",
|
||||
"The following code could not be evaluated:",
|
||||
"\n",
|
||||
messageBundleDeclarationTsCode
|
||||
].join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
return messageBundle;
|
||||
})();
|
||||
|
||||
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
|
||||
languageTag,
|
||||
{
|
||||
...messages,
|
||||
...(messageBundle === undefined
|
||||
? {}
|
||||
: messageBundle[languageTag] ??
|
||||
messageBundle[FALLBACK_LANGUAGE_TAG] ??
|
||||
messageBundle[Object.keys(messageBundle)[0]] ??
|
||||
{})
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
|
||||
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
|
||||
languageTag,
|
||||
propertiesFileSource: [
|
||||
"",
|
||||
...(themeType !== "account" ? ["parent=base"] : []),
|
||||
...Object.entries(messages).map(
|
||||
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
||||
),
|
||||
""
|
||||
].join("\n")
|
||||
}));
|
||||
|
||||
return messageProperties;
|
||||
}
|
42
src/bin/keycloakify/generateResources/generateResources.ts
Normal file
42
src/bin/keycloakify/generateResources/generateResources.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import {
|
||||
generateResourcesForMainTheme,
|
||||
type BuildContextLike as BuildContextLike_generateResourcesForMainTheme
|
||||
} from "./generateResourcesForMainTheme";
|
||||
import { generateResourcesForThemeVariant } from "./generateResourcesForThemeVariant";
|
||||
import fs from "fs";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generateResourcesForMainTheme & {
|
||||
themeNames: string[];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateResources(params: {
|
||||
buildContext: BuildContextLike;
|
||||
resourcesDirPath: string;
|
||||
}): Promise<void> {
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const [themeName, ...themeVariantNames] = buildContext.themeNames;
|
||||
|
||||
if (fs.existsSync(resourcesDirPath)) {
|
||||
rmSync(resourcesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await generateResourcesForMainTheme({
|
||||
resourcesDirPath,
|
||||
themeName,
|
||||
buildContext
|
||||
});
|
||||
|
||||
for (const themeVariantName of themeVariantNames) {
|
||||
generateResourcesForThemeVariant({
|
||||
resourcesDirPath,
|
||||
themeName,
|
||||
themeVariantName
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
resolve as pathResolve,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import {
|
||||
generateFtlFilesCodeFactory,
|
||||
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
||||
} from "../generateFtl";
|
||||
import {
|
||||
type ThemeType,
|
||||
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
|
||||
KEYCLOAK_RESOURCES,
|
||||
ACCOUNT_V1_THEME_NAME,
|
||||
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS
|
||||
} from "../../shared/constants";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import {
|
||||
downloadKeycloakStaticResources,
|
||||
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
|
||||
} from "../../shared/downloadKeycloakStaticResources";
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import {
|
||||
bringInAccountV1,
|
||||
type BuildContextLike as BuildContextLike_bringInAccountV1
|
||||
} from "./bringInAccountV1";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_downloadKeycloakStaticResources &
|
||||
BuildContextLike_bringInAccountV1 & {
|
||||
extraThemeProperties: string[] | undefined;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
themeSrcDirPath: string;
|
||||
bundler: { type: "vite" } | { type: "webpack" };
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateResourcesForMainTheme(params: {
|
||||
themeName: string;
|
||||
resourcesDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { themeName, resourcesDirPath, buildContext } = params;
|
||||
|
||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
||||
const { themeType } = params;
|
||||
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
||||
};
|
||||
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
const isAccountV3 = themeType === "account" && buildContext.doUseAccountV3;
|
||||
if (!buildContext.recordIsImplementedByThemeType[themeType]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
|
||||
);
|
||||
|
||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||
rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
if (
|
||||
themeType === "account" &&
|
||||
buildContext.recordIsImplementedByThemeType.login
|
||||
) {
|
||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
getThemeTypeDirPath({
|
||||
themeType: "login"
|
||||
}),
|
||||
"resources",
|
||||
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
{
|
||||
const dirPath = pathJoin(
|
||||
buildContext.projectBuildDirPath,
|
||||
KEYCLOAK_RESOURCES
|
||||
);
|
||||
|
||||
if (fs.existsSync(dirPath)) {
|
||||
assert(buildContext.bundler.type === "webpack");
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
`Keycloakify build error: The ${KEYCLOAK_RESOURCES} directory shouldn't exist in your build directory.`,
|
||||
`(${pathRelative(process.cwd(), dirPath)}).\n`,
|
||||
`Theses assets are only required for local development with Storybook.",
|
||||
"Please remove this directory as an additional step of your command.\n`,
|
||||
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
|
||||
].join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: buildContext.projectBuildDirPath,
|
||||
destDirPath,
|
||||
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
|
||||
if (filePath.endsWith(".css")) {
|
||||
const { fixedCssCode } = replaceImportsInCssCode({
|
||||
cssCode: sourceCode.toString("utf8"),
|
||||
cssFileRelativeDirPath: pathDirname(fileRelativePath),
|
||||
buildContext
|
||||
});
|
||||
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
|
||||
};
|
||||
}
|
||||
|
||||
if (filePath.endsWith(".js")) {
|
||||
const { fixedJsCode } = replaceImportsInJsCode({
|
||||
jsCode: sourceCode.toString("utf8"),
|
||||
buildContext
|
||||
});
|
||||
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
|
||||
};
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return LOGIN_THEME_PAGE_IDS;
|
||||
case "account":
|
||||
return isAccountV3 ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
||||
}
|
||||
})(),
|
||||
...(isAccountV3
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
i18n_messages_generation: {
|
||||
if (isAccountV3) {
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
generateMessageProperties({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const propertiesFilePath = pathJoin(
|
||||
messagesDirPath,
|
||||
`messages_${languageTag}.properties`
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
propertiesFilePath,
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
keycloak_static_resources: {
|
||||
if (isAccountV3) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
|
||||
case "login":
|
||||
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||
}
|
||||
})(),
|
||||
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeType,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return isAccountV3 ? "base" : ACCOUNT_V1_THEME_NAME;
|
||||
case "login":
|
||||
return "keycloak";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(isAccountV3 ? ["deprecatedMode=false"] : []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
)
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
email: {
|
||||
if (!buildContext.recordIsImplementedByThemeType.email) {
|
||||
break email;
|
||||
}
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v1: {
|
||||
if (buildContext.doUseAccountV3) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
if (!buildContext.recordIsImplementedByThemeType.account) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
await bringInAccountV1({
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v3_i18n_messages: {
|
||||
if (!buildContext.doUseAccountV3) {
|
||||
break bring_in_account_v3_i18n_messages;
|
||||
}
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("theme", "keycloak.v3", "account", "messages")
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await writeFile({
|
||||
fileRelativePath: pathBasename(fileRelativePath)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeType: "account" }),
|
||||
"messages"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
||||
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: themeName,
|
||||
types: objectEntries(buildContext.recordIsImplementedByThemeType)
|
||||
.filter(([, isImplemented]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
|
||||
if (buildContext.recordIsImplementedByThemeType.account) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: ACCOUNT_V1_THEME_NAME,
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export type BuildContextLike = {
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generateResourcesForThemeVariant(params: {
|
||||
resourcesDirPath: string;
|
||||
themeName: string;
|
||||
themeVariantName: string;
|
||||
}) {
|
||||
const { resourcesDirPath, themeName, themeVariantName } = params;
|
||||
|
||||
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: mainThemeDirPath,
|
||||
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (
|
||||
pathExtname(fileRelativePath) === ".ftl" &&
|
||||
fileRelativePath.split(pathSep).length === 2
|
||||
) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
Buffer.from(sourceCode)
|
||||
.toString("utf-8")
|
||||
.replace(
|
||||
`"themeName": "${themeName}"`,
|
||||
`"themeName": "${themeVariantName}"`
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
|
||||
const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
|
||||
|
||||
newMetaInfKeycloakTheme.themes.push({
|
||||
name: themeVariantName,
|
||||
types: (() => {
|
||||
const theme = newMetaInfKeycloakTheme.themes.find(
|
||||
({ name }) => name === themeName
|
||||
);
|
||||
assert(theme !== undefined);
|
||||
return theme.types;
|
||||
})()
|
||||
});
|
||||
|
||||
return newMetaInfKeycloakTheme;
|
||||
}
|
||||
});
|
||||
}
|
1
src/bin/keycloakify/generateResources/index.ts
Normal file
1
src/bin/keycloakify/generateResources/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./generateResources";
|
@ -3,17 +3,26 @@ import { id } from "tsafe/id";
|
||||
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../../shared/constants";
|
||||
import {
|
||||
type ThemeType,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
LOGIN_THEME_PAGE_IDS
|
||||
} from "../../shared/constants";
|
||||
|
||||
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||
export function readExtraPagesNames(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const filePaths = crawl({
|
||||
"dirPath": pathJoin(themeSrcDirPath, themeType),
|
||||
"returnedPathsType": "absolute"
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
|
||||
const candidateFilePaths = filePaths.filter(filePath =>
|
||||
/[kK]cContext\.[^.]+$/.test(filePath)
|
||||
);
|
||||
|
||||
if (candidateFilePaths.length === 0) {
|
||||
candidateFilePaths.push(...filePaths);
|
||||
@ -24,15 +33,17 @@ export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType
|
||||
for (const candidateFilPath of candidateFilePaths) {
|
||||
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
|
||||
|
||||
extraPages.push(...Array.from(rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g), m => m[1]));
|
||||
extraPages.push(
|
||||
...Array.from(rawSourceFile.matchAll(/["']([^.\s]+.ftl)["']:/g), m => m[1])
|
||||
);
|
||||
}
|
||||
|
||||
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return !id<readonly string[]>(accountThemePageIds).includes(pageId);
|
||||
return !id<readonly string[]>(ACCOUNT_THEME_PAGE_IDS).includes(pageId);
|
||||
case "login":
|
||||
return !id<readonly string[]>(loginThemePageIds).includes(pageId);
|
||||
return !id<readonly string[]>(LOGIN_THEME_PAGE_IDS).includes(pageId);
|
||||
}
|
||||
});
|
||||
}
|
91
src/bin/keycloakify/generateResources/readFieldNameUsage.ts
Normal file
91
src/bin/keycloakify/generateResources/readFieldNameUsage.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import * as fs from "fs";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
// NOTE: We pre-populate with the synthetic user attributes defined in useUserProfileForm (can't be parsed automatically)
|
||||
const fieldNames = new Set<string>([
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"username",
|
||||
"password",
|
||||
"password-confirm"
|
||||
]);
|
||||
|
||||
for (const srcDirPath of [
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
|
||||
pathJoin(themeSrcDirPath, themeType)
|
||||
]) {
|
||||
const filePaths = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "absolute"
|
||||
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
if (!rawSourceFile.includes("messagesPerField")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const functionName of [
|
||||
"printIfExists",
|
||||
"existsError",
|
||||
"get",
|
||||
"exists",
|
||||
"getFirstError"
|
||||
] as const) {
|
||||
if (!rawSourceFile.includes(functionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
rawSourceFile
|
||||
.split(functionName)
|
||||
.filter(part => part.startsWith("("))
|
||||
.map(part => {
|
||||
let [p1] = part.split(")");
|
||||
|
||||
p1 = p1.slice(1);
|
||||
|
||||
return p1;
|
||||
})
|
||||
.map(part => {
|
||||
return part
|
||||
.split(",")
|
||||
.map(a => a.trim())
|
||||
.filter((...[, i]) =>
|
||||
functionName !== "printIfExists" ? true : i === 0
|
||||
)
|
||||
.filter(
|
||||
a =>
|
||||
a.startsWith('"') ||
|
||||
a.startsWith("'") ||
|
||||
a.startsWith("`")
|
||||
)
|
||||
.filter(
|
||||
a =>
|
||||
a.endsWith('"') ||
|
||||
a.endsWith("'") ||
|
||||
a.endsWith("`")
|
||||
)
|
||||
.map(a => a.slice(1).slice(0, -1));
|
||||
})
|
||||
.flat()
|
||||
.forEach(fieldName => fieldNames.add(fieldName));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(fieldNames);
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../shared/constants";
|
||||
import { downloadBuiltinKeycloakTheme } from "../../shared/downloadBuiltinKeycloakTheme";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
|
||||
type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "bringInAccountV1_tmp");
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
"keycloakVersion": lastKeycloakVersionWithAccountV1,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
|
||||
"destDirPath": accountV1DirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
|
||||
"destDirPath": pathJoin(accountV1DirPath, "resources")
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
|
||||
});
|
||||
|
||||
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=" +
|
||||
[
|
||||
"css/account.css",
|
||||
"img/icon-sidebar-active.png",
|
||||
"img/logo.png",
|
||||
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
|
||||
fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
|
||||
)
|
||||
].join(" "),
|
||||
"",
|
||||
"##### css classes for form buttons",
|
||||
"# main class used for all buttons",
|
||||
"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"
|
||||
)
|
||||
);
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import * as recast from "recast";
|
||||
import * as babelParser from "@babel/parser";
|
||||
import babelGenerate from "@babel/generator";
|
||||
import * as babelTypes from "@babel/types";
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
let files = crawl({
|
||||
"dirPath": pathJoin(themeSrcDirPath, themeType),
|
||||
"returnedPathsType": "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file => readFileSync(file).toString("utf8").includes("createUseI18n"));
|
||||
|
||||
if (files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extraMessages = files
|
||||
.map(file => {
|
||||
const root = recast.parse(readFileSync(file).toString("utf8"), {
|
||||
"parser": {
|
||||
"parse": (code: string) => babelParser.parse(code, { "sourceType": "module", "plugins": ["typescript"] }),
|
||||
"generator": babelGenerate,
|
||||
"types": babelTypes
|
||||
}
|
||||
});
|
||||
|
||||
const codes: string[] = [];
|
||||
|
||||
recast.visit(root, {
|
||||
"visitCallExpression": function (path) {
|
||||
if (path.node.callee.type === "Identifier" && path.node.callee.name === "createUseI18n") {
|
||||
codes.push(babelGenerate(path.node.arguments[0] as any).code);
|
||||
}
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
return codes;
|
||||
})
|
||||
.flat()
|
||||
.map(code => {
|
||||
let extraMessages: { [languageTag: string]: Record<string, string> } = {};
|
||||
|
||||
try {
|
||||
eval(`${symToStr({ extraMessages })} = ${code}`);
|
||||
} catch {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript",
|
||||
"runtime where only the node globals are available.",
|
||||
"This is important because we need to put your i18n messages in messages_*.properties files",
|
||||
"or they won't be available server side.",
|
||||
"\n",
|
||||
"The following code could not be evaluated:",
|
||||
"\n",
|
||||
code
|
||||
].join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
return extraMessages;
|
||||
});
|
||||
|
||||
const languageTags = extraMessages
|
||||
.map(extraMessage => Object.keys(extraMessage))
|
||||
.flat()
|
||||
.reduce(...removeDuplicates<string>());
|
||||
|
||||
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
|
||||
|
||||
for (const languageTag of languageTags) {
|
||||
const keyValueMap: Record<string, string> = {};
|
||||
|
||||
for (const extraMessage of extraMessages) {
|
||||
const keyValueMap_i = extraMessage[languageTag];
|
||||
|
||||
if (keyValueMap_i === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(keyValueMap_i)) {
|
||||
if (keyValueMap[key] !== undefined) {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: The following key is defined multiple times:",
|
||||
"\n",
|
||||
key,
|
||||
"\n",
|
||||
"The following value will be ignored:",
|
||||
"\n",
|
||||
value,
|
||||
"\n",
|
||||
"The following value was already defined:",
|
||||
"\n",
|
||||
keyValueMap[key]
|
||||
].join(" ")
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
keyValueMap[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
keyValueMapByLanguageTag[languageTag] = keyValueMap;
|
||||
}
|
||||
|
||||
const out: { languageTag: string; propertiesFileSource: string }[] = [];
|
||||
|
||||
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
|
||||
const propertiesFileSource = Object.entries(keyValueMap)
|
||||
.map(([key, value]) => `${key}=${escapeString(value)}`)
|
||||
.join("\n");
|
||||
|
||||
out.push({
|
||||
languageTag,
|
||||
"propertiesFileSource": ["# This file was generated by keycloakify", "", "parent=base", "", propertiesFileSource, ""].join("\n")
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Convert a JavaScript string to UTF-16 encoding
|
||||
function toUTF16(codePoint: number): string {
|
||||
if (codePoint <= 0xffff) {
|
||||
// BMP character
|
||||
return "\\u" + codePoint.toString(16).padStart(4, "0");
|
||||
} else {
|
||||
// Non-BMP character
|
||||
codePoint -= 0x10000;
|
||||
let highSurrogate = (codePoint >> 10) + 0xd800;
|
||||
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
|
||||
return "\\u" + highSurrogate.toString(16).padStart(4, "0") + "\\u" + lowSurrogate.toString(16).padStart(4, "0");
|
||||
}
|
||||
}
|
||||
|
||||
// Escapes special characters and converts unicode to UTF-16 encoding
|
||||
function escapeString(str: string): string {
|
||||
let escapedStr = "";
|
||||
for (const char of [...str]) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (!codePoint) continue;
|
||||
if (char === "'") {
|
||||
escapedStr += "''"; // double single quotes
|
||||
} else if (codePoint > 0x7f) {
|
||||
escapedStr += toUTF16(codePoint); // non-ascii characters
|
||||
} else {
|
||||
escapedStr += char;
|
||||
}
|
||||
}
|
||||
return escapedStr;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import {
|
||||
generateSrcMainResourcesForMainTheme,
|
||||
type BuildOptionsLike as BuildOptionsLike_generateSrcMainResourcesForMainTheme
|
||||
} from "./generateSrcMainResourcesForMainTheme";
|
||||
import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike_generateSrcMainResourcesForMainTheme & {
|
||||
themeNames: string[];
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function generateSrcMainResources(params: { buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const [themeName, ...themeVariantNames] = buildOptions.themeNames;
|
||||
|
||||
await generateSrcMainResourcesForMainTheme({
|
||||
themeName,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
for (const themeVariantName of themeVariantNames) {
|
||||
generateSrcMainResourcesForThemeVariant({
|
||||
themeName,
|
||||
themeVariantName,
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, resolve as pathResolve } from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import { generateFtlFilesCodeFactory } from "../generateFtl";
|
||||
import {
|
||||
type ThemeType,
|
||||
lastKeycloakVersionWithAccountV1,
|
||||
keycloak_resources,
|
||||
accountV1ThemeName,
|
||||
basenameOfTheKeycloakifyResourcesDir,
|
||||
loginThemePageIds,
|
||||
accountThemePageIds
|
||||
} from "../../shared/constants";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { downloadKeycloakStaticResources } from "../../shared/downloadKeycloakStaticResources";
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import { bringInAccountV1 } from "./bringInAccountV1";
|
||||
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import { writeMetaInfKeycloakThemes, type MetaInfKeycloakTheme } from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
extraThemeProperties: string[] | undefined;
|
||||
themeVersion: string;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
reactAppBuildDirPath: string;
|
||||
cacheDirPath: string;
|
||||
assetsDirPath: string;
|
||||
urlPathname: string | undefined;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
reactAppRootDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function generateSrcMainResourcesForMainTheme(params: { themeName: string; buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
const { themeName, buildOptions } = params;
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
||||
|
||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
||||
const { themeType } = params;
|
||||
return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType);
|
||||
};
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
|
||||
"login": false,
|
||||
"account": false,
|
||||
"email": false
|
||||
};
|
||||
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
implementedThemeTypes[themeType] = true;
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
|
||||
|
||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||
rmSync(destDirPath, { "recursive": true, "force": true });
|
||||
|
||||
if (themeType === "account" && implementedThemeTypes.login) {
|
||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(
|
||||
getThemeTypeDirPath({
|
||||
"themeType": "login"
|
||||
}),
|
||||
"resources",
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": buildOptions.reactAppBuildDirPath,
|
||||
destDirPath,
|
||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
|
||||
if (
|
||||
isInside({
|
||||
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (/\.css?$/i.test(filePath)) {
|
||||
const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
|
||||
"cssCode": sourceCode.toString("utf8")
|
||||
});
|
||||
|
||||
Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
|
||||
cssGlobalsToDefine[key] = value;
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
const { fixedJsCode } = replaceImportsInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
buildOptions
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||
cssGlobalsToDefine,
|
||||
buildOptions,
|
||||
"keycloakifyVersion": readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
"fieldNames": readFieldNameUsage({
|
||||
themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
});
|
||||
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return loginThemePageIds;
|
||||
case "account":
|
||||
return accountThemePageIds;
|
||||
}
|
||||
})(),
|
||||
...readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath
|
||||
})
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.mkdirSync(themeTypeDirPath, { "recursive": true });
|
||||
|
||||
fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
|
||||
});
|
||||
|
||||
generateMessageProperties({
|
||||
themeSrcDirPath,
|
||||
themeType
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true });
|
||||
|
||||
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
|
||||
|
||||
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
|
||||
});
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
"keycloakVersion": (() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return lastKeycloakVersionWithAccountV1;
|
||||
case "login":
|
||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
||||
}
|
||||
})(),
|
||||
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeType,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return accountV1ThemeName;
|
||||
case "login":
|
||||
return "keycloak";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(buildOptions.extraThemeProperties ?? [])
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
email: {
|
||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||
|
||||
if (!fs.existsSync(emailThemeSrcDirPath)) {
|
||||
break email;
|
||||
}
|
||||
|
||||
implementedThemeTypes.email = true;
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": emailThemeSrcDirPath,
|
||||
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
|
||||
});
|
||||
}
|
||||
|
||||
if (implementedThemeTypes.account) {
|
||||
await bringInAccountV1({
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { "themes": [] };
|
||||
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
"name": themeName,
|
||||
"types": objectEntries(implementedThemeTypes)
|
||||
.filter(([, isImplemented]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
|
||||
if (implementedThemeTypes.account) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
"name": accountV1ThemeName,
|
||||
"types": ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { readMetaInfKeycloakThemes, writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function generateSrcMainResourcesForThemeVariant(params: { themeName: string; themeVariantName: string; buildOptions: BuildOptionsLike }) {
|
||||
const { themeName, themeVariantName, buildOptions } = params;
|
||||
|
||||
const mainThemeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": mainThemeDirPath,
|
||||
"destDirPath": pathJoin(mainThemeDirPath, "..", themeVariantName),
|
||||
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
|
||||
if (pathExtname(fileRelativePath) === ".ftl" && fileRelativePath.split(pathSep).length === 2) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
Buffer.from(sourceCode)
|
||||
.toString("utf-8")
|
||||
.replace(`out["themeName"] = "${themeName}";`, `out["themeName"] = "${themeVariantName}";`),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({ "keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath });
|
||||
|
||||
updatedMetaInfKeycloakThemes.themes.push({
|
||||
"name": themeVariantName,
|
||||
"types": (() => {
|
||||
const theme = updatedMetaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
|
||||
assert(theme !== undefined);
|
||||
return theme.types;
|
||||
})()
|
||||
});
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
"metaInfKeycloakThemes": updatedMetaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./generateSrcMainResources";
|
@ -1,56 +0,0 @@
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import * as fs from "fs";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const fieldNames = new Set<string>();
|
||||
|
||||
for (const srcDirPath of [pathJoin(getThisCodebaseRootDirPath(), "src", themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
if (!rawSourceFile.includes("messagesPerField")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const functionName of ["printIfExists", "existsError", "get", "exists", "getFirstError"] as const) {
|
||||
if (!rawSourceFile.includes(functionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
rawSourceFile
|
||||
.split(functionName)
|
||||
.filter(part => part.startsWith("("))
|
||||
.map(part => {
|
||||
let [p1] = part.split(")");
|
||||
|
||||
p1 = p1.slice(1);
|
||||
|
||||
return p1;
|
||||
})
|
||||
.map(part => {
|
||||
return part
|
||||
.split(",")
|
||||
.map(a => a.trim())
|
||||
.filter((...[, i]) => (functionName !== "printIfExists" ? true : i === 0))
|
||||
.filter(a => a.startsWith('"') || a.startsWith("'") || a.startsWith("`"))
|
||||
.filter(a => a.endsWith('"') || a.endsWith("'") || a.endsWith("`"))
|
||||
.map(a => a.slice(1).slice(0, -1));
|
||||
})
|
||||
.flat()
|
||||
.forEach(fieldName => fieldNames.add(fieldName));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(fieldNames);
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../shared/buildOptions";
|
||||
import { accountV1ThemeName } from "../shared/constants";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
keycloakifyBuildDirPath: string;
|
||||
themeNames: string[];
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||
|
||||
const containerName = "keycloak-testing-container";
|
||||
const keycloakVersion = "24.0.4";
|
||||
|
||||
/** Files for being able to run a hot reload keycloak container */
|
||||
export function generateStartKeycloakTestingContainer(params: {
|
||||
jarFilePath: string;
|
||||
doesImplementAccountTheme: boolean;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { jarFilePath, doesImplementAccountTheme, buildOptions } = params;
|
||||
|
||||
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename),
|
||||
Buffer.from(
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"",
|
||||
`docker rm ${containerName} || true`,
|
||||
"",
|
||||
`cd "${buildOptions.keycloakifyBuildDirPath}"`,
|
||||
"",
|
||||
"docker run \\",
|
||||
" -p 8080:8080 \\",
|
||||
` --name ${containerName} \\`,
|
||||
" -e KEYCLOAK_ADMIN=admin \\",
|
||||
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
|
||||
` -v "${pathJoin(
|
||||
"$(pwd)",
|
||||
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
|
||||
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
|
||||
[...(doesImplementAccountTheme ? [accountV1ThemeName] : []), ...buildOptions.themeNames].map(
|
||||
themeName =>
|
||||
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
|
||||
),
|
||||
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
||||
` start-dev`,
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
),
|
||||
{ "mode": 0o755 }
|
||||
);
|
||||
}
|
@ -1,21 +1,24 @@
|
||||
import { generateSrcMainResources } from "./generateSrcMainResources";
|
||||
import { generateResources } from "./generateResources";
|
||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { readBuildOptions } from "../shared/buildOptions";
|
||||
import { vitePluginSubScriptEnvNames } from "../shared/constants";
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
|
||||
import { buildJars } from "./buildJars";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import chalk from "chalk";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import * as os from "os";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
exit_if_maven_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
||||
try {
|
||||
commandOutput = child_process.execSync("mvn --version", { "stdio": ["ignore", "pipe", "ignore"] });
|
||||
commandOutput = child_process.execSync("mvn --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
});
|
||||
} catch {}
|
||||
|
||||
if (commandOutput?.toString("utf8").includes("Apache Maven")) {
|
||||
@ -34,49 +37,82 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(installationCommand)}\` (for example)`);
|
||||
console.log(
|
||||
`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(
|
||||
installationCommand
|
||||
)}\` (for example)`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
console.log(
|
||||
[
|
||||
chalk.cyan(`keycloakify v${readThisNpmPackageVersion()}`),
|
||||
chalk.green(`Building the keycloak theme in .${pathSep}${pathRelative(process.cwd(), buildOptions.keycloakifyBuildDirPath)} ...`)
|
||||
chalk.green(
|
||||
`Building the keycloak theme in .${pathSep}${pathRelative(
|
||||
process.cwd(),
|
||||
buildContext.keycloakifyBuildDirPath
|
||||
)} ...`
|
||||
)
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
{
|
||||
if (!fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
|
||||
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, { "recursive": true });
|
||||
if (!fs.existsSync(buildContext.keycloakifyBuildDirPath)) {
|
||||
fs.mkdirSync(buildContext.keycloakifyBuildDirPath, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
fs.writeFileSync(
|
||||
pathJoin(buildContext.keycloakifyBuildDirPath, ".gitignore"),
|
||||
Buffer.from("*", "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
await generateSrcMainResources({ buildOptions });
|
||||
const resourcesDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "resources");
|
||||
|
||||
await generateResources({
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
});
|
||||
|
||||
run_post_build_script: {
|
||||
if (buildOptions.bundler !== "vite") {
|
||||
if (buildContext.bundler.type !== "vite") {
|
||||
break run_post_build_script;
|
||||
}
|
||||
|
||||
child_process.execSync("npx vite", {
|
||||
"cwd": buildOptions.reactAppRootDirPath,
|
||||
"env": {
|
||||
cwd: buildContext.projectDirPath,
|
||||
env: {
|
||||
...process.env,
|
||||
[vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify(buildOptions)
|
||||
[VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.RUN_POST_BUILD_SCRIPT]: JSON.stringify(
|
||||
{
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await buildJars({ buildOptions });
|
||||
await buildJars({
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
});
|
||||
|
||||
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
|
||||
rmSync(resourcesDirPath, { recursive: true });
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ keycloak theme built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,57 +1,53 @@
|
||||
import * as crypto from "crypto";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
||||
import { posix } from "path";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
export type BuildContextLike = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
export function replaceImportsInCssCode(params: {
|
||||
cssCode: string;
|
||||
cssFileRelativeDirPath: string | undefined;
|
||||
buildContext: BuildContextLike;
|
||||
}): {
|
||||
fixedCssCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
} {
|
||||
const { cssCode } = params;
|
||||
const { cssCode, cssFileRelativeDirPath, buildContext } = params;
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
const fixedCssCode = cssCode.replace(
|
||||
/url\(["']?(\/[^/][^)"']+)["']?\)/g,
|
||||
(match, assetFileAbsoluteUrlPathname) => {
|
||||
if (buildContext.urlPathname !== undefined) {
|
||||
if (!assetFileAbsoluteUrlPathname.startsWith(buildContext.urlPathname)) {
|
||||
// NOTE: Should never happen
|
||||
return match;
|
||||
}
|
||||
assetFileAbsoluteUrlPathname = assetFileAbsoluteUrlPathname.replace(
|
||||
buildContext.urlPathname,
|
||||
"/"
|
||||
);
|
||||
}
|
||||
|
||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
|
||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
|
||||
inline_style_in_html: {
|
||||
if (cssFileRelativeDirPath !== undefined) {
|
||||
break inline_style_in_html;
|
||||
}
|
||||
|
||||
return `url(\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}${assetFileAbsoluteUrlPathname})`;
|
||||
}
|
||||
|
||||
const assetFileRelativeUrlPathname = posix.relative(
|
||||
cssFileRelativeDirPath.replace(/\\/g, "/"),
|
||||
assetFileAbsoluteUrlPathname.replace(/^\//, "")
|
||||
);
|
||||
|
||||
return `url(${assetFileRelativeUrlPathname})`;
|
||||
}
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
const { cssGlobalsToDefine, buildOptions } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName =>
|
||||
[
|
||||
`--${cssVariableName}:`,
|
||||
cssGlobalsToDefine[cssVariableName].replace(
|
||||
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
)
|
||||
].join(" ")
|
||||
)
|
||||
.map(line => ` ${line};`),
|
||||
"}"
|
||||
].join("\n")
|
||||
};
|
||||
return { fixedCssCode };
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||
fixedCssCode: string;
|
||||
} {
|
||||
const { cssCode, buildOptions } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
buildOptions.urlPathname === undefined
|
||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
}
|
@ -1,35 +1,38 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../../shared/buildContext";
|
||||
import { replaceImportsInJsCode_vite } from "./vite";
|
||||
import { replaceImportsInJsCode_webpack } from "./webpack";
|
||||
import * as fs from "fs";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
reactAppBuildDirPath: string;
|
||||
export type BuildContextLike = {
|
||||
projectBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
urlPathname: string | undefined;
|
||||
bundler: "vite" | "webpack";
|
||||
bundler: { type: "vite" } | { type: "webpack" };
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }) {
|
||||
const { jsCode, buildOptions } = params;
|
||||
export function replaceImportsInJsCode(params: {
|
||||
jsCode: string;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { jsCode, buildContext } = params;
|
||||
|
||||
const { fixedJsCode } = (() => {
|
||||
switch (buildOptions.bundler) {
|
||||
switch (buildContext.bundler.type) {
|
||||
case "vite":
|
||||
return replaceImportsInJsCode_vite({
|
||||
jsCode,
|
||||
buildOptions,
|
||||
"basenameOfAssetsFiles": readAssetsDirSync({
|
||||
"assetsDirPath": params.buildOptions.assetsDirPath
|
||||
buildContext,
|
||||
basenameOfAssetsFiles: readAssetsDirSync({
|
||||
assetsDirPath: params.buildContext.assetsDirPath
|
||||
})
|
||||
});
|
||||
case "webpack":
|
||||
return replaceImportsInJsCode_webpack({
|
||||
jsCode,
|
||||
buildOptions
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
@ -1,59 +1,74 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../../shared/buildContext";
|
||||
import * as nodePath from "path";
|
||||
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
reactAppBuildDirPath: string;
|
||||
export type BuildContextLike = {
|
||||
projectBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function replaceImportsInJsCode_vite(params: {
|
||||
jsCode: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
buildContext: BuildContextLike;
|
||||
basenameOfAssetsFiles: string[];
|
||||
systemType?: "posix" | "win32";
|
||||
}): {
|
||||
fixedJsCode: string;
|
||||
} {
|
||||
const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
|
||||
const {
|
||||
jsCode,
|
||||
buildContext,
|
||||
basenameOfAssetsFiles,
|
||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||
} = params;
|
||||
|
||||
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
|
||||
|
||||
let fixedJsCode = jsCode;
|
||||
|
||||
replace_base_javacript_import: {
|
||||
if (buildOptions.urlPathname === undefined) {
|
||||
break replace_base_javacript_import;
|
||||
replace_base_js_import: {
|
||||
if (buildContext.urlPathname === undefined) {
|
||||
break replace_base_js_import;
|
||||
}
|
||||
// Optimization
|
||||
if (!jsCode.includes(buildOptions.urlPathname)) {
|
||||
break replace_base_javacript_import;
|
||||
if (!jsCode.includes(buildContext.urlPathname)) {
|
||||
break replace_base_js_import;
|
||||
}
|
||||
|
||||
// Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
|
||||
fixedJsCode = fixedJsCode.replace(
|
||||
new RegExp(
|
||||
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
|
||||
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(
|
||||
buildContext.urlPathname,
|
||||
"/",
|
||||
"\\/"
|
||||
)}"\\+\\2\\}`,
|
||||
"g"
|
||||
),
|
||||
(...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
|
||||
(...[, funcName, paramName]) =>
|
||||
`${funcName}=function(${paramName}){return"/"+${paramName}}`
|
||||
);
|
||||
}
|
||||
|
||||
replace_javascript_relatives_import_paths: {
|
||||
// Example: "assets/ or "foo/bar/"
|
||||
const staticDir = (() => {
|
||||
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
|
||||
let out = pathRelative(
|
||||
buildContext.projectBuildDirPath,
|
||||
buildContext.assetsDirPath
|
||||
);
|
||||
|
||||
out = replaceAll(out, pathSep, "/") + "/";
|
||||
|
||||
if (out === "/") {
|
||||
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
|
||||
throw new Error(
|
||||
`The assetsDirPath must be a subdirectory of projectBuildDirPath`
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
@ -70,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: {
|
||||
fixedJsCode = replaceAll(
|
||||
fixedJsCode,
|
||||
`"${relativePathOfAssetFile}"`,
|
||||
`(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
|
||||
);
|
||||
|
||||
fixedJsCode = replaceAll(
|
||||
fixedJsCode,
|
||||
`"${buildOptions.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
|
||||
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
|
||||
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,64 +1,96 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
||||
import type { BuildContext } from "../../../shared/buildContext";
|
||||
import * as nodePath from "path";
|
||||
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
reactAppBuildDirPath: string;
|
||||
export type BuildContextLike = {
|
||||
projectBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike; systemType?: "posix" | "win32" }): {
|
||||
export function replaceImportsInJsCode_webpack(params: {
|
||||
jsCode: string;
|
||||
buildContext: BuildContextLike;
|
||||
systemType?: "posix" | "win32";
|
||||
}): {
|
||||
fixedJsCode: string;
|
||||
} {
|
||||
const { jsCode, buildOptions, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
|
||||
const {
|
||||
jsCode,
|
||||
buildContext,
|
||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||
} = params;
|
||||
|
||||
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
|
||||
|
||||
let fixedJsCode = jsCode;
|
||||
|
||||
if (buildOptions.urlPathname !== undefined) {
|
||||
if (buildContext.urlPathname !== undefined) {
|
||||
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
|
||||
fixedJsCode = fixedJsCode.replace(
|
||||
new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
|
||||
new RegExp(
|
||||
`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(
|
||||
buildContext.urlPathname,
|
||||
"/",
|
||||
"\\/"
|
||||
)}",`,
|
||||
"g"
|
||||
),
|
||||
(...[, assignTo]) => `,${assignTo}="/",`
|
||||
);
|
||||
}
|
||||
|
||||
// Example: "static/ or "foo/bar/"
|
||||
const staticDir = (() => {
|
||||
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
|
||||
let out = pathRelative(
|
||||
buildContext.projectBuildDirPath,
|
||||
buildContext.assetsDirPath
|
||||
);
|
||||
|
||||
out = replaceAll(out, pathSep, "/") + "/";
|
||||
|
||||
if (out === "/") {
|
||||
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
|
||||
throw new Error(
|
||||
`The assetsDirPath must be a subdirectory of projectBuildDirPath`
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
})();
|
||||
|
||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
|
||||
const getReplaceArgs = (
|
||||
language: "js" | "css"
|
||||
): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(
|
||||
`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(
|
||||
/\//g,
|
||||
"\\/"
|
||||
)}${language}\\/"`,
|
||||
"g"
|
||||
),
|
||||
(...[, n, u, matchedFunction, eForFunction]) => {
|
||||
const isArrowFunction = matchedFunction.includes("=>");
|
||||
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
|
||||
const e = isArrowFunction
|
||||
? matchedFunction.replace("=>", "").trim()
|
||||
: eForFunction;
|
||||
|
||||
return `
|
||||
${n}[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; },
|
||||
get: function() { return window.kcContext["x-keycloakify"].resourcesPath; },
|
||||
set: function() {}
|
||||
});
|
||||
}
|
||||
return "${u}";
|
||||
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
|
||||
})()] = ${
|
||||
isArrowFunction ? `${e} =>` : `function(${e}) { return `
|
||||
} "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}${language}/"`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
@ -68,8 +100,11 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
|
||||
.replace(...getReplaceArgs("js"))
|
||||
.replace(...getReplaceArgs("css"))
|
||||
.replace(
|
||||
new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g"),
|
||||
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
|
||||
new RegExp(
|
||||
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
|
||||
"g"
|
||||
),
|
||||
`window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
|
143
src/bin/main.ts
143
src/bin/main.ts
@ -3,19 +3,22 @@
|
||||
import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
|
||||
export type CliCommandOptions = {
|
||||
reactAppRootDirPath: string | undefined;
|
||||
projectDirPath: string | undefined;
|
||||
};
|
||||
|
||||
assertNoPnpmDlx();
|
||||
|
||||
const program = termost<CliCommandOptions>(
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"description": "Keycloakify CLI",
|
||||
"version": readThisNpmPackageVersion()
|
||||
name: "keycloakify",
|
||||
description: "Keycloakify CLI",
|
||||
version: readThisNpmPackageVersion()
|
||||
},
|
||||
{
|
||||
"onException": error => {
|
||||
onException: error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
@ -25,8 +28,8 @@ const program = termost<CliCommandOptions>(
|
||||
const optionsKeys: string[] = [];
|
||||
|
||||
program.option({
|
||||
"key": "reactAppRootDirPath",
|
||||
"name": (() => {
|
||||
key: "projectDirPath",
|
||||
name: (() => {
|
||||
const long = "project";
|
||||
const short = "p";
|
||||
|
||||
@ -34,19 +37,25 @@ program.option({
|
||||
|
||||
return { long, short };
|
||||
})(),
|
||||
"description": [
|
||||
description: [
|
||||
`For monorepos, path to the keycloakify project.`,
|
||||
"Example: `npx keycloakify build --project packages/keycloak-theme`",
|
||||
"https://docs.keycloakify.dev/build-options#project-or-p-cli-option"
|
||||
].join(" "),
|
||||
"defaultValue": undefined
|
||||
defaultValue: undefined
|
||||
});
|
||||
|
||||
function skip(_context: any, argv: { options: Record<string, unknown> }) {
|
||||
const unrecognizedOptionKey = Object.keys(argv.options).find(key => !optionsKeys.includes(key));
|
||||
const unrecognizedOptionKey = Object.keys(argv.options).find(
|
||||
key => !optionsKeys.includes(key)
|
||||
);
|
||||
|
||||
if (unrecognizedOptionKey !== undefined) {
|
||||
console.error(`keycloakify: Unrecognized option: ${unrecognizedOptionKey.length === 1 ? "-" : "--"}${unrecognizedOptionKey}`);
|
||||
console.error(
|
||||
`keycloakify: Unrecognized option: ${
|
||||
unrecognizedOptionKey.length === 1 ? "-" : "--"
|
||||
}${unrecognizedOptionKey}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -55,12 +64,12 @@ function skip(_context: any, argv: { options: Record<string, unknown> }) {
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "build",
|
||||
"description": "Build the theme (default subcommand)."
|
||||
name: "build",
|
||||
description: "Build the theme (default subcommand)."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./keycloakify");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
@ -68,37 +77,60 @@ program
|
||||
});
|
||||
|
||||
program
|
||||
.command<{ port: number; keycloakVersion: string | undefined }>({
|
||||
"name": "start-keycloak",
|
||||
"description": "Spin up a pre configured Docker image of Keycloak to test your theme."
|
||||
.command<{
|
||||
port: number;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
}>({
|
||||
name: "start-keycloak",
|
||||
description:
|
||||
"Spin up a pre configured Docker image of Keycloak to test your theme."
|
||||
})
|
||||
.option({
|
||||
"key": "port",
|
||||
"name": (() => {
|
||||
key: "port",
|
||||
name: (() => {
|
||||
const name = "port";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
"description": "Keycloak server port.",
|
||||
"defaultValue": 8080
|
||||
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
||||
defaultValue: 8080
|
||||
})
|
||||
.option({
|
||||
"key": "keycloakVersion",
|
||||
"name": (() => {
|
||||
key: "keycloakVersion",
|
||||
name: (() => {
|
||||
const name = "keycloak-version";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
"description": "Use a specific version of Keycloak.",
|
||||
"defaultValue": undefined
|
||||
description: [
|
||||
"Use a specific version of Keycloak.",
|
||||
"Example `--keycloak-version 21.1.1`"
|
||||
].join(" "),
|
||||
defaultValue: undefined
|
||||
})
|
||||
.option({
|
||||
key: "realmJsonFilePath",
|
||||
name: (() => {
|
||||
const name = "import";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
defaultValue: undefined,
|
||||
description: [
|
||||
"Import your own realm configuration file",
|
||||
"Example `--import path/to/myrealm-realm.json`"
|
||||
].join(" ")
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./start-keycloak");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
@ -107,13 +139,13 @@ program
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "download-builtin-keycloak-theme",
|
||||
"description": "Download the built-in Keycloak theme."
|
||||
name: "eject-page",
|
||||
description: "Eject a Keycloak page."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
const { command } = await import("./download-builtin-keycloak-theme");
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./eject-page");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
@ -121,13 +153,13 @@ program
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "eject-keycloak-page",
|
||||
"description": "Eject a Keycloak page."
|
||||
name: "add-story",
|
||||
description: "Add *.stories.tsx file for a specific page to in your Storybook."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
const { command } = await import("./eject-keycloak-page");
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./add-story");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
@ -135,12 +167,12 @@ program
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "initialize-email-theme",
|
||||
"description": "Initialize an email theme."
|
||||
name: "initialize-email-theme",
|
||||
description: "Initialize an email theme."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./initialize-email-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
@ -149,26 +181,49 @@ program
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "copy-keycloak-resources-to-public",
|
||||
"description": "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
|
||||
name: "copy-keycloak-resources-to-public",
|
||||
description:
|
||||
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "update-kc-gen",
|
||||
description:
|
||||
"(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./update-kc-gen");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback to build command if no command is provided
|
||||
{
|
||||
const [, , ...rest] = process.argv;
|
||||
|
||||
if (rest.length === 0 || (rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")) {
|
||||
const { status } = child_process.spawnSync("npx", ["keycloakify", "build", ...rest], {
|
||||
"stdio": "inherit"
|
||||
});
|
||||
if (
|
||||
rest.length === 0 ||
|
||||
(rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")
|
||||
) {
|
||||
const { status } = child_process.spawnSync(
|
||||
"npx",
|
||||
["keycloakify", "build", ...rest],
|
||||
{
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
|
||||
process.exit(status ?? 1);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
export type KeycloakVersionRange = KeycloakVersionRange.WithAccountTheme | KeycloakVersionRange.WithoutAccountTheme;
|
||||
export type KeycloakVersionRange =
|
||||
| KeycloakVersionRange.WithAccountV1Theme
|
||||
| KeycloakVersionRange.WithoutAccountV1Theme;
|
||||
|
||||
export namespace KeycloakVersionRange {
|
||||
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
|
||||
export type WithoutAccountV1Theme = "21-and-below" | "22-and-above";
|
||||
|
||||
export type WithAccountTheme = "21-and-below" | "23" | "24-and-above";
|
||||
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
|
||||
}
|
||||
|
877
src/bin/shared/buildContext.ts
Normal file
877
src/bin/shared/buildContext.ts
Normal file
@ -0,0 +1,877 @@
|
||||
import { parse as urlParse } from "url";
|
||||
import {
|
||||
join as pathJoin,
|
||||
sep as pathSep,
|
||||
relative as pathRelative,
|
||||
resolve as pathResolve,
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as child_process from "child_process";
|
||||
import {
|
||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME
|
||||
} from "./constants";
|
||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||
import { exclude } from "tsafe";
|
||||
import { crawl } from "../tools/crawl";
|
||||
import { THEME_TYPES } from "./constants";
|
||||
import { objectFromEntries } from "tsafe/objectFromEntries";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { type ThemeType } from "./constants";
|
||||
import { id } from "tsafe/id";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import chalk from "chalk";
|
||||
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
|
||||
export type BuildContext = {
|
||||
themeVersion: string;
|
||||
themeNames: [string, ...string[]];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
keycloakifyBuildDirPath: string;
|
||||
publicDirPath: string;
|
||||
cacheDirPath: string;
|
||||
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
fetchOptions: ProxyFetchOptions;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
recordIsImplementedByThemeType: Readonly<Record<ThemeType | "email", boolean>>;
|
||||
jarTargets: {
|
||||
keycloakVersionRange: KeycloakVersionRange;
|
||||
jarFileBasename: string;
|
||||
}[];
|
||||
bundler:
|
||||
| {
|
||||
type: "vite";
|
||||
}
|
||||
| {
|
||||
type: "webpack";
|
||||
packageJsonDirPath: string;
|
||||
packageJsonScripts: Record<string, string>;
|
||||
};
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
export type BuildOptions = {
|
||||
themeName?: string | string[];
|
||||
themeVersion?: string;
|
||||
environmentVariables?: { name: string; default: string }[];
|
||||
extraThemeProperties?: string[];
|
||||
artifactId?: string;
|
||||
groupId?: string;
|
||||
loginThemeResourcesFromKeycloakVersion?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
kcContextExclusionsFtl?: string;
|
||||
/** https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions */
|
||||
keycloakVersionTargets?: BuildOptions.KeycloakVersionTargets;
|
||||
doUseAccountV3?: boolean;
|
||||
};
|
||||
|
||||
export namespace BuildOptions {
|
||||
export type KeycloakVersionTargets =
|
||||
| ({ hasAccountTheme: true } & Record<
|
||||
KeycloakVersionRange.WithAccountV1Theme,
|
||||
string | boolean
|
||||
>)
|
||||
| ({ hasAccountTheme: false } & Record<
|
||||
KeycloakVersionRange.WithoutAccountV1Theme,
|
||||
string | boolean
|
||||
>);
|
||||
}
|
||||
|
||||
export type ResolvedViteConfig = {
|
||||
buildDir: string;
|
||||
publicDir: string;
|
||||
assetsDir: string;
|
||||
urlPathname: string | undefined;
|
||||
buildOptions: BuildOptions;
|
||||
};
|
||||
|
||||
export function getBuildContext(params: {
|
||||
cliCommandOptions: CliCommandOptions;
|
||||
}): BuildContext {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const projectDirPath =
|
||||
cliCommandOptions.projectDirPath !== undefined
|
||||
? getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
})
|
||||
: process.cwd();
|
||||
|
||||
const { themeSrcDirPath } = (() => {
|
||||
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
if (themeSrcDirPath !== undefined) {
|
||||
return { themeSrcDirPath };
|
||||
}
|
||||
|
||||
for (const themeType of [...THEME_TYPES, "email"]) {
|
||||
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
return { themeSrcDirPath: srcDirPath };
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
|
||||
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
|
||||
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const { resolvedViteConfig } = (() => {
|
||||
if (
|
||||
fs
|
||||
.readdirSync(projectDirPath)
|
||||
.find(fileBasename => fileBasename.startsWith("vite.config")) ===
|
||||
undefined
|
||||
) {
|
||||
return { resolvedViteConfig: undefined };
|
||||
}
|
||||
|
||||
const output = child_process
|
||||
.execSync("npx vite", {
|
||||
cwd: projectDirPath,
|
||||
env: {
|
||||
...process.env,
|
||||
[VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.RESOLVE_VITE_CONFIG]: "true"
|
||||
}
|
||||
})
|
||||
.toString("utf8");
|
||||
|
||||
assert(
|
||||
output.includes(VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.RESOLVE_VITE_CONFIG),
|
||||
"Seems like the Keycloakify's Vite plugin is not installed."
|
||||
);
|
||||
|
||||
const resolvedViteConfigStr = output
|
||||
.split(VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.RESOLVE_VITE_CONFIG)
|
||||
.reverse()[0];
|
||||
|
||||
const resolvedViteConfig: ResolvedViteConfig = JSON.parse(resolvedViteConfigStr);
|
||||
|
||||
return { resolvedViteConfig };
|
||||
})();
|
||||
|
||||
const packageJsonFilePath = (function getPackageJSonDirPath(upCount: number): string {
|
||||
const dirPath = pathResolve(
|
||||
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
|
||||
);
|
||||
|
||||
assert(dirPath !== pathSep, "Root package.json not found");
|
||||
|
||||
success: {
|
||||
const packageJsonFilePath = pathJoin(dirPath, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageJsonFilePath)) {
|
||||
break success;
|
||||
}
|
||||
|
||||
const parsedPackageJson = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
})
|
||||
.parse(JSON.parse(fs.readFileSync(packageJsonFilePath).toString("utf8")));
|
||||
|
||||
if (
|
||||
parsedPackageJson.dependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.devDependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
|
||||
) {
|
||||
break success;
|
||||
}
|
||||
|
||||
return packageJsonFilePath;
|
||||
}
|
||||
|
||||
return getPackageJSonDirPath(upCount + 1);
|
||||
})(0);
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type BuildOptions_packageJson = BuildOptions & {
|
||||
projectBuildDirPath?: string;
|
||||
staticDirPathInProjectBuildDirPath?: string;
|
||||
publicDirPath?: string;
|
||||
doUseAccountV3?: boolean;
|
||||
};
|
||||
|
||||
type ParsedPackageJson = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
keycloakify?: BuildOptions_packageJson;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
|
||||
(() => {
|
||||
const zBuildOptions_packageJson = z.object({
|
||||
extraThemeProperties: z.array(z.string()).optional(),
|
||||
artifactId: z.string().optional(),
|
||||
groupId: z.string().optional(),
|
||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||
projectBuildDirPath: z.string().optional(),
|
||||
keycloakifyBuildDirPath: z.string().optional(),
|
||||
kcContextExclusionsFtl: z.string().optional(),
|
||||
environmentVariables: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
default: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
themeName: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
themeVersion: z.string().optional(),
|
||||
staticDirPathInProjectBuildDirPath: z.string().optional(),
|
||||
publicDirPath: z.string().optional(),
|
||||
keycloakVersionTargets: id<
|
||||
z.ZodType<BuildOptions.KeycloakVersionTargets>
|
||||
>(
|
||||
(() => {
|
||||
const zKeycloakVersionTargets = z.union([
|
||||
z.object({
|
||||
hasAccountTheme: z.literal(true),
|
||||
"21-and-below": z.union([
|
||||
z.boolean(),
|
||||
z.string()
|
||||
]),
|
||||
"23": z.union([z.boolean(), z.string()]),
|
||||
"24": z.union([z.boolean(), z.string()]),
|
||||
"25-and-above": z.union([z.boolean(), z.string()])
|
||||
}),
|
||||
z.object({
|
||||
hasAccountTheme: z.literal(false),
|
||||
"21-and-below": z.union([
|
||||
z.boolean(),
|
||||
z.string()
|
||||
]),
|
||||
"22-and-above": z.union([z.boolean(), z.string()])
|
||||
})
|
||||
]);
|
||||
|
||||
{
|
||||
type Got = z.infer<typeof zKeycloakVersionTargets>;
|
||||
type Expected = BuildOptions.KeycloakVersionTargets;
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
||||
return zKeycloakVersionTargets;
|
||||
})()
|
||||
).optional(),
|
||||
doUseAccountV3: z.boolean().optional()
|
||||
});
|
||||
|
||||
{
|
||||
type Got = z.infer<typeof zBuildOptions_packageJson>;
|
||||
type Expected = BuildOptions_packageJson;
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
||||
return zBuildOptions_packageJson;
|
||||
})()
|
||||
).optional()
|
||||
});
|
||||
|
||||
{
|
||||
type Got = z.infer<typeof zParsedPackageJson>;
|
||||
type Expected = ParsedPackageJson;
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
||||
const configurationPackageJsonFilePath = (() => {
|
||||
const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json");
|
||||
|
||||
return fs.existsSync(rootPackageJsonFilePath)
|
||||
? rootPackageJsonFilePath
|
||||
: packageJsonFilePath;
|
||||
})();
|
||||
|
||||
return zParsedPackageJson.parse(
|
||||
JSON.parse(fs.readFileSync(configurationPackageJsonFilePath).toString("utf8"))
|
||||
);
|
||||
})();
|
||||
|
||||
const buildOptions = {
|
||||
...parsedPackageJson.keycloakify,
|
||||
...resolvedViteConfig?.buildOptions
|
||||
};
|
||||
|
||||
const recordIsImplementedByThemeType = objectFromEntries(
|
||||
(["login", "account", "email"] as const).map(themeType => [
|
||||
themeType,
|
||||
fs.existsSync(pathJoin(themeSrcDirPath, themeType))
|
||||
])
|
||||
);
|
||||
|
||||
const themeNames = ((): [string, ...string[]] => {
|
||||
if (buildOptions.themeName === undefined) {
|
||||
return parsedPackageJson.name === undefined
|
||||
? ["keycloakify"]
|
||||
: [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-")
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof buildOptions.themeName === "string") {
|
||||
return [buildOptions.themeName];
|
||||
}
|
||||
|
||||
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
|
||||
|
||||
assert(mainThemeName !== undefined);
|
||||
|
||||
return [mainThemeName, ...themeVariantNames];
|
||||
})();
|
||||
|
||||
const projectBuildDirPath = (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (buildOptions.projectBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.projectBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(projectDirPath, "build");
|
||||
}
|
||||
|
||||
return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
|
||||
})();
|
||||
|
||||
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
|
||||
|
||||
const doUseAccountV3 = buildOptions.doUseAccountV3 ?? false;
|
||||
|
||||
return {
|
||||
bundler:
|
||||
resolvedViteConfig !== undefined
|
||||
? { type: "vite" }
|
||||
: (() => {
|
||||
const { scripts } = z
|
||||
.object({
|
||||
scripts: z.record(z.string()).optional()
|
||||
})
|
||||
.parse(
|
||||
JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
type: "webpack",
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
||||
packageJsonScripts: scripts ?? {}
|
||||
};
|
||||
})(),
|
||||
themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
|
||||
themeNames,
|
||||
extraThemeProperties: buildOptions.extraThemeProperties,
|
||||
groupId: (() => {
|
||||
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
||||
|
||||
return (
|
||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||
buildOptions.groupId ??
|
||||
(parsedPackageJson.homepage === undefined
|
||||
? fallbackGroupId
|
||||
: urlParse(parsedPackageJson.homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
artifactId:
|
||||
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
|
||||
buildOptions.artifactId ??
|
||||
`${themeNames[0]}-keycloak-theme`,
|
||||
loginThemeResourcesFromKeycloakVersion:
|
||||
buildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
||||
projectDirPath,
|
||||
projectBuildDirPath,
|
||||
keycloakifyBuildDirPath: (() => {
|
||||
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
projectDirPath,
|
||||
resolvedViteConfig?.buildDir === undefined
|
||||
? "build_keycloak"
|
||||
: `${resolvedViteConfig.buildDir}_keycloak`
|
||||
);
|
||||
})(),
|
||||
publicDirPath: (() => {
|
||||
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: process.env.PUBLIC_DIR_PATH,
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (buildOptions.publicDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.publicDirPath,
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(projectDirPath, "public");
|
||||
}
|
||||
|
||||
return pathJoin(projectDirPath, resolvedViteConfig.publicDir);
|
||||
})(),
|
||||
cacheDirPath: (() => {
|
||||
const cacheDirPath = pathJoin(
|
||||
(() => {
|
||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: process.env.XDG_CACHE_HOME,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
pathDirname(packageJsonFilePath),
|
||||
"node_modules",
|
||||
".cache"
|
||||
);
|
||||
})(),
|
||||
"keycloakify"
|
||||
);
|
||||
|
||||
return cacheDirPath;
|
||||
})(),
|
||||
urlPathname: (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
let url: URL | undefined = undefined;
|
||||
|
||||
if (homepage !== undefined) {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||
return out === "/" ? undefined : out;
|
||||
}
|
||||
|
||||
return resolvedViteConfig.urlPathname;
|
||||
})(),
|
||||
assetsDirPath: (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (buildOptions.staticDirPathInProjectBuildDirPath !== undefined) {
|
||||
getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.staticDirPathInProjectBuildDirPath,
|
||||
cwd: projectBuildDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(projectBuildDirPath, "static");
|
||||
}
|
||||
|
||||
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
|
||||
})(),
|
||||
kcContextExclusionsFtlCode: (() => {
|
||||
if (buildOptions.kcContextExclusionsFtl === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (buildOptions.kcContextExclusionsFtl.endsWith(".ftl")) {
|
||||
const kcContextExclusionsFtlPath = getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.kcContextExclusionsFtl,
|
||||
cwd: projectDirPath
|
||||
});
|
||||
|
||||
return fs.readFileSync(kcContextExclusionsFtlPath).toString("utf8");
|
||||
}
|
||||
|
||||
return buildOptions.kcContextExclusionsFtl;
|
||||
})(),
|
||||
environmentVariables: buildOptions.environmentVariables ?? [],
|
||||
recordIsImplementedByThemeType,
|
||||
themeSrcDirPath,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: (function callee(upCount: number): string {
|
||||
const dirPath = pathResolve(
|
||||
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
|
||||
);
|
||||
|
||||
assert(
|
||||
dirPath !== pathSep,
|
||||
"Couldn't find a place to run 'npm config get'"
|
||||
);
|
||||
|
||||
try {
|
||||
child_process.execSync("npm config get", {
|
||||
cwd: dirPath,
|
||||
stdio: "pipe"
|
||||
});
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
return callee(upCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return dirPath;
|
||||
})(0)
|
||||
}),
|
||||
jarTargets: (() => {
|
||||
const getDefaultJarFileBasename = (range: string) =>
|
||||
`keycloak-theme-for-kc-${range}.jar`;
|
||||
|
||||
build_for_specific_keycloak_major_version: {
|
||||
const buildForKeycloakMajorVersionNumber = (() => {
|
||||
const envValue =
|
||||
process.env[BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME];
|
||||
|
||||
if (envValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const major = parseInt(envValue);
|
||||
|
||||
assert(!isNaN(major));
|
||||
|
||||
return major;
|
||||
})();
|
||||
|
||||
if (buildForKeycloakMajorVersionNumber === undefined) {
|
||||
break build_for_specific_keycloak_major_version;
|
||||
}
|
||||
|
||||
const keycloakVersionRange: KeycloakVersionRange = (() => {
|
||||
const doesImplementAccountV1Theme =
|
||||
!doUseAccountV3 && recordIsImplementedByThemeType.account;
|
||||
|
||||
if (doesImplementAccountV1Theme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (buildForKeycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
assert(buildForKeycloakMajorVersionNumber !== 22);
|
||||
|
||||
if (buildForKeycloakMajorVersionNumber === 23) {
|
||||
return "23" as const;
|
||||
}
|
||||
|
||||
if (buildForKeycloakMajorVersionNumber === 24) {
|
||||
return "24" as const;
|
||||
}
|
||||
|
||||
return "25-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountV1Theme
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (buildForKeycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
return "22-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountV1Theme
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
}
|
||||
})();
|
||||
|
||||
const jarFileBasename = (() => {
|
||||
use_custom_jar_basename: {
|
||||
const { keycloakVersionTargets } = buildOptions;
|
||||
|
||||
if (keycloakVersionTargets === undefined) {
|
||||
break use_custom_jar_basename;
|
||||
}
|
||||
|
||||
const entry = objectEntries(keycloakVersionTargets).find(
|
||||
([keycloakVersionRange_entry]) =>
|
||||
keycloakVersionRange_entry === keycloakVersionRange
|
||||
);
|
||||
|
||||
if (entry === undefined) {
|
||||
break use_custom_jar_basename;
|
||||
}
|
||||
|
||||
const maybeJarFileBasename = entry[1];
|
||||
|
||||
if (typeof maybeJarFileBasename !== "string") {
|
||||
break use_custom_jar_basename;
|
||||
}
|
||||
|
||||
return maybeJarFileBasename;
|
||||
}
|
||||
|
||||
return getDefaultJarFileBasename(keycloakVersionRange);
|
||||
})();
|
||||
|
||||
return [
|
||||
{
|
||||
keycloakVersionRange,
|
||||
jarFileBasename
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const jarTargets_default = (() => {
|
||||
const jarTargets: BuildContext["jarTargets"] = [];
|
||||
|
||||
if (!doUseAccountV3 && recordIsImplementedByThemeType.account) {
|
||||
for (const keycloakVersionRange of [
|
||||
"21-and-below",
|
||||
"23",
|
||||
"24",
|
||||
"25-and-above"
|
||||
] as const) {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountV1Theme
|
||||
>
|
||||
>(true);
|
||||
jarTargets.push({
|
||||
keycloakVersionRange,
|
||||
jarFileBasename:
|
||||
getDefaultJarFileBasename(keycloakVersionRange)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const keycloakVersionRange of [
|
||||
"21-and-below",
|
||||
"22-and-above"
|
||||
] as const) {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountV1Theme
|
||||
>
|
||||
>(true);
|
||||
jarTargets.push({
|
||||
keycloakVersionRange,
|
||||
jarFileBasename:
|
||||
getDefaultJarFileBasename(keycloakVersionRange)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return jarTargets;
|
||||
})();
|
||||
|
||||
if (buildOptions.keycloakVersionTargets === undefined) {
|
||||
return jarTargets_default;
|
||||
}
|
||||
|
||||
if (
|
||||
buildOptions.keycloakVersionTargets.hasAccountTheme !== doUseAccountV3
|
||||
? false
|
||||
: recordIsImplementedByThemeType.account
|
||||
) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
(() => {
|
||||
const { keycloakVersionTargets } = buildOptions;
|
||||
|
||||
let message = `Bad ${symToStr({ keycloakVersionTargets })} configuration.\n`;
|
||||
|
||||
if (keycloakVersionTargets.hasAccountTheme) {
|
||||
message +=
|
||||
"Your codebase does not seem to implement an account theme ";
|
||||
} else {
|
||||
message += "Your codebase implements an account theme ";
|
||||
}
|
||||
|
||||
const { hasAccountTheme } = keycloakVersionTargets;
|
||||
|
||||
message += `but you have set ${symToStr({ keycloakVersionTargets })}.${symToStr({ hasAccountTheme })}`;
|
||||
message += ` to ${hasAccountTheme} in your `;
|
||||
message += (() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return "vite.config.ts";
|
||||
case "webpack":
|
||||
return "package.json";
|
||||
}
|
||||
assert<Equals<typeof bundler, never>>(false);
|
||||
})();
|
||||
message += `. Please set it to ${!hasAccountTheme} `;
|
||||
message +=
|
||||
"and fill up the relevant keycloak version ranges.\n";
|
||||
message += "Example:\n";
|
||||
message += JSON.stringify(
|
||||
id<Pick<BuildOptions, "keycloakVersionTargets">>({
|
||||
keycloakVersionTargets: {
|
||||
hasAccountTheme:
|
||||
recordIsImplementedByThemeType.account,
|
||||
...objectFromEntries(
|
||||
jarTargets_default.map(
|
||||
({
|
||||
keycloakVersionRange,
|
||||
jarFileBasename
|
||||
}) => [
|
||||
keycloakVersionRange,
|
||||
jarFileBasename
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}),
|
||||
null,
|
||||
2
|
||||
);
|
||||
message +=
|
||||
"\nSee: https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions";
|
||||
|
||||
return message;
|
||||
})()
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jarTargets: BuildContext["jarTargets"] = [];
|
||||
|
||||
const { hasAccountTheme, ...rest } = buildOptions.keycloakVersionTargets;
|
||||
|
||||
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(rest)) {
|
||||
if (jarNameOrBoolean === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (jarNameOrBoolean === true) {
|
||||
jarTargets.push({
|
||||
keycloakVersionRange: keycloakVersionRange,
|
||||
jarFileBasename: getDefaultJarFileBasename(keycloakVersionRange)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const jarFileBasename = jarNameOrBoolean;
|
||||
|
||||
if (!jarFileBasename.endsWith(".jar")) {
|
||||
console.log(
|
||||
chalk.red(`Bad ${jarFileBasename} should end with '.jar'\n`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (jarFileBasename.includes("/") || jarFileBasename.includes("\\")) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
`Invalid ${jarFileBasename}. It's not supposed to be a path,`,
|
||||
`Only the basename of the jar file is expected.`,
|
||||
`Example: keycloak-theme.jar`
|
||||
].join(" ")
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
jarTargets.push({
|
||||
keycloakVersionRange: keycloakVersionRange,
|
||||
jarFileBasename: jarNameOrBoolean
|
||||
});
|
||||
}
|
||||
|
||||
if (jarTargets.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
"All jar targets are disabled. Please enable at least one jar target."
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return jarTargets;
|
||||
})(),
|
||||
doUseAccountV3
|
||||
};
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
import { parse as urlParse } from "url";
|
||||
import { join as pathJoin } from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe";
|
||||
import * as child_process from "child_process";
|
||||
import { vitePluginSubScriptEnvNames } from "./constants";
|
||||
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = {
|
||||
bundler: "vite" | "webpack";
|
||||
themeVersion: string;
|
||||
themeNames: string[];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
reactAppRootDirPath: string;
|
||||
// TODO: Remove from vite type
|
||||
reactAppBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
keycloakifyBuildDirPath: string;
|
||||
publicDirPath: string;
|
||||
cacheDirPath: string;
|
||||
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
export type UserProvidedBuildOptions = {
|
||||
extraThemeProperties?: string[];
|
||||
artifactId?: string;
|
||||
groupId?: string;
|
||||
loginThemeResourcesFromKeycloakVersion?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
themeName?: string | string[];
|
||||
};
|
||||
|
||||
export type ResolvedViteConfig = {
|
||||
buildDir: string;
|
||||
publicDir: string;
|
||||
assetsDir: string;
|
||||
urlPathname: string | undefined;
|
||||
userProvidedBuildOptions: UserProvidedBuildOptions;
|
||||
};
|
||||
|
||||
export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions }): BuildOptions {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const reactAppRootDirPath = (() => {
|
||||
if (cliCommandOptions.reactAppRootDirPath === undefined) {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": cliCommandOptions.reactAppRootDirPath,
|
||||
"cwd": process.cwd()
|
||||
});
|
||||
})();
|
||||
|
||||
const { resolvedViteConfig } = (() => {
|
||||
if (fs.readdirSync(reactAppRootDirPath).find(fileBasename => fileBasename.startsWith("vite.config")) === undefined) {
|
||||
return { "resolvedViteConfig": undefined };
|
||||
}
|
||||
|
||||
const output = child_process
|
||||
.execSync("npx vite", {
|
||||
"cwd": reactAppRootDirPath,
|
||||
"env": {
|
||||
...process.env,
|
||||
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
|
||||
}
|
||||
})
|
||||
.toString("utf8");
|
||||
|
||||
assert(output.includes(vitePluginSubScriptEnvNames.resolveViteConfig), "Seems like the Keycloakify's Vite plugin is not installed.");
|
||||
|
||||
const resolvedViteConfigStr = output.split(vitePluginSubScriptEnvNames.resolveViteConfig).reverse()[0];
|
||||
|
||||
const resolvedViteConfig: ResolvedViteConfig = JSON.parse(resolvedViteConfigStr);
|
||||
|
||||
return { resolvedViteConfig };
|
||||
})();
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
keycloakify?: UserProvidedBuildOptions & { reactAppBuildDirPath?: string };
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
"name": z.string(),
|
||||
"version": z.string().optional(),
|
||||
"homepage": z.string().optional(),
|
||||
"keycloakify": z
|
||||
.object({
|
||||
"extraThemeProperties": z.array(z.string()).optional(),
|
||||
"artifactId": z.string().optional(),
|
||||
"groupId": z.string().optional(),
|
||||
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
|
||||
"reactAppBuildDirPath": z.string().optional(),
|
||||
"keycloakifyBuildDirPath": z.string().optional(),
|
||||
"themeName": z.union([z.string(), z.array(z.string())]).optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
{
|
||||
type Got = ReturnType<(typeof zParsedPackageJson)["parse"]>;
|
||||
type Expected = ParsedPackageJson;
|
||||
assert<Got extends Expected ? true : false>();
|
||||
assert<Expected extends Got ? true : false>();
|
||||
}
|
||||
|
||||
return zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
|
||||
})();
|
||||
|
||||
const userProvidedBuildOptions: UserProvidedBuildOptions = {
|
||||
...parsedPackageJson.keycloakify,
|
||||
...resolvedViteConfig?.userProvidedBuildOptions
|
||||
};
|
||||
|
||||
const themeNames = (() => {
|
||||
if (userProvidedBuildOptions.themeName === undefined) {
|
||||
return [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-")
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof userProvidedBuildOptions.themeName === "string") {
|
||||
return [userProvidedBuildOptions.themeName];
|
||||
}
|
||||
|
||||
return userProvidedBuildOptions.themeName;
|
||||
})();
|
||||
|
||||
const reactAppBuildDirPath = (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": parsedPackageJson.keycloakify.reactAppBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(reactAppRootDirPath, "build");
|
||||
}
|
||||
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
|
||||
})();
|
||||
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||
|
||||
return {
|
||||
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
||||
themeNames,
|
||||
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
|
||||
"groupId": (() => {
|
||||
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
||||
|
||||
return (
|
||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||
userProvidedBuildOptions.groupId ??
|
||||
(parsedPackageJson.homepage === undefined
|
||||
? fallbackGroupId
|
||||
: urlParse(parsedPackageJson.homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
|
||||
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
||||
reactAppRootDirPath,
|
||||
reactAppBuildDirPath,
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
reactAppRootDirPath,
|
||||
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
|
||||
);
|
||||
})(),
|
||||
"publicDirPath": (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.PUBLIC_DIR_PATH,
|
||||
"cwd": reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(reactAppRootDirPath, "public");
|
||||
}
|
||||
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
|
||||
})(),
|
||||
"cacheDirPath": (() => {
|
||||
const cacheDirPath = pathJoin(
|
||||
(() => {
|
||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.XDG_CACHE_HOME,
|
||||
"cwd": process.cwd()
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
|
||||
})(),
|
||||
"keycloakify"
|
||||
);
|
||||
|
||||
return cacheDirPath;
|
||||
})(),
|
||||
"urlPathname": (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
let url: URL | undefined = undefined;
|
||||
|
||||
if (homepage !== undefined) {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||
return out === "/" ? undefined : out;
|
||||
}
|
||||
|
||||
return resolvedViteConfig.urlPathname;
|
||||
})(),
|
||||
"assetsDirPath": (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
}
|
||||
|
||||
return pathJoin(reactAppBuildDirPath, "static");
|
||||
}
|
||||
|
||||
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
||||
})(),
|
||||
npmWorkspaceRootDirPath
|
||||
};
|
||||
}
|
@ -1,20 +1,22 @@
|
||||
export const nameOfTheGlobal = "kcContext";
|
||||
export const keycloak_resources = "keycloak-resources";
|
||||
export const resources_common = "resources-common";
|
||||
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
|
||||
export const basenameOfTheKeycloakifyResourcesDir = "build";
|
||||
export const KEYCLOAK_RESOURCES = "keycloak-resources";
|
||||
export const RESOURCES_COMMON = "resources-common";
|
||||
export const LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 = "21.1.2";
|
||||
export const BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR = "dist";
|
||||
|
||||
export const themeTypes = ["login", "account"] as const;
|
||||
export const accountV1ThemeName = "account-v1";
|
||||
export const THEME_TYPES = ["login", "account"] as const;
|
||||
export const ACCOUNT_V1_THEME_NAME = "account-v1";
|
||||
|
||||
export type ThemeType = (typeof themeTypes)[number];
|
||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||
|
||||
export const vitePluginSubScriptEnvNames = {
|
||||
"runPostBuildScript": "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||
"resolveViteConfig": "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||
export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
|
||||
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||
} as const;
|
||||
|
||||
export const loginThemePageIds = [
|
||||
export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME =
|
||||
"KEYCLOAKIFY_BUILD_FOR_KEYCLOAK_MAJOR_VERSION";
|
||||
|
||||
export const LOGIN_THEME_PAGE_IDS = [
|
||||
"login.ftl",
|
||||
"login-username.ftl",
|
||||
"login-password.ftl",
|
||||
@ -51,7 +53,7 @@ export const loginThemePageIds = [
|
||||
"webauthn-error.ftl"
|
||||
] as const;
|
||||
|
||||
export const accountThemePageIds = [
|
||||
export const ACCOUNT_THEME_PAGE_IDS = [
|
||||
"password.ftl",
|
||||
"account.ftl",
|
||||
"sessions.ftl",
|
||||
@ -61,5 +63,9 @@ export const accountThemePageIds = [
|
||||
"federatedIdentity.ftl"
|
||||
] as const;
|
||||
|
||||
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||
export type LoginThemePageId = (typeof LOGIN_THEME_PAGE_IDS)[number];
|
||||
export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
|
||||
|
||||
export const CONTAINER_NAME = "keycloak-keycloakify";
|
||||
|
||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||
|
@ -1,37 +1,43 @@
|
||||
import {
|
||||
downloadKeycloakStaticResources,
|
||||
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
|
||||
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
|
||||
} from "./downloadKeycloakStaticResources";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "../shared/constants";
|
||||
import {
|
||||
THEME_TYPES,
|
||||
KEYCLOAK_RESOURCES,
|
||||
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1
|
||||
} from "../shared/constants";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
import type { BuildOptions } from "./buildOptions";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakStaticResources & {
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakStaticResources & {
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
publicDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function copyKeycloakResourcesToPublic(params: { buildOptions: BuildOptionsLike }) {
|
||||
const { buildOptions } = params;
|
||||
export async function copyKeycloakResourcesToPublic(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
|
||||
const destDirPath = pathJoin(buildContext.publicDirPath, KEYCLOAK_RESOURCES);
|
||||
|
||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||
|
||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||
{
|
||||
destDirPath,
|
||||
"keycloakifyVersion": readThisNpmPackageVersion(),
|
||||
"buildOptions": {
|
||||
"loginThemeResourcesFromKeycloakVersion": readThisNpmPackageVersion(),
|
||||
"cacheDirPath": pathRelative(destDirPath, buildOptions.cacheDirPath),
|
||||
"npmWorkspaceRootDirPath": pathRelative(destDirPath, buildOptions.npmWorkspaceRootDirPath)
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
buildContext: {
|
||||
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
||||
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
|
||||
fetchOptions: buildContext.fetchOptions
|
||||
}
|
||||
},
|
||||
null,
|
||||
@ -43,7 +49,9 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs
|
||||
.readFileSync(keycloakifyBuildinfoFilePath)
|
||||
.toString("utf8");
|
||||
|
||||
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
||||
break skip_if_already_done;
|
||||
@ -52,25 +60,25 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
return;
|
||||
}
|
||||
|
||||
rmSync(destDirPath, { "force": true, "recursive": true });
|
||||
rmSync(destDirPath, { force: true, recursive: true });
|
||||
|
||||
fs.mkdirSync(destDirPath, { "recursive": true });
|
||||
fs.mkdirSync(destDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
for (const themeType of themeTypes) {
|
||||
for (const themeType of THEME_TYPES) {
|
||||
await downloadKeycloakStaticResources({
|
||||
"keycloakVersion": (() => {
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
||||
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||
case "account":
|
||||
return lastKeycloakVersionWithAccountV1;
|
||||
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
|
||||
}
|
||||
})(),
|
||||
themeType,
|
||||
"themeDirPath": destDirPath,
|
||||
buildOptions
|
||||
themeDirPath: destDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
@ -86,5 +94,8 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
|
||||
fs.writeFileSync(
|
||||
keycloakifyBuildinfoFilePath,
|
||||
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
||||
);
|
||||
}
|
||||
|
@ -1,203 +0,0 @@
|
||||
import { createHash } from "crypto";
|
||||
import { mkdir, writeFile, unlink } from "fs/promises";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { unzip, zip } from "../tools/unzip";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import * as child_process from "child_process";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import type { BuildOptions } from "./buildOptions";
|
||||
import { getProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function downloadAndUnzip(params: {
|
||||
url: string;
|
||||
destDirPath: string;
|
||||
specificDirsToExtract?: string[];
|
||||
preCacheTransform?: {
|
||||
actionCacheId: string;
|
||||
action: (params: { destDirPath: string }) => Promise<void>;
|
||||
};
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
|
||||
|
||||
const { extractDirPath, zipFilePath } = (() => {
|
||||
const zipFileBasenameWithoutExt = generateFileNameFromURL({
|
||||
url,
|
||||
"preCacheTransform":
|
||||
preCacheTransform === undefined
|
||||
? undefined
|
||||
: {
|
||||
"actionCacheId": preCacheTransform.actionCacheId,
|
||||
"actionFootprint": preCacheTransform.action.toString()
|
||||
}
|
||||
});
|
||||
|
||||
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
|
||||
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
|
||||
|
||||
return { zipFilePath, extractDirPath };
|
||||
})();
|
||||
|
||||
download_zip_and_transform: {
|
||||
if (await existsAsync(zipFilePath)) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
const { response, isFromRemoteCache } = await (async () => {
|
||||
const proxyFetchOptions = await getProxyFetchOptions({
|
||||
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
|
||||
proxyFetchOptions
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
response,
|
||||
"isFromRemoteCache": true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"response": await fetch(url, proxyFetchOptions),
|
||||
"isFromRemoteCache": false
|
||||
};
|
||||
})();
|
||||
|
||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||
|
||||
/**
|
||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||
* octokit upgrades.
|
||||
*/
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
|
||||
await writeFile(zipFilePath, response.body);
|
||||
|
||||
if (isFromRemoteCache) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
||||
|
||||
try {
|
||||
await preCacheTransform?.action({
|
||||
"destDirPath": extractDirPath
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await unlink(zipFilePath);
|
||||
|
||||
await zip(extractDirPath, zipFilePath);
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
|
||||
upload_to_remot_cache_if_admin: {
|
||||
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
||||
|
||||
if (githubToken === undefined) {
|
||||
break upload_to_remot_cache_if_admin;
|
||||
}
|
||||
|
||||
console.log("uploading to remote cache");
|
||||
|
||||
try {
|
||||
child_process.execSync(`which putasset`);
|
||||
} catch {
|
||||
child_process.execSync(`npm install -g putasset`);
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execFileSync("putasset", [
|
||||
"--owner",
|
||||
"keycloakify",
|
||||
"--repo",
|
||||
"keycloakify",
|
||||
"--tag",
|
||||
"v0.0.1",
|
||||
"--filename",
|
||||
zipFilePath,
|
||||
"--token",
|
||||
githubToken
|
||||
]);
|
||||
} catch {
|
||||
console.log("upload failed, asset probably already exists in remote cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
function generateFileNameFromURL(params: {
|
||||
url: string;
|
||||
preCacheTransform:
|
||||
| {
|
||||
actionCacheId: string;
|
||||
actionFootprint: string;
|
||||
}
|
||||
| undefined;
|
||||
}): string {
|
||||
const { preCacheTransform } = params;
|
||||
|
||||
// Parse the URL
|
||||
const url = new URL(params.url);
|
||||
|
||||
// Extract pathname and remove leading slashes
|
||||
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
||||
|
||||
// Optionally, add query parameters replacing special characters
|
||||
if (url.search) {
|
||||
fileName += url.search.replace(/[&=?]/g, "-");
|
||||
}
|
||||
|
||||
// Replace any characters that are not valid in filenames
|
||||
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
|
||||
// Trim or pad the fileName to a specific length
|
||||
fileName = fileName.substring(0, 50);
|
||||
|
||||
add_pre_cache_transform: {
|
||||
if (preCacheTransform === undefined) {
|
||||
break add_pre_cache_transform;
|
||||
}
|
||||
|
||||
// Sanitize actionCacheId the same way as other components
|
||||
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
||||
|
||||
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadAndUnzip } from "./downloadAndUnzip";
|
||||
import { type BuildOptions } from "./buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
import { lastKeycloakVersionWithAccountV1 } from "../shared/constants";
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: 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({
|
||||
destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
||||
buildOptions,
|
||||
"preCacheTransform": {
|
||||
"actionCacheId": "npm install and build",
|
||||
"action": async ({ destDirPath }) => {
|
||||
install_common_node_modules: {
|
||||
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
|
||||
|
||||
if (!fs.existsSync(commonResourcesDirPath)) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
if (fs.existsSync(pathJoin(commonResourcesDirPath, "node_modules"))) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
child_process.execSync("npm install --omit=dev", {
|
||||
"cwd": commonResourcesDirPath,
|
||||
"stdio": "ignore"
|
||||
});
|
||||
}
|
||||
|
||||
repatriate_common_resources_from_base_login_theme: {
|
||||
const baseLoginThemeResourceDir = pathJoin(destDirPath, "base", "login", "resources");
|
||||
|
||||
if (!fs.existsSync(baseLoginThemeResourceDir)) {
|
||||
break repatriate_common_resources_from_base_login_theme;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": baseLoginThemeResourceDir,
|
||||
"destDirPath": pathJoin(destDirPath, "keycloak", "login", "resources")
|
||||
});
|
||||
}
|
||||
|
||||
install_and_move_to_common_resources_generated_in_keycloak_v2: {
|
||||
if (!fs.readFileSync(pathJoin(destDirPath, "keycloak", "login", "theme.properties")).toString("utf8").includes("web_modules")) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
|
||||
|
||||
if (!fs.existsSync(accountV2DirSrcDirPath)) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
||||
|
||||
if (packageManager === "pnpm") {
|
||||
try {
|
||||
child_process.execSync(`which pnpm`);
|
||||
} catch {
|
||||
console.log(`Installing pnpm globally`);
|
||||
child_process.execSync(`npm install -g pnpm`);
|
||||
}
|
||||
}
|
||||
|
||||
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
|
||||
|
||||
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
|
||||
|
||||
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
|
||||
|
||||
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
|
||||
.replace(`${packageManager} run check-types`, "true")
|
||||
.replace(`${packageManager} run babel`, "true");
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
|
||||
|
||||
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
|
||||
|
||||
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
|
||||
}
|
||||
|
||||
remove_keycloak_v2: {
|
||||
const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
|
||||
|
||||
if (!fs.existsSync(keycloakV2DirPath)) {
|
||||
break remove_keycloak_v2;
|
||||
}
|
||||
|
||||
rmSync(keycloakV2DirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
// Note, this is an optimization for reducing the size of the jar
|
||||
remove_unused_node_modules: {
|
||||
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
|
||||
|
||||
if (!fs.existsSync(nodeModuleDirPath)) {
|
||||
break remove_unused_node_modules;
|
||||
}
|
||||
|
||||
const toDeletePerfixes = [
|
||||
"angular",
|
||||
"bootstrap",
|
||||
"rcue",
|
||||
"font-awesome",
|
||||
"ng-file-upload",
|
||||
pathJoin("patternfly", "dist", "sass"),
|
||||
pathJoin("patternfly", "dist", "less"),
|
||||
pathJoin("patternfly", "dist", "js"),
|
||||
"d3",
|
||||
pathJoin("jquery", "src"),
|
||||
"c3",
|
||||
"core-js",
|
||||
"eonasdan-bootstrap-datetimepicker",
|
||||
"moment",
|
||||
"react",
|
||||
"patternfly-bootstrap-treeview",
|
||||
"popper.js",
|
||||
"tippy.js",
|
||||
"jquery-match-height",
|
||||
"google-code-prettify",
|
||||
"patternfly-bootstrap-combobox",
|
||||
"focus-trap",
|
||||
"tabbable",
|
||||
"scheduler",
|
||||
"@types",
|
||||
"datatables.net",
|
||||
"datatables.net-colreorder",
|
||||
"tslib",
|
||||
"prop-types",
|
||||
"file-selector",
|
||||
"datatables.net-colreorder-bs",
|
||||
"object-assign",
|
||||
"warning",
|
||||
"js-tokens",
|
||||
"loose-envify",
|
||||
"prop-types-extra",
|
||||
"attr-accept",
|
||||
"datatables.net-select",
|
||||
"drmonty-datatables-colvis",
|
||||
"datatables.net-bs",
|
||||
pathJoin("@patternfly", "react"),
|
||||
pathJoin("@patternfly", "patternfly", "docs")
|
||||
];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": nodeModuleDirPath,
|
||||
"destDirPath": nodeModuleDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (fileRelativePath.endsWith(".map")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
|
||||
if (
|
||||
!fileRelativePath.endsWith(".woff2") &&
|
||||
!fileRelativePath.endsWith(".woff") &&
|
||||
!fileRelativePath.endsWith(".ttf")
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Just like node_modules
|
||||
remove_unused_lib: {
|
||||
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
|
||||
|
||||
if (!fs.existsSync(libDirPath)) {
|
||||
break remove_unused_lib;
|
||||
}
|
||||
|
||||
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace"];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": libDirPath,
|
||||
"destDirPath": libDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (fileRelativePath.endsWith(".map")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
{
|
||||
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
|
||||
|
||||
fs.writeFileSync(
|
||||
accountCssFilePath,
|
||||
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
// Note, this is an optimization for reducing the size of the jar,
|
||||
// For this version we know exactly which resources are used.
|
||||
{
|
||||
const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
|
||||
|
||||
const toKeepPrefixes = [
|
||||
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
|
||||
pathJoin("patternfly", "dist", "css", fileBasename)
|
||||
),
|
||||
pathJoin("patternfly", "dist", "fonts")
|
||||
];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": nodeModulesDirPath,
|
||||
"destDirPath": nodeModulesDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
297
src/bin/shared/downloadKeycloakDefaultTheme.ts
Normal file
297
src/bin/shared/downloadKeycloakDefaultTheme.ts
Normal file
@ -0,0 +1,297 @@
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { type BuildContext } from "./buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "./constants";
|
||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function downloadKeycloakDefaultTheme(params: {
|
||||
keycloakVersion: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ defaultThemeDirPath: string }> {
|
||||
const { keycloakVersion, buildContext } = params;
|
||||
|
||||
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||
onArchiveFile: async params => {
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
if (fileRelativePath.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = params;
|
||||
|
||||
skip_keycloak_v2: {
|
||||
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
||||
break skip_keycloak_v2;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
skip_web_modules: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "web_modules")
|
||||
)
|
||||
) {
|
||||
break skip_web_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_lib: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "lib")
|
||||
)
|
||||
) {
|
||||
break skip_lib;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_node_modules: {
|
||||
const nodeModulesRelativeDirPath = pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules"
|
||||
);
|
||||
|
||||
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
|
||||
kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([
|
||||
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"css",
|
||||
"patternfly-additions.min.css"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Regular-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Light-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.woff"
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
|
||||
nodeModulesRelativeDirPath.length + 1
|
||||
);
|
||||
|
||||
if (
|
||||
kcNodeModulesKeepFilePaths_lastAccountV1.has(
|
||||
fileRelativeToNodeModulesPath
|
||||
)
|
||||
) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
patch_account_css: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "account", "resources", "css", "account.css")
|
||||
) {
|
||||
break patch_account_css;
|
||||
}
|
||||
|
||||
await writeFile({
|
||||
fileRelativePath,
|
||||
modifiedData: Buffer.from(
|
||||
(await readFile())
|
||||
.toString("utf8")
|
||||
.replace("top: -34px;", "top: -34px !important;"),
|
||||
"utf8"
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
skip_unused_resources: {
|
||||
if (keycloakVersion !== "24.0.4") {
|
||||
break skip_unused_resources;
|
||||
}
|
||||
|
||||
skip_node_modules: {
|
||||
const nodeModulesRelativeDirPath = pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules"
|
||||
);
|
||||
|
||||
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
if (kcNodeModulesKeepFilePaths === undefined) {
|
||||
kcNodeModulesKeepFilePaths = new Set([
|
||||
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
|
||||
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"css",
|
||||
"patternfly-additions.min.css"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Regular-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Light-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"fontawesome-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||
pathJoin("jquery", "dist", "jquery.min.js")
|
||||
]);
|
||||
}
|
||||
|
||||
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
|
||||
nodeModulesRelativeDirPath.length + 1
|
||||
);
|
||||
|
||||
if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_vendor: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "vendor")
|
||||
)
|
||||
) {
|
||||
break skip_vendor;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_rollup_config: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "common", "resources", "rollup.config.js")
|
||||
) {
|
||||
break skip_rollup_config;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile({ fileRelativePath });
|
||||
}
|
||||
});
|
||||
|
||||
return { defaultThemeDirPath: extractedDirPath };
|
||||
}
|
@ -1,49 +1,53 @@
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadBuiltinKeycloakTheme } from "./downloadBuiltinKeycloakTheme";
|
||||
import { resources_common, type ThemeType } from "./constants";
|
||||
import type { BuildOptions } from "./buildOptions";
|
||||
import {
|
||||
downloadKeycloakDefaultTheme,
|
||||
type BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
|
||||
} from "./downloadKeycloakDefaultTheme";
|
||||
import { RESOURCES_COMMON, type ThemeType } from "./constants";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as crypto from "crypto";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme & {};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function downloadKeycloakStaticResources(params: {
|
||||
themeType: ThemeType;
|
||||
themeDirPath: string;
|
||||
keycloakVersion: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
|
||||
const { themeType, themeDirPath, keycloakVersion, buildContext } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(
|
||||
buildOptions.cacheDirPath,
|
||||
`downloadKeycloakStaticResources_tmp_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
|
||||
);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
buildOptions
|
||||
buildContext
|
||||
});
|
||||
|
||||
const resourcesPath = pathJoin(themeDirPath, themeType, "resources");
|
||||
const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources");
|
||||
|
||||
repatriate_base_resources: {
|
||||
const srcDirPath = pathJoin(defaultThemeDirPath, "base", themeType, "resources");
|
||||
|
||||
if (!(await existsAsync(srcDirPath))) {
|
||||
break repatriate_base_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath,
|
||||
destDirPath: resourcesDirPath
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||
"destDirPath": resourcesPath
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", themeType, "resources"),
|
||||
destDirPath: resourcesDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(resourcesPath, resources_common)
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(resourcesDirPath, RESOURCES_COMMON)
|
||||
});
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true });
|
||||
}
|
||||
|
68
src/bin/shared/generateKcGenTs.ts
Normal file
68
src/bin/shared/generateKcGenTs.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
themeNames: string[];
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateKcGenTs(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
|
||||
|
||||
const currentContent = (await existsAsync(filePath))
|
||||
? await fs.readFile(filePath)
|
||||
: undefined;
|
||||
|
||||
const newContent = Buffer.from(
|
||||
[
|
||||
`/* prettier-ignore-start */`,
|
||||
``,
|
||||
`/* eslint-disable */`,
|
||||
``,
|
||||
`// @ts-nocheck`,
|
||||
``,
|
||||
`// noinspection JSUnusedGlobalSymbols`,
|
||||
``,
|
||||
`// This file is auto-generated by Keycloakify`,
|
||||
``,
|
||||
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
|
||||
``,
|
||||
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
|
||||
``,
|
||||
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
|
||||
``,
|
||||
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
|
||||
``,
|
||||
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) => [name, defaultValue]
|
||||
)
|
||||
),
|
||||
null,
|
||||
2
|
||||
)};`,
|
||||
``,
|
||||
`/* prettier-ignore-end */`,
|
||||
``
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
if (currentContent !== undefined && currentContent.equals(newContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, newContent);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||
|
||||
export function getJarFileBasename(params: { keycloakVersionRange: KeycloakVersionRange }) {
|
||||
const { keycloakVersionRange } = params;
|
||||
|
||||
const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`;
|
||||
|
||||
return { jarFileBasename };
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe";
|
||||
import { crawl } from "../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { themeTypes } from "./constants";
|
||||
|
||||
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. */
|
||||
export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
const { reactAppRootDirPath } = params;
|
||||
|
||||
const srcDirPath = pathJoin(reactAppRootDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of themeSrcDirBasenames) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
if (themeSrcDirPath !== undefined) {
|
||||
return { themeSrcDirPath };
|
||||
}
|
||||
|
||||
for (const themeType of [...themeTypes, "email"]) {
|
||||
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
return { "themeSrcDirPath": srcDirPath };
|
||||
}
|
||||
|
||||
console.error(
|
||||
[
|
||||
"Can't locate your theme source directory. It should be either: ",
|
||||
"src/ or src/keycloak-theme or src/keycloak_theme.",
|
||||
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
@ -6,29 +6,35 @@ export type MetaInfKeycloakTheme = {
|
||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
||||
};
|
||||
|
||||
export function getMetaInfKeycloakThemesJsonPath(params: { keycloakifyBuildDirPath: string }) {
|
||||
const { keycloakifyBuildDirPath } = params;
|
||||
export function writeMetaInfKeycloakThemes(params: {
|
||||
resourcesDirPath: string;
|
||||
getNewMetaInfKeycloakTheme: (params: {
|
||||
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
|
||||
}) => MetaInfKeycloakTheme;
|
||||
}) {
|
||||
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
|
||||
|
||||
return pathJoin(keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
|
||||
}
|
||||
const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
|
||||
|
||||
export function readMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string }): MetaInfKeycloakTheme {
|
||||
const { keycloakifyBuildDirPath } = params;
|
||||
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
|
||||
? undefined
|
||||
: (JSON.parse(
|
||||
fs.readFileSync(filePath).toString("utf8")
|
||||
) as MetaInfKeycloakTheme);
|
||||
|
||||
return JSON.parse(fs.readFileSync(getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath })).toString("utf8")) as MetaInfKeycloakTheme;
|
||||
}
|
||||
|
||||
export function writeMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string; metaInfKeycloakThemes: MetaInfKeycloakTheme }) {
|
||||
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
|
||||
|
||||
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath });
|
||||
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
|
||||
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
|
||||
const dirPath = pathDirname(filePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { "recursive": true });
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(metaInfKeycloakThemesJsonPath, Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8"));
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user