Compare commits
325 Commits
v10.0.4
...
admin_them
Author | SHA1 | Date | |
---|---|---|---|
30149ff1f2 | |||
32f471624a | |||
7f5eabb639 | |||
32fb1e2f71 | |||
7c3c6d3643 | |||
b6d2154d56 | |||
b8d4daf4c1 | |||
c03623875a | |||
c423e4cacc | |||
c593f5cb97 | |||
2ad36a8137 | |||
645031543e | |||
b43c02f279 | |||
63877d53be | |||
79a580b4a5 | |||
994f1f8d3d | |||
a73281d46d | |||
a60a0d0696 | |||
a2ea81b3b8 | |||
a0461e3ef0 | |||
93fcf96cde | |||
d7455fd100 | |||
af7a45d125 | |||
5357626317 | |||
552c95c59e | |||
50590697ca | |||
e261736fa3 | |||
db37320280 | |||
263f55fdd3 | |||
2b7f8a24a3 | |||
b0aa0feab5 | |||
0e93d4ed09 | |||
dc4eac1a04 | |||
53a427d190 | |||
ae969f91ac | |||
c83319d6f3 | |||
329b4cb0fb | |||
533f5992d1 | |||
cb103cc3e2 | |||
afdf89fb12 | |||
26a87b8eaa | |||
25d31463f4 | |||
2542c38c9b | |||
7326038424 | |||
12e632d221 | |||
3fc2108214 | |||
babbe39494 | |||
32b4585e39 | |||
43469a869c | |||
6f823e6478 | |||
e33693e20e | |||
ad3f091d4a | |||
3ff01d186d | |||
0cf8caa53b | |||
25920c208d | |||
19da96113f | |||
6e584e809e | |||
4185188a5b | |||
4273322ed5 | |||
ba0532c95d | |||
3a2fe597ba | |||
dda77952a0 | |||
d2e518d96b | |||
f3a97b2538 | |||
cacd017244 | |||
f5b15a5ef6 | |||
de620dca56 | |||
8decf4a3c9 | |||
831326952b | |||
27da578446 | |||
2c1cca168f | |||
e498fb784b | |||
2917719315 | |||
9ed90995e4 | |||
0f99bb5bdc | |||
1f4d4473e4 | |||
5332001ff4 | |||
22241fd7ad | |||
ddeade9775 | |||
f1cb165bdd | |||
9873353990 | |||
b879569b81 | |||
c3e821088b | |||
dc4f386e7a | |||
a40810b364 | |||
1690629717 | |||
9a6a71c8bc | |||
d626699f08 | |||
6aa60e685b | |||
9910762abc | |||
182fb430f1 | |||
bda20e2fbe | |||
bc586eceef | |||
128b27221a | |||
2dfb4eda9d | |||
fed6af4dfe | |||
c4ee6cd85c | |||
8fc307bd8d | |||
9e9ffcd586 | |||
49b064b5f2 | |||
ef6f5a4c23 | |||
e92562fd44 | |||
fe65ddb5f8 | |||
ffd405c6db | |||
9e41868e0d | |||
ca6accc889 | |||
dfe2e1562a | |||
ab43bb73d7 | |||
22b0b95e54 | |||
290ad8b592 | |||
d5519dbb55 | |||
4de9e059e9 | |||
e573aff6ae | |||
908e083dee | |||
ec29724997 | |||
88756e9807 | |||
80d8a0c4e3 | |||
7241f0c741 | |||
8565eb3fb8 | |||
87198f6e56 | |||
fa934da442 | |||
6c4dc711d2 | |||
1f2a755a97 | |||
a0e3dc163a | |||
810dc6ceb5 | |||
7203c742be | |||
2fd04cfb61 | |||
9c44d13f73 | |||
d6436a58a2 | |||
613167f3a6 | |||
ab0c281d98 | |||
c84dc281a2 | |||
835833a61b | |||
9af542ec89 | |||
06e33196bb | |||
36dd324139 | |||
52d4fe920c | |||
0d090d50d4 | |||
e57232edde | |||
dfe1e7ddd1 | |||
5ffc42c9db | |||
c63648a1b0 | |||
80fd4095c4 | |||
31d7a938f2 | |||
ee1b6868f8 | |||
7c7e5544e4 | |||
06fe26fbe7 | |||
c932c7d8f6 | |||
d56c536446 | |||
f5a9a28124 | |||
b86039536e | |||
59c4675e8a | |||
fbf6a329df | |||
ddec3118a4 | |||
94e2786297 | |||
ecfdff5454 | |||
c598a58ec9 | |||
3e38beb190 | |||
61a86e8e82 | |||
9c8e127fa0 | |||
eb23b40e5c | |||
9e19aafcd0 | |||
4cdf26b6f9 | |||
88de58cc22 | |||
f6b48c88b9 | |||
12534e57ad | |||
52e33bba2d | |||
d63e5f4e54 | |||
8d31866a0b | |||
7d818f217a | |||
7156665684 | |||
5045c5e8bf | |||
9de2ed9eaf | |||
096cf7a570 | |||
a04f07d149 | |||
63775b2866 | |||
e8609de7b4 | |||
e62aa89d72 | |||
77f12a940d | |||
0fe49e3d6e | |||
881386a123 | |||
7b9aec4ed0 | |||
cf18f9d06c | |||
052936f769 | |||
590de7a67b | |||
7f608ad8ad | |||
35b012b937 | |||
e3bd7f3bc5 | |||
e14f187fc0 | |||
da495b90ae | |||
8d9b80f549 | |||
2e9da33622 | |||
6f416ad335 | |||
4e982ee898 | |||
bcb514ae9c | |||
cfdad8d71d | |||
39ad1eb8d1 | |||
3d1d2e316b | |||
dd217e8a46 | |||
1339a96ea4 | |||
616e834c90 | |||
80eaa77acc | |||
ce3135c83b | |||
09abc73068 | |||
037d623550 | |||
8c8d2fd6a8 | |||
153a99d63f | |||
939e3ca7ea | |||
a0dc7eeb7c | |||
c21d072231 | |||
2e10ec8073 | |||
1177d6770c | |||
d492a393fe | |||
77952337c5 | |||
6716fcb881 | |||
302fe8d7cd | |||
2ea5e34e81 | |||
d7103b1ad9 | |||
9f8a36fe93 | |||
47ca811878 | |||
8cacb21f1b | |||
a0c95207cf | |||
da3023cf5e | |||
5892cf2ba7 | |||
c9d7fc1b6e | |||
94779c3476 | |||
802a6ab5ec | |||
04307c8226 | |||
ff6b91b801 | |||
c8ca598465 | |||
9444b897ee | |||
3d1951b72c | |||
acc27ae448 | |||
e6993214ff | |||
2f02a4379c | |||
b57d014e9a | |||
f57f311aab | |||
4f11415107 | |||
346fd7175f | |||
7c02d77057 | |||
d3fd4b6bbf | |||
43ef527810 | |||
a6032a1387 | |||
23179cac53 | |||
954c3319bb | |||
eb6ec0275d | |||
890f8bc2d5 | |||
26b8dd9cda | |||
c07af8491c | |||
10d4da9fbf | |||
95e861099f | |||
6dc51dfab3 | |||
ddb0af1dcb | |||
b6e9043d91 | |||
7c553ee10d | |||
2a6b14adc6 | |||
159a5f60d0 | |||
08f03b3118 | |||
f137960f96 | |||
e5ab46727a | |||
8d2679b76e | |||
b0b6b994ed | |||
bb163132fe | |||
439bed2f24 | |||
5a233d8878 | |||
20cdbb6185 | |||
b3c4208e44 | |||
8623037224 | |||
e8d3d3d741 | |||
cc700f0ba0 | |||
801a5cce17 | |||
2a3ad58c18 | |||
969744f4cb | |||
40ebbdebeb | |||
eb64886dcf | |||
81fc9d57bd | |||
66b480f837 | |||
7e6a84ce19 | |||
68e7642827 | |||
b37c7ccc8a | |||
b7c9ba8ffd | |||
c8a31c4b6a | |||
fb6f450bfe | |||
9a97d86ff9 | |||
a5e3ecb38b | |||
9b22d94600 | |||
a42ddb959b | |||
b4e94d3c00 | |||
aad89a2001 | |||
07e4f99f80 | |||
715562c750 | |||
ef4f4d8374 | |||
e15f13646c | |||
8677c17f29 | |||
21ee42b5a4 | |||
d20964ec94 | |||
3155f5da66 | |||
50e38b6a10 | |||
72c31776d7 | |||
7456750828 | |||
b8a08f0789 | |||
28990a12da | |||
7e5abe8589 | |||
1d57f4b4dc | |||
9f875160ea | |||
785ed095bc | |||
359e93a1ba | |||
ee6322aae4 | |||
98d3d1967a | |||
01c3b148e6 | |||
77d3a5190d | |||
93c1c56279 | |||
8340608045 | |||
5502a74994 | |||
c5ef4c973b | |||
dbae909903 | |||
74317a1f3c | |||
569e933f02 | |||
46c40d713a | |||
f3602219f3 | |||
c6b52acf2f | |||
7260589136 | |||
b2e9ddaa4f | |||
4338b3ecb7 | |||
0f81d9f146 |
@ -259,6 +259,56 @@
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "uchar",
|
||||
"name": "Omid",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5172296?v=4",
|
||||
"profile": "https://www.linkedin.com/in/oes-rioniz/",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kathari00",
|
||||
"name": "Katharina Eiserfey",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/42547712?v=4",
|
||||
"profile": "https://github.com/kathari00",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "luca-peruzzo",
|
||||
"name": "Luca Peruzzo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/69015314?v=4",
|
||||
"profile": "https://github.com/luca-peruzzo",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nima70",
|
||||
"name": "Nima Shokouhfar",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
|
||||
"profile": "https://github.com/nima70",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marvinruder",
|
||||
"name": "Marvin A. Ruder",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
|
||||
"profile": "https://mruder.dev",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
storybook:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -49,7 +49,7 @@ jobs:
|
||||
- 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 ./storybook-static -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
|
||||
@ -112,7 +112,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run build
|
||||
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
|
||||
- run: npx -y -p denoify@1.6.13 enable_short_npm_import_path
|
||||
env:
|
||||
DRY_RUN: "0"
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -49,7 +49,7 @@ jspm_packages
|
||||
.idea
|
||||
|
||||
/src/login/i18n/messages_defaultSet/
|
||||
/src/account/i18n/messages_defaultSet/
|
||||
/src/account/i18n/
|
||||
|
||||
# VS Code devcontainers
|
||||
.devcontainer
|
||||
|
@ -9,5 +9,5 @@ module.exports = {
|
||||
core: {
|
||||
builder: "webpack5"
|
||||
},
|
||||
staticDirs: ["./static"]
|
||||
staticDirs: ["./static", "../dist/res/public"]
|
||||
};
|
||||
|
@ -1,49 +0,0 @@
|
||||
## 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]**.
|
@ -1,49 +0,0 @@
|
||||
## 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]**.
|
@ -1,49 +0,0 @@
|
||||
## 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]**.
|
@ -1,3 +1,3 @@
|
||||
Looking to contribute? Thank you! PR are more than welcome.
|
||||
|
||||
Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.
|
||||
Please refers to [this documentation page](https://docs.keycloakify.dev/faq-and-help/contributing) that will help you get started.
|
||||
|
44
README.md
44
README.md
@ -41,23 +41,50 @@
|
||||
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
||||
</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, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
|
||||
> 📣 **Keycloakify 26 Released**
|
||||
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
|
||||
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
|
||||
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
|
||||
|
||||
## Sponsors
|
||||
|
||||
Friends for the project, we trust and recommend their services.
|
||||
Project backers, we trust and recommend their services.
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<i><a href="https://phasetwo.io/?utm_source=keycloakify"><strong>Keycloak as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-?utm_source=keycloakify">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/?utm_source=keycloakify">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/?utm_source=keycloakify">Keycloak support</a> to businesses of all sizes.</i>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@ -82,7 +109,7 @@ Friends for the project, we trust and recommend their services.
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. </a>
|
||||
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company. </a>
|
||||
<br/>
|
||||
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
||||
</p>
|
||||
@ -132,6 +159,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liamlows"><img src="https://avatars.githubusercontent.com/u/1365914?v=4?s=100" width="100px;" alt="Liam Lowsley-Williams"/><br /><sub><b>Liam Lowsley-Williams</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/oes-rioniz/"><img src="https://avatars.githubusercontent.com/u/5172296?v=4?s=100" width="100px;" alt="Omid"/><br /><sub><b>Omid</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kathari00"><img src="https://avatars.githubusercontent.com/u/42547712?v=4?s=100" width="100px;" alt="Katharina Eiserfey"/><br /><sub><b>Katharina Eiserfey</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luca-peruzzo"><img src="https://avatars.githubusercontent.com/u/69015314?v=4?s=100" width="100px;" alt="Luca Peruzzo"/><br /><sub><b>Luca Peruzzo</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nima70"><img src="https://avatars.githubusercontent.com/u/5094767?v=4?s=100" width="100px;" alt="Nima Shokouhfar"/><br /><sub><b>Nima Shokouhfar</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mruder.dev"><img src="https://avatars.githubusercontent.com/u/18495294?v=4?s=100" width="100px;" alt="Marvin A. Ruder"/><br /><sub><b>Marvin A. Ruder</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/issues?q=author%3Amarvinruder" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
31
package.json
31
package.json
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.4",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"version": "11.3.19",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsx scripts/generate-i18n-messages.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"build": "tsx scripts/build/main.ts",
|
||||
"storybook": "tsx scripts/start-storybook.ts",
|
||||
"link-in-starter": "tsx scripts/link-in-starter.ts",
|
||||
"test": "yarn test:types && vitest run",
|
||||
@ -38,12 +38,14 @@
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/",
|
||||
"dist/bin/**/*.d.ts",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/*.d.ts",
|
||||
"dist/bin/shared/*.js.map",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"dist/bin/shared/customHandler.js",
|
||||
"dist/bin/shared/customHandler.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
@ -62,12 +64,13 @@
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"dependencies": {
|
||||
"tsafe": "^1.6.6"
|
||||
"tsafe": "^1.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.5",
|
||||
"@babel/generator": "^7.24.5",
|
||||
"@babel/parser": "^7.24.5",
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
@ -75,20 +78,27 @@
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"eslint-plugin-storybook": "^0.6.7",
|
||||
"@types/babel__generator": "^7.6.4",
|
||||
"@types/dompurify": "^2.0.0",
|
||||
"@types/make-fetch-happen": "^10.0.1",
|
||||
"@types/minimist": "^1.2.2",
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/properties-parser": "^0.3.3",
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"cli-select": "^1.1.2",
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint-plugin-storybook": "^0.6.7",
|
||||
"evt": "^2.5.7",
|
||||
"html-entities": "^2.5.2",
|
||||
"husky": "^4.3.8",
|
||||
"isomorphic-dompurify": "^2.15.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
@ -103,12 +113,13 @@
|
||||
"termost": "^v0.12.1",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tss-react": "^4.9.10",
|
||||
"tsx": "^4.15.5",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0",
|
||||
"webpack": "5.93.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"yauzl": "^2.10.0",
|
||||
"zod": "^3.17.10",
|
||||
"evt": "^2.5.7",
|
||||
"tsx": "^4.15.5"
|
||||
"zod": "^3.17.10"
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,4 @@
|
||||
import * as child_process from "child_process";
|
||||
import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
run("yarn build");
|
||||
|
||||
await copyKeycloakResourcesToStorybookStaticDir();
|
||||
|
||||
run("npx build-storybook");
|
||||
})();
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
run("yarn build");
|
||||
run("npx build-storybook");
|
||||
|
176
scripts/build.ts
176
scripts/build.ts
@ -1,176 +0,0 @@
|
||||
import * as child_process from "child_process";
|
||||
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.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
|
||||
fs.rmSync(join("dist", "bin", fileBasename));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
|
||||
|
||||
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 }) => {
|
||||
if (fileRelativePath === "index.js") {
|
||||
return {
|
||||
newFileName: "main.js",
|
||||
modifiedSourceCode: sourceCode
|
||||
};
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
|
||||
|
||||
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"
|
||||
)}`
|
||||
);
|
||||
|
||||
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 }) => {
|
||||
assert(fileRelativePath === "index.js");
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
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(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
const before = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
const after = before.replace(
|
||||
`var buffer = new Buffer(toRead);`,
|
||||
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
||||
);
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||
|
||||
const hasBeenPatched = after !== before;
|
||||
|
||||
return { hasBeenPatched };
|
||||
}
|
79
scripts/build/createAccountV1Dir.ts
Normal file
79
scripts/build/createAccountV1Dir.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { accountMultiPageSupportedLanguages } from "../generate-i18n-messages";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function createAccountV1Dir() {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: "FOR_ACCOUNT_MULTI_PAGE"
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"account-v1"
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "base", "account"),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "account", "resources"),
|
||||
destDirPath: pathJoin(destDirPath, "resources")
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(
|
||||
destDirPath,
|
||||
"resources",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
|
||||
)
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(destDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
"accountResourceProvider=account-v1",
|
||||
"",
|
||||
`locales=${accountMultiPageSupportedLanguages.join(",")}`,
|
||||
"",
|
||||
"styles=" +
|
||||
[
|
||||
"css/account.css",
|
||||
"img/icon-sidebar-active.png",
|
||||
"img/logo.png",
|
||||
...[
|
||||
"patternfly.min.css",
|
||||
"patternfly-additions.min.css",
|
||||
"patternfly-additions.min.css"
|
||||
].map(
|
||||
fileBasename =>
|
||||
`${WELL_KNOWN_DIRECTORY_BASE_NAME.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"
|
||||
)
|
||||
);
|
||||
}
|
73
scripts/build/createPublicKeycloakifyDevResourcesDir.ts
Normal file
73
scripts/build/createPublicKeycloakifyDevResourcesDir.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function createPublicKeycloakifyDevResourcesDir() {
|
||||
await Promise.all(
|
||||
(["login", "account"] as const).map(async themeType => {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
||||
themeType
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
base_resources: {
|
||||
const srcDirPath = pathJoin(
|
||||
extractedDirPath,
|
||||
"base",
|
||||
themeType,
|
||||
"resources"
|
||||
);
|
||||
|
||||
if (!(await existsAsync(srcDirPath))) {
|
||||
break base_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath,
|
||||
destDirPath
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
extractedDirPath,
|
||||
"keycloak",
|
||||
themeType,
|
||||
"resources"
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(
|
||||
destDirPath,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
184
scripts/build/main.ts
Normal file
184
scripts/build/main.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
|
||||
import { createAccountV1Dir } from "./createAccountV1Dir";
|
||||
import chalk from "chalk";
|
||||
import { run } from "../shared/run";
|
||||
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||
|
||||
(async () => {
|
||||
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.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
|
||||
fs.rmSync(join("dist", "bin", fileBasename));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
|
||||
|
||||
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")} --external prettier -o ${join("dist", "ncc_out")}`
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
destDirPath: join("dist", "bin"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath === "index.js") {
|
||||
return {
|
||||
newFileName: "main.js",
|
||||
modifiedSourceCode: sourceCode
|
||||
};
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
|
||||
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
|
||||
vendorFrontendDependencies({ distDirPath: join(process.cwd(), "dist") });
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
|
||||
|
||||
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")} --external prettier -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 }) => {
|
||||
assert(fileRelativePath === "index.js");
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
const dirBasename = "src";
|
||||
|
||||
const destDirPath = join("dist", dirBasename);
|
||||
|
||||
fs.rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
fs.cpSync(dirBasename, destDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await createPublicKeycloakifyDevResourcesDir();
|
||||
await createAccountV1Dir();
|
||||
|
||||
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 patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
const before = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
const after = before.replace(
|
||||
`var buffer = new Buffer(toRead);`,
|
||||
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
||||
);
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||
|
||||
const hasBeenPatched = after !== before;
|
||||
|
||||
return { hasBeenPatched };
|
||||
}
|
95
scripts/build/vendorFrontendDependencies.ts
Normal file
95
scripts/build/vendorFrontendDependencies.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, basename as pathBasename, dirname as pathDirname } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { run } from "../shared/run";
|
||||
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
||||
|
||||
export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
const { distDirPath } = params;
|
||||
|
||||
const vendorDirPath = pathJoin(distDirPath, "tools", "vendor");
|
||||
const cacheDirPath = pathJoin(cacheDirPath_base, "vendorFrontendDependencies");
|
||||
|
||||
const extraBundleFileBasenames = new Set<string>();
|
||||
|
||||
fs.readdirSync(vendorDirPath)
|
||||
.filter(fileBasename => fileBasename.endsWith(".js"))
|
||||
.map(fileBasename => pathJoin(vendorDirPath, fileBasename))
|
||||
.forEach(filePath => {
|
||||
{
|
||||
const mapFilePath = `${filePath}.map`;
|
||||
|
||||
if (fs.existsSync(mapFilePath)) {
|
||||
fs.unlinkSync(mapFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cacheDirPath)) {
|
||||
fs.mkdirSync(cacheDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
const webpackConfigJsFilePath = pathJoin(cacheDirPath, "webpack.config.js");
|
||||
const webpackOutputDirPath = pathJoin(cacheDirPath, "webpack_output");
|
||||
const webpackOutputFilePath = pathJoin(webpackOutputDirPath, "index.js");
|
||||
|
||||
fs.writeFileSync(
|
||||
webpackConfigJsFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
``,
|
||||
`module.exports = {`,
|
||||
` mode: 'production',`,
|
||||
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` output: {`,
|
||||
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
||||
` libraryTarget: 'module',`,
|
||||
` },`,
|
||||
` target: "web",`,
|
||||
` module: {`,
|
||||
` rules: [`,
|
||||
` {`,
|
||||
` test: /\.js$/,`,
|
||||
` use: {`,
|
||||
` loader: 'babel-loader',`,
|
||||
` options: {`,
|
||||
` presets: ['@babel/preset-env'],`,
|
||||
` }`,
|
||||
` }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` },`,
|
||||
` experiments: {`,
|
||||
` outputModule: true`,
|
||||
` }`,
|
||||
`};`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
run(`npx webpack --config ${webpackConfigJsFilePath}`);
|
||||
|
||||
fs.readdirSync(webpackOutputDirPath)
|
||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||
.map(fileBasename => pathJoin(webpackOutputDirPath, fileBasename))
|
||||
.forEach(bundleFilePath => {
|
||||
assert(bundleFilePath.endsWith(".js"));
|
||||
|
||||
if (pathBasename(bundleFilePath) === "index.js") {
|
||||
fs.renameSync(webpackOutputFilePath, filePath);
|
||||
} else {
|
||||
const bundleFileBasename = pathBasename(bundleFilePath);
|
||||
|
||||
assert(!extraBundleFileBasenames.has(bundleFileBasename));
|
||||
extraBundleFileBasenames.add(bundleFileBasename);
|
||||
|
||||
fs.renameSync(
|
||||
bundleFilePath,
|
||||
pathJoin(pathDirname(filePath), bundleFileBasename)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(webpackOutputDirPath, { recursive: true });
|
||||
});
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||
import { LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
||||
|
||||
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
||||
await copyKeycloakResourcesToPublic({
|
||||
buildContext: {
|
||||
cacheDirPath: pathJoin(__dirname, "..", "node_modules", ".cache", "scripts"),
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: pathJoin(__dirname, "..")
|
||||
}),
|
||||
loginThemeResourcesFromKeycloakVersion:
|
||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
||||
}
|
||||
});
|
||||
}
|
@ -6,6 +6,7 @@ import chalk from "chalk";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
{
|
||||
@ -84,9 +85,3 @@ import { is } from "tsafe/is";
|
||||
|
||||
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,4 +1,3 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
@ -6,83 +5,93 @@ import {
|
||||
dirname as pathDirname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { same } from "evt/tools/inDepth";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { deepAssign } from "../src/tools/deepAssign";
|
||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||
import { THEME_TYPES } from "../src/bin/shared/constants";
|
||||
import { transformCodebase } from "../src/bin/tools/transformCodebase";
|
||||
import propertiesParser from "properties-parser";
|
||||
|
||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
// update the version array for generating for newer version.
|
||||
|
||||
//@ts-ignore
|
||||
const propertiesParser = require("properties-parser");
|
||||
|
||||
async function main() {
|
||||
const keycloakVersion = "24.0.4";
|
||||
if (require.main === module) {
|
||||
generateI18nMessages();
|
||||
}
|
||||
|
||||
async function generateI18nMessages() {
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
buildContext: {
|
||||
cacheDirPath: pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"node_modules",
|
||||
".cache",
|
||||
"keycloakify"
|
||||
),
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: thisCodebaseRootDirPath
|
||||
})
|
||||
}
|
||||
});
|
||||
const accountI18nDirPath = pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"src",
|
||||
"account",
|
||||
"i18n"
|
||||
);
|
||||
|
||||
if (fs.existsSync(accountI18nDirPath)) {
|
||||
fs.rmSync(accountI18nDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
type Dictionary = { [idiomId: string]: string };
|
||||
|
||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
|
||||
crawl({
|
||||
dirPath: baseThemeDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
}).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, typeOfPage, language] = match;
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||
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])
|
||||
);
|
||||
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(record).forEach(themeType => {
|
||||
if (themeType !== "login" && themeType !== "account") {
|
||||
return;
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(extractedDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
|
||||
crawl({
|
||||
dirPath: baseThemeDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
}).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, themeType_here, language] = match;
|
||||
|
||||
if (themeType_here !== themeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
|
||||
Object.fromEntries(
|
||||
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
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const recordForThemeType = record[themeType];
|
||||
@ -99,6 +108,29 @@ async function main() {
|
||||
assert(false);
|
||||
})();
|
||||
|
||||
/* Migration helper
|
||||
|
||||
console.log({ themeType });
|
||||
|
||||
{
|
||||
|
||||
const all = new Set<string>();
|
||||
|
||||
languages.forEach(languages => all.add(languages));
|
||||
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
|
||||
currentlySupportedLanguages.forEach(languages => all.add(languages));
|
||||
|
||||
all.forEach(language => {
|
||||
console.log([
|
||||
`"${language}": `,
|
||||
`isInLanguages: ${languages.includes(language)}`,
|
||||
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
|
||||
].join(" "))
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
assert(
|
||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||
takeIntoAccountArraysOrdering: false
|
||||
@ -118,6 +150,26 @@ async function main() {
|
||||
"messages_defaultSet"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(messagesDirPath)) {
|
||||
fs.mkdirSync(messagesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(messagesDirPath, "types.ts"),
|
||||
Buffer.from(
|
||||
[
|
||||
``,
|
||||
`export const languageTags = ${JSON.stringify(languages, null, 2)} as const;`,
|
||||
``,
|
||||
`export type LanguageTag = typeof languageTags[number];`,
|
||||
``,
|
||||
`export type MessageKey = keyof typeof import("./en")["default"];`,
|
||||
``
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
const generatedFileHeader = [
|
||||
`//This code was automatically generated by running ${pathRelative(
|
||||
thisCodebaseRootDirPath,
|
||||
@ -180,6 +232,18 @@ async function main() {
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(thisCodebaseRootDirPath, "src", "login", "i18n"),
|
||||
destDirPath: accountI18nDirPath,
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath.startsWith("messages_defaultSet")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,6 +267,7 @@ const keycloakifyExtraMessages_login: Record<
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
@ -210,7 +275,9 @@ const keycloakifyExtraMessages_login: Record<
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "zh-CN",
|
||||
| "ka"
|
||||
| "zh-CN"
|
||||
| "zh-TW",
|
||||
Record<
|
||||
| "shouldBeEqual"
|
||||
| "shouldBeDifferent"
|
||||
@ -434,6 +501,17 @@ const keycloakifyExtraMessages_login: Record<
|
||||
addValue: "Dodaj wartość",
|
||||
languages: "Języki"
|
||||
},
|
||||
pt: {
|
||||
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"
|
||||
},
|
||||
"pt-BR": {
|
||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||
@ -511,6 +589,17 @@ const keycloakifyExtraMessages_login: Record<
|
||||
addValue: "Додати значення",
|
||||
languages: "Мови"
|
||||
},
|
||||
ka: {
|
||||
shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის",
|
||||
shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის",
|
||||
shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`",
|
||||
mustBeAnInteger: "უნდა იყოს მთელი რიცხვი",
|
||||
notAValidOption: "არასწორი ვარიანტი",
|
||||
selectAnOption: "აირჩიეთ ვარიანტი",
|
||||
remove: "წაშალეთ",
|
||||
addValue: "დაამატეთ მნიშვნელობა",
|
||||
languages: "ენები"
|
||||
},
|
||||
"zh-CN": {
|
||||
shouldBeEqual: "{0} 应该等于 {1}",
|
||||
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||
@ -521,38 +610,49 @@ const keycloakifyExtraMessages_login: Record<
|
||||
remove: "移除",
|
||||
addValue: "添加值",
|
||||
languages: "语言"
|
||||
},
|
||||
"zh-TW": {
|
||||
shouldBeEqual: "{0} 應該等於 {1}",
|
||||
shouldBeDifferent: "{0} 應該不同於 {1}",
|
||||
shouldMatchPattern: "模式應匹配: `/{0}/`",
|
||||
mustBeAnInteger: "必須是整數",
|
||||
notAValidOption: "不是有效選項",
|
||||
selectAnOption: "選擇一個選項",
|
||||
remove: "移除",
|
||||
addValue: "添加值",
|
||||
languages: "語言"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
||||
|
||||
export const accountMultiPageSupportedLanguages = [
|
||||
"en",
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"da",
|
||||
"de",
|
||||
"es",
|
||||
"fi",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"lt",
|
||||
"lv",
|
||||
"nl",
|
||||
"no",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh-CN"
|
||||
] as const;
|
||||
|
||||
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",
|
||||
(typeof accountMultiPageSupportedLanguages)[number],
|
||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||
> = {
|
||||
en: {
|
||||
@ -580,18 +680,10 @@ const keycloakifyExtraMessages_account: Record<
|
||||
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ää"
|
||||
@ -649,25 +741,13 @@ const keycloakifyExtraMessages_account: Record<
|
||||
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();
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { constants } from "fs";
|
||||
import { chmod, stat } from "fs/promises";
|
||||
|
||||
(async () => {
|
||||
const thisCodebaseRootDirPath = pathJoin(__dirname, "..");
|
||||
|
||||
const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json"));
|
||||
|
||||
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;
|
||||
await chmod(fullPath, newMode);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
})();
|
@ -55,7 +55,6 @@ const commonThirdPartyDeps = [
|
||||
Buffer.from(modifiedPackageJsonContent, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||
|
||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||
@ -64,6 +63,21 @@ fs.mkdirSync(yarnGlobalDirPath);
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
const { targetModuleName, cwd } = params;
|
||||
|
||||
if (targetModuleName === undefined) {
|
||||
const packageJsonFilePath = pathJoin(cwd, "package.json");
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
delete packageJson["packageManager"];
|
||||
|
||||
fs.writeFileSync(
|
||||
packageJsonFilePath,
|
||||
Buffer.from(JSON.stringify(packageJson, null, 2))
|
||||
);
|
||||
}
|
||||
|
||||
const cmd = [
|
||||
"yarn",
|
||||
"link",
|
||||
@ -77,7 +91,10 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
env: {
|
||||
...process.env,
|
||||
...(os.platform() === "win32"
|
||||
? { USERPROFILE: yarnGlobalDirPath }
|
||||
? {
|
||||
USERPROFILE: yarnGlobalDirPath,
|
||||
LOCALAPPDATA: yarnGlobalDirPath
|
||||
}
|
||||
: { HOME: yarnGlobalDirPath })
|
||||
}
|
||||
});
|
||||
@ -108,7 +125,54 @@ if (testAppPaths.length === 0) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
||||
testAppPaths.forEach(testAppPath => {
|
||||
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
|
||||
|
||||
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
|
||||
|
||||
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
let hasPostInstallOrPrepareScript = false;
|
||||
|
||||
if (parsedPackageJson.scripts !== undefined) {
|
||||
for (const scriptName of ["postinstall", "prepare"]) {
|
||||
if (parsedPackageJson.scripts[scriptName] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hasPostInstallOrPrepareScript = true;
|
||||
|
||||
delete parsedPackageJson.scripts[scriptName];
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPostInstallOrPrepareScript) {
|
||||
fs.writeFileSync(
|
||||
packageJsonFilePath,
|
||||
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
const restorePackageJson = () => {
|
||||
if (!hasPostInstallOrPrepareScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
|
||||
};
|
||||
|
||||
try {
|
||||
execSync("yarn install", { cwd: testAppPath });
|
||||
} catch (error) {
|
||||
restorePackageJson();
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
restorePackageJson();
|
||||
});
|
||||
|
||||
console.log("=== Linking common dependencies ===");
|
||||
|
||||
@ -155,4 +219,20 @@ testAppPaths.forEach(testAppPath =>
|
||||
})
|
||||
);
|
||||
|
||||
testAppPaths.forEach(testAppPath => {
|
||||
const { scripts = {} } = JSON.parse(
|
||||
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
|
||||
) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
for (const scriptName of ["postinstall", "prepare"]) {
|
||||
if (scripts[scriptName] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
|
||||
}
|
||||
});
|
||||
|
||||
export {};
|
||||
|
@ -1,55 +1,88 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
import { run } from "./shared/run";
|
||||
import cliSelect from "cli-select";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import chalk from "chalk";
|
||||
import { removeNodeModules } from "./tools/removeNodeModules";
|
||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
|
||||
{
|
||||
const dirPath = "node_modules";
|
||||
(async () => {
|
||||
const parentDirPath = pathJoin(getThisCodebaseRootDirPath(), "..");
|
||||
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// NOTE: This is a workaround for windows
|
||||
// we can't remove locked executables.
|
||||
const { starterName } = await (async () => {
|
||||
const starterNames = fs
|
||||
.readdirSync(parentDirPath)
|
||||
.filter(
|
||||
basename =>
|
||||
basename.includes("starter") &&
|
||||
basename.includes("keycloakify") &&
|
||||
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
|
||||
);
|
||||
|
||||
crawl({
|
||||
dirPath,
|
||||
returnedPathsType: "absolute"
|
||||
}).forEach(filePath => {
|
||||
try {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
} catch (error) {
|
||||
if (filePath.endsWith(".exe")) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
if (starterNames.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const starterName = await (async () => {
|
||||
if (starterNames.length === 1) {
|
||||
return starterNames[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmSync("dist", { recursive: true, force: true });
|
||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||
console.log(chalk.cyan(`\nSelect a starter to link in:`));
|
||||
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
const { value } = await cliSelect<string>({
|
||||
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const starterName = "keycloakify-starter";
|
||||
return value.split(pathSep)[1];
|
||||
})();
|
||||
|
||||
fs.rmSync(join("..", starterName, "node_modules"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
return { starterName };
|
||||
})();
|
||||
|
||||
run("yarn install", { cwd: join("..", starterName) });
|
||||
const startTime = Date.now();
|
||||
|
||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
||||
console.log(chalk.cyan(`\n\nLinking in ..${pathSep}${starterName}...`));
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
removeNodeModules({
|
||||
nodeModulesDirPath: pathJoin(getThisCodebaseRootDirPath(), "node_modules")
|
||||
});
|
||||
|
||||
function run(command: string, options?: { cwd: string }) {
|
||||
console.log(`$ ${command}`);
|
||||
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), "dist"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), ".yarn_home"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
|
||||
const starterDirPath = pathJoin(parentDirPath, starterName);
|
||||
|
||||
removeNodeModules({
|
||||
nodeModulesDirPath: pathJoin(starterDirPath, "node_modules")
|
||||
});
|
||||
|
||||
run("yarn install", { cwd: starterDirPath });
|
||||
|
||||
run(`npx tsx ${pathJoin("scripts", "link-in-app.ts")} ${starterName}`);
|
||||
|
||||
const durationSeconds = Math.round((Date.now() - startTime) / 1000);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
console.log(chalk.green(`\n\nLinked in ${starterName} in ${durationSeconds}s`));
|
||||
})();
|
||||
|
9
scripts/shared/cacheDirPath.ts
Normal file
9
scripts/shared/cacheDirPath.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export const cacheDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"node_modules",
|
||||
".cache",
|
||||
"scripts"
|
||||
);
|
@ -1,30 +1,33 @@
|
||||
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";
|
||||
import { relative as pathRelative } from "path";
|
||||
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
|
||||
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { cacheDirPath } from "./cacheDirPath";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
const KEYCLOAK_VERSION = {
|
||||
FOR_LOGIN_THEME: "25.0.4",
|
||||
FOR_ACCOUNT_MULTI_PAGE: "21.1.2"
|
||||
} as const;
|
||||
|
||||
export async function downloadKeycloakDefaultTheme(params: {
|
||||
keycloakVersion: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ defaultThemeDirPath: string }> {
|
||||
const { keycloakVersion, buildContext } = params;
|
||||
keycloakVersionId: keyof typeof KEYCLOAK_VERSION;
|
||||
}) {
|
||||
const { keycloakVersionId } = params;
|
||||
|
||||
const keycloakVersion = KEYCLOAK_VERSION[keycloakVersionId];
|
||||
|
||||
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",
|
||||
cacheDirPath,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
||||
}),
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles",
|
||||
onArchiveFile: async params => {
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
@ -34,16 +37,44 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
|
||||
const { readFile, writeFile } = params;
|
||||
|
||||
skip_keycloak_v2: {
|
||||
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
||||
break skip_keycloak_v2;
|
||||
}
|
||||
|
||||
if (
|
||||
!fileRelativePath.startsWith("base") &&
|
||||
!fileRelativePath.startsWith("keycloak")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (keycloakVersion) {
|
||||
case KEYCLOAK_VERSION.FOR_LOGIN_THEME:
|
||||
if (
|
||||
!fileRelativePath.startsWith(pathJoin("base", "login")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "login")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".ftl")) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE:
|
||||
if (
|
||||
!fileRelativePath.startsWith(pathJoin("base", "account")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "account")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
assert<Equals<typeof keycloakVersion, never>>(false);
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
@ -169,7 +200,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
}
|
||||
|
||||
skip_unused_resources: {
|
||||
if (keycloakVersion !== "24.0.4") {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_LOGIN_THEME) {
|
||||
break skip_unused_resources;
|
||||
}
|
||||
|
||||
@ -250,7 +281,8 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||
pathJoin("jquery", "dist", "jquery.min.js")
|
||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||
]);
|
||||
}
|
||||
|
||||
@ -287,11 +319,21 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_package_json: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "common", "resources", "package.json")
|
||||
) {
|
||||
break skip_package_json;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile({ fileRelativePath });
|
||||
}
|
||||
});
|
||||
|
||||
return { defaultThemeDirPath: extractedDirPath };
|
||||
return { extractedDirPath };
|
||||
}
|
8
scripts/shared/run.ts
Normal file
8
scripts/shared/run.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function run(command: string, options?: { cwd: string }) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
import * as child_process from "child_process";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
|
||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
run("yarn build");
|
||||
|
||||
await copyKeycloakResourcesToStorybookStaticDir();
|
||||
|
||||
{
|
||||
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
|
||||
shell: true
|
||||
@ -21,9 +19,3 @@ import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourc
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
})();
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
27
scripts/tools/removeNodeModules.ts
Normal file
27
scripts/tools/removeNodeModules.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import * as fs from "fs";
|
||||
import { crawl } from "../../src/bin/tools/crawl";
|
||||
|
||||
export function removeNodeModules(params: { nodeModulesDirPath: string }) {
|
||||
const { nodeModulesDirPath } = params;
|
||||
|
||||
try {
|
||||
fs.rmSync(nodeModulesDirPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// NOTE: This is a workaround for windows
|
||||
// we can't remove locked executables.
|
||||
|
||||
crawl({
|
||||
dirPath: nodeModulesDirPath,
|
||||
returnedPathsType: "absolute"
|
||||
}).forEach(filePath => {
|
||||
try {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
} catch (error) {
|
||||
if (filePath.endsWith(".exe")) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "keycloakify/bin/shared/constants";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
@ -6,7 +6,9 @@ import { assert } from "tsafe/assert";
|
||||
* This works both in your main app and in your Keycloak theme.
|
||||
*/
|
||||
export const PUBLIC_URL = (() => {
|
||||
const kcContext = (window as any).kcContext;
|
||||
const kcContext: { "x-keycloakify": { resourcesPath: string } } | undefined = (
|
||||
window as any
|
||||
).kcContext;
|
||||
|
||||
if (kcContext === undefined || process.env.NODE_ENV === "development") {
|
||||
assert(
|
||||
@ -17,5 +19,5 @@ export const PUBLIC_URL = (() => {
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
|
||||
return `${kcContext.url.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}`;
|
||||
return `${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}`;
|
||||
})();
|
||||
|
@ -1,10 +1,12 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { RESOURCES_COMMON, KEYCLOAK_RESOURCES } from "keycloakify/bin/shared/constants";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { LanguageTag } from "keycloakify/account/i18n/messages_defaultSet/types";
|
||||
|
||||
const resourcesPath = `${BASE_URL}${KEYCLOAK_RESOURCES}/account/resources`;
|
||||
const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES}/account`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
themeVersion: "0.0.0",
|
||||
@ -13,7 +15,7 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
themeName: "my-theme-name",
|
||||
url: {
|
||||
resourcesPath,
|
||||
resourcesCommonPath: `${resourcesPath}/${RESOURCES_COMMON}`,
|
||||
resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`,
|
||||
resourceUrl: "#",
|
||||
accountUrl: "#",
|
||||
applicationsUrl: "#",
|
||||
@ -38,35 +40,53 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
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
|
||||
),
|
||||
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"],
|
||||
["ar", "العربية"],
|
||||
["da", "Dansk"],
|
||||
["fi", "Suomi"],
|
||||
["hu", "Magyar"],
|
||||
["lv", "Latviešu"]
|
||||
/* spell-checker: enable */
|
||||
] as const
|
||||
).map(([languageTag, label]) => {
|
||||
{
|
||||
type Got = typeof languageTag;
|
||||
type Expected = LanguageTag;
|
||||
|
||||
type Missing = Exclude<Expected, Got>;
|
||||
type Unexpected = Exclude<Got, Expected>;
|
||||
|
||||
assert<Equals<Missing, never>>;
|
||||
assert<Equals<Unexpected, never>>;
|
||||
}
|
||||
|
||||
return {
|
||||
languageTag,
|
||||
label,
|
||||
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
|
||||
} as const;
|
||||
}),
|
||||
|
||||
currentLanguageTag: "en"
|
||||
},
|
||||
features: {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/account/Template.useInitialize";
|
||||
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
@ -13,9 +13,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
|
||||
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||
const { url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = msgStr("accountManagementTitle");
|
||||
@ -31,30 +31,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
className: clsx("admin-console", "user", kcClsx("kcBodyClass"))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { currentLanguageTag } = locale ?? {};
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (currentLanguageTag === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = currentLanguageTag;
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
]
|
||||
});
|
||||
|
||||
if (!areAllStyleSheetsLoaded) {
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -70,16 +49,16 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
<div className="navbar-collapse navbar-collapse-1">
|
||||
<div className="container">
|
||||
<ul className="nav navbar-nav navbar-utility">
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
{enabledLanguages.length > 1 && (
|
||||
<li>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
{currentLanguage.label}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
{enabledLanguages.map(({ languageTag, label, href }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href={getChangeLocaleUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
|
||||
<a href={href}>{label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -148,7 +127,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.summary
|
||||
__html: kcSanitize(message.summary)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
35
src/account/Template.useInitialize.ts
Normal file
35
src/account/Template.useInitialize.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import type { KcContext } from "keycloakify/account/KcContext";
|
||||
|
||||
export type KcContextLike = {
|
||||
url: {
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export function useInitialize(params: {
|
||||
kcContext: KcContextLike;
|
||||
doUseDefaultCss: boolean;
|
||||
}) {
|
||||
const { kcContext, doUseDefaultCss } = params;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
]
|
||||
});
|
||||
|
||||
return { isReadyToRender: areAllStyleSheetsLoaded };
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ClassKey } from "keycloakify/account/lib/kcClsx";
|
||||
|
||||
export type TemplateProps<KcContext, I18n> = {
|
||||
kcContext: KcContext;
|
||||
@ -10,17 +11,4 @@ export type TemplateProps<KcContext, I18n> = {
|
||||
active: string;
|
||||
};
|
||||
|
||||
export type ClassKey =
|
||||
| "kcHtmlClass"
|
||||
| "kcBodyClass"
|
||||
| "kcButtonClass"
|
||||
| "kcButtonPrimaryClass"
|
||||
| "kcButtonLargeClass"
|
||||
| "kcButtonDefaultClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcFormClass"
|
||||
| "kcFormGroupClass"
|
||||
| "kcInputWrapperClass"
|
||||
| "kcLabelClass"
|
||||
| "kcInputClass"
|
||||
| "kcInputErrorMessageClass";
|
||||
export type { ClassKey };
|
||||
|
@ -1,6 +0,0 @@
|
||||
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,250 +0,0 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { assert } from "tsafe/assert";
|
||||
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 GenericI18n_noJsx<MessageKey extends string> = {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
* The current language
|
||||
*/
|
||||
currentLanguageTag: string;
|
||||
/**
|
||||
* Redirect to this url to change the language.
|
||||
* After reload currentLanguageTag === newLanguageTag
|
||||
*/
|
||||
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
||||
/**
|
||||
* e.g. "en" => "English", "fr" => "Français", ...
|
||||
*
|
||||
* Used to render a select that enable user to switch language.
|
||||
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||
* */
|
||||
labelBySupportedLanguageTag: Record<string, string>;
|
||||
/**
|
||||
*
|
||||
* 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"
|
||||
* {
|
||||
* 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 MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
||||
|
||||
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
||||
}) {
|
||||
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;
|
||||
|
||||
use_cache: {
|
||||
const cachedResult = cachedResultByKcContext.get(kcContext);
|
||||
|
||||
if (cachedResult === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
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 startIndex = message
|
||||
.match(/{[0-9]+}/g)
|
||||
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||
.map(indexStr => parseInt(indexStr))
|
||||
.sort((a, b) => a - b)[0];
|
||||
|
||||
if (startIndex === undefined) {
|
||||
// No {0} in message (no arguments expected)
|
||||
return message;
|
||||
}
|
||||
|
||||
let messageWithArgsInjected = message;
|
||||
|
||||
args.forEach((arg, i) => {
|
||||
if (arg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
||||
);
|
||||
});
|
||||
|
||||
return messageWithArgsInjected;
|
||||
}
|
||||
|
||||
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 })
|
||||
};
|
||||
}
|
||||
|
||||
return { createI18nTranslationFunctions };
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
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";
|
@ -1,95 +0,0 @@
|
||||
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,3 +1,3 @@
|
||||
export type { ExtendKcContext } from "keycloakify/account/KcContext";
|
||||
export type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
export { createUseI18n } from "keycloakify/account/i18n";
|
||||
export { i18nBuilder, type MessageKey_defaultSet } from "keycloakify/account/i18n";
|
||||
|
@ -1,5 +1,19 @@
|
||||
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export type ClassKey =
|
||||
| "kcHtmlClass"
|
||||
| "kcBodyClass"
|
||||
| "kcButtonClass"
|
||||
| "kcButtonPrimaryClass"
|
||||
| "kcButtonLargeClass"
|
||||
| "kcButtonDefaultClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcFormClass"
|
||||
| "kcFormGroupClass"
|
||||
| "kcInputWrapperClass"
|
||||
| "kcLabelClass"
|
||||
| "kcInputClass"
|
||||
| "kcInputErrorMessageClass";
|
||||
|
||||
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||
defaultClasses: {
|
||||
@ -20,6 +34,4 @@ export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||
}
|
||||
});
|
||||
|
||||
export type { ClassKey };
|
||||
|
||||
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
@ -159,7 +160,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("totp")
|
||||
__html: kcSanitize(messagesPerField.get("totp"))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -190,7 +191,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("userLabel")
|
||||
__html: kcSanitize(messagesPerField.get("userLabel"))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -5,25 +5,30 @@ import {
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
THEME_TYPES
|
||||
} 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 type { BuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -33,6 +38,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
@ -43,7 +50,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect<ThemeType>({
|
||||
const { value } = await cliSelect({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
@ -62,6 +69,16 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log(
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
@ -102,7 +119,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const componentCode = fs
|
||||
let sourceCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -116,6 +133,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
.replace('import React from "react";\n', "")
|
||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
@ -124,7 +152,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
[
|
||||
|
@ -1,13 +1,96 @@
|
||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "./tools/fs.rmSync";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
await copyKeycloakResourcesToPublic({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "copy-keycloak-resources-to-public",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
buildContext.publicDirPath,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||
);
|
||||
|
||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||
|
||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||
{
|
||||
keycloakifyVersion: readThisNpmPackageVersion()
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
skip_if_already_done: {
|
||||
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs
|
||||
.readFileSync(keycloakifyBuildinfoFilePath)
|
||||
.toString("utf8");
|
||||
|
||||
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
rmSync(destDirPath, { force: true, recursive: true });
|
||||
|
||||
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
|
||||
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
|
||||
force: true,
|
||||
recursive: true
|
||||
});
|
||||
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
|
||||
force: true,
|
||||
recursive: true
|
||||
});
|
||||
|
||||
fs.mkdirSync(destDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(destDirPath, "README.txt"),
|
||||
Buffer.from(
|
||||
// prettier-ignore
|
||||
[
|
||||
"This directory is only used in dev mode by Keycloakify",
|
||||
"It won't be included in your final build.",
|
||||
"Do not modify anything in this directory.",
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
keycloakifyBuildinfoFilePath,
|
||||
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
||||
);
|
||||
}
|
||||
|
68
src/bin/eject-file.ts
Normal file
68
src/bin/eject-file.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied";
|
||||
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
||||
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { getUiModuleMetas } from "./postinstall/uiModuleMeta";
|
||||
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
||||
import * as fsPr from "fs/promises";
|
||||
import {
|
||||
readManagedGitignoreFile,
|
||||
writeManagedGitignoreFile
|
||||
} from "./postinstall/managedGitignoreFile";
|
||||
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
cliCommandOptions: {
|
||||
file: string;
|
||||
};
|
||||
}) {
|
||||
const { buildContext, cliCommandOptions } = params;
|
||||
|
||||
const fileRelativePath = pathRelative(
|
||||
buildContext.themeSrcDirPath,
|
||||
getAbsoluteAndInOsFormatPath({
|
||||
cwd: buildContext.themeSrcDirPath,
|
||||
pathIsh: cliCommandOptions.file
|
||||
})
|
||||
);
|
||||
|
||||
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
||||
|
||||
const uiModuleMeta = uiModuleMetas.find(({ files }) =>
|
||||
files.map(({ fileRelativePath }) => fileRelativePath).includes(fileRelativePath)
|
||||
);
|
||||
|
||||
if (!uiModuleMeta) {
|
||||
throw new Error(`No UI module found for the file ${fileRelativePath}`);
|
||||
}
|
||||
|
||||
const uiModuleDirPath = await getInstalledModuleDirPath({
|
||||
moduleName: uiModuleMeta.moduleName,
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath),
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({
|
||||
buildContext,
|
||||
fileRelativePath,
|
||||
isForEjection: true,
|
||||
uiModuleName: uiModuleMeta.moduleName,
|
||||
uiModuleDirPath,
|
||||
uiModuleVersion: uiModuleMeta.version
|
||||
});
|
||||
|
||||
await fsPr.writeFile(
|
||||
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
||||
sourceCode
|
||||
);
|
||||
|
||||
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||
buildContext
|
||||
});
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
uiModuleMetas,
|
||||
ejectedFilesRelativePaths: [...ejectedFilesRelativePaths, fileRelativePath]
|
||||
});
|
||||
}
|
@ -7,8 +7,7 @@ import {
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
THEME_TYPES
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
@ -20,17 +19,23 @@ import {
|
||||
} from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -40,6 +45,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
@ -50,7 +57,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect<ThemeType>({
|
||||
const { value } = await cliSelect({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
@ -59,6 +66,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
return value;
|
||||
})();
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log(
|
||||
"Use `npx keycloakify eject-file` command instead, see documentation"
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
@ -68,13 +83,13 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
pathDirname(buildContext.packageJsonFilePath),
|
||||
"node_modules",
|
||||
"@keycloakify",
|
||||
"keycloak-account-ui",
|
||||
`keycloak-account-ui`,
|
||||
"src"
|
||||
);
|
||||
|
||||
console.log(
|
||||
[
|
||||
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
|
||||
`There isn't an interactive CLI to eject components of the Account SPA UI.`,
|
||||
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
||||
``,
|
||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||
@ -83,41 +98,44 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
);
|
||||
|
||||
eject_entrypoint: {
|
||||
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
||||
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const;
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"account"
|
||||
);
|
||||
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
accountThemeSrcDirPath,
|
||||
kcAccountUiTsxFileRelativePath
|
||||
);
|
||||
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
break eject_entrypoint;
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
|
||||
targetFilePath
|
||||
);
|
||||
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath);
|
||||
|
||||
{
|
||||
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
|
||||
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx");
|
||||
|
||||
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
||||
|
||||
const componentName = pathBasename(
|
||||
kcAccountUiTsxFileRelativePath
|
||||
).replace(/.tsx$/, "");
|
||||
const componentName = pathBasename(kcUiTsxFileRelativePath).replace(
|
||||
/.tsx$/,
|
||||
""
|
||||
);
|
||||
|
||||
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||
let modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
||||
`./${componentName}`
|
||||
);
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
modifiedKcPageTsxCode = await runPrettier({
|
||||
filePath: kcPageTsxFilePath,
|
||||
sourceCode: modifiedKcPageTsxCode
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
kcPageTsxFilePath,
|
||||
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
||||
@ -133,13 +151,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
[
|
||||
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
||||
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
|
||||
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
|
||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``,
|
||||
`then update the import of routes in ${kcUiTsxFileRelativePath}.`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
@ -216,7 +235,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const componentCode = fs
|
||||
let componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -228,6 +247,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
)
|
||||
.toString("utf8");
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
componentCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: componentCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
@ -244,12 +274,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
)} copy pasted from the Keycloakify source code into your project`
|
||||
);
|
||||
|
||||
edit_KcApp: {
|
||||
edit_KcPage: {
|
||||
if (
|
||||
pageIdOrComponent !== templateValue &&
|
||||
pageIdOrComponent !== userProfileFormFieldsValue
|
||||
) {
|
||||
break edit_KcApp;
|
||||
break edit_KcPage;
|
||||
}
|
||||
|
||||
const kcAppTsxPath = pathJoin(
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import cliSelect from "cli-select";
|
||||
import child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import * as fs from "fs";
|
||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||
import { generateKcGenTs } from "../shared/generateKcGenTs";
|
||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||
|
||||
@ -31,37 +38,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
exit_if_uncommitted_changes: {
|
||||
let hasUncommittedChanges: boolean | undefined = undefined;
|
||||
|
||||
try {
|
||||
hasUncommittedChanges =
|
||||
child_process
|
||||
.execSync(`git status --porcelain`, {
|
||||
cwd: buildContext.projectDirPath
|
||||
})
|
||||
.toString()
|
||||
.trim() !== "";
|
||||
} catch {
|
||||
// Probably not a git repository
|
||||
break exit_if_uncommitted_changes;
|
||||
}
|
||||
|
||||
if (!hasUncommittedChanges) {
|
||||
break exit_if_uncommitted_changes;
|
||||
}
|
||||
console.warn(
|
||||
[
|
||||
chalk.red(
|
||||
"Please commit or stash your changes before running this command.\n"
|
||||
),
|
||||
"This command will modify your project's files so it's better to have a clean working directory",
|
||||
"so that you can easily see what has been changed and revert if needed."
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
const { value: accountThemeType } = await cliSelect({
|
||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||
@ -97,7 +76,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||
|
||||
await generateKcGenTs({
|
||||
await updateKcGenCommand({
|
||||
buildContext: {
|
||||
...buildContext,
|
||||
implementedThemeTypes: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import chalk from "chalk";
|
||||
@ -6,6 +6,7 @@ import {
|
||||
getLatestsSemVersionedTag,
|
||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||
} from "../shared/getLatestsSemVersionedTag";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
@ -13,7 +14,6 @@ import { is } from "tsafe/is";
|
||||
import { id } from "tsafe/id";
|
||||
import { npmInstall } from "../tools/npmInstall";
|
||||
import { copyBoilerplate } from "./copyBoilerplate";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
@ -68,7 +68,9 @@ export async function initializeAccountTheme_singlePage(params: {
|
||||
})()
|
||||
);
|
||||
|
||||
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = semVersionedTag.tag;
|
||||
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = SemVer.stringify(
|
||||
semVersionedTag.version
|
||||
);
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
@ -118,20 +120,7 @@ export async function initializeAccountTheme_singlePage(params: {
|
||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||
);
|
||||
|
||||
run_npm_install: {
|
||||
if (
|
||||
JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
||||
.toString("utf8")
|
||||
)["version"] === "0.0.0"
|
||||
) {
|
||||
//NOTE: Linked version
|
||||
break run_npm_install;
|
||||
}
|
||||
|
||||
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
||||
}
|
||||
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
||||
|
||||
copyBoilerplate({
|
||||
accountThemeType: "Single-Page",
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { createUseI18n } from "keycloakify/account";
|
||||
import { i18nBuilder } from "keycloakify/account";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
.withThemeName<ThemeName>()
|
||||
.withExtraLanguages({})
|
||||
.withCustomTranslations({})
|
||||
.build();
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n, type I18n };
|
||||
|
@ -5,15 +5,18 @@ import * as fs from "fs";
|
||||
import chalk from "chalk";
|
||||
import { z } from "zod";
|
||||
import { id } from "tsafe/id";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
export type BuildContextLike = {
|
||||
bundler: BuildContext["bundler"];
|
||||
projectDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function updateAccountThemeImplementationInConfig(params: {
|
||||
buildContext: BuildContext;
|
||||
buildContext: BuildContextLike;
|
||||
accountThemeType: "Single-Page" | "Multi-Page";
|
||||
}) {
|
||||
const { buildContext, accountThemeType } = params;
|
||||
@ -81,6 +84,8 @@ export function updateAccountThemeImplementationInConfig(params: {
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
|
@ -1,21 +1,34 @@
|
||||
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 { getBuildContext } from "./shared/buildContext";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { SemVer } from "./tools/SemVer";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-email-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||
if (
|
||||
fs.existsSync(emailThemeSrcDirPath) &&
|
||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
`There is already a ${pathRelative(
|
||||
`There is already a non empty ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
@ -26,7 +39,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
let { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
@ -34,13 +47,51 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
buildContext
|
||||
});
|
||||
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
buildContext
|
||||
const getUrl = (keycloakVersion: string) => {
|
||||
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
|
||||
};
|
||||
|
||||
keycloakVersion = await (async () => {
|
||||
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
|
||||
|
||||
while (true) {
|
||||
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
|
||||
|
||||
const response = await fetch(url, buildContext.fetchOptions);
|
||||
|
||||
if (response.ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
assert(keycloakVersionParsed.patch !== 0);
|
||||
|
||||
keycloakVersionParsed.patch--;
|
||||
}
|
||||
|
||||
return SemVer.stringify(keycloakVersionParsed);
|
||||
})();
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: getUrl(keycloakVersion),
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
const fileRelativePath_target = pathRelative(
|
||||
pathJoin("theme", "base", "email"),
|
||||
fileRelativePath
|
||||
);
|
||||
|
||||
if (fileRelativePath_target.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
||||
}
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "base", "email"),
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
@ -50,7 +101,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
fs.writeFileSync(
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`,
|
||||
[
|
||||
`parent=base`,
|
||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
@ -7,7 +7,6 @@ import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { ACCOUNT_V1_THEME_NAME } from "../../shared/constants";
|
||||
import {
|
||||
generatePom,
|
||||
BuildContextLike as BuildContextLike_generatePom
|
||||
@ -17,6 +16,7 @@ import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||
keycloakifyBuildDirPath: string;
|
||||
@ -75,7 +75,7 @@ export async function buildJar(params: {
|
||||
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin("theme", ACCOUNT_V1_THEME_NAME),
|
||||
dirPath: pathJoin("theme", "account-v1"),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
@ -90,10 +90,7 @@ export async function buildJar(params: {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(
|
||||
`parent=${ACCOUNT_V1_THEME_NAME}`,
|
||||
"parent=keycloak"
|
||||
),
|
||||
.replace(`parent=account-v1`, "parent=keycloak"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
@ -126,7 +123,7 @@ export async function buildJar(params: {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
|
||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
||||
({ name }) => name !== ACCOUNT_V1_THEME_NAME
|
||||
({ name }) => name !== "account-v1"
|
||||
);
|
||||
|
||||
return metaInfKeycloakTheme;
|
||||
@ -139,59 +136,49 @@ export async function buildJar(params: {
|
||||
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.
|
||||
const doBreak: boolean = (() => {
|
||||
switch (keycloakAccountV1Version) {
|
||||
case null:
|
||||
return false;
|
||||
case "0.3":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
await Promise.all(
|
||||
(["register.ftl", "login-update-profile.ftl"] as const)
|
||||
.map(pageId =>
|
||||
buildContext.themeNames.map(async themeName => {
|
||||
const ftlFilePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"theme",
|
||||
themeName,
|
||||
"login",
|
||||
pageId
|
||||
);
|
||||
|
||||
// TODO: Remove this optimization, it's a bit hacky.
|
||||
if (doBreak) {
|
||||
break route_legacy_pages;
|
||||
}
|
||||
// NOTE: https://github.com/keycloakify/keycloakify/issues/665
|
||||
if (!(await existsAsync(ftlFilePath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
||||
buildContext.themeNames.map(themeName => {
|
||||
const ftlFilePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"theme",
|
||||
themeName,
|
||||
"login",
|
||||
pageId
|
||||
);
|
||||
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
||||
|
||||
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
||||
const ftlFileBasename = (() => {
|
||||
switch (pageId) {
|
||||
case "register.ftl":
|
||||
return "register-user-profile.ftl";
|
||||
case "login-update-profile.ftl":
|
||||
return "update-user-profile.ftl";
|
||||
}
|
||||
assert<Equals<typeof pageId, never>>(false);
|
||||
})();
|
||||
|
||||
const ftlFileBasename = (() => {
|
||||
switch (pageId) {
|
||||
case "register.ftl":
|
||||
return "register-user-profile.ftl";
|
||||
case "login-update-profile.ftl":
|
||||
return "update-user-profile.ftl";
|
||||
}
|
||||
assert<Equals<typeof pageId, never>>(false);
|
||||
})();
|
||||
const modifiedFtlFileContent = ftlFileContent.replace(
|
||||
`"ftlTemplateFileName": "${pageId}"`,
|
||||
`"ftlTemplateFileName": "${ftlFileBasename}"`
|
||||
);
|
||||
|
||||
const modifiedFtlFileContent = ftlFileContent.replace(
|
||||
`"ftlTemplateFileName": "${pageId}"`,
|
||||
`"ftlTemplateFileName": "${ftlFileBasename}"`
|
||||
);
|
||||
assert(modifiedFtlFileContent !== ftlFileContent);
|
||||
|
||||
assert(modifiedFtlFileContent !== ftlFileContent);
|
||||
|
||||
fs.writeFile(
|
||||
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
|
||||
Buffer.from(modifiedFtlFileContent, "utf8")
|
||||
);
|
||||
})
|
||||
await fs.writeFile(
|
||||
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
|
||||
Buffer.from(modifiedFtlFileContent, "utf8")
|
||||
);
|
||||
})
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
}
|
||||
|
||||
@ -210,7 +197,7 @@ export async function buildJar(params: {
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
`mvn install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
|
@ -52,9 +52,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "0.6":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
return undefined;
|
||||
return "26-and-above" as const;
|
||||
case "1.1.5":
|
||||
return "25-and-above" as const;
|
||||
return "25" as const;
|
||||
}
|
||||
}
|
||||
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
|
||||
@ -75,9 +75,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
}
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
return "21-and-below";
|
||||
return "all-other-versions";
|
||||
case "1.1.5":
|
||||
return "22-and-above";
|
||||
return "22-to-25";
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
|
||||
false
|
||||
|
@ -11,11 +11,7 @@ import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import {
|
||||
type ThemeType,
|
||||
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
|
||||
RESOURCES_COMMON
|
||||
} from "../../shared/constants";
|
||||
import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
||||
@ -94,7 +90,7 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
new RegExp(
|
||||
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
||||
),
|
||||
`\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/`
|
||||
`\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/`
|
||||
)
|
||||
);
|
||||
})
|
||||
@ -119,7 +115,7 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
.replace("{{keycloakifyVersion}}", keycloakifyVersion)
|
||||
.replace("{{themeVersion}}", buildContext.themeVersion)
|
||||
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
|
||||
.replace("{{RESOURCES_COMMON}}", RESOURCES_COMMON)
|
||||
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
|
||||
.replace(
|
||||
"{{userDefinedExclusions}}",
|
||||
buildContext.kcContextExclusionsFtlCode ?? ""
|
||||
|
@ -85,6 +85,21 @@ attributes_to_attributesByName: {
|
||||
});
|
||||
}
|
||||
window.kcContext = kcContext;
|
||||
|
||||
<#if xKeycloakify.themeType == "login" >
|
||||
{
|
||||
const script = document.createElement("script");
|
||||
script.type = "importmap";
|
||||
script.textContent = JSON.stringify({
|
||||
imports: {
|
||||
"rfc4648": kcContext.url.resourcesCommonPath + "/node_modules/rfc4648/lib/rfc4648.js"
|
||||
}
|
||||
}, null, 2);
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
</#if>
|
||||
|
||||
function decodeHtmlEntities(htmlStr){
|
||||
var element = decodeHtmlEntities.element;
|
||||
if (!element) {
|
||||
@ -151,7 +166,7 @@ function decodeHtmlEntities(htmlStr){
|
||||
areSamePath(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
|
||||
) || (
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole", "smtpConfig"]?seq_contains(key) &&
|
||||
areSamePath(path, ["realm"])
|
||||
) || (
|
||||
xKeycloakify.pageId == "error.ftl" &&
|
||||
@ -220,6 +235,9 @@ function decodeHtmlEntities(htmlStr){
|
||||
"identityFederationEnabled",
|
||||
"userManagedAccessAllowed"
|
||||
]?seq_contains(key)
|
||||
) || (
|
||||
["flowContext", "session", "realm"]?seq_contains(key) &&
|
||||
areSamePath(path, ["social"])
|
||||
)
|
||||
>
|
||||
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
||||
|
@ -1,89 +0,0 @@
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { type ThemeType, FALLBACK_LANGUAGE_TAG } from "../../shared/constants";
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import * as recast from "recast";
|
||||
import * as babelParser from "@babel/parser";
|
||||
@ -10,12 +10,27 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||
|
||||
export type BuildContextLike = {
|
||||
themeNames: string[];
|
||||
themeSrcDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
buildContext: BuildContextLike;
|
||||
themeType: Exclude<ThemeType, "admin">;
|
||||
}): {
|
||||
languageTags: string[];
|
||||
writeMessagePropertiesFiles: (params: {
|
||||
messageDirPath: string;
|
||||
themeName: string;
|
||||
}) => void;
|
||||
} {
|
||||
const { buildContext, themeType } = params;
|
||||
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -25,51 +40,49 @@ export function generateMessageProperties(params: {
|
||||
"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/);
|
||||
const messages_defaultSet_by_languageTag_defaultSet: {
|
||||
[languageTag_defaultSet: string]: Record<string, string>;
|
||||
} = Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(baseMessagesDirPath)
|
||||
.filter(basename => basename !== "index.ts" && basename !== "types.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 messagesJson = "{";
|
||||
|
||||
let isInDeclaration = false;
|
||||
let isInDeclaration = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!isInDeclaration) {
|
||||
if (line.startsWith("const messages")) {
|
||||
isInDeclaration = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
for (const line of lines) {
|
||||
if (!isInDeclaration) {
|
||||
if (line.startsWith("const messages")) {
|
||||
isInDeclaration = true;
|
||||
}
|
||||
|
||||
if (line.startsWith("}")) {
|
||||
messagesJson += "}";
|
||||
break;
|
||||
}
|
||||
|
||||
messagesJson += line;
|
||||
continue;
|
||||
}
|
||||
|
||||
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
||||
if (line.startsWith("}")) {
|
||||
messagesJson += "}";
|
||||
break;
|
||||
}
|
||||
|
||||
return [languageTag, messages];
|
||||
})
|
||||
);
|
||||
messagesJson += line;
|
||||
}
|
||||
|
||||
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
||||
|
||||
return [languageTag, messages];
|
||||
})
|
||||
);
|
||||
|
||||
const { i18nTsFilePath } = (() => {
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
dirPath: pathJoin(buildContext.themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
@ -88,7 +101,7 @@ export function generateMessageProperties(params: {
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
|
||||
fs.readFileSync(file).toString("utf8").includes("i18nBuilder")
|
||||
);
|
||||
|
||||
const i18nTsFilePath: string | undefined = files[0];
|
||||
@ -96,97 +109,334 @@ export function generateMessageProperties(params: {
|
||||
return { i18nTsFilePath };
|
||||
})();
|
||||
|
||||
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
|
||||
(() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const i18nTsRoot = (() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const root = recastParseTs(i18nTsFilePath);
|
||||
return root;
|
||||
})();
|
||||
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"]
|
||||
}),
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
const messages_defaultSet_by_languageTag_notInDefaultSet:
|
||||
| { [languageTag_notInDefaultSet: string]: Record<string, string> }
|
||||
| undefined = (() => {
|
||||
if (i18nTsRoot === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
let extraLanguageEntryByLanguageTag: Record<
|
||||
string,
|
||||
{ label: string; path: string }
|
||||
> = {};
|
||||
|
||||
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;
|
||||
recast.visit(i18nTsRoot, {
|
||||
visitCallExpression: function (path) {
|
||||
const node = path.node;
|
||||
|
||||
// Check if the callee is a MemberExpression with property 'withExtraLanguages'
|
||||
if (
|
||||
node.callee.type === "MemberExpression" &&
|
||||
node.callee.property.type === "Identifier" &&
|
||||
node.callee.property.name === "withExtraLanguages"
|
||||
) {
|
||||
const arg = node.arguments[0];
|
||||
if (arg && arg.type === "ObjectExpression") {
|
||||
// Iterate over the properties of the object
|
||||
arg.properties.forEach(prop => {
|
||||
if (
|
||||
prop.type === "ObjectProperty" &&
|
||||
prop.key.type === "Identifier"
|
||||
) {
|
||||
const lang = prop.key.name;
|
||||
const value = prop.value;
|
||||
|
||||
if (value.type === "ObjectExpression") {
|
||||
let label: string | undefined = undefined;
|
||||
let pathStr: string | undefined = undefined;
|
||||
|
||||
// Iterate over the properties of the language object
|
||||
value.properties.forEach(p => {
|
||||
if (
|
||||
p.type === "ObjectProperty" &&
|
||||
p.key.type === "Identifier"
|
||||
) {
|
||||
if (
|
||||
p.key.name === "label" &&
|
||||
p.value.type === "StringLiteral"
|
||||
) {
|
||||
label = p.value.value;
|
||||
}
|
||||
if (
|
||||
p.key.name === "getMessages" &&
|
||||
(p.value.type ===
|
||||
"ArrowFunctionExpression" ||
|
||||
p.value.type === "FunctionExpression")
|
||||
) {
|
||||
// Extract the import path from the function body
|
||||
const body = p.value.body;
|
||||
if (
|
||||
body.type === "CallExpression" &&
|
||||
body.callee.type === "Import"
|
||||
) {
|
||||
const importArg = body.arguments[0];
|
||||
if (
|
||||
importArg.type === "StringLiteral"
|
||||
) {
|
||||
pathStr = importArg.value;
|
||||
}
|
||||
} else if (
|
||||
body.type === "BlockStatement"
|
||||
) {
|
||||
// If the function body is a block (e.g., function with braces {})
|
||||
// Look for return statement
|
||||
body.body.forEach(statement => {
|
||||
if (
|
||||
statement.type ===
|
||||
"ReturnStatement" &&
|
||||
statement.argument &&
|
||||
statement.argument.type ===
|
||||
"CallExpression" &&
|
||||
statement.argument.callee
|
||||
.type === "Import"
|
||||
) {
|
||||
const importArg =
|
||||
statement.argument
|
||||
.arguments[0];
|
||||
if (
|
||||
importArg.type ===
|
||||
"StringLiteral"
|
||||
) {
|
||||
pathStr = importArg.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (label && pathStr) {
|
||||
extraLanguageEntryByLanguageTag[lang] = {
|
||||
label,
|
||||
path: pathStr
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
return false; // Stop traversing this 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(" ")
|
||||
);
|
||||
this.traverse(path); // Continue traversing other paths
|
||||
}
|
||||
});
|
||||
|
||||
return messageBundle;
|
||||
})();
|
||||
const messages_defaultSet_by_languageTag_notInDefaultSet = Object.fromEntries(
|
||||
Object.entries(extraLanguageEntryByLanguageTag).map(
|
||||
([languageTag, { path: relativePathWithoutExt }]) => [
|
||||
languageTag,
|
||||
(() => {
|
||||
const filePath = getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: relativePathWithoutExt.endsWith(".ts")
|
||||
? relativePathWithoutExt
|
||||
: `${relativePathWithoutExt}.ts`,
|
||||
cwd: pathDirname(i18nTsFilePath)
|
||||
});
|
||||
|
||||
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 root = recastParseTs(filePath);
|
||||
|
||||
let declarationCode: string | undefined = "";
|
||||
|
||||
recast.visit(root, {
|
||||
visitVariableDeclarator: function (path) {
|
||||
const node = path.node;
|
||||
|
||||
// Check if the variable name is 'messages'
|
||||
if (
|
||||
node.id.type === "Identifier" &&
|
||||
node.id.name === "messages"
|
||||
) {
|
||||
// Ensure there is an initializer
|
||||
if (node.init) {
|
||||
// Generate code from the initializer, preserving comments
|
||||
declarationCode = recast
|
||||
.print(node.init)
|
||||
.code.replace(/}.*$/, "}");
|
||||
}
|
||||
return false; // Stop traversing this path
|
||||
}
|
||||
|
||||
this.traverse(path); // Continue traversing other paths
|
||||
}
|
||||
});
|
||||
|
||||
assert(
|
||||
declarationCode !== undefined,
|
||||
`${filePath} does not contain a 'messages' variable declaration`
|
||||
);
|
||||
|
||||
let messages: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
eval(`${symToStr({ messages })} = ${declarationCode};`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`The declaration of 'message' in ${filePath} cannot be statically evaluated: ${declarationCode}`
|
||||
);
|
||||
}
|
||||
|
||||
return messages;
|
||||
})()
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
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 messages_defaultSet_by_languageTag_notInDefaultSet;
|
||||
})();
|
||||
|
||||
return messageProperties;
|
||||
const messages_defaultSet_by_languageTag = {
|
||||
...messages_defaultSet_by_languageTag_defaultSet,
|
||||
...messages_defaultSet_by_languageTag_notInDefaultSet
|
||||
};
|
||||
|
||||
const messages_themeDefined_by_languageTag:
|
||||
| {
|
||||
[languageTag: string]:
|
||||
| Record<string, string | Record<string, string>>
|
||||
| undefined;
|
||||
}
|
||||
| undefined = (() => {
|
||||
if (i18nTsRoot === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let firstArgumentCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(i18nTsRoot, {
|
||||
visitCallExpression: function (path) {
|
||||
const node = path.node;
|
||||
|
||||
if (
|
||||
node.callee.type === "MemberExpression" &&
|
||||
node.callee.property.type === "Identifier" &&
|
||||
node.callee.property.name === "withCustomTranslations"
|
||||
) {
|
||||
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
if (firstArgumentCode === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let messages_themeDefined_by_languageTag: {
|
||||
[languageTag: string]: Record<string, string | Record<string, string>>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(
|
||||
`${symToStr({ messages_themeDefined_by_languageTag })} = ${firstArgumentCode}`
|
||||
);
|
||||
} catch {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: The argument of withCustomTranslations can't be statically evaluated!",
|
||||
"This needs to be fixed refer to the documentation: https://docs.keycloakify.dev/i18n",
|
||||
firstArgumentCode
|
||||
].join(" ")
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return messages_themeDefined_by_languageTag;
|
||||
})();
|
||||
|
||||
const languageTags = Object.keys(messages_defaultSet_by_languageTag);
|
||||
|
||||
return {
|
||||
languageTags,
|
||||
writeMessagePropertiesFiles: ({ messageDirPath, themeName }) => {
|
||||
for (const languageTag of languageTags) {
|
||||
const messages = {
|
||||
...messages_defaultSet_by_languageTag[languageTag]
|
||||
};
|
||||
|
||||
add_theme_defined_messages: {
|
||||
if (messages_themeDefined_by_languageTag === undefined) {
|
||||
break add_theme_defined_messages;
|
||||
}
|
||||
|
||||
let messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[languageTag];
|
||||
|
||||
if (messages_themeDefined === undefined) {
|
||||
messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[FALLBACK_LANGUAGE_TAG];
|
||||
}
|
||||
if (messages_themeDefined === undefined) {
|
||||
messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[
|
||||
Object.keys(messages_themeDefined_by_languageTag)[0]
|
||||
];
|
||||
}
|
||||
if (messages_themeDefined === undefined) {
|
||||
break add_theme_defined_messages;
|
||||
}
|
||||
|
||||
for (const [key, messageOrMessageByThemeName] of Object.entries(
|
||||
messages_themeDefined
|
||||
)) {
|
||||
const message = (() => {
|
||||
if (typeof messageOrMessageByThemeName === "string") {
|
||||
return messageOrMessageByThemeName;
|
||||
}
|
||||
|
||||
const message = messageOrMessageByThemeName[themeName];
|
||||
|
||||
assert(message !== undefined);
|
||||
|
||||
return message;
|
||||
})();
|
||||
|
||||
messages[key] = message;
|
||||
}
|
||||
}
|
||||
|
||||
const propertiesFileSource = [
|
||||
"",
|
||||
...Object.entries(messages).map(
|
||||
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
||||
),
|
||||
""
|
||||
].join("\n");
|
||||
|
||||
fs.mkdirSync(messageDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(messageDirPath, `messages_${languageTag}.properties`),
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function recastParseTs(filePath: string): recast.types.ASTNode {
|
||||
return recast.parse(fs.readFileSync(filePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"]
|
||||
}),
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,57 @@
|
||||
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";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
extname as pathExtname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import {
|
||||
generateFtlFilesCodeFactory,
|
||||
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
||||
} from "../generateFtl";
|
||||
import {
|
||||
type ThemeType,
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||
THEME_TYPES
|
||||
} from "../../shared/constants";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import {
|
||||
generateMessageProperties,
|
||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||
} from "./generateMessageProperties";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import * as child_process from "child_process";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import propertiesParser from "properties-parser";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generateResourcesForMainTheme & {
|
||||
themeNames: string[];
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_generateMessageProperties & {
|
||||
themeNames: string[];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
||||
themeSrcDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
@ -20,23 +61,474 @@ export async function generateResources(params: {
|
||||
}): Promise<void> {
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const [themeName, ...themeVariantNames] = buildContext.themeNames;
|
||||
const [themeName] = buildContext.themeNames;
|
||||
|
||||
if (fs.existsSync(resourcesDirPath)) {
|
||||
rmSync(resourcesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await generateResourcesForMainTheme({
|
||||
resourcesDirPath,
|
||||
themeName,
|
||||
buildContext
|
||||
});
|
||||
const getThemeTypeDirPath = (params: {
|
||||
themeType: ThemeType | "email";
|
||||
themeName: string;
|
||||
}) => {
|
||||
const { themeType, themeName } = params;
|
||||
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
||||
};
|
||||
|
||||
for (const themeVariantName of themeVariantNames) {
|
||||
generateResourcesForThemeVariant({
|
||||
resourcesDirPath,
|
||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||
> = {};
|
||||
|
||||
for (const themeType of THEME_TYPES) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const getAccountThemeType = () => {
|
||||
assert(themeType === "account");
|
||||
|
||||
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||
|
||||
return buildContext.implementedThemeTypes.account.type;
|
||||
};
|
||||
|
||||
const isSpa = (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return false;
|
||||
case "account":
|
||||
return getAccountThemeType() === "Single-Page";
|
||||
case "admin":
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
||||
);
|
||||
|
||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||
rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
if (
|
||||
themeType !== "login" &&
|
||||
buildContext.implementedThemeTypes.login.isImplemented
|
||||
) {
|
||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
getThemeTypeDirPath({
|
||||
themeName,
|
||||
themeType: "login"
|
||||
}),
|
||||
"resources",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
{
|
||||
const dirPath = pathJoin(
|
||||
buildContext.projectBuildDirPath,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
||||
);
|
||||
|
||||
if (fs.existsSync(dirPath)) {
|
||||
assert(buildContext.bundler === "webpack");
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_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,
|
||||
themeVariantName
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: isSpa
|
||||
? []
|
||||
: (assert(themeType !== "admin"),
|
||||
readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}))
|
||||
});
|
||||
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return LOGIN_THEME_PAGE_IDS;
|
||||
case "account":
|
||||
return getAccountThemeType() === "Single-Page"
|
||||
? ["index.ftl"]
|
||||
: ACCOUNT_THEME_PAGE_IDS;
|
||||
case "admin":
|
||||
return ["index.ftl"];
|
||||
}
|
||||
})(),
|
||||
...(isSpa
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
let languageTags: string[] | undefined = undefined;
|
||||
|
||||
i18n_messages_generation: {
|
||||
if (isSpa) {
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
assert(themeType !== "admin");
|
||||
|
||||
const wrap = generateMessageProperties({
|
||||
buildContext,
|
||||
themeType
|
||||
});
|
||||
|
||||
languageTags = wrap.languageTags;
|
||||
const { writeMessagePropertiesFiles } = wrap;
|
||||
|
||||
writeMessagePropertiesFilesByThemeType[themeType] =
|
||||
writeMessagePropertiesFiles;
|
||||
}
|
||||
|
||||
bring_in_spas_messages: {
|
||||
if (!isSpa) {
|
||||
break bring_in_spas_messages;
|
||||
}
|
||||
|
||||
assert(themeType !== "login");
|
||||
|
||||
const accountUiDirPath = child_process
|
||||
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||
})
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
|
||||
|
||||
if (!fs.existsSync(messageDirPath_defaults)) {
|
||||
throw new Error(
|
||||
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
|
||||
);
|
||||
}
|
||||
|
||||
const messagesDirPath_dest = pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType }),
|
||||
"messages"
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: messageDirPath_defaults,
|
||||
destDirPath: messagesDirPath_dest
|
||||
});
|
||||
|
||||
apply_theme_changes: {
|
||||
const messagesDirPath_theme = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"messages"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||
break apply_theme_changes;
|
||||
}
|
||||
|
||||
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||
const filePath_src = pathJoin(messagesDirPath_theme, basename);
|
||||
const filePath_dest = pathJoin(messagesDirPath_dest, basename);
|
||||
|
||||
if (!fs.existsSync(filePath_dest)) {
|
||||
fs.cpSync(filePath_src, filePath_dest);
|
||||
}
|
||||
|
||||
const messages_src = propertiesParser.parse(
|
||||
fs.readFileSync(filePath_src).toString("utf8")
|
||||
);
|
||||
const messages_dest = propertiesParser.parse(
|
||||
fs.readFileSync(filePath_dest).toString("utf8")
|
||||
);
|
||||
|
||||
const messages = {
|
||||
...messages_dest,
|
||||
...messages_src
|
||||
};
|
||||
|
||||
const editor = propertiesParser.createEditor();
|
||||
|
||||
Object.entries(messages).forEach(([key, value]) => {
|
||||
editor.set(key, value);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath_dest,
|
||||
Buffer.from(editor.toString(), "utf8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
languageTags = fs
|
||||
.readdirSync(messagesDirPath_dest)
|
||||
.map(basename =>
|
||||
basename.replace(/^messages_/, "").replace(/\.properties$/, "")
|
||||
);
|
||||
}
|
||||
|
||||
keycloak_static_resources: {
|
||||
if (isSpa) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
||||
themeType
|
||||
),
|
||||
destDirPath: pathJoin(themeTypeDirPath, "resources")
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
switch (getAccountThemeType()) {
|
||||
case "Multi-Page":
|
||||
return "account-v1";
|
||||
case "Single-Page":
|
||||
return "base";
|
||||
}
|
||||
case "login":
|
||||
return "keycloak";
|
||||
case "admin":
|
||||
return "base";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(themeType === "account" && getAccountThemeType() === "Single-Page"
|
||||
? ["deprecatedMode=false"]
|
||||
: []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
),
|
||||
...(languageTags === undefined
|
||||
? []
|
||||
: [`locales=${languageTags.join(",")}`])
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
email: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break email;
|
||||
}
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v1: {
|
||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: "account-v1",
|
||||
themeType: "account"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: themeName,
|
||||
types: objectEntries(buildContext.implementedThemeTypes)
|
||||
.filter(([, { isImplemented }]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
}
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: "account-v1",
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
|
||||
for (const themeVariantName of buildContext.themeNames) {
|
||||
if (themeVariantName === themeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
|
||||
destDirPath: pathJoin(resourcesDirPath, "theme", 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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
|
||||
writeMessagePropertiesFilesByThemeType
|
||||
)) {
|
||||
// NOTE: This is just a quirk of the type system: We can't really differentiate in a record
|
||||
// between the case where the key isn't present and the case where the value is `undefined`.
|
||||
if (writeMessagePropertiesFiles === undefined) {
|
||||
return;
|
||||
}
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType }),
|
||||
"messages"
|
||||
),
|
||||
themeName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modify_email_theme_per_variant: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break modify_email_theme_per_variant;
|
||||
}
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
const emailThemeDirPath = getThemeTypeDirPath({
|
||||
themeName,
|
||||
themeType: "email"
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeDirPath,
|
||||
destDirPath: emailThemeDirPath,
|
||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||
if (!filePath.endsWith(".ftl")) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,366 +0,0 @@
|
||||
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 }[];
|
||||
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
||||
themeSrcDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isForAccountSpa =
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
||||
|
||||
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.implementedThemeTypes.login.isImplemented
|
||||
) {
|
||||
// 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 === "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 isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
||||
}
|
||||
})(),
|
||||
...(isForAccountSpa
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
i18n_messages_generation: {
|
||||
if (isForAccountSpa) {
|
||||
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 (isForAccountSpa) {
|
||||
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 isForAccountSpa ? "base" : ACCOUNT_V1_THEME_NAME;
|
||||
case "login":
|
||||
return "keycloak";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
)
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
email: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break email;
|
||||
}
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v1: {
|
||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
await bringInAccountV1({
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v3_i18n_messages: {
|
||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
break bring_in_account_v3_i18n_messages;
|
||||
}
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
|
||||
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.implementedThemeTypes)
|
||||
.filter(([, { isImplemented }]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: ACCOUNT_V1_THEME_NAME,
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
themeType: Exclude<ThemeType, "admin">;
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
|
@ -2,16 +2,17 @@ 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 { getBuildContext } from "../shared/buildContext";
|
||||
import type { BuildContext } 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 }) {
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
exit_if_maven_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
||||
@ -25,31 +26,44 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
break exit_if_maven_not_installed;
|
||||
}
|
||||
|
||||
const installationCommand = (() => {
|
||||
switch (os.platform()) {
|
||||
case "darwin":
|
||||
return "brew install mvn";
|
||||
case "win32":
|
||||
return "choco install mvn";
|
||||
case "linux":
|
||||
default:
|
||||
return "sudo apt-get install mvn";
|
||||
}
|
||||
})();
|
||||
if (
|
||||
fs
|
||||
.readFileSync(buildContext.packageJsonFilePath)
|
||||
.toString("utf8")
|
||||
.includes(`"mvn"`)
|
||||
) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
"Please remove the 'mvn' package from your package.json'dependencies list,",
|
||||
"reinstall your dependencies and try again.",
|
||||
"We need the Apache Maven CLI, not this: https://www.npmjs.com/package/mvn"
|
||||
].join(" ")
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const installationCommand = (() => {
|
||||
switch (os.platform()) {
|
||||
case "darwin":
|
||||
return "brew install mvn";
|
||||
case "win32":
|
||||
return "choco install mvn";
|
||||
case "linux":
|
||||
default:
|
||||
return "sudo apt-get install mvn";
|
||||
}
|
||||
})();
|
||||
|
||||
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 buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
console.log(
|
||||
[
|
||||
chalk.cyan(`keycloakify v${readThisNpmPackageVersion()}`),
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../shared/constants";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { posix } from "path";
|
||||
|
||||
@ -50,7 +50,7 @@ export function replaceImportsInCssCode(params: {
|
||||
break inline_style_in_html;
|
||||
}
|
||||
|
||||
return `url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}${assetFileAbsoluteUrlPathname}")`;
|
||||
return `url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}${assetFileAbsoluteUrlPathname}")`;
|
||||
}
|
||||
|
||||
const assetFileRelativeUrlPathname = posix.relative(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../../shared/buildContext";
|
||||
import * as nodePath from "path";
|
||||
@ -85,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: {
|
||||
fixedJsCode = replaceAll(
|
||||
fixedJsCode,
|
||||
`"${relativePathOfAssetFile}"`,
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")`
|
||||
);
|
||||
|
||||
fixedJsCode = replaceAll(
|
||||
fixedJsCode,
|
||||
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
|
||||
`(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../../shared/buildContext";
|
||||
import * as nodePath from "path";
|
||||
@ -90,7 +90,7 @@ export function replaceImportsInJsCode_webpack(params: {
|
||||
return "${u}";
|
||||
})()] = ${
|
||||
isArrowFunction ? `${e} =>` : `function(${e}) { return `
|
||||
} "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}${language}/"`
|
||||
} "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}${language}/"`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
@ -104,7 +104,7 @@ export function replaceImportsInJsCode_webpack(params: {
|
||||
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
|
||||
"g"
|
||||
),
|
||||
`window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}`
|
||||
`window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
|
@ -4,8 +4,9 @@ import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
|
||||
export type CliCommandOptions = {
|
||||
type CliCommandOptions = {
|
||||
projectDirPath: string | undefined;
|
||||
};
|
||||
|
||||
@ -69,10 +70,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./keycloakify");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -130,10 +131,13 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||
const { command } = await import("./start-keycloak");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -144,10 +148,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./eject-page");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -158,10 +162,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./add-story");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -172,10 +176,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./initialize-email-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -186,10 +190,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./initialize-account-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -201,10 +205,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -216,10 +220,60 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./update-kc-gen");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "postinstall",
|
||||
description: "Initialize all the Keycloakify UI modules installed in the project."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./postinstall");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command<{
|
||||
file: string;
|
||||
}>({
|
||||
name: "eject-file",
|
||||
description: [
|
||||
"WARNING: Not usable yet, will be used for future features",
|
||||
"Take ownership over a given file"
|
||||
].join(" ")
|
||||
})
|
||||
.option({
|
||||
key: "file",
|
||||
name: (() => {
|
||||
const name = "file";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
description: [
|
||||
"Relative path of the file relative to the directory of your keycloak theme source",
|
||||
"Example `--file src/login/page/Login.tsx`"
|
||||
].join(" ")
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath, file }) => {
|
||||
const { command } = await import("./eject-file");
|
||||
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: { file }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||
|
||||
export type BuildContextLike = {
|
||||
themeSrcDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
|
||||
buildContext: BuildContextLike;
|
||||
fileRelativePath: string;
|
||||
isForEjection: boolean;
|
||||
uiModuleDirPath: string;
|
||||
uiModuleName: string;
|
||||
uiModuleVersion: string;
|
||||
}): Promise<Buffer> {
|
||||
const {
|
||||
buildContext,
|
||||
uiModuleDirPath,
|
||||
fileRelativePath,
|
||||
isForEjection,
|
||||
uiModuleName,
|
||||
uiModuleVersion
|
||||
} = params;
|
||||
|
||||
let sourceCode = (
|
||||
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
|
||||
).toString("utf8");
|
||||
|
||||
const comment = (() => {
|
||||
if (isForEjection) {
|
||||
return [
|
||||
`/*`,
|
||||
`This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`,
|
||||
`*/`
|
||||
].join("\n");
|
||||
} else {
|
||||
return [
|
||||
`/*`,
|
||||
`WARNING: Before modifying this file run the following command:`,
|
||||
``,
|
||||
`npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}\``,
|
||||
``,
|
||||
`This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
|
||||
`*/`
|
||||
];
|
||||
}
|
||||
})();
|
||||
|
||||
sourceCode = [comment, ``, sourceCode].join("\n");
|
||||
|
||||
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
||||
|
||||
format: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break format;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: destFilePath,
|
||||
sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
return Buffer.from(sourceCode, "utf8");
|
||||
}
|
1
src/bin/postinstall/index.ts
Normal file
1
src/bin/postinstall/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./postinstall";
|
157
src/bin/postinstall/installUiModulesPeerDependencies.ts
Normal file
157
src/bin/postinstall/installUiModulesPeerDependencies.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||
import { z } from "zod";
|
||||
import { id } from "tsafe/id";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { same } from "evt/tools/inDepth/same";
|
||||
import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
|
||||
import { npmInstall } from "../tools/npmInstall";
|
||||
|
||||
export type BuildContextLike = {
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export type UiModuleMetaLike = {
|
||||
moduleName: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
|
||||
|
||||
export async function installUiModulesPeerDependencies(params: {
|
||||
buildContext: BuildContextLike;
|
||||
uiModuleMetas: UiModuleMetaLike[];
|
||||
}): Promise<void | never> {
|
||||
const { buildContext, uiModuleMetas } = params;
|
||||
|
||||
const { uiModulesPerDependencies } = (() => {
|
||||
const uiModulesPerDependencies: Record<string, string> = {};
|
||||
|
||||
for (const { peerDependencies } of uiModuleMetas) {
|
||||
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||
peerDependencies
|
||||
)) {
|
||||
const versionRange = (() => {
|
||||
const versionRange_current =
|
||||
uiModulesPerDependencies[peerDependencyName];
|
||||
|
||||
if (versionRange_current === undefined) {
|
||||
return versionRange_candidate;
|
||||
}
|
||||
|
||||
if (versionRange_current === "*") {
|
||||
return versionRange_candidate;
|
||||
}
|
||||
|
||||
if (versionRange_candidate === "*") {
|
||||
return versionRange_current;
|
||||
}
|
||||
|
||||
const { versionRange } = [
|
||||
versionRange_current,
|
||||
versionRange_candidate
|
||||
]
|
||||
.map(versionRange => ({
|
||||
versionRange,
|
||||
semVer: SemVer.parse(
|
||||
(() => {
|
||||
if (
|
||||
versionRange.startsWith("^") ||
|
||||
versionRange.startsWith("~")
|
||||
) {
|
||||
return versionRange.slice(1);
|
||||
}
|
||||
|
||||
return versionRange;
|
||||
})()
|
||||
)
|
||||
}))
|
||||
.sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0];
|
||||
|
||||
return versionRange;
|
||||
})();
|
||||
|
||||
uiModulesPerDependencies[peerDependencyName] = versionRange;
|
||||
}
|
||||
}
|
||||
|
||||
return { uiModulesPerDependencies };
|
||||
})();
|
||||
|
||||
const parsedPackageJson = await (async () => {
|
||||
type ParsedPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zParsedPackageJson>;
|
||||
|
||||
assert<Equals<InferredType, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zParsedPackageJson);
|
||||
})();
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
(await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
||||
|
||||
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
|
||||
if (moduleName.startsWith("@types/")) {
|
||||
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsedPackageJson.devDependencies !== undefined) {
|
||||
delete parsedPackageJson.devDependencies[moduleName];
|
||||
}
|
||||
|
||||
(parsedPackageJson.dependencies ??= {})[moduleName] = versionRange;
|
||||
}
|
||||
|
||||
if (same(parsedPackageJson, parsedPackageJson_before)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2);
|
||||
|
||||
format: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break format;
|
||||
}
|
||||
|
||||
packageJsonContentStr = await runPrettier({
|
||||
sourceCode: packageJsonContentStr,
|
||||
filePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
}
|
||||
|
||||
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
||||
|
||||
npmInstall({
|
||||
packageJsonDirPath: buildContext.packageJsonFilePath
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
}
|
135
src/bin/postinstall/managedGitignoreFile.ts
Normal file
135
src/bin/postinstall/managedGitignoreFile.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import * as fsPr from "fs/promises";
|
||||
import {
|
||||
join as pathJoin,
|
||||
sep as pathSep,
|
||||
dirname as pathDirname,
|
||||
relative as pathRelative
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
|
||||
export type BuildContextLike = {
|
||||
themeSrcDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
const DELIMITER_START = `# === Ejected files start ===`;
|
||||
const DELIMITER_END = `# === Ejected files end =====`;
|
||||
|
||||
export async function writeManagedGitignoreFile(params: {
|
||||
buildContext: BuildContextLike;
|
||||
uiModuleMetas: UiModuleMeta[];
|
||||
ejectedFilesRelativePaths: string[];
|
||||
}): Promise<void> {
|
||||
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
|
||||
|
||||
if (uiModuleMetas.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||
|
||||
const content_new = Buffer.from(
|
||||
[
|
||||
`# This file is managed by Keycloakify, do not edit it manually.`,
|
||||
``,
|
||||
DELIMITER_START,
|
||||
...ejectedFilesRelativePaths.map(fileRelativePath =>
|
||||
fileRelativePath.split(pathSep).join("/")
|
||||
),
|
||||
DELIMITER_END,
|
||||
``,
|
||||
...uiModuleMetas
|
||||
.map(uiModuleMeta => [
|
||||
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
|
||||
...uiModuleMeta.files
|
||||
.map(({ fileRelativePath }) => fileRelativePath)
|
||||
.filter(
|
||||
fileRelativePath =>
|
||||
!ejectedFilesRelativePaths.includes(fileRelativePath)
|
||||
)
|
||||
.map(
|
||||
fileRelativePath =>
|
||||
`/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}`
|
||||
),
|
||||
|
||||
``
|
||||
])
|
||||
.flat()
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const content_current = await (async () => {
|
||||
if (!(await existsAsync(filePath))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await fsPr.readFile(filePath);
|
||||
})();
|
||||
|
||||
if (content_current !== undefined && content_current.equals(content_new)) {
|
||||
return;
|
||||
}
|
||||
|
||||
create_dir: {
|
||||
const dirPath = pathDirname(filePath);
|
||||
|
||||
if (await existsAsync(dirPath)) {
|
||||
break create_dir;
|
||||
}
|
||||
|
||||
await fsPr.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await fsPr.writeFile(filePath, content_new);
|
||||
}
|
||||
|
||||
export async function readManagedGitignoreFile(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
ejectedFilesRelativePaths: string[];
|
||||
}> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||
|
||||
if (!(await existsAsync(filePath))) {
|
||||
return { ejectedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
||||
|
||||
const payload = (() => {
|
||||
const index_start = contentStr.indexOf(DELIMITER_START);
|
||||
const index_end = contentStr.indexOf(DELIMITER_END);
|
||||
|
||||
if (index_start === -1 || index_end === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim();
|
||||
})();
|
||||
|
||||
if (payload === undefined) {
|
||||
return { ejectedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
const ejectedFilesRelativePaths = payload
|
||||
.split("\n")
|
||||
.map(line => line.trim())
|
||||
.filter(line => line !== "")
|
||||
.map(line =>
|
||||
getAbsoluteAndInOsFormatPath({
|
||||
cwd: buildContext.themeSrcDirPath,
|
||||
pathIsh: line
|
||||
})
|
||||
)
|
||||
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
||||
|
||||
return { ejectedFilesRelativePaths };
|
||||
}
|
79
src/bin/postinstall/postinstall.ts
Normal file
79
src/bin/postinstall/postinstall.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
|
||||
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
|
||||
import {
|
||||
readManagedGitignoreFile,
|
||||
writeManagedGitignoreFile
|
||||
} from "./managedGitignoreFile";
|
||||
import { dirname as pathDirname } from "path";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
||||
|
||||
await installUiModulesPeerDependencies({
|
||||
buildContext,
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||
buildContext
|
||||
});
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
ejectedFilesRelativePaths,
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
uiModuleMetas
|
||||
.map(uiModuleMeta =>
|
||||
Promise.all(
|
||||
uiModuleMeta.files.map(
|
||||
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
||||
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destFilePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
fileRelativePath
|
||||
);
|
||||
|
||||
skip_condition: {
|
||||
if (!(await existsAsync(destFilePath))) {
|
||||
break skip_condition;
|
||||
}
|
||||
|
||||
const destFileHash = computeHash(
|
||||
await fsPr.readFile(destFilePath)
|
||||
);
|
||||
|
||||
if (destFileHash !== hash) {
|
||||
break skip_condition;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const dirName = pathDirname(copyableFilePath);
|
||||
|
||||
if (!(await existsAsync(dirName))) {
|
||||
await fsPr.mkdir(dirName, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
await fsPr.copyFile(copyableFilePath, destFilePath);
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
}
|
303
src/bin/postinstall/uiModuleMeta.ts
Normal file
303
src/bin/postinstall/uiModuleMeta.ts
Normal file
@ -0,0 +1,303 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import { z } from "zod";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as fsPr from "fs/promises";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { is } from "tsafe/is";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { listInstalledModules } from "../tools/listInstalledModules";
|
||||
import { crawlAsync } from "../tools/crawlAsync";
|
||||
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
getUiModuleFileSourceCodeReadyToBeCopied,
|
||||
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
|
||||
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
|
||||
import * as crypto from "crypto";
|
||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||
|
||||
export type UiModuleMeta = {
|
||||
moduleName: string;
|
||||
version: string;
|
||||
files: {
|
||||
fileRelativePath: string;
|
||||
hash: string;
|
||||
copyableFilePath: string;
|
||||
}[];
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
const zUiModuleMeta = (() => {
|
||||
type ExpectedType = UiModuleMeta;
|
||||
|
||||
const zTargetType = z.object({
|
||||
moduleName: z.string(),
|
||||
version: z.string(),
|
||||
files: z.array(
|
||||
z.object({
|
||||
fileRelativePath: z.string(),
|
||||
hash: z.string(),
|
||||
copyableFilePath: z.string()
|
||||
})
|
||||
),
|
||||
peerDependencies: z.record(z.string())
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<InferredType, ExpectedType>>();
|
||||
|
||||
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||
})();
|
||||
|
||||
type ParsedCacheFile = {
|
||||
keycloakifyVersion: string;
|
||||
prettierConfigHash: string | null;
|
||||
thisFilePath: string;
|
||||
uiModuleMetas: UiModuleMeta[];
|
||||
};
|
||||
|
||||
const zParsedCacheFile = (() => {
|
||||
type ExpectedType = ParsedCacheFile;
|
||||
|
||||
const zTargetType = z.object({
|
||||
keycloakifyVersion: z.string(),
|
||||
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||
thisFilePath: z.string(),
|
||||
uiModuleMetas: z.array(zUiModuleMeta)
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<InferredType, ExpectedType>>();
|
||||
|
||||
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
|
||||
|
||||
export type BuildContextLike =
|
||||
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
|
||||
cacheDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
projectDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function getUiModuleMetas(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<UiModuleMeta[]> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
||||
|
||||
const keycloakifyVersion = readThisNpmPackageVersion();
|
||||
|
||||
const prettierConfigHash = await (async () => {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { configHash } = await getPrettier();
|
||||
|
||||
return configHash;
|
||||
})();
|
||||
|
||||
const installedUiModules = await (async () => {
|
||||
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||
projectDirPath: buildContext.packageJsonFilePath,
|
||||
filter: ({ moduleName }) =>
|
||||
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
installedModulesWithKeycloakifyInTheName.filter(async ({ dirPath }) =>
|
||||
existsAsync(pathJoin(dirPath, KEYCLOAK_THEME))
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
||||
const cacheContent = await (async () => {
|
||||
if (!(await existsAsync(cacheFilePath))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await fsPr.readFile(cacheFilePath);
|
||||
})();
|
||||
|
||||
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
|
||||
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
|
||||
if (cacheContent === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cacheContentStr = cacheContent.toString("utf8");
|
||||
|
||||
let parsedCacheFile: unknown;
|
||||
|
||||
try {
|
||||
parsedCacheFile = JSON.parse(cacheContentStr);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
zParsedCacheFile.parse(parsedCacheFile);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
assert(is<ParsedCacheFile>(parsedCacheFile));
|
||||
|
||||
return parsedCacheFile;
|
||||
})();
|
||||
|
||||
if (parsedCacheFile === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
|
||||
uiModuleMeta => {
|
||||
const correspondingInstalledUiModule = installedUiModules.find(
|
||||
installedUiModule =>
|
||||
installedUiModule.moduleName === uiModuleMeta.moduleName
|
||||
);
|
||||
|
||||
if (correspondingInstalledUiModule === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return correspondingInstalledUiModule.version === uiModuleMeta.version;
|
||||
}
|
||||
);
|
||||
|
||||
return uiModuleMetas_cacheUpToDate;
|
||||
})();
|
||||
|
||||
const uiModuleMetas = await Promise.all(
|
||||
installedUiModules.map(
|
||||
async ({
|
||||
moduleName,
|
||||
version,
|
||||
peerDependencies,
|
||||
dirPath
|
||||
}): Promise<UiModuleMeta> => {
|
||||
use_cache: {
|
||||
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
|
||||
uiModuleMeta => uiModuleMeta.moduleName === moduleName
|
||||
);
|
||||
|
||||
if (uiModuleMeta_cache === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return uiModuleMeta_cache;
|
||||
}
|
||||
|
||||
const files: UiModuleMeta["files"] = [];
|
||||
|
||||
{
|
||||
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||
|
||||
await crawlAsync({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath",
|
||||
onFileFound: async fileRelativePath => {
|
||||
const sourceCode =
|
||||
await getUiModuleFileSourceCodeReadyToBeCopied({
|
||||
buildContext,
|
||||
fileRelativePath,
|
||||
isForEjection: false,
|
||||
uiModuleDirPath: dirPath,
|
||||
uiModuleName: moduleName,
|
||||
uiModuleVersion: version
|
||||
});
|
||||
|
||||
const hash = computeHash(sourceCode);
|
||||
|
||||
const copyableFilePath = pathJoin(
|
||||
pathDirname(cacheFilePath),
|
||||
KEYCLOAK_THEME,
|
||||
fileRelativePath
|
||||
);
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(copyableFilePath);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
await fsPr.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fsPr.writeFile(copyableFilePath, sourceCode);
|
||||
|
||||
files.push({
|
||||
fileRelativePath,
|
||||
hash,
|
||||
copyableFilePath
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return id<UiModuleMeta>({
|
||||
moduleName,
|
||||
version,
|
||||
files,
|
||||
peerDependencies
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
update_cache: {
|
||||
const parsedCacheFile = id<ParsedCacheFile>({
|
||||
keycloakifyVersion,
|
||||
prettierConfigHash,
|
||||
thisFilePath: cacheFilePath,
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
const cacheContent_new = Buffer.from(
|
||||
JSON.stringify(parsedCacheFile, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) {
|
||||
break update_cache;
|
||||
}
|
||||
|
||||
create_dir: {
|
||||
const dirPath = pathDirname(cacheFilePath);
|
||||
|
||||
if (await existsAsync(dirPath)) {
|
||||
break create_dir;
|
||||
}
|
||||
|
||||
await fsPr.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await fsPr.writeFile(cacheFilePath, cacheContent_new);
|
||||
}
|
||||
|
||||
return uiModuleMetas;
|
||||
}
|
||||
|
||||
export function computeHash(data: Buffer) {
|
||||
return crypto.createHash("sha256").update(data).digest("hex");
|
||||
}
|
@ -3,7 +3,7 @@ export type KeycloakVersionRange =
|
||||
| KeycloakVersionRange.WithoutAccountV1Theme;
|
||||
|
||||
export namespace KeycloakVersionRange {
|
||||
export type WithoutAccountV1Theme = "21-and-below" | "22-and-above";
|
||||
export type WithoutAccountV1Theme = "22-to-25" | "all-other-versions";
|
||||
|
||||
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
|
||||
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25" | "26-and-above";
|
||||
}
|
||||
|
@ -7,25 +7,23 @@ import {
|
||||
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,
|
||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
|
||||
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 { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { type ThemeType } from "./constants";
|
||||
import { id } from "tsafe/id";
|
||||
import chalk from "chalk";
|
||||
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
export type BuildContext = {
|
||||
themeVersion: string;
|
||||
@ -33,7 +31,6 @@ export type BuildContext = {
|
||||
extraThemeProperties: string[] | undefined;
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
@ -44,7 +41,7 @@ export type BuildContext = {
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
fetchOptions: ProxyFetchOptions;
|
||||
fetchOptions: FetchOptionsLike;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
@ -54,6 +51,7 @@ export type BuildContext = {
|
||||
account:
|
||||
| { isImplemented: false }
|
||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||
admin: { isImplemented: boolean };
|
||||
};
|
||||
packageJsonFilePath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
@ -85,7 +83,6 @@ export type BuildOptions = {
|
||||
extraThemeProperties?: string[];
|
||||
artifactId?: string;
|
||||
groupId?: string;
|
||||
loginThemeResourcesFromKeycloakVersion?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
kcContextExclusionsFtl?: string;
|
||||
startKeycloakOptions?: {
|
||||
@ -131,14 +128,12 @@ export type ResolvedViteConfig = {
|
||||
};
|
||||
|
||||
export function getBuildContext(params: {
|
||||
cliCommandOptions: CliCommandOptions;
|
||||
projectDirPath: string | undefined;
|
||||
}): BuildContext {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const projectDirPath =
|
||||
cliCommandOptions.projectDirPath !== undefined
|
||||
params.projectDirPath !== undefined
|
||||
? getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
pathIsh: params.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
})
|
||||
: process.cwd();
|
||||
@ -151,7 +146,10 @@ export function getBuildContext(params: {
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
||||
for (const themeSrcDirBasename of [
|
||||
KEYCLOAK_THEME,
|
||||
KEYCLOAK_THEME.replace(/-/g, "_")
|
||||
]) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
@ -177,7 +175,7 @@ export function getBuildContext(params: {
|
||||
[
|
||||
`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`
|
||||
`If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
@ -243,8 +241,7 @@ export function getBuildContext(params: {
|
||||
|
||||
if (
|
||||
parsedPackageJson.dependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.devDependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
|
||||
parsedPackageJson.devDependencies?.keycloakify === undefined
|
||||
) {
|
||||
break success;
|
||||
}
|
||||
@ -280,7 +277,8 @@ export function getBuildContext(params: {
|
||||
"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()])
|
||||
"25": z.union([z.boolean(), z.string()]),
|
||||
"26-and-above": z.union([z.boolean(), z.string()])
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
@ -301,8 +299,8 @@ export function getBuildContext(params: {
|
||||
]),
|
||||
keycloakVersionTargets: z
|
||||
.object({
|
||||
"21-and-below": z.union([z.boolean(), z.string()]),
|
||||
"22-and-above": z.union([z.boolean(), z.string()])
|
||||
"22-to-25": z.union([z.boolean(), z.string()]),
|
||||
"all-other-versions": z.union([z.boolean(), z.string()])
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
@ -357,7 +355,6 @@ export function getBuildContext(params: {
|
||||
extraThemeProperties: z.array(z.string()).optional(),
|
||||
artifactId: z.string().optional(),
|
||||
groupId: z.string().optional(),
|
||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||
keycloakifyBuildDirPath: z.string().optional(),
|
||||
kcContextExclusionsFtl: z.string().optional(),
|
||||
startKeycloakOptions: zStartKeycloakOptions.optional()
|
||||
@ -454,7 +451,10 @@ export function getBuildContext(params: {
|
||||
isImplemented: true,
|
||||
type: buildOptions.accountThemeImplementation
|
||||
};
|
||||
})()
|
||||
})(),
|
||||
admin: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
@ -474,26 +474,53 @@ export function getBuildContext(params: {
|
||||
}
|
||||
|
||||
const themeNames = ((): [string, ...string[]] => {
|
||||
if (buildOptions.themeName === undefined) {
|
||||
return parsedPackageJson.name === undefined
|
||||
? ["keycloakify"]
|
||||
: [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-")
|
||||
];
|
||||
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];
|
||||
})();
|
||||
|
||||
for (const themeName of themeNames) {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(themeName)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
[
|
||||
`Invalid theme name: ${themeName}`,
|
||||
`Theme names should only contain letters, numbers, and "_" or "-"`
|
||||
].join(" ")
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof buildOptions.themeName === "string") {
|
||||
return [buildOptions.themeName];
|
||||
return themeNames;
|
||||
})();
|
||||
|
||||
const relativePathsCwd = (() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return projectDirPath;
|
||||
case "webpack":
|
||||
return pathDirname(packageJsonFilePath);
|
||||
}
|
||||
|
||||
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
|
||||
|
||||
assert(mainThemeName !== undefined);
|
||||
|
||||
return [mainThemeName, ...themeVariantNames];
|
||||
})();
|
||||
|
||||
const projectBuildDirPath = (() => {
|
||||
@ -507,7 +534,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -545,16 +572,13 @@ export function getBuildContext(params: {
|
||||
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
|
||||
buildOptions.artifactId ??
|
||||
`${themeNames[0]}-keycloak-theme`,
|
||||
loginThemeResourcesFromKeycloakVersion:
|
||||
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||
projectDirPath,
|
||||
projectBuildDirPath,
|
||||
keycloakifyBuildDirPath: (() => {
|
||||
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -583,7 +607,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -655,7 +679,7 @@ export function getBuildContext(params: {
|
||||
pathIsh:
|
||||
parsedPackageJson.keycloakify
|
||||
.staticDirPathInProjectBuildDirPath,
|
||||
cwd: projectBuildDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -756,7 +780,11 @@ export function getBuildContext(params: {
|
||||
return "24" as const;
|
||||
}
|
||||
|
||||
return "25-and-above" as const;
|
||||
if (buildForKeycloakMajorVersionNumber === 25) {
|
||||
return "25" as const;
|
||||
}
|
||||
|
||||
return "26-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
@ -769,11 +797,14 @@ export function getBuildContext(params: {
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (buildForKeycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
if (
|
||||
buildForKeycloakMajorVersionNumber <= 21 ||
|
||||
buildForKeycloakMajorVersionNumber >= 26
|
||||
) {
|
||||
return "all-other-versions" as const;
|
||||
}
|
||||
|
||||
return "22-and-above" as const;
|
||||
return "22-to-25" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
@ -791,6 +822,12 @@ export function getBuildContext(params: {
|
||||
use_custom_jar_basename: {
|
||||
const { keycloakVersionTargets } = buildOptions;
|
||||
|
||||
assert(
|
||||
is<Record<KeycloakVersionRange, string | boolean>>(
|
||||
keycloakVersionTargets
|
||||
)
|
||||
);
|
||||
|
||||
if (keycloakVersionTargets === undefined) {
|
||||
break use_custom_jar_basename;
|
||||
}
|
||||
@ -835,7 +872,8 @@ export function getBuildContext(params: {
|
||||
"21-and-below",
|
||||
"23",
|
||||
"24",
|
||||
"25-and-above"
|
||||
"25",
|
||||
"26-and-above"
|
||||
] as const) {
|
||||
assert<
|
||||
Equals<
|
||||
@ -851,8 +889,8 @@ export function getBuildContext(params: {
|
||||
}
|
||||
} else {
|
||||
for (const keycloakVersionRange of [
|
||||
"21-and-below",
|
||||
"22-and-above"
|
||||
"22-to-25",
|
||||
"all-other-versions"
|
||||
] as const) {
|
||||
assert<
|
||||
Equals<
|
||||
@ -878,7 +916,17 @@ export function getBuildContext(params: {
|
||||
const jarTargets: BuildContext["jarTargets"] = [];
|
||||
|
||||
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(
|
||||
buildOptions.keycloakVersionTargets
|
||||
(() => {
|
||||
const { keycloakVersionTargets } = buildOptions;
|
||||
|
||||
assert(
|
||||
is<Record<KeycloakVersionRange, string | boolean>>(
|
||||
keycloakVersionTargets
|
||||
)
|
||||
);
|
||||
|
||||
return keycloakVersionTargets;
|
||||
})()
|
||||
)) {
|
||||
if (jarNameOrBoolean === false) {
|
||||
continue;
|
||||
@ -959,7 +1007,7 @@ export function getBuildContext(params: {
|
||||
type: "path",
|
||||
path: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: urlOrPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -969,7 +1017,7 @@ export function getBuildContext(params: {
|
||||
? undefined
|
||||
: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
}),
|
||||
port: buildOptions.startKeycloakOptions?.port
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
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 WELL_KNOWN_DIRECTORY_BASE_NAME = {
|
||||
KEYCLOAKIFY_DEV_RESOURCES: "keycloakify-dev-resources",
|
||||
RESOURCES_COMMON: "resources-common",
|
||||
DIST: "dist"
|
||||
} as const;
|
||||
|
||||
export const THEME_TYPES = ["login", "account"] as const;
|
||||
export const ACCOUNT_V1_THEME_NAME = "account-v1";
|
||||
export const THEME_TYPES = ["login", "account", "admin"] as const;
|
||||
|
||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||
|
||||
@ -50,7 +50,9 @@ export const LOGIN_THEME_PAGE_IDS = [
|
||||
"login-recovery-authn-code-input.ftl",
|
||||
"login-reset-otp.ftl",
|
||||
"login-x509-info.ftl",
|
||||
"webauthn-error.ftl"
|
||||
"webauthn-error.ftl",
|
||||
"login-passkeys-conditional-authenticate.ftl",
|
||||
"login-idp-link-confirm-override.ftl"
|
||||
] as const;
|
||||
|
||||
export const ACCOUNT_THEME_PAGE_IDS = [
|
||||
@ -70,4 +72,9 @@ export const CONTAINER_NAME = "keycloak-keycloakify";
|
||||
|
||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||
|
||||
export const LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
||||
export const CUSTOM_HANDLER_ENV_NAMES = {
|
||||
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
|
||||
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
|
||||
};
|
||||
|
||||
export const KEYCLOAK_THEME = "keycloak-theme";
|
||||
|
@ -1,101 +0,0 @@
|
||||
import {
|
||||
downloadKeycloakStaticResources,
|
||||
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
|
||||
} from "./downloadKeycloakStaticResources";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
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 { BuildContext } from "./buildContext";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakStaticResources & {
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
publicDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function copyKeycloakResourcesToPublic(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const destDirPath = pathJoin(buildContext.publicDirPath, KEYCLOAK_RESOURCES);
|
||||
|
||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||
|
||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||
{
|
||||
destDirPath,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
buildContext: {
|
||||
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
||||
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
|
||||
fetchOptions: buildContext.fetchOptions
|
||||
}
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
skip_if_already_done: {
|
||||
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs
|
||||
.readFileSync(keycloakifyBuildinfoFilePath)
|
||||
.toString("utf8");
|
||||
|
||||
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
rmSync(destDirPath, { force: true, recursive: true });
|
||||
|
||||
fs.mkdirSync(destDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
for (const themeType of THEME_TYPES) {
|
||||
await downloadKeycloakStaticResources({
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||
case "account":
|
||||
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
|
||||
}
|
||||
})(),
|
||||
themeType,
|
||||
themeDirPath: destDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(destDirPath, "README.txt"),
|
||||
Buffer.from(
|
||||
// prettier-ignore
|
||||
[
|
||||
"This is just a test folder that helps develop",
|
||||
"the login and register page without having to run a Keycloak container\n",
|
||||
"This directory will be automatically excluded from the final build."
|
||||
].join(" ")
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
keycloakifyBuildinfoFilePath,
|
||||
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
||||
);
|
||||
}
|
42
src/bin/shared/customHandler.ts
Normal file
42
src/bin/shared/customHandler.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||
|
||||
export const BIN_NAME = "_keycloakify-custom-handler";
|
||||
|
||||
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
|
||||
|
||||
export type CommandName =
|
||||
| "update-kc-gen"
|
||||
| "eject-page"
|
||||
| "add-story"
|
||||
| "initialize-account-theme"
|
||||
| "initialize-admin-theme"
|
||||
| "initialize-email-theme"
|
||||
| "copy-keycloak-resources-to-public";
|
||||
|
||||
export type ApiVersion = "v1";
|
||||
|
||||
export function readParams(params: { apiVersion: ApiVersion }) {
|
||||
const { apiVersion } = params;
|
||||
|
||||
assert(apiVersion === "v1");
|
||||
|
||||
const commandName = (() => {
|
||||
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
|
||||
|
||||
assert(envValue !== undefined);
|
||||
|
||||
return envValue as CommandName;
|
||||
})();
|
||||
|
||||
const buildContext = (() => {
|
||||
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
|
||||
|
||||
assert(envValue !== undefined);
|
||||
|
||||
return JSON.parse(envValue) as BuildContext;
|
||||
})();
|
||||
|
||||
return { commandName, buildContext };
|
||||
}
|
48
src/bin/shared/customHandler_delegate.ts
Normal file
48
src/bin/shared/customHandler_delegate.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||
import {
|
||||
NOT_IMPLEMENTED_EXIT_CODE,
|
||||
type CommandName,
|
||||
BIN_NAME,
|
||||
ApiVersion
|
||||
} from "./customHandler";
|
||||
import * as child_process from "child_process";
|
||||
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
|
||||
import * as fs from "fs";
|
||||
|
||||
assert<Equals<ApiVersion, "v1">>();
|
||||
|
||||
export function maybeDelegateCommandToCustomHandler(params: {
|
||||
commandName: CommandName;
|
||||
buildContext: BuildContext;
|
||||
}): { hasBeenHandled: boolean } {
|
||||
const { commandName, buildContext } = params;
|
||||
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
|
||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||
return { hasBeenHandled: false };
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execSync(`npx ${BIN_NAME}`, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
|
||||
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
const status = error.status;
|
||||
|
||||
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
|
||||
return { hasBeenHandled: false };
|
||||
}
|
||||
|
||||
process.exit(status);
|
||||
}
|
||||
|
||||
return { hasBeenHandled: true };
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { join as pathJoin } from "path";
|
||||
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 { existsAsync } from "../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme & {};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function downloadKeycloakStaticResources(params: {
|
||||
themeType: ThemeType;
|
||||
themeDirPath: string;
|
||||
keycloakVersion: string;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { themeType, themeDirPath, keycloakVersion, buildContext } = params;
|
||||
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
buildContext
|
||||
});
|
||||
|
||||
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(defaultThemeDirPath, "keycloak", themeType, "resources"),
|
||||
destDirPath: resourcesDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(resourcesDirPath, RESOURCES_COMMON)
|
||||
});
|
||||
}
|
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
|
||||
const { projectDirPath } = params;
|
||||
|
||||
let hasUncommittedChanges: boolean | undefined = undefined;
|
||||
|
||||
try {
|
||||
hasUncommittedChanges =
|
||||
child_process
|
||||
.execSync(`git status --porcelain`, {
|
||||
cwd: projectDirPath
|
||||
})
|
||||
.toString()
|
||||
.trim() !== "";
|
||||
} catch {
|
||||
// Probably not a git repository
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasUncommittedChanges) {
|
||||
return;
|
||||
}
|
||||
console.warn(
|
||||
[
|
||||
chalk.red(
|
||||
"Please commit or stash your changes before running this command.\n"
|
||||
),
|
||||
"This command will modify your project's files so it's better to have a clean working directory",
|
||||
"so that you can easily see what has been changed and revert if needed."
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { z } from "zod";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
themeNames: string[];
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
implementedThemeTypes: Pick<
|
||||
BuildContext["implementedThemeTypes"],
|
||||
"login" | "account"
|
||||
>;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateKcGenTs(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const isReactProject: boolean = await (async () => {
|
||||
const parsedPackageJson = await (async () => {
|
||||
type ParsedPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
return zParsedPackageJson.parse(
|
||||
JSON.parse(
|
||||
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
{
|
||||
...parsedPackageJson.dependencies,
|
||||
...parsedPackageJson.devDependencies
|
||||
}.react !== undefined
|
||||
);
|
||||
})();
|
||||
|
||||
const filePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
`kc.gen.ts${isReactProject ? "x" : ""}`
|
||||
);
|
||||
|
||||
const currentContent = (await existsAsync(filePath))
|
||||
? await fs.readFile(filePath)
|
||||
: undefined;
|
||||
|
||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||
|
||||
const newContent = Buffer.from(
|
||||
[
|
||||
`/* prettier-ignore-start */`,
|
||||
``,
|
||||
`/* eslint-disable */`,
|
||||
``,
|
||||
`// @ts-nocheck`,
|
||||
``,
|
||||
`// noinspection JSUnusedGlobalSymbols`,
|
||||
``,
|
||||
`// This file is auto-generated by Keycloakify`,
|
||||
``,
|
||||
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
|
||||
``,
|
||||
`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
|
||||
)};`,
|
||||
``,
|
||||
`export type KcContext =`,
|
||||
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
||||
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
||||
` ;`,
|
||||
``,
|
||||
`declare global {`,
|
||||
` interface Window {`,
|
||||
` kcContext?: KcContext;`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
...(!isReactProject
|
||||
? []
|
||||
: [
|
||||
hasLoginTheme &&
|
||||
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
||||
hasAccountTheme &&
|
||||
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
||||
``,
|
||||
`export function KcPage(`,
|
||||
` props: {`,
|
||||
` kcContext: KcContext;`,
|
||||
` fallback?: ReactNode;`,
|
||||
` }`,
|
||||
`) {`,
|
||||
` const { kcContext, fallback } = props;`,
|
||||
` return (`,
|
||||
` <Suspense fallback={fallback}>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.themeType) {`,
|
||||
hasLoginTheme &&
|
||||
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
||||
hasAccountTheme &&
|
||||
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
`}`
|
||||
]),
|
||||
``,
|
||||
`/* prettier-ignore-end */`,
|
||||
``
|
||||
]
|
||||
.filter(item => typeof item === "string")
|
||||
.join("\n"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
if (currentContent !== undefined && currentContent.equals(newContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, newContent);
|
||||
|
||||
delete_legacy_file: {
|
||||
if (!isReactProject) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
||||
|
||||
if (!(await existsAsync(legacyFilePath))) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
await fs.unlink(legacyFilePath);
|
||||
}
|
||||
}
|
2400
src/bin/start-keycloak/myrealm-realm-26.json
Normal file
2400
src/bin/start-keycloak/myrealm-realm-26.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,7 @@
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
|
||||
import { CONTAINER_NAME } from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
@ -29,23 +28,26 @@ import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
|
||||
export type CliCommandOptions = CliCommandOptions_common & {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
cliCommandOptions: {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
}) {
|
||||
exit_if_docker_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
let commandOutput: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
commandOutput = child_process.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
});
|
||||
commandOutput = child_process
|
||||
.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
})
|
||||
?.toString("utf8");
|
||||
} catch {}
|
||||
|
||||
if (commandOutput?.toString("utf8").includes("Docker")) {
|
||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
|
||||
@ -86,9 +88,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { cliCommandOptions, buildContext } = params;
|
||||
|
||||
const { dockerImageTag } = await (async () => {
|
||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||
@ -396,12 +396,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
...(realmJsonFilePath === undefined
|
||||
? []
|
||||
: [
|
||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
|
||||
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
|
||||
]),
|
||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||
...extensionJarFilePaths.map(
|
||||
jarFilePath =>
|
||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
||||
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
||||
),
|
||||
...(keycloakMajorVersionNumber <= 20
|
||||
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
||||
@ -409,13 +409,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
...[
|
||||
...buildContext.themeNames,
|
||||
...(fs.existsSync(
|
||||
pathJoin(
|
||||
buildContext.keycloakifyBuildDirPath,
|
||||
"theme",
|
||||
ACCOUNT_V1_THEME_NAME
|
||||
)
|
||||
pathJoin(buildContext.keycloakifyBuildDirPath, "theme", "account-v1")
|
||||
)
|
||||
? [ACCOUNT_V1_THEME_NAME]
|
||||
? ["account-v1"]
|
||||
: [])
|
||||
]
|
||||
.map(themeName => ({
|
||||
@ -428,7 +424,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
}))
|
||||
.map(
|
||||
({ localDirPath, containerDirPath }) =>
|
||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
|
||||
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
|
||||
),
|
||||
...buildContext.environmentVariables
|
||||
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||
|
51
src/bin/tools/crawlAsync.ts
Normal file
51
src/bin/tools/crawlAsync.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import * as fsPr from "fs/promises";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
|
||||
/** List all files in a given directory return paths relative to the dir_path */
|
||||
export async function crawlAsync(params: {
|
||||
dirPath: string;
|
||||
returnedPathsType: "absolute" | "relative to dirPath";
|
||||
onFileFound: (filePath: string) => void;
|
||||
}) {
|
||||
const { dirPath, returnedPathsType, onFileFound } = params;
|
||||
|
||||
await crawlAsyncRec({
|
||||
dirPath,
|
||||
onFileFound: ({ filePath }) => {
|
||||
switch (returnedPathsType) {
|
||||
case "absolute":
|
||||
onFileFound(filePath);
|
||||
return;
|
||||
case "relative to dirPath":
|
||||
onFileFound(pathRelative(dirPath, filePath));
|
||||
return;
|
||||
}
|
||||
assert<Equals<typeof returnedPathsType, never>>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function crawlAsyncRec(params: {
|
||||
dirPath: string;
|
||||
onFileFound: (params: { filePath: string }) => void;
|
||||
}) {
|
||||
const { dirPath, onFileFound } = params;
|
||||
|
||||
await Promise.all(
|
||||
(await fsPr.readdir(dirPath)).map(async basename => {
|
||||
const fileOrDirPath = pathJoin(dirPath, basename);
|
||||
|
||||
const isDirectory = await fsPr
|
||||
.lstat(fileOrDirPath)
|
||||
.then(stat => stat.isDirectory());
|
||||
|
||||
if (isDirectory) {
|
||||
await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound });
|
||||
return;
|
||||
}
|
||||
|
||||
onFileFound({ filePath: fileOrDirPath });
|
||||
})
|
||||
);
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
export type FetchOptionsLike = {
|
||||
proxy: string | undefined;
|
||||
noProxy: string | string[];
|
||||
strictSSL: boolean;
|
||||
cert: string | string[] | undefined;
|
||||
ca: string[] | undefined;
|
||||
};
|
||||
|
||||
export function getProxyFetchOptions(params: {
|
||||
npmConfigGetCwd: string;
|
||||
}): ProxyFetchOptions {
|
||||
}): FetchOptionsLike {
|
||||
const { npmConfigGetCwd } = params;
|
||||
|
||||
const cfg = (() => {
|
||||
|
51
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
51
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
import * as child_process from "child_process";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export async function getInstalledModuleDirPath(params: {
|
||||
moduleName: string;
|
||||
packageJsonDirPath: string;
|
||||
projectDirPath: string;
|
||||
}) {
|
||||
const { moduleName, packageJsonDirPath, projectDirPath } = params;
|
||||
|
||||
common_case: {
|
||||
const dirPath = pathJoin(
|
||||
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
|
||||
);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
node_modules_at_root_case: {
|
||||
if (projectDirPath === packageJsonDirPath) {
|
||||
break node_modules_at_root_case;
|
||||
}
|
||||
|
||||
const dirPath = pathJoin(
|
||||
...[projectDirPath, "node_modules", ...moduleName.split("/")]
|
||||
);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
break node_modules_at_root_case;
|
||||
}
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
const dirPath = child_process
|
||||
.execSync(`npm list ${moduleName}`, {
|
||||
cwd: packageJsonDirPath
|
||||
})
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
assert(dirPath !== "");
|
||||
|
||||
return dirPath;
|
||||
}
|
131
src/bin/tools/listInstalledModules.ts
Normal file
131
src/bin/tools/listInstalledModules.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import { z } from "zod";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { is } from "tsafe/is";
|
||||
import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export async function listInstalledModules(params: {
|
||||
packageJsonFilePath: string;
|
||||
projectDirPath: string;
|
||||
filter: (params: { moduleName: string }) => boolean;
|
||||
}): Promise<
|
||||
{
|
||||
moduleName: string;
|
||||
version: string;
|
||||
dirPath: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
}[]
|
||||
> {
|
||||
const { packageJsonFilePath, projectDirPath, filter } = params;
|
||||
|
||||
const parsedPackageJson = await readPackageJsonDependencies({
|
||||
packageJsonFilePath
|
||||
});
|
||||
|
||||
const uiModuleNames = (
|
||||
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
||||
)
|
||||
.filter(exclude(undefined))
|
||||
.map(obj => Object.keys(obj))
|
||||
.flat()
|
||||
.filter(moduleName => filter({ moduleName }));
|
||||
|
||||
const result = await Promise.all(
|
||||
uiModuleNames.map(async moduleName => {
|
||||
const dirPath = await getInstalledModuleDirPath({
|
||||
moduleName,
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
||||
projectDirPath
|
||||
});
|
||||
|
||||
const { version, peerDependencies } =
|
||||
await readPackageJsonVersionAndPeerDependencies({
|
||||
packageJsonFilePath: pathJoin(dirPath, "package.json")
|
||||
});
|
||||
|
||||
return { moduleName, version, peerDependencies, dirPath } as const;
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const { readPackageJsonDependencies } = (() => {
|
||||
type ParsedPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
async function readPackageJsonDependencies(params: { packageJsonFilePath: string }) {
|
||||
const { packageJsonFilePath } = params;
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
}
|
||||
|
||||
return { readPackageJsonDependencies };
|
||||
})();
|
||||
|
||||
const { readPackageJsonVersionAndPeerDependencies } = (() => {
|
||||
type ParsedPackageJson = {
|
||||
version: string;
|
||||
peerDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
version: z.string(),
|
||||
peerDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
async function readPackageJsonVersionAndPeerDependencies(params: {
|
||||
packageJsonFilePath: string;
|
||||
}): Promise<{ version: string; peerDependencies: Record<string, string> }> {
|
||||
const { packageJsonFilePath } = params;
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return {
|
||||
version: parsedPackageJson.version,
|
||||
peerDependencies: parsedPackageJson.peerDependencies ?? {}
|
||||
};
|
||||
}
|
||||
|
||||
return { readPackageJsonVersionAndPeerDependencies };
|
||||
})();
|
38
src/bin/tools/nodeModulesBinDirPath.ts
Normal file
38
src/bin/tools/nodeModulesBinDirPath.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { sep as pathSep } from "path";
|
||||
|
||||
let cache: string | undefined = undefined;
|
||||
|
||||
export function getNodeModulesBinDirPath() {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const binPath = process.argv[1];
|
||||
|
||||
const segments: string[] = [".bin"];
|
||||
|
||||
let foundNodeModules = false;
|
||||
|
||||
for (const segment of binPath.split(pathSep).reverse()) {
|
||||
skip_segment: {
|
||||
if (foundNodeModules) {
|
||||
break skip_segment;
|
||||
}
|
||||
|
||||
if (segment === "node_modules") {
|
||||
foundNodeModules = true;
|
||||
break skip_segment;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
segments.unshift(segment);
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||
|
||||
cache = nodeModulesBinDirPath;
|
||||
|
||||
return nodeModulesBinDirPath;
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import { is } from "tsafe/is";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { rmSync } from "./fs.rmSync";
|
||||
|
||||
export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
const { packageJsonDirPath } = params;
|
||||
@ -23,6 +31,10 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
{
|
||||
binName: "bun",
|
||||
lockFileBasename: "bun.lockdb"
|
||||
},
|
||||
{
|
||||
binName: "deno",
|
||||
lockFileBasename: "deno.lock"
|
||||
}
|
||||
] as const;
|
||||
|
||||
@ -37,27 +49,411 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
throw new Error(
|
||||
"No lock file found, cannot tell which package manager to use for installing dependencies."
|
||||
);
|
||||
})();
|
||||
|
||||
install_dependencies: {
|
||||
if (packageManagerBinName === undefined) {
|
||||
break install_dependencies;
|
||||
console.log(`Installing the new dependencies...`);
|
||||
|
||||
install_without_breaking_links: {
|
||||
if (packageManagerBinName !== "yarn") {
|
||||
break install_without_breaking_links;
|
||||
}
|
||||
|
||||
console.log(`Installing the new dependencies...`);
|
||||
const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath });
|
||||
|
||||
try {
|
||||
child_process.execSync(`${packageManagerBinName} install`, {
|
||||
cwd: packageJsonDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
||||
)
|
||||
);
|
||||
if (garronejLinkInfos === undefined) {
|
||||
break install_without_breaking_links;
|
||||
}
|
||||
|
||||
console.log(chalk.green("Installing in a way that won't break the links..."));
|
||||
|
||||
installWithoutBreakingLinks({
|
||||
packageJsonDirPath,
|
||||
garronejLinkInfos
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execSync(`${packageManagerBinName} install`, {
|
||||
cwd: packageJsonDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getGarronejLinkInfos(params: {
|
||||
packageJsonDirPath: string;
|
||||
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
||||
const { packageJsonDirPath } = params;
|
||||
|
||||
const nodeModuleDirPath = pathJoin(packageJsonDirPath, "node_modules");
|
||||
|
||||
if (!fs.existsSync(nodeModuleDirPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const linkedModuleNames: string[] = [];
|
||||
|
||||
let yarnHomeDirPath: string | undefined = undefined;
|
||||
|
||||
const getIsLinkedByGarronejScript = (path: string) => {
|
||||
let realPath: string;
|
||||
|
||||
try {
|
||||
realPath = fs.readlinkSync(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const doesIncludeYarnHome = realPath.includes(".yarn_home");
|
||||
|
||||
if (!doesIncludeYarnHome) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_yarnHomeDirPath: {
|
||||
if (yarnHomeDirPath !== undefined) {
|
||||
break set_yarnHomeDirPath;
|
||||
}
|
||||
|
||||
const [firstElement] = getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: realPath,
|
||||
cwd: pathDirname(path)
|
||||
}).split(".yarn_home");
|
||||
|
||||
yarnHomeDirPath = pathJoin(firstElement, ".yarn_home");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const basename of fs.readdirSync(nodeModuleDirPath)) {
|
||||
const path = pathJoin(nodeModuleDirPath, basename);
|
||||
|
||||
if (fs.lstatSync(path).isSymbolicLink()) {
|
||||
if (basename.startsWith("@")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!getIsLinkedByGarronejScript(path)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
linkedModuleNames.push(basename);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fs.lstatSync(path).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (basename.startsWith("@")) {
|
||||
for (const subBasename of fs.readdirSync(path)) {
|
||||
const subPath = pathJoin(path, subBasename);
|
||||
|
||||
if (!fs.lstatSync(subPath).isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!getIsLinkedByGarronejScript(subPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
linkedModuleNames.push(`${basename}/${subBasename}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (yarnHomeDirPath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { linkedModuleNames, yarnHomeDirPath };
|
||||
}
|
||||
|
||||
function installWithoutBreakingLinks(params: {
|
||||
packageJsonDirPath: string;
|
||||
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
|
||||
}) {
|
||||
const {
|
||||
packageJsonDirPath,
|
||||
garronejLinkInfos: { linkedModuleNames, yarnHomeDirPath }
|
||||
} = params;
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
const packageJsonFilePath = pathJoin(packageJsonDirPath, "package.json");
|
||||
|
||||
type ParsedPackageJson = {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
scripts: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
) as unknown;
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
const isImplementedScriptByName = {
|
||||
postinstall: false,
|
||||
prepare: false
|
||||
};
|
||||
|
||||
delete_postinstall_script: {
|
||||
if (parsedPackageJson.scripts === undefined) {
|
||||
break delete_postinstall_script;
|
||||
}
|
||||
|
||||
for (const scriptName of objectKeys(isImplementedScriptByName)) {
|
||||
if (parsedPackageJson.scripts[scriptName] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isImplementedScriptByName[scriptName] = true;
|
||||
|
||||
delete parsedPackageJson.scripts[scriptName];
|
||||
}
|
||||
}
|
||||
|
||||
const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject");
|
||||
|
||||
if (fs.existsSync(tmpProjectDirPath)) {
|
||||
rmSync(tmpProjectDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(tmpProjectDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(tmpProjectDirPath, "package.json"),
|
||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||
);
|
||||
|
||||
const YARN_LOCK = "yarn.lock";
|
||||
|
||||
fs.copyFileSync(
|
||||
pathJoin(packageJsonDirPath, YARN_LOCK),
|
||||
pathJoin(tmpProjectDirPath, YARN_LOCK)
|
||||
);
|
||||
|
||||
child_process.execSync(`yarn install`, {
|
||||
cwd: tmpProjectDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
// NOTE: Moving the modules from the tmp project to the actual project
|
||||
// without messing up the links.
|
||||
{
|
||||
const { getAreSameVersions } = (() => {
|
||||
type ParsedPackageJson = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
version: z.string()
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
function readVersion(params: { moduleDirPath: string }): string {
|
||||
const { moduleDirPath } = params;
|
||||
|
||||
const packageJsonFilePath = pathJoin(moduleDirPath, "package.json");
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(packageJson);
|
||||
assert(is<ParsedPackageJson>(packageJson));
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
function getAreSameVersions(params: {
|
||||
moduleDirPath_a: string;
|
||||
moduleDirPath_b: string;
|
||||
}): boolean {
|
||||
const { moduleDirPath_a, moduleDirPath_b } = params;
|
||||
|
||||
return (
|
||||
readVersion({ moduleDirPath: moduleDirPath_a }) ===
|
||||
readVersion({ moduleDirPath: moduleDirPath_b })
|
||||
);
|
||||
}
|
||||
|
||||
return { getAreSameVersions };
|
||||
})();
|
||||
|
||||
const nodeModulesDirPath_tmpProject = pathJoin(tmpProjectDirPath, "node_modules");
|
||||
const nodeModulesDirPath = pathJoin(packageJsonDirPath, "node_modules");
|
||||
|
||||
const modulePaths = fs
|
||||
.readdirSync(nodeModulesDirPath_tmpProject)
|
||||
.map(basename => {
|
||||
if (basename.startsWith(".")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const path = pathJoin(nodeModulesDirPath_tmpProject, basename);
|
||||
|
||||
if (basename.startsWith("@")) {
|
||||
return fs
|
||||
.readdirSync(path)
|
||||
.map(subBasename => {
|
||||
if (subBasename.startsWith(".")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const subPath = pathJoin(path, subBasename);
|
||||
|
||||
if (!fs.lstatSync(subPath).isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
moduleName: `${basename}/${subBasename}`,
|
||||
moduleDirPath_tmpProject: subPath,
|
||||
moduleDirPath: pathJoin(
|
||||
nodeModulesDirPath,
|
||||
basename,
|
||||
subBasename
|
||||
)
|
||||
};
|
||||
})
|
||||
.filter(exclude(undefined));
|
||||
}
|
||||
|
||||
if (!fs.lstatSync(path).isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
moduleName: basename,
|
||||
moduleDirPath_tmpProject: path,
|
||||
moduleDirPath: pathJoin(nodeModulesDirPath, basename)
|
||||
}
|
||||
];
|
||||
})
|
||||
.filter(exclude(undefined))
|
||||
.flat();
|
||||
|
||||
for (const {
|
||||
moduleName,
|
||||
moduleDirPath,
|
||||
moduleDirPath_tmpProject
|
||||
} of modulePaths) {
|
||||
if (linkedModuleNames.includes(moduleName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doesTargetModuleExist = false;
|
||||
|
||||
skip_condition: {
|
||||
if (!fs.existsSync(moduleDirPath)) {
|
||||
break skip_condition;
|
||||
}
|
||||
|
||||
doesTargetModuleExist = true;
|
||||
|
||||
const areSameVersions = getAreSameVersions({
|
||||
moduleDirPath_a: moduleDirPath,
|
||||
moduleDirPath_b: moduleDirPath_tmpProject
|
||||
});
|
||||
|
||||
if (!areSameVersions) {
|
||||
break skip_condition;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doesTargetModuleExist) {
|
||||
rmSync(moduleDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(moduleDirPath);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.renameSync(moduleDirPath_tmpProject, moduleDirPath);
|
||||
}
|
||||
|
||||
move_bin: {
|
||||
const binDirPath_tmpProject = pathJoin(nodeModulesDirPath_tmpProject, ".bin");
|
||||
const binDirPath = pathJoin(nodeModulesDirPath, ".bin");
|
||||
|
||||
if (!fs.existsSync(binDirPath_tmpProject)) {
|
||||
break move_bin;
|
||||
}
|
||||
|
||||
for (const basename of fs.readdirSync(binDirPath_tmpProject)) {
|
||||
const path_tmpProject = pathJoin(binDirPath_tmpProject, basename);
|
||||
const path = pathJoin(binDirPath, basename);
|
||||
|
||||
if (fs.existsSync(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.renameSync(path_tmpProject, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(tmpProjectDirPath, YARN_LOCK),
|
||||
pathJoin(packageJsonDirPath, YARN_LOCK)
|
||||
);
|
||||
|
||||
rmSync(tmpProjectDirPath, { recursive: true });
|
||||
|
||||
for (const scriptName of objectKeys(isImplementedScriptByName)) {
|
||||
if (!isImplementedScriptByName[scriptName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
child_process.execSync(`yarn run ${scriptName}`, {
|
||||
cwd: packageJsonDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,13 @@ import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
|
||||
let cache: string | undefined = undefined;
|
||||
|
||||
export function readThisNpmPackageVersion(): string {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const version = JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
||||
@ -12,5 +18,7 @@ export function readThisNpmPackageVersion(): string {
|
||||
|
||||
assert(typeof version === "string");
|
||||
|
||||
cache = version;
|
||||
|
||||
return version;
|
||||
}
|
||||
|
76
src/bin/tools/runFormat.ts
Normal file
76
src/bin/tools/runFormat.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import * as fs from "fs";
|
||||
import { dirname as pathDirname } from "path";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import chalk from "chalk";
|
||||
import { id } from "tsafe/id";
|
||||
import { z } from "zod";
|
||||
import { is } from "tsafe/is";
|
||||
import * as child_process from "child_process";
|
||||
|
||||
export function runFormat(params: { packageJsonFilePath: string }) {
|
||||
const { packageJsonFilePath } = params;
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
scripts: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
const { scripts } = parsedPackageJson;
|
||||
|
||||
if (scripts === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptKeys = Object.keys(scripts);
|
||||
const scriptNames = scriptKeys.filter(scriptKey =>
|
||||
scriptKey.trim().match(/^(lint|format)/)
|
||||
);
|
||||
|
||||
for (const scriptName of scriptNames) {
|
||||
if (!(scriptName in scripts)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const command = `npm run ${scriptName}`;
|
||||
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
try {
|
||||
child_process.execSync(`npm run ${scriptName}`, {
|
||||
stdio: "inherit",
|
||||
cwd: pathDirname(packageJsonFilePath)
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\`${command}\` failed, it does not matter, please format your code manually, continuing...`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
106
src/bin/tools/runPrettier.ts
Normal file
106
src/bin/tools/runPrettier.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { getNodeModulesBinDirPath } from "./nodeModulesBinDirPath";
|
||||
import { join as pathJoin } from "path";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert } from "tsafe/assert";
|
||||
import chalk from "chalk";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
getIsPrettierAvailable.cache = id<boolean | undefined>(undefined);
|
||||
|
||||
export async function getIsPrettierAvailable(): Promise<boolean> {
|
||||
if (getIsPrettierAvailable.cache !== undefined) {
|
||||
return getIsPrettierAvailable.cache;
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
|
||||
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
||||
|
||||
const stats = await fsPr.stat(prettierBinPath).catch(() => undefined);
|
||||
|
||||
const isPrettierAvailable = stats?.isFile() ?? false;
|
||||
|
||||
getIsPrettierAvailable.cache = isPrettierAvailable;
|
||||
|
||||
return isPrettierAvailable;
|
||||
}
|
||||
|
||||
type PrettierAndConfigHash = {
|
||||
prettier: typeof import("prettier");
|
||||
configHash: string;
|
||||
};
|
||||
|
||||
getPrettier.cache = id<PrettierAndConfigHash | undefined>(undefined);
|
||||
|
||||
export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||
assert(getIsPrettierAvailable());
|
||||
|
||||
if (getPrettier.cache !== undefined) {
|
||||
return getPrettier.cache;
|
||||
}
|
||||
|
||||
const prettier = await import("prettier");
|
||||
|
||||
const configHash = await (async () => {
|
||||
const configFilePath = await prettier.resolveConfigFile(
|
||||
pathJoin(getNodeModulesBinDirPath(), "..")
|
||||
);
|
||||
|
||||
if (configFilePath === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const data = await fsPr.readFile(configFilePath);
|
||||
|
||||
return crypto.createHash("sha256").update(data).digest("hex");
|
||||
})();
|
||||
|
||||
const prettierAndConfig: PrettierAndConfigHash = {
|
||||
prettier,
|
||||
configHash
|
||||
};
|
||||
|
||||
getPrettier.cache = prettierAndConfig;
|
||||
|
||||
return prettierAndConfig;
|
||||
}
|
||||
|
||||
export async function runPrettier(params: {
|
||||
sourceCode: string;
|
||||
filePath: string;
|
||||
}): Promise<string> {
|
||||
const { sourceCode, filePath } = params;
|
||||
|
||||
let formattedSourceCode: string;
|
||||
|
||||
try {
|
||||
const { prettier } = await getPrettier();
|
||||
|
||||
const { ignored, inferredParser } = await prettier.getFileInfo(filePath, {
|
||||
resolveConfig: true
|
||||
});
|
||||
|
||||
if (ignored) {
|
||||
return sourceCode;
|
||||
}
|
||||
|
||||
const config = await prettier.resolveConfig(filePath);
|
||||
|
||||
formattedSourceCode = await prettier.format(sourceCode, {
|
||||
...config,
|
||||
filePath,
|
||||
parser: inferredParser ?? undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`You probably need to upgrade the version of prettier in your project`
|
||||
)
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return formattedSourceCode;
|
||||
}
|
@ -1,13 +1,146 @@
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import { generateKcGenTs } from "./shared/generateKcGenTs";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "./tools/fs.existsAsync";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import * as crypto from "crypto";
|
||||
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "update-kc-gen",
|
||||
buildContext
|
||||
});
|
||||
|
||||
await generateKcGenTs({ buildContext });
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx");
|
||||
|
||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
|
||||
let newContent = [
|
||||
``,
|
||||
`/* eslint-disable */`,
|
||||
``,
|
||||
`// @ts-nocheck`,
|
||||
``,
|
||||
`// noinspection JSUnusedGlobalSymbols`,
|
||||
``,
|
||||
`import { lazy, Suspense, type ReactNode } from "react";`,
|
||||
``,
|
||||
`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
|
||||
)};`,
|
||||
``,
|
||||
`type KcContext =`,
|
||||
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
||||
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
||||
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
|
||||
` ;`,
|
||||
``,
|
||||
`declare global {`,
|
||||
` interface Window {`,
|
||||
` kcContext?: KcContext;`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
hasLoginTheme &&
|
||||
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
||||
hasAccountTheme &&
|
||||
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
||||
hasAdminTheme &&
|
||||
`export const KcAdminPage = lazy(() => import("./admin/KcPage"));`,
|
||||
``,
|
||||
`export function KcPage(`,
|
||||
` props: {`,
|
||||
` kcContext: KcContext;`,
|
||||
` fallback?: ReactNode;`,
|
||||
` }`,
|
||||
`) {`,
|
||||
` const { kcContext, fallback } = props;`,
|
||||
` return (`,
|
||||
` <Suspense fallback={fallback}>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.themeType) {`,
|
||||
hasLoginTheme &&
|
||||
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
||||
hasAccountTheme &&
|
||||
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
||||
hasAdminTheme &&
|
||||
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
`}`,
|
||||
``
|
||||
]
|
||||
.filter(item => typeof item === "string")
|
||||
.join("\n");
|
||||
|
||||
const hash = crypto.createHash("sha256").update(newContent).digest("hex");
|
||||
|
||||
skip_if_no_changes: {
|
||||
if (!(await existsAsync(filePath))) {
|
||||
break skip_if_no_changes;
|
||||
}
|
||||
|
||||
const currentContent = (await fs.readFile(filePath)).toString("utf8");
|
||||
|
||||
if (!currentContent.includes(hash)) {
|
||||
break skip_if_no_changes;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
newContent = [
|
||||
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
|
||||
`// Hash: ${hash}`,
|
||||
``,
|
||||
newContent
|
||||
].join("\n");
|
||||
|
||||
format: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break format;
|
||||
}
|
||||
|
||||
newContent = await runPrettier({
|
||||
filePath,
|
||||
sourceCode: newContent
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, Buffer.from(newContent, "utf8"));
|
||||
|
||||
delete_legacy_file: {
|
||||
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
||||
|
||||
if (!(await existsAsync(legacyFilePath))) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
await fs.unlink(legacyFilePath);
|
||||
}
|
||||
}
|
||||
|
252
src/lib/kcSanitize/HtmlPolicyBuilder.ts
Normal file
252
src/lib/kcSanitize/HtmlPolicyBuilder.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import { DOMPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
type TagType = {
|
||||
name: string;
|
||||
attributes: AttributeType[];
|
||||
};
|
||||
type AttributeType = {
|
||||
name: string;
|
||||
matchRegex?: RegExp;
|
||||
matchFunction?: (value: string) => boolean;
|
||||
};
|
||||
|
||||
// implementation for org.owasp.html.HtmlPolicyBuilder
|
||||
// https://www.javadoc.io/static/com.googlecode.owasp-java-html-sanitizer/owasp-java-html-sanitizer/20160628.1/index.html?org/owasp/html/HtmlPolicyBuilder.html
|
||||
// It supports the methods that KCSanitizerPolicy needs and nothing more
|
||||
|
||||
export class HtmlPolicyBuilder {
|
||||
private globalAttributesAllowed: Set<AttributeType> = new Set();
|
||||
private tagsAllowed: Map<string, TagType> = new Map();
|
||||
private tagsAllowedWithNoAttribute: Set<string> = new Set();
|
||||
private currentAttribute: AttributeType | null = null;
|
||||
private isStylingAllowed: boolean = false;
|
||||
private allowedProtocols: Set<string> = new Set();
|
||||
private enforceRelNofollow: boolean = false;
|
||||
private DOMPurify: typeof DOMPurify;
|
||||
|
||||
// add a constructor
|
||||
constructor(
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof DOMPurify;
|
||||
}>
|
||||
) {
|
||||
this.DOMPurify = dependencyInjections.DOMPurify ?? DOMPurify;
|
||||
}
|
||||
|
||||
allowWithoutAttributes(tag: string): this {
|
||||
this.tagsAllowedWithNoAttribute.add(tag);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Adds the attributes for validation
|
||||
allowAttributes(...args: string[]): this {
|
||||
if (args.length) {
|
||||
const attr = args[0];
|
||||
this.currentAttribute = { name: attr }; // Default regex, will be set later
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Matching regex for value of allowed attributes
|
||||
matching(matchingPattern: RegExp | ((value: string) => boolean)): this {
|
||||
if (this.currentAttribute) {
|
||||
if (matchingPattern instanceof RegExp) {
|
||||
this.currentAttribute.matchRegex = matchingPattern;
|
||||
} else {
|
||||
this.currentAttribute.matchFunction = matchingPattern;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Make attributes in prev call global
|
||||
globally(): this {
|
||||
if (this.currentAttribute) {
|
||||
this.currentAttribute.matchRegex = /.*/;
|
||||
this.globalAttributesAllowed.add(this.currentAttribute);
|
||||
this.currentAttribute = null; // Reset after global application
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Allow styling globally
|
||||
allowStyling(): this {
|
||||
this.isStylingAllowed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Save attributes for specific tag
|
||||
onElements(...tags: string[]): this {
|
||||
if (this.currentAttribute) {
|
||||
tags.forEach(tag => {
|
||||
const element = this.tagsAllowed.get(tag) || {
|
||||
name: tag,
|
||||
attributes: []
|
||||
};
|
||||
element.attributes.push(this.currentAttribute!);
|
||||
this.tagsAllowed.set(tag, element);
|
||||
});
|
||||
this.currentAttribute = null; // Reset after applying to elements
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Make specific tag allowed
|
||||
allowElements(...tags: string[]): this {
|
||||
tags.forEach(tag => {
|
||||
if (!this.tagsAllowed.has(tag)) {
|
||||
this.tagsAllowed.set(tag, { name: tag, attributes: [] });
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// Handle rel=nofollow on links
|
||||
requireRelNofollowOnLinks(): this {
|
||||
this.enforceRelNofollow = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Allow standard URL protocols (could include further implementation)
|
||||
allowStandardUrlProtocols(): this {
|
||||
this.allowedProtocols.add("http");
|
||||
this.allowedProtocols.add("https");
|
||||
this.allowedProtocols.add("mailto");
|
||||
return this;
|
||||
}
|
||||
|
||||
apply(html: string): string {
|
||||
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
||||
this.DOMPurify.clearConfig();
|
||||
this.DOMPurify.removeAllHooks();
|
||||
this.setupHooks();
|
||||
return this.DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
||||
ALLOWED_ATTR: this.getAllowedAttributes(),
|
||||
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
||||
ADD_TAGS: this.isStylingAllowed ? ["style"] : [],
|
||||
ADD_ATTR: this.isStylingAllowed ? ["style"] : []
|
||||
});
|
||||
}
|
||||
|
||||
private setupHooks(): void {
|
||||
// Check allowed attribute and global attributes and it doesnt exist in them remove it
|
||||
this.DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
|
||||
if (!hookEvent) return;
|
||||
|
||||
const tagName = currentNode.tagName.toLowerCase();
|
||||
const allowedAttributes = this.tagsAllowed.get(tagName)?.attributes || [];
|
||||
|
||||
//Add global attributes to allowed attributes
|
||||
this.globalAttributesAllowed.forEach(attribute => {
|
||||
allowedAttributes.push(attribute);
|
||||
});
|
||||
|
||||
//Add style attribute to allowed attributes
|
||||
if (this.isStylingAllowed) {
|
||||
let styleAttribute: AttributeType = { name: "style", matchRegex: /.*/ };
|
||||
allowedAttributes.push(styleAttribute);
|
||||
}
|
||||
|
||||
// Check if the attribute is allowed
|
||||
if (!allowedAttributes.some(attr => attr.name === hookEvent.attrName)) {
|
||||
hookEvent.forceKeepAttr = false;
|
||||
hookEvent.keepAttr = false;
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
return;
|
||||
} else {
|
||||
const attributeType = allowedAttributes.find(
|
||||
attr => attr.name === hookEvent.attrName
|
||||
);
|
||||
if (attributeType) {
|
||||
//Check if attribute value is allowed
|
||||
if (
|
||||
attributeType.matchRegex &&
|
||||
!attributeType.matchRegex.test(hookEvent.attrValue)
|
||||
) {
|
||||
hookEvent.forceKeepAttr = false;
|
||||
hookEvent.keepAttr = false;
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
attributeType.matchFunction &&
|
||||
!attributeType.matchFunction(hookEvent.attrValue)
|
||||
) {
|
||||
hookEvent.forceKeepAttr = false;
|
||||
hookEvent.keepAttr = false;
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// both attribute and value already checked so they should be ok
|
||||
// set forceKeep to true to make sure next hooks won't delete them
|
||||
// except for href that we will check later
|
||||
if (hookEvent.attrName !== "href") {
|
||||
hookEvent.keepAttr = true;
|
||||
hookEvent.forceKeepAttr = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
|
||||
// if tag is not allowed to have no attribute then remove it completely
|
||||
if (
|
||||
currentNode.attributes.length == 0 &&
|
||||
currentNode.childNodes.length == 0
|
||||
) {
|
||||
if (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
|
||||
currentNode.remove();
|
||||
}
|
||||
} else {
|
||||
//in case of <a> or <img> if we have no attribute we need to remove them even if they have child
|
||||
if (currentNode.tagName === "A" || currentNode.tagName === "IMG") {
|
||||
if (currentNode.attributes.length == 0) {
|
||||
//add currentNode children to parent node
|
||||
while (currentNode.firstChild) {
|
||||
currentNode?.parentNode?.insertBefore(
|
||||
currentNode.firstChild,
|
||||
currentNode
|
||||
);
|
||||
}
|
||||
// Remove the currentNode itself
|
||||
currentNode.remove();
|
||||
}
|
||||
}
|
||||
//
|
||||
if (currentNode.tagName === "A") {
|
||||
if (this.enforceRelNofollow) {
|
||||
if (!currentNode.hasAttribute("rel")) {
|
||||
currentNode.setAttribute("rel", "nofollow");
|
||||
} else if (
|
||||
!currentNode.getAttribute("rel")?.includes("nofollow")
|
||||
) {
|
||||
currentNode.setAttribute(
|
||||
"rel",
|
||||
currentNode.getAttribute("rel") + " nofollow"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getAllowedAttributes(): string[] {
|
||||
const allowedAttributes: Set<string> = new Set();
|
||||
this.tagsAllowed.forEach(element => {
|
||||
element.attributes.forEach(attribute => {
|
||||
allowedAttributes.add(attribute.name);
|
||||
});
|
||||
});
|
||||
this.globalAttributesAllowed.forEach(attribute => {
|
||||
allowedAttributes.add(attribute.name);
|
||||
});
|
||||
return Array.from(allowedAttributes);
|
||||
}
|
||||
|
||||
private getAllowedUriRegexp(): RegExp {
|
||||
const protocols = Array.from(this.allowedProtocols).join("|");
|
||||
return new RegExp(`^(?:${protocols})://`, "i");
|
||||
}
|
||||
}
|
60
src/lib/kcSanitize/KcSanitizer.ts
Normal file
60
src/lib/kcSanitize/KcSanitizer.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
|
||||
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
||||
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
|
||||
export class KcSanitizer {
|
||||
private static HREF_PATTERN = /\s+href="([^"]*)"/g;
|
||||
private static textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
public static sanitize(
|
||||
html: string,
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof ofTypeDomPurify;
|
||||
htmlEntitiesDecode: (html: string) => string;
|
||||
}>
|
||||
): string {
|
||||
if (html === "") return "";
|
||||
|
||||
html =
|
||||
dependencyInjections?.htmlEntitiesDecode !== undefined
|
||||
? dependencyInjections.htmlEntitiesDecode(html)
|
||||
: this.decodeHtml(html);
|
||||
const sanitized = KcSanitizerPolicy.sanitize(html, dependencyInjections);
|
||||
return this.fixURLs(sanitized);
|
||||
}
|
||||
|
||||
private static decodeHtml(html: string): string {
|
||||
if (!KcSanitizer.textarea) {
|
||||
KcSanitizer.textarea = document.createElement("textarea");
|
||||
}
|
||||
KcSanitizer.textarea.innerHTML = html;
|
||||
return KcSanitizer.textarea.value;
|
||||
}
|
||||
|
||||
// This will remove unwanted characters from url
|
||||
private static fixURLs(msg: string): string {
|
||||
const HREF_PATTERN = this.HREF_PATTERN;
|
||||
const result = [];
|
||||
let last = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
do {
|
||||
match = HREF_PATTERN.exec(msg);
|
||||
if (match) {
|
||||
const href = match[0]
|
||||
.replace(/=/g, "=")
|
||||
.replace(/\.\./g, ".")
|
||||
.replace(/&/g, "&");
|
||||
|
||||
result.push(msg.substring(last, match.index!));
|
||||
result.push(href);
|
||||
|
||||
last = HREF_PATTERN.lastIndex;
|
||||
}
|
||||
} while (match);
|
||||
|
||||
result.push(msg.substring(last));
|
||||
return result.join("");
|
||||
}
|
||||
}
|
294
src/lib/kcSanitize/KcSanitizerPolicy.ts
Normal file
294
src/lib/kcSanitize/KcSanitizerPolicy.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { HtmlPolicyBuilder } from "./HtmlPolicyBuilder";
|
||||
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
//implementation of java Sanitizer policy ( KeycloakSanitizerPolicy )
|
||||
// All regex directly copied from the keycloak source but some of them changed slightly to work with typescript(ONSITE_URL and OFFSITE_URL)
|
||||
// Also replaced ?i with "i" tag as second parameter of RegExp
|
||||
//https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerPolicy.java#L29
|
||||
export class KcSanitizerPolicy {
|
||||
public static readonly COLOR_NAME = new RegExp(
|
||||
"(?:aqua|black|blue|fuchsia|gray|grey|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)"
|
||||
);
|
||||
|
||||
public static readonly COLOR_CODE = new RegExp(
|
||||
"(?:#(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))"
|
||||
);
|
||||
|
||||
public static readonly NUMBER_OR_PERCENT = new RegExp("[0-9]+%?");
|
||||
|
||||
public static readonly PARAGRAPH = new RegExp(
|
||||
"(?:[\\p{L}\\p{N},'\\.\\s\\-_\\(\\)]|&[0-9]{2};)*",
|
||||
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||
);
|
||||
|
||||
public static readonly HTML_ID = new RegExp("[a-zA-Z0-9\\:\\-_\\.]+");
|
||||
|
||||
public static readonly HTML_TITLE = new RegExp(
|
||||
"[\\p{L}\\p{N}\\s\\-_',:\\[\\]!\\./\\\\\\(\\)&]*",
|
||||
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||
);
|
||||
|
||||
public static readonly HTML_CLASS = new RegExp("[a-zA-Z0-9\\s,\\-_]+");
|
||||
|
||||
public static readonly ONSITE_URL = new RegExp(
|
||||
"(?:[\\p{L}\\p{N}.#@\\$%+&;\\-_~,?=/!]+|#(\\w)+)",
|
||||
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||
);
|
||||
|
||||
public static readonly OFFSITE_URL = new RegExp(
|
||||
"\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]+" +
|
||||
"[\\p{L}\\p{N}\\p{Zs}.#@\\$%+&;:\\-_~,?=/!()]*\\s*",
|
||||
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||
);
|
||||
|
||||
public static readonly NUMBER = new RegExp(
|
||||
"[+-]?(?:(?:[0-9]+(?:\\.[0-9]*)?)|\\.[0-9]+)"
|
||||
);
|
||||
public static readonly NAME = new RegExp("[a-zA-Z0-9\\-_\\$]+");
|
||||
|
||||
public static readonly ALIGN = new RegExp(
|
||||
"\\b(center|left|right|justify|char)\\b",
|
||||
"i" // Case-insensitive flag
|
||||
);
|
||||
|
||||
public static readonly VALIGN = new RegExp(
|
||||
"\\b(baseline|bottom|middle|top)\\b",
|
||||
"i" // Case-insensitive flag
|
||||
);
|
||||
|
||||
public static readonly HISTORY_BACK = new RegExp(
|
||||
"(?:javascript:)?\\Qhistory.go(-1)\\E"
|
||||
);
|
||||
|
||||
public static readonly ONE_CHAR = new RegExp(
|
||||
".?",
|
||||
"s" // Dotall flag for . to match newlines
|
||||
);
|
||||
|
||||
private static COLOR_NAME_OR_COLOR_CODE(s: string): boolean {
|
||||
return (
|
||||
KcSanitizerPolicy.COLOR_NAME.test(s) || KcSanitizerPolicy.COLOR_CODE.test(s)
|
||||
);
|
||||
}
|
||||
|
||||
private static ONSITE_OR_OFFSITE_URL(s: string): boolean {
|
||||
return (
|
||||
KcSanitizerPolicy.ONSITE_URL.test(s) || KcSanitizerPolicy.OFFSITE_URL.test(s)
|
||||
);
|
||||
}
|
||||
|
||||
public static sanitize(
|
||||
html: string,
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof ofTypeDomPurify;
|
||||
}>
|
||||
): string {
|
||||
return new HtmlPolicyBuilder(dependencyInjections)
|
||||
.allowWithoutAttributes("span")
|
||||
|
||||
.allowAttributes("id")
|
||||
.matching(this.HTML_ID)
|
||||
.globally()
|
||||
|
||||
.allowAttributes("class")
|
||||
.matching(this.HTML_CLASS)
|
||||
.globally()
|
||||
|
||||
.allowAttributes("lang")
|
||||
.matching(/[a-zA-Z]{2,20}/)
|
||||
.globally()
|
||||
|
||||
.allowAttributes("title")
|
||||
.matching(this.HTML_TITLE)
|
||||
.globally()
|
||||
|
||||
.allowStyling()
|
||||
|
||||
.allowAttributes("align")
|
||||
.matching(this.ALIGN)
|
||||
.onElements("p")
|
||||
|
||||
.allowAttributes("for")
|
||||
.matching(this.HTML_ID)
|
||||
.onElements("label")
|
||||
|
||||
.allowAttributes("color")
|
||||
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||
.onElements("font")
|
||||
|
||||
.allowAttributes("face")
|
||||
.matching(/[\w;, \-]+/)
|
||||
.onElements("font")
|
||||
|
||||
.allowAttributes("size")
|
||||
.matching(this.NUMBER)
|
||||
.onElements("font")
|
||||
|
||||
.allowAttributes("href")
|
||||
.matching(this.ONSITE_OR_OFFSITE_URL)
|
||||
.onElements("a")
|
||||
|
||||
.allowStandardUrlProtocols()
|
||||
.allowAttributes("nohref")
|
||||
.onElements("a")
|
||||
|
||||
.allowAttributes("name")
|
||||
.matching(this.NAME)
|
||||
.onElements("a")
|
||||
|
||||
.allowAttributes("onfocus", "onblur", "onclick", "onmousedown", "onmouseup")
|
||||
.matching(this.HISTORY_BACK)
|
||||
.onElements("a")
|
||||
|
||||
.requireRelNofollowOnLinks()
|
||||
.allowAttributes("src")
|
||||
.matching(this.ONSITE_OR_OFFSITE_URL)
|
||||
.onElements("img")
|
||||
|
||||
.allowAttributes("name")
|
||||
.matching(this.NAME)
|
||||
.onElements("img")
|
||||
|
||||
.allowAttributes("alt")
|
||||
.matching(this.PARAGRAPH)
|
||||
.onElements("img")
|
||||
|
||||
.allowAttributes("border", "hspace", "vspace")
|
||||
.matching(this.NUMBER)
|
||||
.onElements("img")
|
||||
|
||||
.allowAttributes("border", "cellpadding", "cellspacing")
|
||||
.matching(this.NUMBER)
|
||||
.onElements("table")
|
||||
|
||||
.allowAttributes("bgcolor")
|
||||
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||
.onElements("table")
|
||||
|
||||
.allowAttributes("background")
|
||||
.matching(this.ONSITE_URL)
|
||||
.onElements("table")
|
||||
|
||||
.allowAttributes("align")
|
||||
.matching(this.ALIGN)
|
||||
.onElements("table")
|
||||
|
||||
.allowAttributes("noresize")
|
||||
.matching(new RegExp("noresize", "i"))
|
||||
.onElements("table")
|
||||
|
||||
.allowAttributes("background")
|
||||
.matching(this.ONSITE_URL)
|
||||
.onElements("td", "th", "tr")
|
||||
|
||||
.allowAttributes("bgcolor")
|
||||
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("abbr")
|
||||
.matching(this.PARAGRAPH)
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("axis", "headers")
|
||||
.matching(this.NAME)
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("scope")
|
||||
.matching(new RegExp("(?:row|col)(?:group)?", "i"))
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("nowrap")
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("height", "width")
|
||||
.matching(this.NUMBER_OR_PERCENT)
|
||||
.onElements("table", "td", "th", "tr", "img")
|
||||
|
||||
.allowAttributes("align")
|
||||
.matching(this.ALIGN)
|
||||
.onElements(
|
||||
"thead",
|
||||
"tbody",
|
||||
"tfoot",
|
||||
"img",
|
||||
"td",
|
||||
"th",
|
||||
"tr",
|
||||
"colgroup",
|
||||
"col"
|
||||
)
|
||||
|
||||
.allowAttributes("valign")
|
||||
.matching(this.VALIGN)
|
||||
.onElements("thead", "tbody", "tfoot", "td", "th", "tr", "colgroup", "col")
|
||||
|
||||
.allowAttributes("charoff")
|
||||
.matching(this.NUMBER_OR_PERCENT)
|
||||
.onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
|
||||
|
||||
.allowAttributes("char")
|
||||
.matching(this.ONE_CHAR)
|
||||
.onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
|
||||
|
||||
.allowAttributes("colspan", "rowspan")
|
||||
.matching(this.NUMBER)
|
||||
.onElements("td", "th")
|
||||
|
||||
.allowAttributes("span", "width")
|
||||
.matching(this.NUMBER_OR_PERCENT)
|
||||
.onElements("colgroup", "col")
|
||||
.allowElements(
|
||||
"a",
|
||||
"label",
|
||||
"noscript",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"p",
|
||||
"i",
|
||||
"b",
|
||||
"u",
|
||||
"strong",
|
||||
"em",
|
||||
"small",
|
||||
"big",
|
||||
"pre",
|
||||
"code",
|
||||
"cite",
|
||||
"samp",
|
||||
"sub",
|
||||
"sup",
|
||||
"strike",
|
||||
"center",
|
||||
"blockquote",
|
||||
"hr",
|
||||
"br",
|
||||
"col",
|
||||
"font",
|
||||
"map",
|
||||
"span",
|
||||
"div",
|
||||
"img",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"dd",
|
||||
"dt",
|
||||
"dl",
|
||||
"tbody",
|
||||
"thead",
|
||||
"tfoot",
|
||||
"table",
|
||||
"td",
|
||||
"th",
|
||||
"tr",
|
||||
"colgroup",
|
||||
"fieldset",
|
||||
"legend"
|
||||
)
|
||||
.apply(html);
|
||||
}
|
||||
}
|
5
src/lib/kcSanitize/index.ts
Normal file
5
src/lib/kcSanitize/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { KcSanitizer } from "./KcSanitizer";
|
||||
|
||||
export function kcSanitize(html: string): string {
|
||||
return KcSanitizer.sanitize(html, {});
|
||||
}
|
@ -40,6 +40,8 @@ const LoginRecoveryAuthnCodeInput = lazy(() => import("keycloakify/login/pages/L
|
||||
const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp"));
|
||||
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
||||
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
||||
const LoginPasskeysConditionalAuthenticate = lazy(() => import("keycloakify/login/pages/LoginPasskeysConditionalAuthenticate"));
|
||||
const LoginIdpLinkConfirmOverride = lazy(() => import("keycloakify/login/pages/LoginIdpLinkConfirmOverride"));
|
||||
|
||||
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||
@ -121,6 +123,10 @@ export default function DefaultPage(props: DefaultPageProps) {
|
||||
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
||||
case "webauthn-error.ftl":
|
||||
return <WebauthnError kcContext={kcContext} {...rest} />;
|
||||
case "login-passkeys-conditional-authenticate.ftl":
|
||||
return <LoginPasskeysConditionalAuthenticate kcContext={kcContext} {...rest} />;
|
||||
case "login-idp-link-confirm-override.ftl":
|
||||
return <LoginIdpLinkConfirmOverride kcContext={kcContext} {...rest} />;
|
||||
}
|
||||
assert<Equals<typeof kcContext, never>>(false);
|
||||
})()}
|
||||
|
@ -2,7 +2,7 @@ import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constan
|
||||
import type { ValueOf } from "keycloakify/tools/ValueOf";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import type { ClassKey } from "keycloakify/login/lib/kcClsx";
|
||||
|
||||
export type ExtendKcContext<
|
||||
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||
@ -59,7 +59,9 @@ export type KcContext =
|
||||
| KcContext.LoginRecoveryAuthnCodeInput
|
||||
| KcContext.LoginResetOtp
|
||||
| KcContext.LoginX509Info
|
||||
| KcContext.WebauthnError;
|
||||
| KcContext.WebauthnError
|
||||
| KcContext.LoginPasskeysConditionalAuthenticate
|
||||
| KcContext.LoginIdpLinkConfirmOverride;
|
||||
|
||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
||||
|
||||
@ -92,6 +94,7 @@ export declare namespace KcContext {
|
||||
languageTag: string;
|
||||
}[];
|
||||
currentLanguageTag: string;
|
||||
rtl?: boolean;
|
||||
};
|
||||
auth?: {
|
||||
showUsername?: boolean;
|
||||
@ -99,7 +102,7 @@ export declare namespace KcContext {
|
||||
showTryAnotherWayLink?: boolean;
|
||||
attemptedUsername?: string;
|
||||
};
|
||||
scripts: string[];
|
||||
scripts?: string[];
|
||||
message?: {
|
||||
type: "success" | "warning" | "error" | "info";
|
||||
summary: string;
|
||||
@ -147,11 +150,6 @@ export declare namespace KcContext {
|
||||
|
||||
getFirstError: (...fieldNames: string[]) => string;
|
||||
};
|
||||
authenticationSession?: {
|
||||
authSessionId: string;
|
||||
tabId: string;
|
||||
ssoLoginInOtherTabsUrl: string;
|
||||
};
|
||||
properties: {};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
@ -191,7 +189,7 @@ export declare namespace KcContext {
|
||||
password?: string;
|
||||
};
|
||||
usernameHidden?: boolean;
|
||||
social: {
|
||||
social?: {
|
||||
displayInfo: boolean;
|
||||
providers?: {
|
||||
loginUrl: string;
|
||||
@ -211,9 +209,12 @@ export declare namespace KcContext {
|
||||
registrationAction: string;
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
recaptchaRequired: boolean;
|
||||
recaptchaRequired?: boolean;
|
||||
recaptchaVisible?: boolean;
|
||||
recaptchaSiteKey?: string;
|
||||
recaptchaAction?: string;
|
||||
termsAcceptanceRequired?: boolean;
|
||||
messageHeader?: string;
|
||||
};
|
||||
|
||||
export type Info = Common & {
|
||||
@ -328,7 +329,7 @@ export declare namespace KcContext {
|
||||
rememberMe?: string;
|
||||
};
|
||||
usernameHidden?: boolean;
|
||||
social: Login["social"];
|
||||
social?: Login["social"];
|
||||
};
|
||||
|
||||
export type LoginPassword = Common & {
|
||||
@ -346,9 +347,6 @@ export declare namespace KcContext {
|
||||
showTryAnotherWayLink?: boolean;
|
||||
attemptedUsername?: string;
|
||||
};
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebauthnAuthenticate = Common & {
|
||||
@ -360,13 +358,9 @@ export declare namespace KcContext {
|
||||
// I hate this:
|
||||
userVerification: UserVerificationRequirement | "not specified";
|
||||
rpId: string;
|
||||
createTimeout: string;
|
||||
createTimeout: string | number;
|
||||
isUserIdentified: "true" | "false";
|
||||
shouldDisplayAuthenticators: boolean;
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
login: {};
|
||||
realm: {
|
||||
password: boolean;
|
||||
registrationAllowed: boolean;
|
||||
@ -401,7 +395,7 @@ export declare namespace KcContext {
|
||||
authenticatorAttachment: string;
|
||||
requireResidentKey: string;
|
||||
userVerificationRequirement: string;
|
||||
createTimeout: number;
|
||||
createTimeout: number | string;
|
||||
excludeCredentialIds: string;
|
||||
isSetRetry?: boolean;
|
||||
isAppInitiatedAction?: boolean;
|
||||
@ -577,6 +571,40 @@ export declare namespace KcContext {
|
||||
pageId: "webauthn-error.ftl";
|
||||
isAppInitiatedAction?: boolean;
|
||||
};
|
||||
|
||||
export type LoginPasskeysConditionalAuthenticate = Common & {
|
||||
pageId: "login-passkeys-conditional-authenticate.ftl";
|
||||
realm: {
|
||||
registrationAllowed: boolean;
|
||||
password: boolean;
|
||||
};
|
||||
url: {
|
||||
registrationUrl: string;
|
||||
};
|
||||
registrationDisabled?: boolean;
|
||||
isUserIdentified: boolean | "true" | "false";
|
||||
challenge: string;
|
||||
userVerification: string;
|
||||
rpId: string;
|
||||
createTimeout: number | string;
|
||||
|
||||
authenticators?: {
|
||||
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
||||
};
|
||||
shouldDisplayAuthenticators?: boolean;
|
||||
usernameHidden?: boolean;
|
||||
login: {
|
||||
username?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginIdpLinkConfirmOverride = Common & {
|
||||
pageId: "login-idp-link-confirm-override.ftl";
|
||||
url: {
|
||||
loginRestartFlowUrl: string;
|
||||
};
|
||||
idpDisplayName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type UserProfile = {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import type { KcContext, Attribute } from "./KcContext";
|
||||
import {
|
||||
RESOURCES_COMMON,
|
||||
KEYCLOAK_RESOURCES,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||
type LoginThemePageId
|
||||
} from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
import type { LanguageTag } from "keycloakify/login/i18n/messages_defaultSet/types";
|
||||
|
||||
const attributesByName = Object.fromEntries(
|
||||
id<Attribute[]>([
|
||||
@ -76,7 +76,7 @@ const attributesByName = Object.fromEntries(
|
||||
]).map(attribute => [attribute.name, attribute])
|
||||
);
|
||||
|
||||
const resourcesPath = `${BASE_URL}${KEYCLOAK_RESOURCES}/login/resources`;
|
||||
const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES}/login`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
themeVersion: "0.0.0",
|
||||
@ -86,7 +86,7 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
url: {
|
||||
loginAction: "#",
|
||||
resourcesPath,
|
||||
resourcesCommonPath: `${resourcesPath}/${RESOURCES_COMMON}`,
|
||||
resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`,
|
||||
loginRestartFlowUrl: "#",
|
||||
loginUrl: "#",
|
||||
ssoLoginInOtherTabsUrl: "#"
|
||||
@ -117,35 +117,59 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
}
|
||||
},
|
||||
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
|
||||
),
|
||||
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"],
|
||||
["ar", "العربية"],
|
||||
["da", "Dansk"],
|
||||
["el", "Ελληνικά"],
|
||||
["fa", "فارسی"],
|
||||
["fi", "Suomi"],
|
||||
["hu", "Magyar"],
|
||||
["ka", "ქართული"],
|
||||
["lv", "Latviešu"],
|
||||
["pt", "Português"],
|
||||
["th", "ไทย"],
|
||||
["uk", "Українська"],
|
||||
["zh-TW", "中文繁體"]
|
||||
/* spell-checker: enable */
|
||||
] as const
|
||||
).map(([languageTag, label]) => {
|
||||
{
|
||||
type Got = typeof languageTag;
|
||||
type Expected = LanguageTag;
|
||||
|
||||
type Missing = Exclude<Expected, Got>;
|
||||
type Unexpected = Exclude<Got, Expected>;
|
||||
|
||||
assert<Equals<Missing, never>>;
|
||||
assert<Equals<Unexpected, never>>;
|
||||
}
|
||||
|
||||
return {
|
||||
languageTag,
|
||||
label,
|
||||
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
|
||||
} as const;
|
||||
}),
|
||||
|
||||
currentLanguageTag: "en"
|
||||
},
|
||||
@ -327,9 +351,6 @@ export const kcContextMocks = [
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
resetPasswordAllowed: true
|
||||
},
|
||||
social: {
|
||||
displayInfo: false
|
||||
}
|
||||
}),
|
||||
id<KcContext.WebauthnAuthenticate>({
|
||||
@ -349,11 +370,7 @@ export const kcContextMocks = [
|
||||
rpId: "",
|
||||
createTimeout: "0",
|
||||
isUserIdentified: "false",
|
||||
shouldDisplayAuthenticators: false,
|
||||
social: {
|
||||
displayInfo: false
|
||||
},
|
||||
login: {}
|
||||
shouldDisplayAuthenticators: false
|
||||
}),
|
||||
id<KcContext.LoginUpdatePassword>({
|
||||
...kcContextCommonMock,
|
||||
@ -567,6 +584,39 @@ export const kcContextMocks = [
|
||||
pageId: "webauthn-error.ftl",
|
||||
...kcContextCommonMock,
|
||||
isAppInitiatedAction: true
|
||||
}),
|
||||
id<KcContext.LoginPasskeysConditionalAuthenticate>({
|
||||
pageId: "login-passkeys-conditional-authenticate.ftl",
|
||||
...kcContextCommonMock,
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
registrationUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
password: true,
|
||||
registrationAllowed: true
|
||||
},
|
||||
registrationDisabled: false,
|
||||
isUserIdentified: "false",
|
||||
challenge: "",
|
||||
userVerification: "not specified",
|
||||
rpId: "",
|
||||
createTimeout: 0,
|
||||
authenticators: {
|
||||
authenticators: []
|
||||
},
|
||||
shouldDisplayAuthenticators: false,
|
||||
login: {}
|
||||
}),
|
||||
id<KcContext.LoginIdpLinkConfirmOverride>({
|
||||
pageId: "login-idp-link-confirm-override.ftl",
|
||||
...kcContextCommonMock,
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
loginRestartFlowUrl: "#"
|
||||
},
|
||||
idpDisplayName: "Google"
|
||||
})
|
||||
];
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/login/Template.useInitialize";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
@ -28,9 +27,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
|
||||
const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext;
|
||||
const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = documentTitle ?? msgStr("loginTitle", kcContext.realm.displayName);
|
||||
@ -46,71 +45,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
className: bodyClassName ?? kcClsx("kcBodyClass")
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { currentLanguageTag } = locale ?? {};
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (currentLanguageTag === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = currentLanguageTag;
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesCommonPath}/lib/pficon/pficon.css`,
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
]
|
||||
});
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "Template",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "module",
|
||||
src: `${url.resourcesPath}/js/menu-button-links.js`
|
||||
},
|
||||
...(authenticationSession === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: "module",
|
||||
textContent: [
|
||||
`import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`,
|
||||
``,
|
||||
`checkCookiesAndSetTimer(`,
|
||||
` "${authenticationSession.authSessionId}",`,
|
||||
` "${authenticationSession.tabId}",`,
|
||||
` "${url.ssoLoginInOtherTabsUrl}"`,
|
||||
`);`
|
||||
].join("\n")
|
||||
} as const
|
||||
]),
|
||||
...scripts.map(
|
||||
script =>
|
||||
({
|
||||
type: "text/javascript",
|
||||
src: script
|
||||
}) as const
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (areAllStyleSheetsLoaded) {
|
||||
insertScriptTags();
|
||||
}
|
||||
}, [areAllStyleSheetsLoaded]);
|
||||
|
||||
if (!areAllStyleSheetsLoaded) {
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -121,10 +58,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={kcClsx("kcFormCardClass")}>
|
||||
<header className={kcClsx("kcFormHeaderClass")}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
|
||||
{enabledLanguages.length > 1 && (
|
||||
<div className={kcClsx("kcLocaleMainClass")} id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={kcClsx("kcLocaleWrapperClass")}>
|
||||
<div id="kc-locale-dropdown" className={clsx("menu-button-links", kcClsx("kcLocaleDropDownClass"))}>
|
||||
@ -136,7 +72,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
aria-expanded="false"
|
||||
aria-controls="language-switch1"
|
||||
>
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
{currentLanguage.label}
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
@ -146,15 +82,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
id="language-switch1"
|
||||
className={kcClsx("kcLocaleListClass")}
|
||||
>
|
||||
{locale.supported.map(({ languageTag }, i) => (
|
||||
{enabledLanguages.map(({ languageTag, label, href }, i) => (
|
||||
<li key={languageTag} className={kcClsx("kcLocaleListItemClass")} role="none">
|
||||
<a
|
||||
role="menuitem"
|
||||
id={`language-${i + 1}`}
|
||||
className={kcClsx("kcLocaleItemClass")}
|
||||
href={getChangeLocaleUrl(languageTag)}
|
||||
>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
<a role="menuitem" id={`language-${i + 1}`} className={kcClsx("kcLocaleItemClass")} href={href}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
@ -215,7 +146,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
<span
|
||||
className={kcClsx("kcAlertTitleClass")}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.summary
|
||||
__html: kcSanitize(message.summary)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
72
src/login/Template.useInitialize.ts
Normal file
72
src/login/Template.useInitialize.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import type { KcContext } from "keycloakify/login/KcContext";
|
||||
|
||||
export type KcContextLike = {
|
||||
url: {
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
ssoLoginInOtherTabsUrl: string;
|
||||
};
|
||||
scripts?: string[];
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export function useInitialize(params: {
|
||||
kcContext: KcContextLike;
|
||||
doUseDefaultCss: boolean;
|
||||
}) {
|
||||
const { kcContext, doUseDefaultCss } = params;
|
||||
|
||||
const { url, scripts } = kcContext;
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesCommonPath}/lib/pficon/pficon.css`,
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
]
|
||||
});
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "Template",
|
||||
scriptTags: [
|
||||
// NOTE: The importmap is added in by the FTL script because it's too late to add it here.
|
||||
{
|
||||
type: "module",
|
||||
src: `${url.resourcesPath}/js/menu-button-links.js`
|
||||
},
|
||||
...(scripts === undefined
|
||||
? []
|
||||
: scripts.map(src => ({
|
||||
type: "text/javascript" as const,
|
||||
src
|
||||
}))),
|
||||
{
|
||||
type: "module",
|
||||
textContent: `
|
||||
import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";
|
||||
|
||||
checkCookiesAndSetTimer("${url.ssoLoginInOtherTabsUrl}");
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (areAllStyleSheetsLoaded) {
|
||||
insertScriptTags();
|
||||
}
|
||||
}, [areAllStyleSheetsLoaded]);
|
||||
|
||||
return { isReadyToRender: areAllStyleSheetsLoaded };
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user