Compare commits
149 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
2033a9ce0c | |||
fca18d9209 | |||
4f99088449 | |||
b1da684008 | |||
89fb6de2d5 | |||
b665bae3bb | |||
0b5a7544ca | |||
183826ca0d | |||
e507aace6b | |||
43c93ef0b4 | |||
093e51e092 | |||
17e1655eaf | |||
6b570f2b9a | |||
f239d105a7 | |||
776d8378e3 | |||
dd770cd7c6 | |||
4b3de54e18 | |||
5741cd1b2b | |||
b780d7136e | |||
3c28a05746 | |||
57ac5badba | |||
e873eb5123 | |||
c1a63edd71 | |||
37a060c4db | |||
157e4ac485 | |||
ba4d9675a8 | |||
e011fb094c | |||
f55a934939 | |||
96a88fe865 | |||
6cdb83d730 | |||
95f06df45d | |||
ec52b357d5 | |||
d84546cd7d | |||
4eee4156da | |||
70f475d13e | |||
3a50a61b12 | |||
a217f617d8 | |||
fdfcd78f02 | |||
56d6d8001a | |||
c3ee8e10e6 | |||
2f42732deb | |||
956b8260e7 | |||
b7954f87e0 | |||
540ce55dc2 | |||
d71a2c98d1 | |||
cb9cec676d | |||
9f2755bc7f | |||
fbe5a1f477 | |||
338642094d | |||
a3270d10f0 | |||
4c5924556a | |||
99a9b62c6c | |||
1497672a4e | |||
01161fd8ef | |||
68f5ee42e6 | |||
53955a0713 | |||
2271fd43b8 | |||
6a44cfb876 | |||
37c90d53e0 | |||
9a5ac5f13f | |||
6603852355 | |||
5670a71e6b | |||
332b1f74d9 | |||
c28caaa495 | |||
74fed835e8 | |||
6bb7f7dc16 | |||
84bb2338d1 | |||
caa42538a1 | |||
f5b9a8de55 | |||
dd33f554da | |||
7e84d0b108 | |||
9e1217fbf0 | |||
26d3c7f9e0 | |||
76542e6859 | |||
3c4bbf8aa7 | |||
ccc5ac6a1f | |||
b34f86d2f0 | |||
ee5f73519a | |||
22e7ff1424 | |||
7a89888d11 | |||
64fe15cf8c | |||
336813646f | |||
53a18c462a | |||
b893eee086 | |||
792020dd18 | |||
0c11ba05af | |||
2cf82f510e | |||
7c0cbe3a31 | |||
08e659cf01 | |||
97c3f4fa5f | |||
06a24d35cb | |||
9b6d1a957f | |||
189bd4697a | |||
d52252cd55 | |||
303bbc8431 | |||
727dc471c2 | |||
45b5c21ab5 | |||
adddce7764 | |||
b35a9f8f61 | |||
34b46a9280 | |||
383a9953e2 | |||
94bc7127fa | |||
289f0efd24 | |||
3f15586dae | |||
b8a33dabd4 | |||
3a4b44a83c | |||
8c6303f016 | |||
b71f3f559a | |||
6541460821 | |||
e63cd68a3e | |||
5602dc58ff | |||
8ed3561a55 | |||
4e72bc3a72 | |||
45c41ad321 | |||
14eccc761b | |||
6b24c4284f | |||
ba44de87d6 | |||
846181cfc7 | |||
2a8849dd11 | |||
5a879ece3c | |||
ffd734cc2d | |||
ac414489ff | |||
10da0cab47 | |||
59f8119047 | |||
a28a1531d9 | |||
977d0dc761 | |||
a0de1b38ce | |||
401f390e5b | |||
e69febe0c0 | |||
eba4ddbbd8 | |||
49bb276c78 | |||
b48bccb706 | |||
a2160fc8ce | |||
af829e9ac9 | |||
2c5473da27 | |||
e248a58201 | |||
d85ab889b3 | |||
49eae307cd | |||
a2563f0b7d | |||
c8d2866ada | |||
2f8d89012b | |||
a3d9016cfe | |||
24c14ea8f6 | |||
c93e787393 | |||
b6cc3ee022 | |||
30e4112f79 | |||
067e148897 | |||
7fc6f7a7ae | |||
60fcb5fe10 |
25
.github/release.yaml
vendored
25
.github/release.yaml
vendored
@ -1,25 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Breaking Changes 🛠
|
||||
labels:
|
||||
- breaking
|
||||
- title: Exciting New Features 🎉
|
||||
labels:
|
||||
- feature
|
||||
- title: Fixes 🔧
|
||||
labels:
|
||||
- fix
|
||||
- title: Documentation 🔧
|
||||
labels:
|
||||
- docs
|
||||
- title: CI 👷
|
||||
labels:
|
||||
- ci
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- '*'
|
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
@ -13,8 +13,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- name: If this step fails run 'yarn format' then commit again.
|
||||
run: yarn format:check
|
||||
@ -27,8 +27,8 @@ jobs:
|
||||
os: [ ubuntu-latest ]
|
||||
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
@ -41,8 +41,8 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- uses: bahmutov/npm-install@v1
|
||||
@ -70,7 +70,7 @@ jobs:
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
@ -88,7 +88,7 @@ jobs:
|
||||
needs:
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
@ -105,18 +105,18 @@ jobs:
|
||||
- create_github_release
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
|
||||
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
|
||||
env:
|
||||
DRY_RUN: "0"
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
with:
|
||||
action_name: remove_dark_mode_specific_images_from_readme
|
||||
- name: Publishing on NPM
|
||||
|
@ -1,11 +1,24 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"printWidth": 90,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "preserve",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
"arrowParens": "avoid",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "useUserProfileForm.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ export function DocsContainer({ children, context }) {
|
||||
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
<BaseContainer
|
||||
context={{
|
||||
@ -64,11 +63,6 @@ export function CanvasContainer({ children }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
19
.storybook/preview-head.html
Normal file
19
.storybook/preview-head.html
Normal file
@ -0,0 +1,19 @@
|
||||
<style>
|
||||
body.sb-show-main.sb-main-padded {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body:not(.kcBodyClass) {
|
||||
background-color: #393939;
|
||||
}
|
||||
|
||||
|
||||
body.sb-show-preparing-docs > .sb-wrapper {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body .sb-preparing-story {
|
||||
visibility: hidden;
|
||||
|
||||
}
|
||||
</style>
|
@ -116,10 +116,45 @@ const { getHardCodedWeight } = (() => {
|
||||
const orderedPagesPrefix = [
|
||||
"Introduction",
|
||||
"login/login.ftl",
|
||||
"login/register-user-profile.ftl",
|
||||
"login/register.ftl",
|
||||
"login/terms.ftl",
|
||||
"login/error.ftl",
|
||||
"login/code.ftl",
|
||||
"login/delete-account-confirm.ftl",
|
||||
"login/delete-credential.ftl",
|
||||
"login/frontchannel-logout.ftl",
|
||||
"login/idp-review-user-profile.ftl",
|
||||
"login/info.ftl",
|
||||
"login/login-config-totp.ftl",
|
||||
"login/login-idp-link-confirm.ftl",
|
||||
"login/login-idp-link-email.ftl",
|
||||
"login/login-oauth-grant.ftl",
|
||||
"login/login-otp.ftl",
|
||||
"login/login-page-expired.ftl",
|
||||
"login/login-password.ftl",
|
||||
"login/login-reset-otp.ftl",
|
||||
"login/login-reset-password.ftl",
|
||||
"login/login-update-password.ftl",
|
||||
"login/login-update-profile.ftl",
|
||||
"login/login-username.ftl",
|
||||
"login/login-verify-email.ftl",
|
||||
"login/login-x509-info.ftl",
|
||||
"login/logout-confirm.ftl",
|
||||
"login/saml-post-form.ftl",
|
||||
"login/select-authenticator.ftl",
|
||||
"login/update-email.ftl",
|
||||
"login/webauthn-authenticate.ftl",
|
||||
"login/webauthn-error.ftl",
|
||||
"login/webauthn-register.ftl",
|
||||
"login/login-oauth2-device-verify-user-code.ftl",
|
||||
"login/login-recovery-authn-code-config.ftl",
|
||||
"login/login-recovery-authn-code-input.ftl",
|
||||
"account/account.ftl",
|
||||
"account/password.ftl",
|
||||
"account/federatedIdentity.ftl",
|
||||
"account/log.ftl",
|
||||
"account/sessions.ftl",
|
||||
"account/totp.ftl",
|
||||
];
|
||||
|
||||
function getHardCodedWeight(kind) {
|
||||
|
49
.storybook/static/tos/tos_en.md
Normal file
49
.storybook/static/tos/tos_en.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Overview
|
||||
|
||||
This Terms of Service document outlines the rules and regulations for the use of **Example Company's** Services.
|
||||
|
||||
## Acceptance of Terms
|
||||
|
||||
By accessing and using our services, you acknowledge that you have read, understood, and agree to be bound by these terms. If you do not accept these terms, you are not authorized to use our services.
|
||||
|
||||
## Description of Service
|
||||
|
||||
**Example Service** (hereinafter referred to as "the Service") is a web-based solution offered by **Example Company** (hereinafter referred to as "the Company"). Our service provides users with access to [documentation](https://example.com/docs) and support for managing their projects effectively.
|
||||
|
||||
## Modifications to the Terms of Service
|
||||
|
||||
The Company reserves the right to modify these terms at any time. Such modifications will be effective immediately upon posting the updated terms on our website. Your continued use of the Service after any such changes shall constitute your consent to such changes.
|
||||
|
||||
## Account Registration
|
||||
|
||||
You may be required to register with the Service to access certain features. When registering, you agree to provide accurate, current, and complete information about yourself as requested.
|
||||
|
||||
## User Responsibilities
|
||||
|
||||
- **Data Security**: Users are responsible for safeguarding their login credentials and should not disclose their passwords to any third party.
|
||||
- **Acceptable Use**: Users are expected to use the Service in a responsible manner that does not infringe upon the rights of others.
|
||||
- **Content Ownership**: Users retain all rights to the content they upload to the Service but grant the Company a license to use and distribute this content as part of the Service.
|
||||
|
||||
## Intellectual Property
|
||||
|
||||
All intellectual property rights related to the Service and its original content, features, and functionality are owned by the Company.
|
||||
|
||||
## Termination
|
||||
|
||||
The Company may terminate or suspend access to our Service immediately, without prior notice or liability, for any reason whatsoever, including, without limitation, breach of these Terms.
|
||||
|
||||
## Governing Law
|
||||
|
||||
These Terms shall be governed and construed in accordance with the laws of [Your Country], without regard to its conflict of law provisions.
|
||||
|
||||
## Contact Information
|
||||
|
||||
For any questions about these Terms, please contact us at [support@example.com](mailto:support@example.com) or visit our [FAQ page](https://example.com/faq).
|
||||
|
||||
## Changes to Terms of Service
|
||||
|
||||
We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect.
|
||||
|
||||
## Effective Date
|
||||
|
||||
These terms are effective as of **[Insert Date]**.
|
49
.storybook/static/tos/tos_es.md
Normal file
49
.storybook/static/tos/tos_es.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Resumen
|
||||
|
||||
Este documento de Términos de Servicio detalla las reglas y regulaciones para el uso de los servicios de **Empresa Ejemplo**.
|
||||
|
||||
## Aceptación de Términos
|
||||
|
||||
Al acceder y utilizar nuestros servicios, usted reconoce que ha leído, entendido y acepta estar vinculado por estos términos. Si no acepta estos términos, no está autorizado para usar nuestros servicios.
|
||||
|
||||
## Descripción del Servicio
|
||||
|
||||
**Servicio Ejemplo** (en adelante denominado "el Servicio") es una solución basada en la web ofrecida por **Empresa Ejemplo** (en adelante denominada "la Empresa"). Nuestro servicio proporciona a los usuarios acceso a [documentación](https://ejemplo.com/docs) y soporte para gestionar sus proyectos de manera efectiva.
|
||||
|
||||
## Modificaciones a los Términos de Servicio
|
||||
|
||||
La Empresa se reserva el derecho de modificar estos términos en cualquier momento. Dichas modificaciones entrarán en vigor inmediatamente después de la publicación de los términos actualizados en nuestro sitio web. Su uso continuado del Servicio después de tales cambios constituirá su consentimiento a dichos cambios.
|
||||
|
||||
## Registro de Cuenta
|
||||
|
||||
Puede ser necesario que se registre en el Servicio para acceder a ciertas características. Al registrarse, usted acepta proporcionar información precisa, actual y completa sobre sí mismo como se solicita.
|
||||
|
||||
## Responsabilidades del Usuario
|
||||
|
||||
- **Seguridad de Datos**: Los usuarios son responsables de salvaguardar sus credenciales de inicio de sesión y no deben divulgar sus contraseñas a terceros.
|
||||
- **Uso Aceptable**: Se espera que los usuarios utilicen el Servicio de manera responsable que no infrinja los derechos de otros.
|
||||
- **Propiedad del Contenido**: Los usuarios retienen todos los derechos sobre el contenido que cargan en el Servicio, pero otorgan a la Empresa una licencia para usar y distribuir este contenido como parte del Servicio.
|
||||
|
||||
## Propiedad Intelectual
|
||||
|
||||
Todos los derechos de propiedad intelectual relacionados con el Servicio y su contenido original, características y funcionalidad son propiedad de la Empresa.
|
||||
|
||||
## Terminación
|
||||
|
||||
La Empresa puede terminar o suspender su acceso a nuestro Servicio de inmediato, sin previo aviso ni responsabilidad, por cualquier motivo, incluido, entre otros, una violación de estos Términos.
|
||||
|
||||
## Ley Aplicable
|
||||
|
||||
Estos Términos se regirán e interpretarán de acuerdo con las leyes de [Su País], sin tener en cuenta sus disposiciones de conflicto de leyes.
|
||||
|
||||
## Información de Contacto
|
||||
|
||||
Para cualquier pregunta sobre estos Términos, contáctenos en [support@ejemplo.com](mailto:support@ejemplo.com) o visite nuestra [página de FAQ](https://ejemplo.com/faq).
|
||||
|
||||
## Cambios a los Términos de Servicio
|
||||
|
||||
Nos reservamos el derecho, a nuestra única discreción, de modificar o reemplazar estos Términos en cualquier momento. Si una revisión es material, proporcionaremos al menos 30 días de aviso antes de que los nuevos términos entren en vigor.
|
||||
|
||||
## Fecha de Efectividad
|
||||
|
||||
Estos términos son efectivos a partir del **[Insertar Fecha]**.
|
49
.storybook/static/tos/tos_fr.md
Normal file
49
.storybook/static/tos/tos_fr.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document des Conditions Générales d'Utilisation détaille les règles et réglementations pour l'utilisation des services de **l'Entreprise Exemple**.
|
||||
|
||||
## Acceptation des Conditions
|
||||
|
||||
En accédant et en utilisant nos services, vous reconnaissez avoir lu, compris et accepté d'être lié par ces conditions. Si vous n'acceptez pas ces termes, vous n'êtes pas autorisé à utiliser nos services.
|
||||
|
||||
## Description du Service
|
||||
|
||||
**Service Exemple** (ci-après dénommé "le Service") est une solution basée sur le web offerte par **l'Entreprise Exemple** (ci-après dénommée "l'Entreprise"). Notre service offre aux utilisateurs un accès à la [documentation](https://exemple.com/docs) et un support pour gérer efficacement leurs projets.
|
||||
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
L'Entreprise se réserve le droit de modifier ces conditions à tout moment. De telles modifications entreront en vigueur immédiatement après la publication des termes mis à jour sur notre site web. Votre utilisation continue du Service après de tels changements constitue votre consentement à ces modifications.
|
||||
|
||||
## Inscription au Compte
|
||||
|
||||
Vous devrez peut-être vous inscrire au Service pour accéder à certaines fonctionnalités. Lors de l'inscription, vous acceptez de fournir des informations précises, actuelles et complètes vous concernant, comme demandé.
|
||||
|
||||
## Responsabilités des Utilisateurs
|
||||
|
||||
- **Sécurité des Données** : Les utilisateurs sont responsables de la sauvegarde de leurs identifiants de connexion et ne doivent divulguer leurs mots de passe à aucun tiers.
|
||||
- **Utilisation Acceptable** : Les utilisateurs sont censés utiliser le Service de manière responsable qui ne porte pas atteinte aux droits d'autrui.
|
||||
- **Propriété du Contenu** : Les utilisateurs conservent tous les droits sur le contenu qu'ils téléchargent sur le Service mais accordent à l'Entreprise une licence pour utiliser et distribuer ce contenu dans le cadre du Service.
|
||||
|
||||
## Propriété Intellectuelle
|
||||
|
||||
Tous les droits de propriété intellectuelle relatifs au Service et à son contenu original, fonctionnalités et fonctionnement sont détenus par l'Entreprise.
|
||||
|
||||
## Résiliation
|
||||
|
||||
L'Entreprise peut résilier ou suspendre votre accès à notre Service immédiatement, sans préavis ni responsabilité, pour quelque raison que ce soit, y compris, sans limitation, en cas de violation de ces Conditions.
|
||||
|
||||
## Loi Applicable
|
||||
|
||||
Ces Conditions seront régies et interprétées conformément aux lois de [Votre Pays], sans égard à ses dispositions de conflit de lois.
|
||||
|
||||
## Informations de Contact
|
||||
|
||||
Pour toute question concernant ces Conditions, veuillez nous contacter à [support@exemple.com](mailto:support@exemple.com) ou visitez notre [page FAQ](https://exemple.com/faq).
|
||||
|
||||
## Modifications des Conditions de Service
|
||||
|
||||
Nous nous réservons le droit, à notre seule discrétion, de modifier ou de remplacer ces Conditions à tout moment. Si une révision est importante, nous vous fournirons un préavis d'au moins 30 jours avant que les nouveaux termes prennent effet.
|
||||
|
||||
## Date d'Effet
|
||||
|
||||
Ces conditions sont effectives à partir du **[Insérer la Date]**.
|
103
package.json
103
package.json
@ -1,31 +1,24 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.6",
|
||||
"version": "10.0.0-rc.25",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn generate-i18n-messages",
|
||||
"build": "tsc -p src/bin && tsc -p src && tsc -p src/vite-plugin && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn run copy-files && cp -r src dist/",
|
||||
"watch": "chokidar './src/**/*' -c 'yarn build'",
|
||||
"grant-exec-perms": "ts-node --skipProject scripts/grant-exec-perms.ts",
|
||||
"copy-files": "copyfiles -u 1 'src/**/*.ftl' dist/",
|
||||
"prepare": "patch-package && ts-node --skipProject scripts/generate-i18n-messages.ts",
|
||||
"build": "ts-node --skipProject scripts/build.ts",
|
||||
"storybook": "ts-node --skipProject scripts/start-storybook.ts",
|
||||
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts",
|
||||
"test": "yarn test:types && vitest run",
|
||||
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
|
||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||
"format": "yarn _format --write",
|
||||
"format:check": "yarn _format --list-different",
|
||||
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
|
||||
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
|
||||
"link-in-starter": "ts-node --skipProject scripts/link-in-app.ts keycloakify-starter && yarn watch",
|
||||
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
|
||||
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
|
||||
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
|
||||
"build-storybook": "ts-node --skipProject scripts/build-storybook.ts",
|
||||
"dump-keycloak-realm": "ts-node --skipProject scripts/dump-keycloak-realm.ts"
|
||||
},
|
||||
"bin": {
|
||||
"keycloakify": "dist/bin/main.js"
|
||||
@ -43,28 +36,46 @@
|
||||
"author": "u/garronej",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src/",
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/tsconfig.tsbuildinfo"
|
||||
"!dist/bin/",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/constants.d.ts",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
"dist/vite-plugin/vite-plugin.d.ts",
|
||||
"dist/vite-plugin/index.js"
|
||||
],
|
||||
"keywords": [
|
||||
"bluehats",
|
||||
"keycloak",
|
||||
"react",
|
||||
"theme",
|
||||
"FreeMarker",
|
||||
"ftl",
|
||||
"login",
|
||||
"register"
|
||||
"register",
|
||||
"account",
|
||||
"bluehats"
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimal-polyfills": "^2.2.3",
|
||||
"react-markdown": "^5.0.3",
|
||||
"tsafe": "^1.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@babel/core": "^7.24.5",
|
||||
"@babel/generator": "^7.24.5",
|
||||
"@babel/parser": "^7.24.5",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@storybook/addon-a11y": "^6.5.16",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
@ -80,48 +91,36 @@
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/yauzl": "^2.10.0",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cli-select": "^1.1.2",
|
||||
"eslint-plugin-storybook": "^0.6.7",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^11.0.0",
|
||||
"powerhooks": "^0.26.7",
|
||||
"prettier": "^2.3.0",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"powerhooks": "^1.0.10",
|
||||
"prettier": "^3.2.5",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recast": "^0.23.3",
|
||||
"run-exclusive": "^2.2.19",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"storybook-dark-mode": "^1.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.3",
|
||||
"tss-react": "^4.8.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.0.12",
|
||||
"vitest": "^0.29.8",
|
||||
"zod-to-json-schema": "^3.20.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.22.9",
|
||||
"@babel/parser": "^7.22.7",
|
||||
"@babel/types": "^7.22.5",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"cli-select": "^1.1.2",
|
||||
"evt": "^2.5.7",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"minimal-polyfills": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"react-markdown": "^5.0.3",
|
||||
"recast": "^0.23.3",
|
||||
"rfc4648": "^1.5.2",
|
||||
"termost": "^0.12.0",
|
||||
"tsafe": "^1.6.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tss-react": "^4.9.10",
|
||||
"typescript": "^4.9.1-beta",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^0.29.8",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1",
|
||||
"zod": "^3.17.10"
|
||||
"zod": "^3.17.10",
|
||||
"evt": "^2.5.7"
|
||||
}
|
||||
}
|
||||
|
136
patches/termost+0.12.0.patch
Normal file
136
patches/termost+0.12.0.patch
Normal file
File diff suppressed because one or more lines are too long
19
scripts/build-storybook.ts
Normal file
19
scripts/build-storybook.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as child_process from "child_process";
|
||||
import { join } from "path";
|
||||
|
||||
run("yarn build");
|
||||
|
||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
||||
env: {
|
||||
...process.env,
|
||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
||||
}
|
||||
});
|
||||
|
||||
run("npx build-storybook");
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
133
scripts/build.ts
Normal file
133
scripts/build.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join, relative } 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)) {
|
||||
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 });
|
||||
|
||||
patchDeprecatedBufferApiUsage(join("dist", "bin", "main.js"));
|
||||
|
||||
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"
|
||||
)}`
|
||||
);
|
||||
|
||||
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 });
|
||||
|
||||
patchDeprecatedBufferApiUsage(join("dist", "vite-plugin", "index.js"));
|
||||
|
||||
fs.rmSync(join("dist", "src"), { recursive: true, force: true });
|
||||
|
||||
fs.cpSync("src", join("dist", "src"), { recursive: true });
|
||||
|
||||
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);`
|
||||
);
|
||||
|
||||
assert(after !== before, `Patch failed for ${relative(process.cwd(), filePath)}`);
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||
}
|
45
scripts/dump-keycloak-realm.ts
Normal file
45
scripts/dump-keycloak-realm.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { containerName } from "../src/bin/shared/constants";
|
||||
import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import chalk from "chalk";
|
||||
|
||||
run(
|
||||
[
|
||||
`docker exec -it ${containerName}`,
|
||||
`/opt/keycloak/bin/kc.sh export`,
|
||||
`--dir /tmp`,
|
||||
`--realm myrealm`,
|
||||
`--users realm_file`
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
.split(":")[1]
|
||||
).major;
|
||||
|
||||
const targetFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
)
|
||||
);
|
||||
|
||||
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
||||
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
|
||||
function run(command: string) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
return child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../src/bin/shared/downloadBuiltinKeycloakTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { getLogger } from "../src/bin/tools/logger";
|
||||
import { rmSync } from "../src/bin/tools/fs.rmSync";
|
||||
|
||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
@ -13,29 +17,21 @@ import { rmSync } from "../src/bin/tools/fs.rmSync";
|
||||
//@ts-ignore
|
||||
const propertiesParser = require("properties-parser");
|
||||
|
||||
const isSilent = true;
|
||||
|
||||
const logger = getLogger({ isSilent });
|
||||
|
||||
async function main() {
|
||||
const keycloakVersion = "24.0.4";
|
||||
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
fs.mkdirSync(tmpDirPath);
|
||||
|
||||
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
"buildOptions": {
|
||||
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
|
||||
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
|
||||
buildOptions: {
|
||||
cacheDirPath: pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"node_modules",
|
||||
".cache",
|
||||
"keycloakify"
|
||||
),
|
||||
npmWorkspaceRootDirPath: thisCodebaseRootDirPath
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,12 +40,14 @@ async function main() {
|
||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
||||
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
|
||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
|
||||
crawl({
|
||||
"dirPath": baseThemeDirPath,
|
||||
"returnedPathsType": "relative to dirPath"
|
||||
dirPath: baseThemeDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
}).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
@ -60,15 +58,20 @@ async function main() {
|
||||
const [, typeOfPage, language] = match;
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
||||
([key, value]: any) => [key, value.replace(/''/g, "'")]
|
||||
)
|
||||
Object.entries(
|
||||
propertiesParser.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||
.toString("utf8")
|
||||
)
|
||||
).map(([key, value]: any) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value.replace(/''/g, "'")
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true });
|
||||
|
||||
Object.keys(record).forEach(themeType => {
|
||||
const recordForPageType = record[themeType];
|
||||
|
||||
@ -76,19 +79,28 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"baseMessages"
|
||||
);
|
||||
|
||||
const languages = Object.keys(recordForPageType);
|
||||
|
||||
const generatedFileHeader = [
|
||||
`//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
|
||||
`//This code was automatically generated by running ${pathRelative(
|
||||
thisCodebaseRootDirPath,
|
||||
__filename
|
||||
)}`,
|
||||
"//PLEASE DO NOT EDIT MANUALLY"
|
||||
].join("\n");
|
||||
|
||||
languages.forEach(language => {
|
||||
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
|
||||
|
||||
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
|
||||
fs.mkdirSync(pathDirname(filePath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
@ -97,7 +109,11 @@ async function main() {
|
||||
generatedFileHeader,
|
||||
"",
|
||||
"/* spell-checker: disable */",
|
||||
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||
`const messages= ${JSON.stringify(
|
||||
recordForPageType[language],
|
||||
null,
|
||||
2
|
||||
)};`,
|
||||
"",
|
||||
"export default messages;",
|
||||
"/* spell-checker: enable */"
|
||||
@ -106,7 +122,7 @@ async function main() {
|
||||
)
|
||||
);
|
||||
|
||||
logger.log(`${filePath} wrote`);
|
||||
//console.log(`${filePath} wrote`);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
@ -122,7 +138,10 @@ async function main() {
|
||||
` case "en": return en;`,
|
||||
...languages
|
||||
.filter(language => language !== "en")
|
||||
.map(language => ` case "${language}": return import("./${language}");`),
|
||||
.map(
|
||||
language =>
|
||||
` case "${language}": return import("./${language}");`
|
||||
),
|
||||
' default: return { "default": {} };',
|
||||
" }",
|
||||
" })();",
|
||||
|
@ -10,7 +10,8 @@ import { chmod, stat } from "fs/promises";
|
||||
const promises = Object.values<string>(bin).map(async scriptPath => {
|
||||
const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
|
||||
const oldMode = (await stat(fullPath)).mode;
|
||||
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||
const newMode =
|
||||
oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||
await chmod(fullPath, newMode);
|
||||
});
|
||||
|
||||
|
@ -13,20 +13,26 @@ fs.writeFileSync(
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
(() => {
|
||||
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
|
||||
const packageJsonParsed = JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(rootDirPath, "package.json"))
|
||||
.toString("utf8")
|
||||
);
|
||||
|
||||
return {
|
||||
...packageJsonParsed,
|
||||
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||
"exports": !("exports" in packageJsonParsed)
|
||||
main: packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||
types: packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||
module: packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||
exports: !("exports" in packageJsonParsed)
|
||||
? undefined
|
||||
: Object.fromEntries(
|
||||
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
|
||||
key,
|
||||
(value as string).replace(/^\.\/dist\//, "./")
|
||||
])
|
||||
Object.entries(packageJsonParsed["exports"]).map(
|
||||
([key, value]) => [
|
||||
key,
|
||||
(value as string).replace(/^\.\/dist\//, "./")
|
||||
]
|
||||
)
|
||||
)
|
||||
};
|
||||
})(),
|
||||
@ -37,8 +43,6 @@ fs.writeFileSync(
|
||||
)
|
||||
);
|
||||
|
||||
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
|
||||
|
||||
const commonThirdPartyDeps = (() => {
|
||||
// For example [ "@emotion" ] it's more convenient than
|
||||
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||
@ -49,7 +53,9 @@ const commonThirdPartyDeps = (() => {
|
||||
...namespaceSingletonDependencies
|
||||
.map(namespaceModuleName =>
|
||||
fs
|
||||
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||
.readdirSync(
|
||||
pathJoin(rootDirPath, "node_modules", namespaceModuleName)
|
||||
)
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||
)
|
||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||
@ -59,21 +65,25 @@ const commonThirdPartyDeps = (() => {
|
||||
|
||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||
|
||||
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
|
||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||
fs.mkdirSync(yarnGlobalDirPath);
|
||||
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
const { targetModuleName, cwd } = params;
|
||||
|
||||
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
|
||||
const cmd = [
|
||||
"yarn",
|
||||
"link",
|
||||
...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])
|
||||
].join(" ");
|
||||
|
||||
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
"env": {
|
||||
env: {
|
||||
...process.env,
|
||||
"HOME": yarnGlobalDirPath
|
||||
HOME: yarnGlobalDirPath
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -89,7 +99,9 @@ const testAppPaths = (() => {
|
||||
return testAppPath;
|
||||
}
|
||||
|
||||
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
|
||||
console.warn(
|
||||
`Skipping ${testAppName} since it cant be found here: ${testAppPath}`
|
||||
);
|
||||
|
||||
return undefined;
|
||||
})
|
||||
@ -101,7 +113,7 @@ if (testAppPaths.length === 0) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
||||
|
||||
console.log("=== Linking common dependencies ===");
|
||||
|
||||
@ -114,29 +126,37 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||
|
||||
const localInstallPath = pathJoin(
|
||||
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
||||
...[
|
||||
rootDirPath,
|
||||
"node_modules",
|
||||
...(commonThirdPartyDep.startsWith("@")
|
||||
? commonThirdPartyDep.split("/")
|
||||
: [commonThirdPartyDep])
|
||||
]
|
||||
);
|
||||
|
||||
execYarnLink({ "cwd": localInstallPath });
|
||||
execYarnLink({ cwd: localInstallPath });
|
||||
});
|
||||
|
||||
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": commonThirdPartyDep
|
||||
cwd: testAppPath,
|
||||
targetModuleName: commonThirdPartyDep
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log("=== Linking in house dependencies ===");
|
||||
|
||||
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
|
||||
execYarnLink({ cwd: pathJoin(rootDirPath, "dist") });
|
||||
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
|
||||
cwd: testAppPath,
|
||||
targetModuleName: JSON.parse(
|
||||
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
|
||||
)["name"]
|
||||
})
|
||||
);
|
||||
|
||||
|
28
scripts/link-in-starter.ts
Normal file
28
scripts/link-in-starter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
|
||||
fs.rmSync("node_modules", { recursive: true, force: true });
|
||||
fs.rmSync("dist", { recursive: true, force: true });
|
||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
|
||||
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
run("yarn install", { cwd: join("..", "keycloakify-starter") });
|
||||
|
||||
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
function run(command: string, options?: { cwd: string }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
31
scripts/start-storybook.ts
Normal file
31
scripts/start-storybook.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||
|
||||
run("yarn build");
|
||||
|
||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
||||
env: {
|
||||
...process.env,
|
||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"]);
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", process.exit.bind(process));
|
||||
}
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
36
scripts/startRebuildOnSrcChange.ts
Normal file
36
scripts/startRebuildOnSrcChange.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import * as child_process from "child_process";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import chokidar from "chokidar";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function startRebuildOnSrcChange() {
|
||||
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
|
||||
|
||||
const runYarnBuild = runExclusive.build(async () => {
|
||||
console.log(chalk.green("Running `yarn build`"));
|
||||
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn("yarn", ["build"]);
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", () => dCompleted.resolve());
|
||||
|
||||
await dCompleted.pr;
|
||||
|
||||
console.log("\n\n");
|
||||
});
|
||||
|
||||
console.log(chalk.green("Watching for changes in src/"));
|
||||
|
||||
chokidar.watch("src", { ignoreInitial: true }).on("all", async () => {
|
||||
await waitForDebounce();
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const testDir = "keycloakify_starter_test";
|
||||
|
||||
if (existsSync(path.join(process.cwd(), testDir))) {
|
||||
rmSync(path.join(process.cwd(), testDir), { recursive: true });
|
||||
}
|
||||
// Build and link package
|
||||
execSync("yarn build");
|
||||
const pkgJSON = JSON.parse(readFileSync(path.join(process.cwd(), "package.json")).toString("utf8"));
|
||||
pkgJSON.main = "./index.js";
|
||||
pkgJSON.types = "./index.d.ts";
|
||||
pkgJSON.scripts.prepare = undefined;
|
||||
writeFileSync(path.join(process.cwd(), "dist", "package.json"), JSON.stringify(pkgJSON));
|
||||
// Wrapped in a try/catch because unlink errors if the package isn't linked
|
||||
try {
|
||||
execSync("yarn unlink");
|
||||
} catch {}
|
||||
execSync("yarn link", { "cwd": path.join(process.cwd(), "dist") });
|
||||
|
||||
// Clone latest keycloakify-starter and link to keycloakify output
|
||||
execSync(`git clone https://github.com/keycloakify/keycloakify-starter.git ${testDir}`);
|
||||
execSync("yarn install", { "cwd": path.join(process.cwd(), testDir) });
|
||||
execSync("yarn link keycloakify", { "cwd": path.join(process.cwd(), testDir) });
|
||||
|
||||
//Ensure keycloak theme can be built
|
||||
execSync("yarn build-keycloak-theme", { "cwd": path.join(process.cwd(), testDir) });
|
@ -1,8 +1,11 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
|
||||
import {
|
||||
nameOfTheGlobal,
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
} from "keycloakify/bin/shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
* This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
|
||||
* This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects.
|
||||
* This works both in your main app and in your Keycloak theme.
|
||||
*/
|
||||
export const PUBLIC_URL = (() => {
|
||||
|
@ -2,14 +2,12 @@ import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import { createUseInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
|
||||
const { useInsertLinkTags } = createUseInsertLinkTags();
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
|
||||
@ -24,13 +22,13 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
"qualifiedName": "html",
|
||||
"className": getClassName("kcHtmlClass")
|
||||
qualifiedName: "html",
|
||||
className: getClassName("kcHtmlClass")
|
||||
});
|
||||
|
||||
useSetClassName({
|
||||
"qualifiedName": "body",
|
||||
"className": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
qualifiedName: "body",
|
||||
className: clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,7 +44,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
"hrefs": !doUseDefaultCss
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
|
@ -2,7 +2,10 @@ import type { ReactNode } from "react";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
|
||||
export type TemplateProps<
|
||||
KcContext extends KcContext.Common,
|
||||
I18nExtended extends I18n
|
||||
> = {
|
||||
kcContext: KcContext;
|
||||
i18n: I18nExtended;
|
||||
doUseDefaultCss: boolean;
|
||||
|
@ -1,11 +1,9 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import fallbackMessages from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { KcContext } from "../kcContext/KcContext";
|
||||
import { Markdown } from "keycloakify/tools/Markdown";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
|
||||
@ -53,16 +51,31 @@ export type GenericI18n<MessageKey extends string> = {
|
||||
*/
|
||||
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
||||
/**
|
||||
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
||||
* in the Keycloak admin for example.
|
||||
*
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||
* {
|
||||
* en: {
|
||||
* "access-denied": "Access denied",
|
||||
* "foo": "Foo {0} {1}",
|
||||
* "bar": "Bar {0}"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* advancedMsg("${access-denied} foo bar") === <span>{msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
||||
* advancedMsg("${bar}", "<strong>c</strong>")
|
||||
* === <span>{msgStr("bar", "<strong>XXX</strong>")}<span>
|
||||
* === <span>Bar <strong>XXX</strong></span> (The html in the arg is partially escaped for security reasons, it might be untrusted)
|
||||
* advancedMsg("${foo} xx ${bar}", "a", "b", "c")
|
||||
* === <span>{msgStr("foo", "a", "b")} xx {msgStr("bar")}<span>
|
||||
* === <span>Foo a b xx Bar {0}</span> (The substitution are only applied in the first message)
|
||||
*/
|
||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||
/**
|
||||
* Examples assuming currentLanguageTag === "en"
|
||||
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
||||
*/
|
||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||
};
|
||||
@ -91,19 +104,19 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
||||
|
||||
setI18n({
|
||||
...createI18nTranslationFunctions({
|
||||
"fallbackMessages": {
|
||||
fallbackMessages: {
|
||||
...fallbackMessages,
|
||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||
} as any,
|
||||
"messages": {
|
||||
messages: {
|
||||
...(await getMessages(currentLanguageTag)),
|
||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||
...(extraMessages[currentLanguageTag] ?? {})
|
||||
} as any
|
||||
}),
|
||||
currentLanguageTag,
|
||||
"getChangeLocalUrl": newLanguageTag => {
|
||||
getChangeLocalUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
@ -114,7 +127,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
||||
|
||||
return targetSupportedLocale.url;
|
||||
},
|
||||
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||
labelBySupportedLanguageTag: Object.fromEntries(
|
||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||
)
|
||||
});
|
||||
@ -133,8 +146,8 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const { fallbackMessages, messages } = params;
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
||||
const { key, args, doRenderMarkdown } = props;
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||
const { key, args, doRenderAsHtml } = props;
|
||||
|
||||
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||
|
||||
@ -163,68 +176,94 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
||||
);
|
||||
});
|
||||
|
||||
return messageWithArgsInjected;
|
||||
})();
|
||||
|
||||
return doRenderMarkdown ? (
|
||||
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||
{messageWithArgsInjectedIfAny}
|
||||
</Markdown>
|
||||
return doRenderAsHtml ? (
|
||||
<span
|
||||
// NOTE: The message is trusted. The arguments are not but are escaped.
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messageWithArgsInjectedIfAny
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
messageWithArgsInjectedIfAny
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
||||
const { key, args, doRenderMarkdown } = props;
|
||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
||||
const { key, args, doRenderAsHtml } = props;
|
||||
|
||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||
if (!/\$\{[^}]+\}/.test(key)) {
|
||||
const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml });
|
||||
|
||||
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||
if (resolvedMessage === undefined) {
|
||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: key }} /> : key;
|
||||
}
|
||||
|
||||
const out = resolveMsg({
|
||||
"key": keyUnwrappedFromCurlyBraces,
|
||||
args,
|
||||
doRenderMarkdown
|
||||
return resolvedMessage;
|
||||
}
|
||||
|
||||
let isFirstMatch = true;
|
||||
|
||||
const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => {
|
||||
const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i;
|
||||
|
||||
isFirstMatch = false;
|
||||
|
||||
return replaceBy;
|
||||
});
|
||||
|
||||
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: resolvedComplexMessage }} /> : resolvedComplexMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
||||
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
||||
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
||||
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
||||
advancedMsg: (key, ...args) =>
|
||||
resolveMsgAdvanced({
|
||||
key,
|
||||
args,
|
||||
doRenderAsHtml: true
|
||||
}) as JSX.Element,
|
||||
advancedMsgStr: (key, ...args) =>
|
||||
resolveMsgAdvanced({
|
||||
key,
|
||||
args,
|
||||
doRenderAsHtml: false
|
||||
}) as string
|
||||
};
|
||||
}
|
||||
|
||||
const keycloakifyExtraMessages = {
|
||||
"en": {
|
||||
"shouldBeEqual": "{0} should be equal to {1}",
|
||||
"shouldBeDifferent": "{0} should be different to {1}",
|
||||
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||
"mustBeAnInteger": "Must be an integer",
|
||||
"notAValidOption": "Not a valid option",
|
||||
"newPasswordSameAsOld": "New password must be different from the old one",
|
||||
"passwordConfirmNotMatch": "Password confirmation does not match"
|
||||
en: {
|
||||
shouldBeEqual: "{0} should be equal to {1}",
|
||||
shouldBeDifferent: "{0} should be different to {1}",
|
||||
shouldMatchPattern: "Pattern should match: `/{0}/`",
|
||||
mustBeAnInteger: "Must be an integer",
|
||||
notAValidOption: "Not a valid option",
|
||||
newPasswordSameAsOld: "New password must be different from the old one",
|
||||
passwordConfirmNotMatch: "Password confirmation does not match"
|
||||
},
|
||||
"fr": {
|
||||
fr: {
|
||||
/* spell-checker: disable */
|
||||
"shouldBeEqual": "{0} doit être égal à {1}",
|
||||
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||
"mustBeAnInteger": "Doit être un nombre entier",
|
||||
"notAValidOption": "N'est pas une option valide",
|
||||
shouldBeEqual: "{0} doit être égal à {1}",
|
||||
shouldBeDifferent: "{0} doit être différent de {1}",
|
||||
shouldMatchPattern: "Dois respecter le schéma: `/{0}/`",
|
||||
mustBeAnInteger: "Doit être un nombre entier",
|
||||
notAValidOption: "N'est pas une option valide",
|
||||
|
||||
"logoutConfirmTitle": "Déconnexion",
|
||||
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||
"doLogout": "Se déconnecter",
|
||||
"newPasswordSameAsOld": "Le nouveau mot de passe doit être différent de l'ancien",
|
||||
"passwordConfirmNotMatch": "La confirmation du mot de passe ne correspond pas"
|
||||
logoutConfirmTitle: "Déconnexion",
|
||||
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||
doLogout: "Se déconnecter",
|
||||
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
|
||||
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
|
||||
/* spell-checker: enable */
|
||||
}
|
||||
};
|
||||
|
@ -2,9 +2,9 @@ import Fallback from "keycloakify/account/Fallback";
|
||||
|
||||
export default Fallback;
|
||||
|
||||
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
|
||||
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/pageIds";
|
||||
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
|
||||
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||
export type { ExtendKcContext } from "keycloakify/account/kcContext";
|
||||
export { createGetKcContextMock } from "keycloakify/account/kcContext";
|
||||
|
||||
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
|
@ -1,7 +1,24 @@
|
||||
import type { AccountThemePageId } from "keycloakify/bin/shared/pageIds";
|
||||
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { ValueOf } from "keycloakify/tools/ValueOf";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import { type ThemeType } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type ExtendKcContext<
|
||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
|
||||
> = ValueOf<{
|
||||
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
|
||||
KcContext,
|
||||
{ pageId: PageId }
|
||||
> extends never
|
||||
? KcContext.Common &
|
||||
KcContextExtraProperties & {
|
||||
pageId: PageId;
|
||||
} & KcContextExtraPropertiesPerPage[PageId]
|
||||
: Extract<KcContext, { pageId: PageId }> &
|
||||
KcContextExtraProperties &
|
||||
KcContextExtraPropertiesPerPage[PageId];
|
||||
}>;
|
||||
|
||||
export type KcContext =
|
||||
| KcContext.Password
|
||||
@ -69,7 +86,10 @@ export declare namespace KcContext {
|
||||
* @param text to return
|
||||
* @return text if message exists for given field, else undefined
|
||||
*/
|
||||
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
|
||||
printIfExists: <T extends string>(
|
||||
fieldName: string,
|
||||
text: T
|
||||
) => T | undefined;
|
||||
/**
|
||||
* Check if exists error message for given fields
|
||||
*
|
||||
|
@ -1,108 +0,0 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { isStorybook } from "keycloakify/lib/isStorybook";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||
|
||||
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
mockProperties?: Record<string, string>;
|
||||
}) {
|
||||
const { mockData, mockProperties } = params ?? {};
|
||||
|
||||
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||
mockPageId?: PageId;
|
||||
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||
}): {
|
||||
kcContext: PageId extends undefined
|
||||
? ExtendKcContext<KcContextExtension> | undefined
|
||||
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
|
||||
} {
|
||||
const { mockPageId, storyPartialKcContext } = params ?? {};
|
||||
|
||||
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
warn_that_mock_is_enbaled: {
|
||||
if (isStorybook) {
|
||||
break warn_that_mock_is_enbaled;
|
||||
}
|
||||
|
||||
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
|
||||
}
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
const partialKcContextCustomMock = (() => {
|
||||
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||
|
||||
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
if (mockDataPick !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": mockDataPick
|
||||
});
|
||||
}
|
||||
|
||||
if (storyPartialKcContext !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": storyPartialKcContext
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(out).length === 0 ? undefined : out;
|
||||
})();
|
||||
|
||||
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
|
||||
console.warn(
|
||||
[
|
||||
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||
`Please check the documentation of the getKcContext function`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
const kcContext: any = {};
|
||||
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||
});
|
||||
|
||||
if (partialKcContextCustomMock !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": partialKcContextCustomMock
|
||||
});
|
||||
}
|
||||
|
||||
if (mockProperties !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext.properties,
|
||||
"source": mockProperties
|
||||
});
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
if (realKcContext === undefined) {
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
if (realKcContext.themeType !== "account") {
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
return { "kcContext": realKcContext as any };
|
||||
}
|
||||
|
||||
return { getKcContext };
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { createGetKcContext } from "./createGetKcContext";
|
||||
|
||||
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
|
||||
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
|
||||
*/
|
||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||
const { mockPageId, mockData } = params ?? {};
|
||||
|
||||
const { getKcContext } = createGetKcContext({
|
||||
mockData
|
||||
});
|
||||
|
||||
const { kcContext } = getKcContext({ mockPageId });
|
||||
|
||||
return { kcContext };
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||
? KcContext
|
||||
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
|
||||
|
||||
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
|
||||
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
|
||||
}
|
80
src/account/kcContext/getKcContextMock.ts
Normal file
80
src/account/kcContext/getKcContextMock.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
|
||||
import type { AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export function createGetKcContextMock<
|
||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtraPropertiesPerPage extends Record<
|
||||
`${string}.ftl`,
|
||||
Record<string, unknown>
|
||||
>
|
||||
>(params: {
|
||||
kcContextExtraProperties: KcContextExtraProperties;
|
||||
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
|
||||
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
|
||||
overridesPerPage?: {
|
||||
[PageId in
|
||||
| AccountThemePageId
|
||||
| keyof KcContextExtraPropertiesPerPage]?: DeepPartial<
|
||||
Extract<
|
||||
ExtendKcContext<
|
||||
KcContextExtraProperties,
|
||||
KcContextExtraPropertiesPerPage
|
||||
>,
|
||||
{ pageId: PageId }
|
||||
>
|
||||
>;
|
||||
};
|
||||
}) {
|
||||
const {
|
||||
kcContextExtraProperties,
|
||||
kcContextExtraPropertiesPerPage,
|
||||
overrides: overrides_global,
|
||||
overridesPerPage: overridesPerPage_global
|
||||
} = params;
|
||||
|
||||
type KcContext = ExtendKcContext<
|
||||
KcContextExtraProperties,
|
||||
KcContextExtraPropertiesPerPage
|
||||
>;
|
||||
|
||||
function getKcContextMock<
|
||||
PageId extends AccountThemePageId | keyof KcContextExtraPropertiesPerPage
|
||||
>(params: {
|
||||
pageId: PageId;
|
||||
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||
}): Extract<KcContext, { pageId: PageId }> {
|
||||
const { pageId, overrides } = params;
|
||||
|
||||
const kcContextMock = structuredCloneButFunctions(
|
||||
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
|
||||
...kcContextCommonMock,
|
||||
pageId
|
||||
}
|
||||
);
|
||||
|
||||
[
|
||||
kcContextExtraProperties,
|
||||
kcContextExtraPropertiesPerPage[pageId],
|
||||
overrides_global,
|
||||
overridesPerPage_global?.[pageId],
|
||||
overrides
|
||||
]
|
||||
.filter(exclude(undefined))
|
||||
.forEach(overrides =>
|
||||
deepAssign({
|
||||
target: kcContextMock,
|
||||
source: overrides
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
return kcContextMock;
|
||||
}
|
||||
|
||||
return { getKcContextMock };
|
||||
}
|
@ -1 +1,2 @@
|
||||
export type { KcContext } from "./KcContext";
|
||||
export type { ExtendKcContext, KcContext } from "./KcContext";
|
||||
export { createGetKcContextMock } from "./getKcContextMock";
|
||||
|
@ -7,254 +7,193 @@ import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "account",
|
||||
"themeName": "my-theme-name",
|
||||
"url": {
|
||||
themeVersion: "0.0.0",
|
||||
keycloakifyVersion: "0.0.0",
|
||||
themeType: "account",
|
||||
themeName: "my-theme-name",
|
||||
url: {
|
||||
resourcesPath,
|
||||
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
|
||||
"resourceUrl": "#",
|
||||
"accountUrl": "#",
|
||||
"applicationsUrl": "#",
|
||||
"logoutUrl": "#",
|
||||
"getLogoutUrl": () => "#",
|
||||
"logUrl": "#",
|
||||
"passwordUrl": "#",
|
||||
"sessionsUrl": "#",
|
||||
"socialUrl": "#",
|
||||
"totpUrl": "#"
|
||||
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
|
||||
resourceUrl: "#",
|
||||
accountUrl: "#",
|
||||
applicationsUrl: "#",
|
||||
logoutUrl: "#",
|
||||
getLogoutUrl: () => "#",
|
||||
logUrl: "#",
|
||||
passwordUrl: "#",
|
||||
sessionsUrl: "#",
|
||||
socialUrl: "#",
|
||||
totpUrl: "#"
|
||||
},
|
||||
"realm": {
|
||||
"internationalizationEnabled": true,
|
||||
"userManagedAccessAllowed": true
|
||||
realm: {
|
||||
internationalizationEnabled: true,
|
||||
userManagedAccessAllowed: true
|
||||
},
|
||||
"messagesPerField": {
|
||||
"printIfExists": () => {
|
||||
messagesPerField: {
|
||||
printIfExists: () => {
|
||||
return undefined;
|
||||
},
|
||||
"existsError": () => false,
|
||||
"get": key => `Fake error for ${key}`,
|
||||
"exists": () => false
|
||||
existsError: () => false,
|
||||
get: key => `Fake error for ${key}`,
|
||||
exists: () => false
|
||||
},
|
||||
"locale": {
|
||||
"supported": [
|
||||
locale: {
|
||||
supported: [
|
||||
/* spell-checker: disable */
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||
"label": "Deutsch",
|
||||
"languageTag": "de"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||
"label": "Norsk",
|
||||
"languageTag": "no"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||
"label": "Русский",
|
||||
"languageTag": "ru"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||
"label": "Svenska",
|
||||
"languageTag": "sv"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||
"label": "Português (Brasil)",
|
||||
"languageTag": "pt-BR"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||
"label": "Lietuvių",
|
||||
"languageTag": "lt"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||
"label": "English",
|
||||
"languageTag": "en"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||
"label": "Italiano",
|
||||
"languageTag": "it"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||
"label": "Français",
|
||||
"languageTag": "fr"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||
"label": "中文简体",
|
||||
"languageTag": "zh-CN"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||
"label": "Español",
|
||||
"languageTag": "es"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||
"label": "Čeština",
|
||||
"languageTag": "cs"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||
"label": "日本語",
|
||||
"languageTag": "ja"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||
"label": "Slovenčina",
|
||||
"languageTag": "sk"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||
"label": "Polski",
|
||||
"languageTag": "pl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||
"label": "Català",
|
||||
"languageTag": "ca"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||
"label": "Nederlands",
|
||||
"languageTag": "nl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||
"label": "Türkçe",
|
||||
"languageTag": "tr"
|
||||
}
|
||||
["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 */
|
||||
],
|
||||
"currentLanguageTag": "en"
|
||||
].map(
|
||||
([languageTag, label]) =>
|
||||
({
|
||||
languageTag,
|
||||
label,
|
||||
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
|
||||
}) as const
|
||||
),
|
||||
currentLanguageTag: "en"
|
||||
},
|
||||
"features": {
|
||||
"authorization": true,
|
||||
"identityFederation": true,
|
||||
"log": true,
|
||||
"passwordUpdateSupported": true
|
||||
features: {
|
||||
authorization: true,
|
||||
identityFederation: true,
|
||||
log: true,
|
||||
passwordUpdateSupported: true
|
||||
},
|
||||
"referrer": undefined,
|
||||
"account": {
|
||||
"firstName": "john",
|
||||
"lastName": "doe",
|
||||
"email": "john.doe@code.gouv.fr",
|
||||
"username": "doe_j"
|
||||
referrer: undefined,
|
||||
account: {
|
||||
firstName: "john",
|
||||
lastName: "doe",
|
||||
email: "john.doe@code.gouv.fr",
|
||||
username: "doe_j"
|
||||
},
|
||||
"properties": {
|
||||
"parent": "account-v1",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"accountResourceProvider": "account-v1",
|
||||
"styles":
|
||||
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
"kcButtonClass": "btn",
|
||||
"kcButtonDefaultClass": "btn-default"
|
||||
properties: {
|
||||
parent: "account-v1",
|
||||
kcButtonLargeClass: "btn-lg",
|
||||
locales:
|
||||
"ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
||||
kcButtonPrimaryClass: "btn-primary",
|
||||
accountResourceProvider: "account-v1",
|
||||
styles: "css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
kcButtonClass: "btn",
|
||||
kcButtonDefaultClass: "btn-default"
|
||||
}
|
||||
};
|
||||
|
||||
export const kcContextMocks: KcContext[] = [
|
||||
id<KcContext.Password>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "password.ftl",
|
||||
"password": {
|
||||
"passwordSet": true
|
||||
pageId: "password.ftl",
|
||||
password: {
|
||||
passwordSet: true
|
||||
},
|
||||
"stateChecker": "state checker"
|
||||
stateChecker: "state checker"
|
||||
}),
|
||||
id<KcContext.Account>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "account.ftl",
|
||||
"url": {
|
||||
pageId: "account.ftl",
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
"referrerURI": "#",
|
||||
"accountUrl": "#"
|
||||
referrerURI: "#",
|
||||
accountUrl: "#"
|
||||
},
|
||||
"realm": {
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
"registrationEmailAsUsername": true,
|
||||
"editUsernameAllowed": true
|
||||
registrationEmailAsUsername: true,
|
||||
editUsernameAllowed: true
|
||||
},
|
||||
"stateChecker": ""
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Sessions>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "sessions.ftl",
|
||||
"sessions": {
|
||||
"sessions": [
|
||||
pageId: "sessions.ftl",
|
||||
sessions: {
|
||||
sessions: [
|
||||
{
|
||||
"ipAddress": "127.0.0.1",
|
||||
"started": new Date().toString(),
|
||||
"lastAccess": new Date().toString(),
|
||||
"expires": new Date().toString(),
|
||||
"clients": ["Chrome", "Firefox"],
|
||||
"id": "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
ipAddress: "127.0.0.1",
|
||||
started: new Date().toString(),
|
||||
lastAccess: new Date().toString(),
|
||||
expires: new Date().toString(),
|
||||
clients: ["Chrome", "Firefox"],
|
||||
id: "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stateChecker": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
stateChecker: "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
}),
|
||||
id<KcContext.Totp>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "totp.ftl",
|
||||
"totp": {
|
||||
"enabled": true,
|
||||
"totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
"qrUrl": "#",
|
||||
"totpSecretQrCode":
|
||||
pageId: "totp.ftl",
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
qrUrl: "#",
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
|
||||
"manualUrl": "#",
|
||||
"totpSecret": "G4nsI8lQagRMUchH8jEG",
|
||||
"otpCredentials": [],
|
||||
"supportedApplications": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
|
||||
"policy": {
|
||||
"algorithm": "HmacSHA1",
|
||||
"digits": 6,
|
||||
"lookAheadWindow": 1,
|
||||
"type": "totp",
|
||||
"period": 30
|
||||
manualUrl: "#",
|
||||
totpSecret: "G4nsI8lQagRMUchH8jEG",
|
||||
otpCredentials: [],
|
||||
supportedApplications: [
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppGoogleName"
|
||||
],
|
||||
policy: {
|
||||
algorithm: "HmacSHA1",
|
||||
digits: 6,
|
||||
lookAheadWindow: 1,
|
||||
type: "totp",
|
||||
period: 30
|
||||
}
|
||||
},
|
||||
"mode": "qr",
|
||||
"isAppInitiatedAction": false,
|
||||
"stateChecker": ""
|
||||
mode: "qr",
|
||||
isAppInitiatedAction: false,
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Log>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "log.ftl",
|
||||
"log": {
|
||||
"events": [
|
||||
pageId: "log.ftl",
|
||||
log: {
|
||||
events: [
|
||||
{
|
||||
"date": "2/21/2024, 1:28:39 PM",
|
||||
"event": "login",
|
||||
"ipAddress": "172.17.0.1",
|
||||
"client": "security-admin-console",
|
||||
"details": [{ key: "openid-connect", value: "admin" }]
|
||||
date: "2/21/2024, 1:28:39 PM",
|
||||
event: "login",
|
||||
ipAddress: "172.17.0.1",
|
||||
client: "security-admin-console",
|
||||
details: [{ key: "openid-connect", value: "admin" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContext.FederatedIdentity>({
|
||||
...kcContextCommonMock,
|
||||
"stateChecker": "",
|
||||
"pageId": "federatedIdentity.ftl",
|
||||
"federatedIdentity": {
|
||||
"identities": [
|
||||
stateChecker: "",
|
||||
pageId: "federatedIdentity.ftl",
|
||||
federatedIdentity: {
|
||||
identities: [
|
||||
{
|
||||
"providerId": "keycloak-oidc",
|
||||
"displayName": "keycloak-oidc",
|
||||
"userName": "John",
|
||||
"connected": true
|
||||
providerId: "keycloak-oidc",
|
||||
displayName: "keycloak-oidc",
|
||||
userName: "John",
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
"removeLinkPossible": true
|
||||
removeLinkPossible: true
|
||||
}
|
||||
})
|
||||
];
|
||||
|
@ -2,19 +2,20 @@ import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcHtmlClass": undefined,
|
||||
"kcBodyClass": undefined,
|
||||
"kcButtonClass": "btn",
|
||||
"kcContentWrapperClass": "row",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
"kcButtonLargeClass": "btn-lg",
|
||||
"kcButtonDefaultClass": "btn-default",
|
||||
"kcFormClass": "form-horizontal",
|
||||
"kcFormGroupClass": "form-group",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcLabelClass": "control-label",
|
||||
"kcInputClass": "form-control",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
defaultClasses: {
|
||||
kcHtmlClass: undefined,
|
||||
kcBodyClass: undefined,
|
||||
kcButtonClass: "btn",
|
||||
kcContentWrapperClass: "row",
|
||||
kcButtonPrimaryClass: "btn-primary",
|
||||
kcButtonLargeClass: "btn-lg",
|
||||
kcButtonDefaultClass: "btn-default",
|
||||
kcFormClass: "form-horizontal",
|
||||
kcFormGroupClass: "form-group",
|
||||
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
kcLabelClass: "control-label",
|
||||
kcInputClass: "form-control",
|
||||
kcInputErrorMessageClass:
|
||||
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
}
|
||||
});
|
||||
|
@ -9,9 +9,9 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
classes: {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
|
||||
kcBodyClass: clsx(classes?.kcBodyClass, "user")
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -10,9 +10,9 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
classes: {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||
kcBodyClass: clsx(classes?.kcBodyClass, "password")
|
||||
}
|
||||
});
|
||||
|
||||
@ -57,18 +57,18 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
{...{
|
||||
kcContext: {
|
||||
...kcContext,
|
||||
"message": (() => {
|
||||
message: (() => {
|
||||
if (newPasswordError !== "") {
|
||||
return {
|
||||
"type": "error",
|
||||
"summary": newPasswordError
|
||||
type: "error",
|
||||
summary: newPasswordError
|
||||
};
|
||||
}
|
||||
|
||||
if (newPasswordConfirmError !== "") {
|
||||
return {
|
||||
"type": "error",
|
||||
"summary": newPasswordConfirmError
|
||||
type: "error",
|
||||
summary: newPasswordConfirmError
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
||||
value={account.username ?? ""}
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
style={{ "display": "none" }}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{password.passwordSet && (
|
||||
|
@ -3,7 +3,6 @@ import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { MessageKey } from "keycloakify/account/i18n/i18n";
|
||||
|
||||
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
@ -15,12 +14,12 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
|
||||
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
|
||||
"HmacSHA1": "SHA1",
|
||||
"HmacSHA256": "SHA256",
|
||||
"HmacSHA512": "SHA512"
|
||||
HmacSHA1: "SHA1",
|
||||
HmacSHA256: "SHA256",
|
||||
HmacSHA512: "SHA512"
|
||||
};
|
||||
|
||||
return (
|
||||
@ -78,11 +77,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
<li>
|
||||
<p>{msg("totpStep1")}</p>
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.supportedApplications?.map(app => (
|
||||
<li key={app}>{msg(app as MessageKey)}</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul id="kc-totp-supported-apps">{totp.supportedApplications?.map(app => <li key={app}>{advancedMsg(app)}</li>)}</ul>
|
||||
</li>
|
||||
|
||||
{mode && mode == "manual" ? (
|
||||
|
@ -8,9 +8,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
|
||||
await copyKeycloakResourcesToPublic({
|
||||
"buildOptions": {
|
||||
...buildOptions,
|
||||
"publicDirPath": buildOptions.reactAppRootDirPath
|
||||
}
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import { downloadBuiltinKeycloakTheme } from "./shared/downloadBuiltinKeycloakTheme";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getLogger } from "./tools/logger";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
const { log } = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
"startingFromMajor": undefined
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
|
||||
|
||||
log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
destDirPath,
|
||||
buildOptions
|
||||
});
|
||||
}
|
63
src/bin/download-keycloak-default-theme.ts
Normal file
63
src/bin/download-keycloak-default-theme.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import chalk from "chalk";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
"Select the Keycloak version from which you want to download the builtins theme:"
|
||||
)
|
||||
);
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
startingFromMajor: undefined,
|
||||
cacheDirPath: buildOptions.cacheDirPath
|
||||
});
|
||||
|
||||
console.log(`→ ${keycloakVersion}`);
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme"
|
||||
);
|
||||
|
||||
console.log(
|
||||
[
|
||||
`Downloading builtins theme of Keycloak ${keycloakVersion} here:`,
|
||||
`- ${chalk.bold(
|
||||
`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`
|
||||
)}`,
|
||||
`- ${chalk.bold(
|
||||
`.${pathSep}${pathJoin(
|
||||
pathRelative(process.cwd(), destDirPath),
|
||||
"keycloak"
|
||||
)}`
|
||||
)}`
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: defaultThemeDirPath,
|
||||
destDirPath
|
||||
});
|
||||
|
||||
console.log(chalk.green(`✓ done`));
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./shared/pageIds";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||
import { themeTypes, type ThemeType } from "./shared/constants";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log("Theme type:");
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
"values": [...themeTypes]
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log("Select the page you want to customize:");
|
||||
|
||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||
"values": (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [...loginThemePageIds];
|
||||
case "account":
|
||||
return [...accountThemePageIds];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
||||
|
||||
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", componentPageBasename);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { "recursive": true });
|
||||
}
|
||||
}
|
||||
|
||||
const componentPageContent = fs
|
||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", componentPageBasename))
|
||||
.toString("utf8");
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
|
||||
|
||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||
|
||||
console.log(
|
||||
[
|
||||
``,
|
||||
`\`${pathJoin(".", pathRelative(process.cwd(), targetFilePath))}\` copy pasted from the Keycloakify source code into your project.`,
|
||||
``,
|
||||
`You now need to update your page router:`,
|
||||
``,
|
||||
`\`${pathJoin(".", pathRelative(process.cwd(), themeSrcDirPath), themeType, "KcApp.tsx")}\`:`,
|
||||
"```",
|
||||
`// ...`,
|
||||
``,
|
||||
`+const ${componentPageBasename.replace(/.tsx$/, "")} = lazy(() => import("./pages/${componentPageBasename}"));`,
|
||||
``,
|
||||
` export default function KcApp(props: { kcContext: KcContext; }) {`,
|
||||
``,
|
||||
` // ...`,
|
||||
``,
|
||||
` return (`,
|
||||
` <Suspense>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.pageId) {`,
|
||||
` // ...`,
|
||||
` case "${pageId}": return (`,
|
||||
`+ <Login`,
|
||||
`+ {...{ kcContext, i18n, classes }}`,
|
||||
`+ Template={Template}`,
|
||||
...(!componentPageContent.includes(userProfileFormFieldComponentName)
|
||||
? []
|
||||
: [`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`]),
|
||||
`+ doUseDefaultCss={true}`,
|
||||
`+ />`,
|
||||
`+ );`,
|
||||
` default: return <Fallback /* .. */ />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
` }`,
|
||||
"```"
|
||||
].join("\n")
|
||||
);
|
||||
}
|
176
src/bin/eject-page.ts
Normal file
176
src/bin/eject-page.ts
Normal file
@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
loginThemePageIds,
|
||||
accountThemePageIds,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
themeTypes,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import chalk from "chalk";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const { value: themeType } = await cliSelect<ThemeType>({
|
||||
values: [...themeTypes]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
|
||||
console.log(chalk.cyan("Select the page you want to customize:"));
|
||||
|
||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [...loginThemePageIds];
|
||||
case "account":
|
||||
return [...accountThemePageIds];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${pageId}`);
|
||||
|
||||
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
|
||||
/ftl$/,
|
||||
"tsx"
|
||||
);
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
||||
});
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
themeSrcDirPath,
|
||||
themeType,
|
||||
"pages",
|
||||
componentPageBasename
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(
|
||||
`${pageId} is already ejected, ${pathRelative(
|
||||
process.cwd(),
|
||||
targetFilePath
|
||||
)} already exists`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const componentPageContent = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"pages",
|
||||
componentPageBasename
|
||||
)
|
||||
)
|
||||
.toString("utf8");
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
|
||||
|
||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||
|
||||
console.log(
|
||||
[
|
||||
``,
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} copy pasted from the Keycloakify source code into your project`,
|
||||
``,
|
||||
`You now need to update your page router:`,
|
||||
``,
|
||||
`${chalk.bold(
|
||||
pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), themeSrcDirPath),
|
||||
themeType,
|
||||
"KcApp.tsx"
|
||||
)
|
||||
)}:`,
|
||||
chalk.grey("```"),
|
||||
`// ...`,
|
||||
``,
|
||||
chalk.green(
|
||||
`+const ${componentPageBasename.replace(
|
||||
/.tsx$/,
|
||||
""
|
||||
)} = lazy(() => import("./pages/${componentPageBasename}"));`
|
||||
),
|
||||
...[
|
||||
``,
|
||||
` export default function KcApp(props: { kcContext: KcContext; }) {`,
|
||||
``,
|
||||
` // ...`,
|
||||
``,
|
||||
` return (`,
|
||||
` <Suspense>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.pageId) {`,
|
||||
` // ...`,
|
||||
`+ case "${pageId}": return (`,
|
||||
`+ <Login`,
|
||||
`+ {...{ kcContext, i18n, classes }}`,
|
||||
`+ Template={Template}`,
|
||||
...(!componentPageContent.includes(userProfileFormFieldComponentName)
|
||||
? []
|
||||
: [
|
||||
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`
|
||||
]),
|
||||
`+ doUseDefaultCss={true}`,
|
||||
`+ />`,
|
||||
`+ );`,
|
||||
` default: return <Fallback /* .. */ />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
` }`
|
||||
].map(line => {
|
||||
if (line.startsWith("+")) {
|
||||
return chalk.green(line);
|
||||
}
|
||||
if (line.startsWith("-")) {
|
||||
return chalk.red(line);
|
||||
}
|
||||
return chalk.grey(line);
|
||||
}),
|
||||
chalk.grey("```")
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
import { downloadBuiltinKeycloakTheme } from "./shared/downloadBuiltinKeycloakTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import * as fs from "fs";
|
||||
import { getLogger } from "./tools/logger";
|
||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||
import { rmSync } from "./tools/fs.rmSync";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
@ -14,46 +12,58 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
|
||||
const logger = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
|
||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
||||
});
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||
|
||||
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
|
||||
console.warn(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
"startingFromMajor": 17
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
cacheDirPath: buildOptions.cacheDirPath
|
||||
});
|
||||
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "initialize-email-theme_tmp");
|
||||
|
||||
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||
"destDirPath": emailThemeSrcDirPath
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "base", "email"),
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
{
|
||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||
|
||||
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||
fs.writeFileSync(
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`,
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
|
||||
|
||||
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
|
||||
console.log(
|
||||
`The \`${pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
||||
)}\` directory have been created.`
|
||||
);
|
||||
console.log("You can delete any file you don't modify.");
|
||||
}
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import * as fs from "fs/promises";
|
||||
import { accountV1ThemeName } from "../../shared/constants";
|
||||
import { generatePom, BuildOptionsLike as BuildOptionsLike_generatePom } from "./generatePom";
|
||||
import {
|
||||
generatePom,
|
||||
BuildOptionsLike as BuildOptionsLike_generatePom
|
||||
} from "./generatePom";
|
||||
import { readFileSync } from "fs";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { getMetaInfKeycloakThemesJsonPath } from "../../shared/metaInfKeycloakThemes";
|
||||
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike_generatePom & {
|
||||
keycloakifyBuildDirPath: string;
|
||||
@ -28,57 +34,152 @@ export async function buildJar(params: {
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<void> {
|
||||
const { jarFileBasename, keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
|
||||
const {
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildOptions
|
||||
} = params;
|
||||
|
||||
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.cacheDirPath, jarFileBasename.replace(".jar", ""));
|
||||
const keycloakifyBuildTmpDirPath = pathJoin(
|
||||
buildOptions.cacheDirPath,
|
||||
jarFileBasename.replace(".jar", "")
|
||||
);
|
||||
|
||||
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true, "force": true });
|
||||
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
|
||||
|
||||
{
|
||||
const keycloakThemesJsonFilePath = getMetaInfKeycloakThemesJsonPath({ "keycloakifyBuildDirPath": "" });
|
||||
const transformCodebase_common = (params: {
|
||||
fileRelativePath: string;
|
||||
sourceCode: Buffer;
|
||||
}): { modifiedSourceCode: Buffer } | undefined => {
|
||||
const { fileRelativePath, sourceCode } = params;
|
||||
|
||||
const themePropertiesFilePathSet = new Set(
|
||||
...buildOptions.themeNames.map(themeName => pathJoin("src", "main", "resources", "theme", themeName, "account", "theme.properties"))
|
||||
);
|
||||
if (
|
||||
fileRelativePath ===
|
||||
getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath: "." })
|
||||
) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
|
||||
const accountV1RelativeDirPath = pathJoin("src", "main", "resources", "theme", accountV1ThemeName);
|
||||
for (const themeName of [...buildOptions.themeNames, accountV1ThemeName]) {
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin("src", "main", "resources", "theme", themeName),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const transformCodebase_patchForUsingBuiltinAccountV1 =
|
||||
keycloakAccountV1Version !== null
|
||||
? undefined
|
||||
: (params: {
|
||||
fileRelativePath: string;
|
||||
sourceCode: Buffer;
|
||||
}): { modifiedSourceCode: Buffer } | undefined => {
|
||||
const { fileRelativePath, sourceCode } = params;
|
||||
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin(
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
accountV1ThemeName
|
||||
),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
fileRelativePath ===
|
||||
getMetaInfKeycloakThemesJsonFilePath({
|
||||
keycloakifyBuildDirPath: "."
|
||||
})
|
||||
) {
|
||||
const keycloakThemesJsonParsed = JSON.parse(
|
||||
sourceCode.toString("utf8")
|
||||
) as {
|
||||
themes: { name: string; types: string[] }[];
|
||||
};
|
||||
|
||||
keycloakThemesJsonParsed.themes =
|
||||
keycloakThemesJsonParsed.themes.filter(
|
||||
({ name }) => name !== accountV1ThemeName
|
||||
);
|
||||
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(
|
||||
JSON.stringify(keycloakThemesJsonParsed, null, 2),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
for (const themeName of buildOptions.themeNames) {
|
||||
if (
|
||||
fileRelativePath ===
|
||||
pathJoin(
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
"account",
|
||||
"theme.properties"
|
||||
)
|
||||
) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(
|
||||
`parent=${accountV1ThemeName}`,
|
||||
"parent=keycloak"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(
|
||||
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||
);
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
};
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
"destDirPath": keycloakifyBuildTmpDirPath,
|
||||
"transformSourceCode":
|
||||
keycloakAccountV1Version !== null
|
||||
? undefined
|
||||
: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath === keycloakThemesJsonFilePath) {
|
||||
const keycloakThemesJsonParsed = JSON.parse(sourceCode.toString("utf8")) as {
|
||||
themes: { name: string; types: string[] }[];
|
||||
};
|
||||
srcDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
destDirPath: keycloakifyBuildTmpDirPath,
|
||||
transformSourceCode: params => {
|
||||
const resultCommon = transformCodebase_common(params);
|
||||
|
||||
keycloakThemesJsonParsed.themes = keycloakThemesJsonParsed.themes.filter(({ name }) => name !== accountV1ThemeName);
|
||||
if (transformCodebase_patchForUsingBuiltinAccountV1 === undefined) {
|
||||
return resultCommon;
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(JSON.stringify(keycloakThemesJsonParsed, null, 2), "utf8") };
|
||||
}
|
||||
if (resultCommon === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isInside({ "dirPath": "target", "filePath": fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
const { modifiedSourceCode } = resultCommon;
|
||||
|
||||
if (isInside({ "dirPath": accountV1RelativeDirPath, "filePath": fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (themePropertiesFilePathSet.has(fileRelativePath)) {
|
||||
return {
|
||||
"modifiedSourceCode": Buffer.from(
|
||||
sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
return transformCodebase_patchForUsingBuiltinAccountV1({
|
||||
...params,
|
||||
sourceCode: modifiedSourceCode
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,7 +204,16 @@ export async function buildJar(params: {
|
||||
|
||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
||||
buildOptions.themeNames.map(themeName => {
|
||||
const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources", "theme", themeName, "login", pageId);
|
||||
const ftlFilePath = pathJoin(
|
||||
keycloakifyBuildTmpDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
"login",
|
||||
pageId
|
||||
);
|
||||
|
||||
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
|
||||
|
||||
@ -124,7 +234,10 @@ export async function buildJar(params: {
|
||||
|
||||
assert(modifiedFtlFileContent !== ftlFileContent);
|
||||
|
||||
fs.writeFile(pathJoin(pathDirname(ftlFilePath), realPageId), Buffer.from(modifiedFtlFileContent, "utf8"));
|
||||
fs.writeFile(
|
||||
pathJoin(pathDirname(ftlFilePath), realPageId),
|
||||
Buffer.from(modifiedFtlFileContent, "utf8")
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -136,35 +249,46 @@ export async function buildJar(params: {
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
});
|
||||
|
||||
await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
|
||||
await fs.writeFile(
|
||||
pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"),
|
||||
Buffer.from(pomFileCode, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec("mvn clean install", { "cwd": keycloakifyBuildTmpDirPath }, error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
child_process.exec(
|
||||
`mvn clean install -Dmaven.repo.local=${pathJoin(keycloakifyBuildTmpDirPath, ".m2")}`,
|
||||
{ cwd: keycloakifyBuildTmpDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await fs.rename(
|
||||
pathJoin(keycloakifyBuildTmpDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`),
|
||||
pathJoin(
|
||||
keycloakifyBuildTmpDirPath,
|
||||
"target",
|
||||
`${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`
|
||||
),
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)
|
||||
);
|
||||
|
||||
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true });
|
||||
rmSync(keycloakifyBuildTmpDirPath, { recursive: true });
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { keycloakAccountV1Versions, keycloakThemeAdditionalInfoExtensionVersions } from "./extensionVersions";
|
||||
import {
|
||||
keycloakAccountV1Versions,
|
||||
keycloakThemeAdditionalInfoExtensionVersions
|
||||
} from "./extensionVersions";
|
||||
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
|
||||
import { buildJar, BuildOptionsLike as BuildOptionsLike_buildJar } from "./buildJar";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
@ -14,11 +17,13 @@ export type BuildOptionsLike = BuildOptionsLike_buildJar & {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function buildJars(params: { buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
export async function buildJars(params: {
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<void> {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
|
||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
||||
}).themes.some(({ name }) => name === accountV1ThemeName);
|
||||
|
||||
await Promise.all(
|
||||
@ -36,24 +41,38 @@ export async function buildJars(params: { buildOptions: BuildOptionsLike }): Pro
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange };
|
||||
})
|
||||
.filter(exclude(undefined))
|
||||
.map(({ keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange }) => {
|
||||
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
|
||||
|
||||
return {
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
jarFileBasename
|
||||
keycloakVersionRange
|
||||
};
|
||||
})
|
||||
.map(({ keycloakThemeAdditionalInfoExtensionVersion, jarFileBasename }) =>
|
||||
buildJar({
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
.filter(exclude(undefined))
|
||||
.map(
|
||||
({
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildOptions
|
||||
})
|
||||
keycloakVersionRange
|
||||
}) => {
|
||||
const { jarFileBasename } = getJarFileBasename({
|
||||
keycloakVersionRange
|
||||
});
|
||||
|
||||
return {
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
jarFileBasename
|
||||
};
|
||||
}
|
||||
)
|
||||
.map(
|
||||
({
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
jarFileBasename
|
||||
}) =>
|
||||
buildJar({
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildOptions
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
|
@ -13,4 +13,5 @@ export const keycloakThemeAdditionalInfoExtensionVersions = [null, "1.1.5"] as c
|
||||
* https://central.sonatype.com/artifact/dev.jcputney/keycloak-theme-additional-info-extension
|
||||
* https://github.com/jcputney/keycloak-theme-additional-info-extension
|
||||
* */
|
||||
export type KeycloakThemeAdditionalInfoExtensionVersion = (typeof keycloakThemeAdditionalInfoExtensionVersions)[number];
|
||||
export type KeycloakThemeAdditionalInfoExtensionVersion =
|
||||
(typeof keycloakThemeAdditionalInfoExtensionVersions)[number];
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
groupId: string;
|
||||
@ -15,7 +18,11 @@ export function generatePom(params: {
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
|
||||
const {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
buildOptions
|
||||
} = params;
|
||||
|
||||
const { pomFileCode } = (function generatePomFileCode(): {
|
||||
pomFileCode: string;
|
||||
@ -35,7 +42,8 @@ export function generatePom(params: {
|
||||
` <properties>`,
|
||||
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
|
||||
` </properties>`,
|
||||
...(keycloakAccountV1Version !== null && keycloakThemeAdditionalInfoExtensionVersion !== null
|
||||
...(keycloakAccountV1Version !== null &&
|
||||
keycloakThemeAdditionalInfoExtensionVersion !== null
|
||||
? [
|
||||
` <build>`,
|
||||
` <plugins>`,
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
|
||||
import type {
|
||||
KeycloakAccountV1Version,
|
||||
KeycloakThemeAdditionalInfoExtensionVersion
|
||||
} from "./extensionVersions";
|
||||
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
|
||||
|
||||
export function getKeycloakVersionRangeForJar(params: {
|
||||
@ -7,7 +10,11 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
}): KeycloakVersionRange | undefined {
|
||||
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, doesImplementAccountTheme } = params;
|
||||
const {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
doesImplementAccountTheme
|
||||
} = params;
|
||||
|
||||
if (doesImplementAccountTheme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
@ -19,7 +26,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return undefined;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
case "0.3":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
@ -27,7 +36,9 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return "23" as const;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
case "0.4":
|
||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||
case null:
|
||||
@ -35,11 +46,18 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return "24-and-above" as const;
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<
|
||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||
>(false);
|
||||
}
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme | undefined>>();
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountTheme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
@ -54,10 +72,17 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
case "1.1.5":
|
||||
return "22-and-above";
|
||||
}
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
|
||||
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
|
||||
false
|
||||
);
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme | undefined>>();
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountTheme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
}
|
||||
|
@ -2,8 +2,6 @@
|
||||
(()=>{
|
||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["messagesPerField"]= {
|
||||
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
|
||||
<#attempt>
|
||||
@ -31,17 +29,7 @@ out["messagesPerField"]= {
|
||||
<#recover>
|
||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||
</#attempt>
|
||||
<#if doExistErrorOnUsernameOrPassword>
|
||||
return text;
|
||||
<#else>
|
||||
<#assign doExistMessageForField = "">
|
||||
<#attempt>
|
||||
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
|
||||
<#recover>
|
||||
<#assign doExistMessageForField = true>
|
||||
</#attempt>
|
||||
return <#if doExistMessageForField>text<#else>undefined</#if>;
|
||||
</#if>
|
||||
return <#if doExistErrorOnUsernameOrPassword>text<#else>undefined</#if>
|
||||
<#else>
|
||||
<#assign doExistMessageForField = "">
|
||||
<#attempt>
|
||||
@ -109,22 +97,18 @@ out["messagesPerField"]= {
|
||||
</#attempt>
|
||||
<#if doExistErrorOnUsernameOrPassword>
|
||||
<#attempt>
|
||||
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
|
||||
return decodeHtmlEntities("${msg('invalidUserMessage')?js_string}");
|
||||
<#recover>
|
||||
return "Invalid username or password.";
|
||||
</#attempt>
|
||||
<#else>
|
||||
<#attempt>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
<#recover>
|
||||
return "";
|
||||
</#attempt>
|
||||
return "";
|
||||
</#if>
|
||||
<#else>
|
||||
<#attempt>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
return decodeHtmlEntities("${messagesPerField.get('${fieldName}')?js_string}");
|
||||
<#recover>
|
||||
return "invalid field";
|
||||
return "Invalid field";
|
||||
</#attempt>
|
||||
</#if>
|
||||
}
|
||||
@ -181,8 +165,37 @@ try {
|
||||
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
|
||||
} catch(error) { }
|
||||
|
||||
<#if profile?? && profile.attributes??>
|
||||
out["lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX"] = {
|
||||
<#list profile.attributes as attribute>
|
||||
<#if attribute.annotations?? && attribute.displayName??>
|
||||
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
|
||||
</#if>
|
||||
<#if attribute.annotations.inputHelperTextBefore??>
|
||||
"${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"),
|
||||
</#if>
|
||||
<#if attribute.annotations.inputHelperTextAfter??>
|
||||
"${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"),
|
||||
</#if>
|
||||
<#if attribute.annotations.inputTypePlaceholder??>
|
||||
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
|
||||
</#if>
|
||||
</#list>
|
||||
};
|
||||
</#if>
|
||||
|
||||
return out;
|
||||
|
||||
function decodeHtmlEntities(htmlStr){
|
||||
var element = decodeHtmlEntities.element;
|
||||
if (!element) {
|
||||
element = document.createElement("textarea");
|
||||
decodeHtmlEntities.element = element;
|
||||
}
|
||||
element.innerHTML = htmlStr;
|
||||
return element.value;
|
||||
}
|
||||
|
||||
})();
|
||||
<#function ftl_object_to_js_code_declaring_an_object object path>
|
||||
|
||||
@ -253,26 +266,34 @@ return out;
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
is_subpath(path, ["applications", "applications"]) &&
|
||||
(
|
||||
key == "realm" ||
|
||||
key == "container"
|
||||
)
|
||||
) &&
|
||||
is_subpath(path, ["applications", "applications"])
|
||||
) || (
|
||||
are_same_path(path, ["user"]) &&
|
||||
key == "delegateForUpdate"
|
||||
key == "delegateForUpdate" &&
|
||||
are_same_path(path, ["user"])
|
||||
) || (
|
||||
<#-- Security audit forwarded by Garth (Gmail) -->
|
||||
are_same_path(path, ["client", "attributes"]) &&
|
||||
key == "saml.signing.private.key"
|
||||
key == "saml.signing.private.key" &&
|
||||
are_same_path(path, ["client", "attributes"])
|
||||
) || (
|
||||
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
|
||||
are_same_path(path, ["login"]) &&
|
||||
key == "password"
|
||||
key == "password" &&
|
||||
are_same_path(path, ["login"])
|
||||
) || (
|
||||
<#-- Remove realmAttributes added by https://github.com/jcputney/keycloak-theme-additional-info-extension for peace of mind. -->
|
||||
are_same_path(path, []) &&
|
||||
key == "realmAttributes"
|
||||
key == "realmAttributes" &&
|
||||
are_same_path(path, [])
|
||||
) || (
|
||||
<#-- attributesByName adds a lot of noise to the output and is not needed -->
|
||||
key == "attributes" &&
|
||||
are_same_path(path, ["profile"])
|
||||
) || (
|
||||
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
|
||||
(key == "attributes" || key == "attributesByName") &&
|
||||
are_same_path(path, ["register"])
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
|
||||
|
@ -6,7 +6,14 @@ import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../shared/constants";
|
||||
import {
|
||||
type ThemeType,
|
||||
nameOfTheGlobal,
|
||||
basenameOfTheKeycloakifyResourcesDir,
|
||||
resources_common,
|
||||
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||
} from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
@ -27,7 +34,15 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
themeType: ThemeType;
|
||||
fieldNames: string[];
|
||||
}) {
|
||||
const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
|
||||
const {
|
||||
themeName,
|
||||
cssGlobalsToDefine,
|
||||
indexHtmlCode,
|
||||
buildOptions,
|
||||
keycloakifyVersion,
|
||||
themeType,
|
||||
fieldNames
|
||||
} = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
@ -37,7 +52,10 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
assert(jsCode !== null);
|
||||
|
||||
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
|
||||
const { fixedJsCode } = replaceImportsInJsCode({
|
||||
jsCode,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
});
|
||||
@ -71,7 +89,9 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
$(element).attr(
|
||||
attrName,
|
||||
href.replace(
|
||||
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
|
||||
new RegExp(
|
||||
`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
||||
),
|
||||
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
)
|
||||
);
|
||||
@ -96,19 +116,37 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const ftlObjectToJsCodeDeclaringAnObject = fs
|
||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"keycloakify",
|
||||
"generateFtl",
|
||||
"ftl_object_to_js_code_declaring_an_object.ftl"
|
||||
)
|
||||
)
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
|
||||
.replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))
|
||||
.replace(
|
||||
"FIELD_NAMES_eKsIY4ZsZ4xeM",
|
||||
fieldNames.map(name => `"${name}"`).join(", ")
|
||||
)
|
||||
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common);
|
||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common)
|
||||
.replace(
|
||||
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
|
||||
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||
);
|
||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||
|
||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||
|
||||
$("head").prepend(`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`);
|
||||
$("head").prepend(
|
||||
`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`
|
||||
);
|
||||
|
||||
// Remove part of the document marked as ignored.
|
||||
{
|
||||
@ -116,7 +154,9 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
startTags.each((...[, startTag]) => {
|
||||
const $startTag = $(startTag);
|
||||
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
|
||||
const $endTag = $startTag
|
||||
.nextAll('meta[name="keycloakify-ignore-end"]')
|
||||
.first();
|
||||
|
||||
if ($endTag.length) {
|
||||
let currentNode = $startTag.next();
|
||||
@ -143,9 +183,13 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
let ftlCode = $.html();
|
||||
|
||||
Object.entries({
|
||||
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]: ftlObjectToJsCodeDeclaringAnObject,
|
||||
"PAGE_ID_xIgLsPgGId9D8e": pageId
|
||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
|
||||
ftlObjectToJsCodeDeclaringAnObject,
|
||||
PAGE_ID_xIgLsPgGId9D8e: pageId
|
||||
}).map(
|
||||
([searchValue, replaceValue]) =>
|
||||
(ftlCode = ftlCode.replace(searchValue, replaceValue))
|
||||
);
|
||||
|
||||
return { ftlCode };
|
||||
}
|
||||
|
@ -2,10 +2,13 @@ import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../shared/constants";
|
||||
import { downloadBuiltinKeycloakTheme } from "../../shared/downloadBuiltinKeycloakTheme";
|
||||
import {
|
||||
resources_common,
|
||||
lastKeycloakVersionWithAccountV1,
|
||||
accountV1ThemeName
|
||||
} from "../../shared/constants";
|
||||
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
|
||||
type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
@ -18,33 +21,36 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "bringInAccountV1_tmp");
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
"keycloakVersion": lastKeycloakVersionWithAccountV1,
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion: lastKeycloakVersionWithAccountV1,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
|
||||
const accountV1DirPath = pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
accountV1ThemeName,
|
||||
"account"
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
|
||||
"destDirPath": accountV1DirPath
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "base", "account"),
|
||||
destDirPath: accountV1DirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
|
||||
"destDirPath": pathJoin(accountV1DirPath, "resources")
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "account", "resources"),
|
||||
destDirPath: pathJoin(accountV1DirPath, "resources")
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(accountV1DirPath, "resources", resources_common)
|
||||
});
|
||||
|
||||
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(accountV1DirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
@ -58,8 +64,13 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
|
||||
"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}`
|
||||
...[
|
||||
"patternfly.min.css",
|
||||
"patternfly-additions.min.css",
|
||||
"patternfly-additions.min.css"
|
||||
].map(
|
||||
fileBasename =>
|
||||
`${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
|
||||
)
|
||||
].join(" "),
|
||||
"",
|
||||
|
@ -16,8 +16,8 @@ export function generateMessageProperties(params: {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
let files = crawl({
|
||||
"dirPath": pathJoin(themeSrcDirPath, themeType),
|
||||
"returnedPathsType": "absolute"
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
@ -34,7 +34,9 @@ export function generateMessageProperties(params: {
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file => readFileSync(file).toString("utf8").includes("createUseI18n"));
|
||||
files = files.filter(file =>
|
||||
readFileSync(file).toString("utf8").includes("createUseI18n")
|
||||
);
|
||||
|
||||
if (files.length === 0) {
|
||||
return [];
|
||||
@ -43,18 +45,25 @@ export function generateMessageProperties(params: {
|
||||
const extraMessages = files
|
||||
.map(file => {
|
||||
const root = recast.parse(readFileSync(file).toString("utf8"), {
|
||||
"parser": {
|
||||
"parse": (code: string) => babelParser.parse(code, { "sourceType": "module", "plugins": ["typescript"] }),
|
||||
"generator": babelGenerate,
|
||||
"types": babelTypes
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"]
|
||||
}),
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
|
||||
const codes: string[] = [];
|
||||
|
||||
recast.visit(root, {
|
||||
"visitCallExpression": function (path) {
|
||||
if (path.node.callee.type === "Identifier" && path.node.callee.name === "createUseI18n") {
|
||||
visitCallExpression: function (path) {
|
||||
if (
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
) {
|
||||
codes.push(babelGenerate(path.node.arguments[0] as any).code);
|
||||
}
|
||||
this.traverse(path);
|
||||
@ -65,7 +74,9 @@ export function generateMessageProperties(params: {
|
||||
})
|
||||
.flat()
|
||||
.map(code => {
|
||||
let extraMessages: { [languageTag: string]: Record<string, string> } = {};
|
||||
let extraMessages: {
|
||||
[languageTag: string]: Record<string, string>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(`${symToStr({ extraMessages })} = ${code}`);
|
||||
@ -140,7 +151,14 @@ export function generateMessageProperties(params: {
|
||||
|
||||
out.push({
|
||||
languageTag,
|
||||
"propertiesFileSource": ["# This file was generated by keycloakify", "", "parent=base", "", propertiesFileSource, ""].join("\n")
|
||||
propertiesFileSource: [
|
||||
"# This file was generated by keycloakify",
|
||||
"",
|
||||
"parent=base",
|
||||
"",
|
||||
propertiesFileSource,
|
||||
""
|
||||
].join("\n")
|
||||
});
|
||||
}
|
||||
|
||||
@ -157,22 +175,56 @@ function toUTF16(codePoint: number): string {
|
||||
codePoint -= 0x10000;
|
||||
let highSurrogate = (codePoint >> 10) + 0xd800;
|
||||
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
|
||||
return "\\u" + highSurrogate.toString(16).padStart(4, "0") + "\\u" + lowSurrogate.toString(16).padStart(4, "0");
|
||||
return (
|
||||
"\\u" +
|
||||
highSurrogate.toString(16).padStart(4, "0") +
|
||||
"\\u" +
|
||||
lowSurrogate.toString(16).padStart(4, "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Escapes special characters and converts unicode to UTF-16 encoding
|
||||
// Escapes special characters for use in a .properties file
|
||||
function escapeString(str: string): string {
|
||||
let escapedStr = "";
|
||||
for (const char of [...str]) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (!codePoint) continue;
|
||||
if (char === "'") {
|
||||
escapedStr += "''"; // double single quotes
|
||||
} else if (codePoint > 0x7f) {
|
||||
escapedStr += toUTF16(codePoint); // non-ascii characters
|
||||
} else {
|
||||
escapedStr += char;
|
||||
|
||||
switch (char) {
|
||||
case "\n":
|
||||
escapedStr += "\\n";
|
||||
break;
|
||||
case "\r":
|
||||
escapedStr += "\\r";
|
||||
break;
|
||||
case "\t":
|
||||
escapedStr += "\\t";
|
||||
break;
|
||||
case "\\":
|
||||
escapedStr += "\\\\";
|
||||
break;
|
||||
case ":":
|
||||
escapedStr += "\\:";
|
||||
break;
|
||||
case "=":
|
||||
escapedStr += "\\=";
|
||||
break;
|
||||
case "#":
|
||||
escapedStr += "\\#";
|
||||
break;
|
||||
case "!":
|
||||
escapedStr += "\\!";
|
||||
break;
|
||||
case "'":
|
||||
escapedStr += "''";
|
||||
break;
|
||||
default:
|
||||
if (codePoint > 0x7f) {
|
||||
escapedStr += toUTF16(codePoint); // Non-ASCII characters
|
||||
} else {
|
||||
escapedStr += char; // ASCII character needs no escape
|
||||
}
|
||||
}
|
||||
}
|
||||
return escapedStr;
|
||||
|
@ -12,7 +12,9 @@ export type BuildOptionsLike = BuildOptionsLike_generateSrcMainResourcesForMainT
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function generateSrcMainResources(params: { buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
export async function generateSrcMainResources(params: {
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<void> {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const [themeName, ...themeVariantNames] = buildOptions.themeNames;
|
||||
|
@ -4,13 +4,14 @@ import { join as pathJoin, resolve as pathResolve } from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import { generateFtlFilesCodeFactory } from "../generateFtl";
|
||||
import { loginThemePageIds, accountThemePageIds } from "../../shared/pageIds";
|
||||
import {
|
||||
type ThemeType,
|
||||
lastKeycloakVersionWithAccountV1,
|
||||
keycloak_resources,
|
||||
accountV1ThemeName,
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
basenameOfTheKeycloakifyResourcesDir,
|
||||
loginThemePageIds,
|
||||
accountThemePageIds
|
||||
} from "../../shared/constants";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
@ -22,8 +23,11 @@ import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import { bringInAccountV1 } from "./bringInAccountV1";
|
||||
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { readThisNpmProjectVersion } from "../../tools/readThisNpmProjectVersion";
|
||||
import { writeMetaInfKeycloakThemes, type MetaInfKeycloakTheme } from "../../shared/metaInfKeycloakThemes";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
@ -42,22 +46,35 @@ export type BuildOptionsLike = {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function generateSrcMainResourcesForMainTheme(params: { themeName: string; buildOptions: BuildOptionsLike }): Promise<void> {
|
||||
export async function generateSrcMainResourcesForMainTheme(params: {
|
||||
themeName: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<void> {
|
||||
const { themeName, buildOptions } = params;
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
||||
});
|
||||
|
||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
||||
const { themeType } = params;
|
||||
return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType);
|
||||
return pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
themeType
|
||||
);
|
||||
};
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
|
||||
"login": false,
|
||||
"account": false,
|
||||
"email": false
|
||||
login: false,
|
||||
account: false,
|
||||
email: false
|
||||
};
|
||||
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
@ -70,18 +87,22 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
);
|
||||
|
||||
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
||||
rmSync(destDirPath, { "recursive": true, "force": true });
|
||||
rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
if (themeType === "account" && implementedThemeTypes.login) {
|
||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(
|
||||
srcDirPath: pathJoin(
|
||||
getThemeTypeDirPath({
|
||||
"themeType": "login"
|
||||
themeType: "login"
|
||||
}),
|
||||
"resources",
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
@ -93,14 +114,17 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": buildOptions.reactAppBuildDirPath,
|
||||
srcDirPath: buildOptions.reactAppBuildDirPath,
|
||||
destDirPath,
|
||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
|
||||
if (
|
||||
isInside({
|
||||
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
|
||||
dirPath: pathJoin(
|
||||
buildOptions.reactAppBuildDirPath,
|
||||
keycloak_resources
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
@ -108,39 +132,50 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
}
|
||||
|
||||
if (/\.css?$/i.test(filePath)) {
|
||||
const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
|
||||
"cssCode": sourceCode.toString("utf8")
|
||||
const {
|
||||
cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
|
||||
fixedCssCode
|
||||
} = replaceImportsInCssCode({
|
||||
cssCode: sourceCode.toString("utf8")
|
||||
});
|
||||
|
||||
Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
|
||||
cssGlobalsToDefine[key] = value;
|
||||
});
|
||||
Object.entries(cssGlobalsToDefineForThisFile).forEach(
|
||||
([key, value]) => {
|
||||
cssGlobalsToDefine[key] = value;
|
||||
}
|
||||
);
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
|
||||
};
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
const { fixedJsCode } = replaceImportsInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
jsCode: sourceCode.toString("utf8"),
|
||||
buildOptions
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
|
||||
};
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html"))
|
||||
.toString("utf8"),
|
||||
cssGlobalsToDefine,
|
||||
buildOptions,
|
||||
"keycloakifyVersion": readThisNpmProjectVersion(),
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
"fieldNames": readFieldNameUsage({
|
||||
fieldNames: readFieldNameUsage({
|
||||
themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
@ -162,9 +197,12 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.mkdirSync(themeTypeDirPath, { "recursive": true });
|
||||
fs.mkdirSync(themeTypeDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
generateMessageProperties({
|
||||
@ -173,15 +211,23 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true });
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
|
||||
const propertiesFilePath = pathJoin(
|
||||
messagesDirPath,
|
||||
`messages_${languageTag}.properties`
|
||||
);
|
||||
|
||||
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
|
||||
fs.writeFileSync(
|
||||
propertiesFilePath,
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
"keycloakVersion": (() => {
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return lastKeycloakVersionWithAccountV1;
|
||||
@ -189,7 +235,7 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
||||
}
|
||||
})(),
|
||||
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeType,
|
||||
buildOptions
|
||||
});
|
||||
@ -224,8 +270,8 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
implementedThemeTypes.email = true;
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": emailThemeSrcDirPath,
|
||||
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
@ -236,24 +282,24 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { "themes": [] };
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
||||
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
"name": themeName,
|
||||
"types": objectEntries(implementedThemeTypes)
|
||||
name: themeName,
|
||||
types: objectEntries(implementedThemeTypes)
|
||||
.filter(([, isImplemented]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
|
||||
if (implementedThemeTypes.account) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
"name": accountV1ThemeName,
|
||||
"types": ["account"]
|
||||
name: accountV1ThemeName,
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildOptions } from "../../shared/buildOptions";
|
||||
import { readMetaInfKeycloakThemes, writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import {
|
||||
readMetaInfKeycloakThemes,
|
||||
writeMetaInfKeycloakThemes
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
@ -10,20 +13,37 @@ export type BuildOptionsLike = {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function generateSrcMainResourcesForThemeVariant(params: { themeName: string; themeVariantName: string; buildOptions: BuildOptionsLike }) {
|
||||
export function generateSrcMainResourcesForThemeVariant(params: {
|
||||
themeName: string;
|
||||
themeVariantName: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { themeName, themeVariantName, buildOptions } = params;
|
||||
|
||||
const mainThemeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName);
|
||||
const mainThemeDirPath = pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": mainThemeDirPath,
|
||||
"destDirPath": pathJoin(mainThemeDirPath, "..", themeVariantName),
|
||||
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
|
||||
if (pathExtname(fileRelativePath) === ".ftl" && fileRelativePath.split(pathSep).length === 2) {
|
||||
srcDirPath: mainThemeDirPath,
|
||||
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (
|
||||
pathExtname(fileRelativePath) === ".ftl" &&
|
||||
fileRelativePath.split(pathSep).length === 2
|
||||
) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
Buffer.from(sourceCode)
|
||||
.toString("utf-8")
|
||||
.replace(`out["themeName"] = "${themeName}";`, `out["themeName"] = "${themeVariantName}";`),
|
||||
.replace(
|
||||
`out["themeName"] = "${themeName}";`,
|
||||
`out["themeName"] = "${themeVariantName}";`
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
@ -32,25 +52,29 @@ export function generateSrcMainResourcesForThemeVariant(params: { themeName: str
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({ "keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath });
|
||||
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
||||
});
|
||||
|
||||
updatedMetaInfKeycloakThemes.themes.push({
|
||||
"name": themeVariantName,
|
||||
"types": (() => {
|
||||
const theme = updatedMetaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
|
||||
name: themeVariantName,
|
||||
types: (() => {
|
||||
const theme = updatedMetaInfKeycloakThemes.themes.find(
|
||||
({ name }) => name === themeName
|
||||
);
|
||||
assert(theme !== undefined);
|
||||
return theme.types;
|
||||
})()
|
||||
});
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
"metaInfKeycloakThemes": updatedMetaInfKeycloakThemes
|
||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,28 @@
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { accountThemePageIds, loginThemePageIds } from "../../shared/pageIds";
|
||||
import { id } from "tsafe/id";
|
||||
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import {
|
||||
type ThemeType,
|
||||
accountThemePageIds,
|
||||
loginThemePageIds
|
||||
} from "../../shared/constants";
|
||||
|
||||
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||
export function readExtraPagesNames(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const filePaths = crawl({
|
||||
"dirPath": pathJoin(themeSrcDirPath, themeType),
|
||||
"returnedPathsType": "absolute"
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
|
||||
const candidateFilePaths = filePaths.filter(filePath =>
|
||||
/kcContext\.[^.]+$/.test(filePath)
|
||||
);
|
||||
|
||||
if (candidateFilePaths.length === 0) {
|
||||
candidateFilePaths.push(...filePaths);
|
||||
@ -25,7 +33,12 @@ export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType
|
||||
for (const candidateFilPath of candidateFilePaths) {
|
||||
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
|
||||
|
||||
extraPages.push(...Array.from(rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g), m => m[1]));
|
||||
extraPages.push(
|
||||
...Array.from(
|
||||
rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g),
|
||||
m => m[1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
|
||||
|
@ -5,13 +5,22 @@ import type { ThemeType } from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||
export function readFieldNameUsage(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: ThemeType;
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const fieldNames = new Set<string>();
|
||||
|
||||
for (const srcDirPath of [pathJoin(getThisCodebaseRootDirPath(), "src", themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
for (const srcDirPath of [
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
|
||||
pathJoin(themeSrcDirPath, themeType)
|
||||
]) {
|
||||
const filePaths = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "absolute"
|
||||
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
|
||||
@ -20,7 +29,13 @@ export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType:
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const functionName of ["printIfExists", "existsError", "get", "exists", "getFirstError"] as const) {
|
||||
for (const functionName of [
|
||||
"printIfExists",
|
||||
"existsError",
|
||||
"get",
|
||||
"exists",
|
||||
"getFirstError"
|
||||
] as const) {
|
||||
if (!rawSourceFile.includes(functionName)) {
|
||||
continue;
|
||||
}
|
||||
@ -40,9 +55,21 @@ export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType:
|
||||
return part
|
||||
.split(",")
|
||||
.map(a => a.trim())
|
||||
.filter((...[, i]) => (functionName !== "printIfExists" ? true : i === 0))
|
||||
.filter(a => a.startsWith('"') || a.startsWith("'") || a.startsWith("`"))
|
||||
.filter(a => a.endsWith('"') || a.endsWith("'") || a.endsWith("`"))
|
||||
.filter((...[, i]) =>
|
||||
functionName !== "printIfExists" ? true : i === 0
|
||||
)
|
||||
.filter(
|
||||
a =>
|
||||
a.startsWith('"') ||
|
||||
a.startsWith("'") ||
|
||||
a.startsWith("`")
|
||||
)
|
||||
.filter(
|
||||
a =>
|
||||
a.endsWith('"') ||
|
||||
a.endsWith("'") ||
|
||||
a.endsWith("`")
|
||||
)
|
||||
.map(a => a.slice(1).slice(0, -1));
|
||||
})
|
||||
.flat()
|
||||
|
@ -1,5 +1,9 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../shared/buildOptions";
|
||||
import { accountV1ThemeName } from "../shared/constants";
|
||||
@ -27,7 +31,10 @@ export function generateStartKeycloakTestingContainer(params: {
|
||||
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename),
|
||||
pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
generateStartKeycloakTestingContainer.basename
|
||||
),
|
||||
Buffer.from(
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
@ -45,9 +52,16 @@ export function generateStartKeycloakTestingContainer(params: {
|
||||
"$(pwd)",
|
||||
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
|
||||
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
|
||||
[...(doesImplementAccountTheme ? [accountV1ThemeName] : []), ...buildOptions.themeNames].map(
|
||||
[
|
||||
...(doesImplementAccountTheme ? [accountV1ThemeName] : []),
|
||||
...buildOptions.themeNames
|
||||
].map(
|
||||
themeName =>
|
||||
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
|
||||
` -v "${pathJoin(
|
||||
"$(pwd)",
|
||||
themeRelativeDirPath,
|
||||
themeName
|
||||
).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
|
||||
),
|
||||
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
||||
` start-dev`,
|
||||
@ -55,6 +69,6 @@ export function generateStartKeycloakTestingContainer(params: {
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
),
|
||||
{ "mode": 0o755 }
|
||||
{ mode: 0o755 }
|
||||
);
|
||||
}
|
||||
|
@ -3,26 +3,77 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { readBuildOptions } from "../shared/buildOptions";
|
||||
import { getLogger } from "../tools/logger";
|
||||
import { vitePluginSubScriptEnvNames } from "../shared/constants";
|
||||
import { vitePluginSubScriptEnvNames, skipBuildJarsEnvName } 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";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
exit_if_maven_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
||||
try {
|
||||
commandOutput = child_process.execSync("mvn --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
});
|
||||
} catch {}
|
||||
|
||||
if (commandOutput?.toString("utf8").includes("Apache Maven")) {
|
||||
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";
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(
|
||||
`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(
|
||||
installationCommand
|
||||
)}\` (for example)`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
|
||||
const logger = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
console.log(
|
||||
[
|
||||
chalk.cyan(`keycloakify v${readThisNpmPackageVersion()}`),
|
||||
chalk.green(
|
||||
`Building the keycloak theme in .${pathSep}${pathRelative(
|
||||
process.cwd(),
|
||||
buildOptions.keycloakifyBuildDirPath
|
||||
)} ...`
|
||||
)
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
logger.log("🔏 Building the keycloak theme...⌚");
|
||||
const startTime = Date.now();
|
||||
|
||||
{
|
||||
if (!fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
|
||||
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, { "recursive": true });
|
||||
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
fs.writeFileSync(
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"),
|
||||
Buffer.from("*", "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
await generateSrcMainResources({ buildOptions });
|
||||
@ -33,20 +84,24 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
}
|
||||
|
||||
child_process.execSync("npx vite", {
|
||||
"cwd": buildOptions.reactAppRootDirPath,
|
||||
"env": {
|
||||
cwd: buildOptions.reactAppRootDirPath,
|
||||
env: {
|
||||
...process.env,
|
||||
[vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify(buildOptions)
|
||||
[vitePluginSubScriptEnvNames.runPostBuildScript]:
|
||||
JSON.stringify(buildOptions)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await buildJars({ buildOptions });
|
||||
build_jars: {
|
||||
if (process.env[skipBuildJarsEnvName]) {
|
||||
break build_jars;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathJoin(
|
||||
pathRelative(process.cwd(), buildOptions.keycloakifyBuildDirPath),
|
||||
"keycloak-theme-for-kc-*.jar"
|
||||
)}`
|
||||
await buildJars({ buildOptions });
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,15 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
|
||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
|
||||
match =>
|
||||
(cssGlobalsToDefine[
|
||||
"url" +
|
||||
crypto
|
||||
.createHash("sha256")
|
||||
.update(match)
|
||||
.digest("hex")
|
||||
.substring(0, 15)
|
||||
] = match)
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
@ -26,26 +34,37 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
|
||||
(fixedCssCode = fixedCssCode
|
||||
.split(cssGlobalsToDefine[cssVariableName])
|
||||
.join(`var(--${cssVariableName})`))
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
|
||||
export function generateCssCodeToDefineGlobals(params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
const { cssGlobalsToDefine, buildOptions } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
cssCodeToPrependInHead: [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName =>
|
||||
[
|
||||
`--${cssVariableName}:`,
|
||||
cssGlobalsToDefine[cssVariableName].replace(
|
||||
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||
new RegExp(
|
||||
`url\\(${(buildOptions.urlPathname ?? "/").replace(
|
||||
/\//g,
|
||||
"\\/"
|
||||
)}`,
|
||||
"g"
|
||||
),
|
||||
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
)
|
||||
].join(" ")
|
||||
|
@ -8,7 +8,10 @@ export type BuildOptionsLike = {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||
export function replaceImportsInInlineCssCode(params: {
|
||||
cssCode: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): {
|
||||
fixedCssCode: string;
|
||||
} {
|
||||
const { cssCode, buildOptions } = params;
|
||||
@ -17,7 +20,8 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
|
||||
buildOptions.urlPathname === undefined
|
||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
|
||||
(...[, group]) =>
|
||||
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
|
@ -13,7 +13,10 @@ export type BuildOptionsLike = {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }) {
|
||||
export function replaceImportsInJsCode(params: {
|
||||
jsCode: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { jsCode, buildOptions } = params;
|
||||
|
||||
const { fixedJsCode } = (() => {
|
||||
@ -22,8 +25,8 @@ export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: B
|
||||
return replaceImportsInJsCode_vite({
|
||||
jsCode,
|
||||
buildOptions,
|
||||
"basenameOfAssetsFiles": readAssetsDirSync({
|
||||
"assetsDirPath": params.buildOptions.assetsDirPath
|
||||
basenameOfAssetsFiles: readAssetsDirSync({
|
||||
assetsDirPath: params.buildOptions.assetsDirPath
|
||||
})
|
||||
});
|
||||
case "webpack":
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
|
||||
import {
|
||||
nameOfTheGlobal,
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
} from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
||||
import * as nodePath from "path";
|
||||
@ -20,7 +23,12 @@ export function replaceImportsInJsCode_vite(params: {
|
||||
}): {
|
||||
fixedJsCode: string;
|
||||
} {
|
||||
const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
|
||||
const {
|
||||
jsCode,
|
||||
buildOptions,
|
||||
basenameOfAssetsFiles,
|
||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||
} = params;
|
||||
|
||||
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
|
||||
|
||||
@ -38,22 +46,32 @@ export function replaceImportsInJsCode_vite(params: {
|
||||
// Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
|
||||
fixedJsCode = fixedJsCode.replace(
|
||||
new RegExp(
|
||||
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
|
||||
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(
|
||||
buildOptions.urlPathname,
|
||||
"/",
|
||||
"\\/"
|
||||
)}"\\+\\2\\}`,
|
||||
"g"
|
||||
),
|
||||
(...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
|
||||
(...[, funcName, paramName]) =>
|
||||
`${funcName}=function(${paramName}){return"/"+${paramName}}`
|
||||
);
|
||||
}
|
||||
|
||||
replace_javascript_relatives_import_paths: {
|
||||
// Example: "assets/ or "foo/bar/"
|
||||
const staticDir = (() => {
|
||||
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
|
||||
let out = pathRelative(
|
||||
buildOptions.reactAppBuildDirPath,
|
||||
buildOptions.assetsDirPath
|
||||
);
|
||||
|
||||
out = replaceAll(out, pathSep, "/") + "/";
|
||||
|
||||
if (out === "/") {
|
||||
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
|
||||
throw new Error(
|
||||
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
|
||||
import {
|
||||
nameOfTheGlobal,
|
||||
basenameOfTheKeycloakifyResourcesDir
|
||||
} from "../../../shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
||||
import * as nodePath from "path";
|
||||
@ -12,10 +15,18 @@ export type BuildOptionsLike = {
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike; systemType?: "posix" | "win32" }): {
|
||||
export function replaceImportsInJsCode_webpack(params: {
|
||||
jsCode: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
systemType?: "posix" | "win32";
|
||||
}): {
|
||||
fixedJsCode: string;
|
||||
} {
|
||||
const { jsCode, buildOptions, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
|
||||
const {
|
||||
jsCode,
|
||||
buildOptions,
|
||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||
} = params;
|
||||
|
||||
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
|
||||
|
||||
@ -24,29 +35,51 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
|
||||
if (buildOptions.urlPathname !== undefined) {
|
||||
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
|
||||
fixedJsCode = fixedJsCode.replace(
|
||||
new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
|
||||
new RegExp(
|
||||
`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(
|
||||
buildOptions.urlPathname,
|
||||
"/",
|
||||
"\\/"
|
||||
)}",`,
|
||||
"g"
|
||||
),
|
||||
(...[, assignTo]) => `,${assignTo}="/",`
|
||||
);
|
||||
}
|
||||
|
||||
// Example: "static/ or "foo/bar/"
|
||||
const staticDir = (() => {
|
||||
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
|
||||
let out = pathRelative(
|
||||
buildOptions.reactAppBuildDirPath,
|
||||
buildOptions.assetsDirPath
|
||||
);
|
||||
|
||||
out = replaceAll(out, pathSep, "/") + "/";
|
||||
|
||||
if (out === "/") {
|
||||
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
|
||||
throw new Error(
|
||||
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
})();
|
||||
|
||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
|
||||
const getReplaceArgs = (
|
||||
language: "js" | "css"
|
||||
): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(
|
||||
`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(
|
||||
/\//g,
|
||||
"\\/"
|
||||
)}${language}\\/"`,
|
||||
"g"
|
||||
),
|
||||
(...[, n, u, matchedFunction, eForFunction]) => {
|
||||
const isArrowFunction = matchedFunction.includes("=>");
|
||||
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
|
||||
const e = isArrowFunction
|
||||
? matchedFunction.replace("=>", "").trim()
|
||||
: eForFunction;
|
||||
|
||||
return `
|
||||
${n}[(function(){
|
||||
@ -58,7 +91,9 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
|
||||
});
|
||||
}
|
||||
return "${u}";
|
||||
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
|
||||
})()] = ${
|
||||
isArrowFunction ? `${e} =>` : `function(${e}) { return `
|
||||
} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
@ -68,7 +103,10 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
|
||||
.replace(...getReplaceArgs("js"))
|
||||
.replace(...getReplaceArgs("css"))
|
||||
.replace(
|
||||
new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g"),
|
||||
new RegExp(
|
||||
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
|
||||
"g"
|
||||
),
|
||||
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
|
||||
);
|
||||
|
||||
|
213
src/bin/main.ts
213
src/bin/main.ts
@ -1,139 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
|
||||
export type CliCommandOptions = {
|
||||
isSilent: boolean;
|
||||
reactAppRootDirPath: string | undefined;
|
||||
};
|
||||
|
||||
const program = termost<CliCommandOptions>("Keycloak theme builder");
|
||||
const program = termost<CliCommandOptions>(
|
||||
{
|
||||
name: "keycloakify",
|
||||
description: "Keycloakify CLI",
|
||||
version: readThisNpmPackageVersion()
|
||||
},
|
||||
{
|
||||
onException: error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const optionsKeys: string[] = [];
|
||||
|
||||
program
|
||||
.option({
|
||||
"key": "reactAppRootDirPath",
|
||||
"name": (() => {
|
||||
const long = "project";
|
||||
const short = "p";
|
||||
program.option({
|
||||
key: "reactAppRootDirPath",
|
||||
name: (() => {
|
||||
const long = "project";
|
||||
const short = "p";
|
||||
|
||||
optionsKeys.push(long, short);
|
||||
optionsKeys.push(long, short);
|
||||
|
||||
return { long, short };
|
||||
})(),
|
||||
"description": "https://docs.keycloakify.dev/build-options#project-or-p-cli-option",
|
||||
"defaultValue": undefined
|
||||
})
|
||||
.option({
|
||||
"key": "isSilent",
|
||||
"name": (() => {
|
||||
const name = "silent";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
"description": "https://docs.keycloakify.dev/build-options#silent",
|
||||
"defaultValue": false
|
||||
});
|
||||
return { long, short };
|
||||
})(),
|
||||
description: [
|
||||
`For monorepos, path to the keycloakify project.`,
|
||||
"Example: `npx keycloakify build --project packages/keycloak-theme`",
|
||||
"https://docs.keycloakify.dev/build-options#project-or-p-cli-option"
|
||||
].join(" "),
|
||||
defaultValue: undefined
|
||||
});
|
||||
|
||||
function skip(_context: any, argv: { options: Record<string, unknown> }) {
|
||||
const unrecognizedOptionKey = Object.keys(argv.options).find(key => !optionsKeys.includes(key));
|
||||
const unrecognizedOptionKey = Object.keys(argv.options).find(
|
||||
key => !optionsKeys.includes(key)
|
||||
);
|
||||
|
||||
if (unrecognizedOptionKey !== undefined) {
|
||||
console.error(`keycloakify: Unrecognized option: ${unrecognizedOptionKey.length === 1 ? "-" : "--"}${unrecognizedOptionKey}`);
|
||||
console.error(
|
||||
`keycloakify: Unrecognized option: ${
|
||||
unrecognizedOptionKey.length === 1 ? "-" : "--"
|
||||
}${unrecognizedOptionKey}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function runAndLogErrors(fn: () => Promise<void>) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "build",
|
||||
"description": "Build the theme (default subcommand)."
|
||||
name: "build",
|
||||
description: "Build the theme (default subcommand)."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./keycloakify");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command<{
|
||||
port: number;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
}>({
|
||||
name: "start-keycloak",
|
||||
description:
|
||||
"Spin up a pre configured Docker image of Keycloak to test your theme."
|
||||
})
|
||||
.option({
|
||||
key: "port",
|
||||
name: (() => {
|
||||
const name = "port";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
||||
defaultValue: 8080
|
||||
})
|
||||
.option({
|
||||
key: "keycloakVersion",
|
||||
name: (() => {
|
||||
const name = "keycloak-version";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
description: [
|
||||
"Use a specific version of Keycloak.",
|
||||
"Example `--keycloak-version 21.1.1`"
|
||||
].join(" "),
|
||||
defaultValue: undefined
|
||||
})
|
||||
.option({
|
||||
key: "realmJsonFilePath",
|
||||
name: (() => {
|
||||
const name = "import";
|
||||
|
||||
optionsKeys.push(name);
|
||||
|
||||
return name;
|
||||
})(),
|
||||
defaultValue: undefined,
|
||||
description: [
|
||||
"Import your own realm configuration file",
|
||||
"Example `--import path/to/myrealm-realm.json`"
|
||||
].join(" ")
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./start-keycloak");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "download-builtin-keycloak-theme",
|
||||
"description": "Download the built-in Keycloak theme."
|
||||
name: "download-keycloak-default-theme",
|
||||
description: "Download the built-in Keycloak theme."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
const { command } = await import("./download-builtin-keycloak-theme");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./download-keycloak-default-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "eject-keycloak-page",
|
||||
"description": "Eject a Keycloak page."
|
||||
name: "eject-page",
|
||||
description: "Eject a Keycloak page."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
const { command } = await import("./eject-keycloak-page");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./eject-page");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "initialize-email-theme",
|
||||
"description": "Initialize an email theme."
|
||||
name: "initialize-email-theme",
|
||||
description: "Initialize an email theme."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./initialize-email-theme");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "start-keycloak-container",
|
||||
"description": "Spin up a Keycloak container with the theme preloaded and the realm pre configured."
|
||||
name: "copy-keycloak-resources-to-public",
|
||||
description:
|
||||
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
const { command } = await import("./start-keycloak-container");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
"name": "copy-keycloak-resources-to-public",
|
||||
"description": "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
"handler": async cliCommandOptions => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||
await runAndLogErrors(() => command({ cliCommandOptions }));
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -141,10 +195,17 @@ program
|
||||
{
|
||||
const [, , ...rest] = process.argv;
|
||||
|
||||
if (rest.length === 0 || (rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")) {
|
||||
const { status } = child_process.spawnSync("npx", ["keycloakify", "build", ...rest], {
|
||||
"stdio": "inherit"
|
||||
});
|
||||
if (
|
||||
rest.length === 0 ||
|
||||
(rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")
|
||||
) {
|
||||
const { status } = child_process.spawnSync(
|
||||
"npx",
|
||||
["keycloakify", "build", ...rest],
|
||||
{
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
|
||||
process.exit(status ?? 1);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
export type KeycloakVersionRange = KeycloakVersionRange.WithAccountTheme | KeycloakVersionRange.WithoutAccountTheme;
|
||||
export type KeycloakVersionRange =
|
||||
| KeycloakVersionRange.WithAccountTheme
|
||||
| KeycloakVersionRange.WithoutAccountTheme;
|
||||
|
||||
export namespace KeycloakVersionRange {
|
||||
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
|
||||
|
@ -12,7 +12,6 @@ import { vitePluginSubScriptEnvNames } from "./constants";
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = {
|
||||
bundler: "vite" | "webpack";
|
||||
isSilent: boolean;
|
||||
themeVersion: string;
|
||||
themeNames: string[];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
@ -50,7 +49,9 @@ export type ResolvedViteConfig = {
|
||||
userProvidedBuildOptions: UserProvidedBuildOptions;
|
||||
};
|
||||
|
||||
export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions }): BuildOptions {
|
||||
export function readBuildOptions(params: {
|
||||
cliCommandOptions: CliCommandOptions;
|
||||
}): BuildOptions {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const reactAppRootDirPath = (() => {
|
||||
@ -59,29 +60,39 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
}
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": cliCommandOptions.reactAppRootDirPath,
|
||||
"cwd": process.cwd()
|
||||
pathIsh: cliCommandOptions.reactAppRootDirPath,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
})();
|
||||
|
||||
const { resolvedViteConfig } = (() => {
|
||||
if (fs.readdirSync(reactAppRootDirPath).find(fileBasename => fileBasename.startsWith("vite.config")) === undefined) {
|
||||
return { "resolvedViteConfig": undefined };
|
||||
if (
|
||||
fs
|
||||
.readdirSync(reactAppRootDirPath)
|
||||
.find(fileBasename => fileBasename.startsWith("vite.config")) ===
|
||||
undefined
|
||||
) {
|
||||
return { resolvedViteConfig: undefined };
|
||||
}
|
||||
|
||||
const output = child_process
|
||||
.execSync("npx vite", {
|
||||
"cwd": reactAppRootDirPath,
|
||||
"env": {
|
||||
cwd: reactAppRootDirPath,
|
||||
env: {
|
||||
...process.env,
|
||||
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
|
||||
}
|
||||
})
|
||||
.toString("utf8");
|
||||
|
||||
assert(output.includes(vitePluginSubScriptEnvNames.resolveViteConfig), "Seems like the Keycloakify's Vite plugin is not installed.");
|
||||
assert(
|
||||
output.includes(vitePluginSubScriptEnvNames.resolveViteConfig),
|
||||
"Seems like the Keycloakify's Vite plugin is not installed."
|
||||
);
|
||||
|
||||
const resolvedViteConfigStr = output.split(vitePluginSubScriptEnvNames.resolveViteConfig).reverse()[0];
|
||||
const resolvedViteConfigStr = output
|
||||
.split(vitePluginSubScriptEnvNames.resolveViteConfig)
|
||||
.reverse()[0];
|
||||
|
||||
const resolvedViteConfig: ResolvedViteConfig = JSON.parse(resolvedViteConfigStr);
|
||||
|
||||
@ -93,22 +104,24 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
name: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
keycloakify?: UserProvidedBuildOptions & { reactAppBuildDirPath?: string };
|
||||
keycloakify?: UserProvidedBuildOptions & {
|
||||
reactAppBuildDirPath?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
"name": z.string(),
|
||||
"version": z.string().optional(),
|
||||
"homepage": z.string().optional(),
|
||||
"keycloakify": z
|
||||
name: z.string(),
|
||||
version: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
keycloakify: z
|
||||
.object({
|
||||
"extraThemeProperties": z.array(z.string()).optional(),
|
||||
"artifactId": z.string().optional(),
|
||||
"groupId": z.string().optional(),
|
||||
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
|
||||
"reactAppBuildDirPath": z.string().optional(),
|
||||
"keycloakifyBuildDirPath": z.string().optional(),
|
||||
"themeName": z.union([z.string(), z.array(z.string())]).optional()
|
||||
extraThemeProperties: z.array(z.string()).optional(),
|
||||
artifactId: z.string().optional(),
|
||||
groupId: z.string().optional(),
|
||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||
reactAppBuildDirPath: z.string().optional(),
|
||||
keycloakifyBuildDirPath: z.string().optional(),
|
||||
themeName: z.union([z.string(), z.array(z.string())]).optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
@ -120,7 +133,13 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
assert<Expected extends Got ? true : false>();
|
||||
}
|
||||
|
||||
return zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
|
||||
return zParsedPackageJson.parse(
|
||||
JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(reactAppRootDirPath, "package.json"))
|
||||
.toString("utf8")
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
||||
const userProvidedBuildOptions: UserProvidedBuildOptions = {
|
||||
@ -153,8 +172,8 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
|
||||
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": parsedPackageJson.keycloakify.reactAppBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
pathIsh: parsedPackageJson.keycloakify.reactAppBuildDirPath,
|
||||
cwd: reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -164,15 +183,18 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
|
||||
})();
|
||||
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
|
||||
reactAppRootDirPath,
|
||||
dependencyExpected: "keycloakify"
|
||||
});
|
||||
|
||||
return {
|
||||
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
||||
"isSilent": cliCommandOptions.isSilent,
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
||||
bundler: resolvedViteConfig !== undefined ? "vite" : "webpack",
|
||||
themeVersion:
|
||||
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
||||
themeNames,
|
||||
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
|
||||
"groupId": (() => {
|
||||
extraThemeProperties: userProvidedBuildOptions.extraThemeProperties,
|
||||
groupId: (() => {
|
||||
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
||||
|
||||
return (
|
||||
@ -187,24 +209,30 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
|
||||
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
||||
artifactId:
|
||||
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
|
||||
userProvidedBuildOptions.artifactId ??
|
||||
`${themeNames[0]}-keycloak-theme`,
|
||||
loginThemeResourcesFromKeycloakVersion:
|
||||
userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
||||
reactAppRootDirPath,
|
||||
reactAppBuildDirPath,
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
keycloakifyBuildDirPath: (() => {
|
||||
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
|
||||
"cwd": reactAppRootDirPath
|
||||
pathIsh: userProvidedBuildOptions.keycloakifyBuildDirPath,
|
||||
cwd: reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(
|
||||
reactAppRootDirPath,
|
||||
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
|
||||
resolvedViteConfig?.buildDir === undefined
|
||||
? "build_keycloak"
|
||||
: `${resolvedViteConfig.buildDir}_keycloak`
|
||||
);
|
||||
})(),
|
||||
"publicDirPath": (() => {
|
||||
publicDirPath: (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
@ -212,8 +240,8 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
|
||||
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.PUBLIC_DIR_PATH,
|
||||
"cwd": reactAppRootDirPath
|
||||
pathIsh: process.env.PUBLIC_DIR_PATH,
|
||||
cwd: reactAppRootDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -222,13 +250,13 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
|
||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
|
||||
})(),
|
||||
"cacheDirPath": (() => {
|
||||
cacheDirPath: (() => {
|
||||
const cacheDirPath = pathJoin(
|
||||
(() => {
|
||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
"pathIsh": process.env.XDG_CACHE_HOME,
|
||||
"cwd": reactAppRootDirPath
|
||||
pathIsh: process.env.XDG_CACHE_HOME,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,7 +267,7 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
|
||||
return cacheDirPath;
|
||||
})(),
|
||||
"urlPathname": (() => {
|
||||
urlPathname: (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
@ -263,7 +291,7 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
|
||||
|
||||
return resolvedViteConfig.urlPathname;
|
||||
})(),
|
||||
"assetsDirPath": (() => {
|
||||
assetsDirPath: (() => {
|
||||
webpack: {
|
||||
if (resolvedViteConfig !== undefined) {
|
||||
break webpack;
|
||||
|
@ -1,4 +1,6 @@
|
||||
export const nameOfTheGlobal = "kcContext";
|
||||
export const nameOfTheLocalizationRealmOverridesUserProfileProperty =
|
||||
"__localizationRealmOverridesUserProfile";
|
||||
export const keycloak_resources = "keycloak-resources";
|
||||
export const resources_common = "resources-common";
|
||||
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
|
||||
@ -10,6 +12,60 @@ export const accountV1ThemeName = "account-v1";
|
||||
export type ThemeType = (typeof themeTypes)[number];
|
||||
|
||||
export const vitePluginSubScriptEnvNames = {
|
||||
"runPostBuildScript": "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||
"resolveViteConfig": "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||
runPostBuildScript: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||
} as const;
|
||||
|
||||
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR";
|
||||
|
||||
export const loginThemePageIds = [
|
||||
"login.ftl",
|
||||
"login-username.ftl",
|
||||
"login-password.ftl",
|
||||
"webauthn-authenticate.ftl",
|
||||
"webauthn-register.ftl",
|
||||
"register.ftl",
|
||||
"info.ftl",
|
||||
"error.ftl",
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-oauth2-device-verify-user-code.ftl",
|
||||
"login-oauth-grant.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-idp-link-email.ftl",
|
||||
"login-page-expired.ftl",
|
||||
"login-config-totp.ftl",
|
||||
"logout-confirm.ftl",
|
||||
"idp-review-user-profile.ftl",
|
||||
"update-email.ftl",
|
||||
"select-authenticator.ftl",
|
||||
"saml-post-form.ftl",
|
||||
"delete-credential.ftl",
|
||||
"code.ftl",
|
||||
"delete-account-confirm.ftl",
|
||||
"frontchannel-logout.ftl",
|
||||
"login-recovery-authn-code-config.ftl",
|
||||
"login-recovery-authn-code-input.ftl",
|
||||
"login-reset-otp.ftl",
|
||||
"login-x509-info.ftl",
|
||||
"webauthn-error.ftl"
|
||||
] as const;
|
||||
|
||||
export const accountThemePageIds = [
|
||||
"password.ftl",
|
||||
"account.ftl",
|
||||
"sessions.ftl",
|
||||
"totp.ftl",
|
||||
"applications.ftl",
|
||||
"log.ftl",
|
||||
"federatedIdentity.ftl"
|
||||
] as const;
|
||||
|
||||
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||
|
||||
export const containerName = "keycloak-keycloakify";
|
||||
|
@ -3,8 +3,12 @@ import {
|
||||
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
|
||||
} from "./downloadKeycloakStaticResources";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "../shared/constants";
|
||||
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
|
||||
import {
|
||||
themeTypes,
|
||||
keycloak_resources,
|
||||
lastKeycloakVersionWithAccountV1
|
||||
} from "../shared/constants";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
@ -17,7 +21,9 @@ export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakStaticResources
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function copyKeycloakResourcesToPublic(params: { buildOptions: BuildOptionsLike }) {
|
||||
export async function copyKeycloakResourcesToPublic(params: {
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
|
||||
@ -27,11 +33,14 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
const keycloakifyBuildinfoRaw = JSON.stringify(
|
||||
{
|
||||
destDirPath,
|
||||
"keycloakifyVersion": readThisNpmProjectVersion(),
|
||||
"buildOptions": {
|
||||
"loginThemeResourcesFromKeycloakVersion": readThisNpmProjectVersion(),
|
||||
"cacheDirPath": pathRelative(destDirPath, buildOptions.cacheDirPath),
|
||||
"npmWorkspaceRootDirPath": pathRelative(destDirPath, buildOptions.npmWorkspaceRootDirPath)
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
buildOptions: {
|
||||
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
||||
cacheDirPath: pathRelative(destDirPath, buildOptions.cacheDirPath),
|
||||
npmWorkspaceRootDirPath: pathRelative(
|
||||
destDirPath,
|
||||
buildOptions.npmWorkspaceRootDirPath
|
||||
)
|
||||
}
|
||||
},
|
||||
null,
|
||||
@ -43,7 +52,9 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
break skip_if_already_done;
|
||||
}
|
||||
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
|
||||
const keycloakifyBuildinfoRaw_previousRun = fs
|
||||
.readFileSync(keycloakifyBuildinfoFilePath)
|
||||
.toString("utf8");
|
||||
|
||||
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
|
||||
break skip_if_already_done;
|
||||
@ -52,11 +63,15 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
return;
|
||||
}
|
||||
|
||||
rmSync(destDirPath, { "force": true, "recursive": true });
|
||||
rmSync(destDirPath, { force: true, recursive: true });
|
||||
|
||||
fs.mkdirSync(destDirPath, { recursive: true });
|
||||
|
||||
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
for (const themeType of themeTypes) {
|
||||
await downloadKeycloakStaticResources({
|
||||
"keycloakVersion": (() => {
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
||||
@ -65,7 +80,7 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
}
|
||||
})(),
|
||||
themeType,
|
||||
"themeDirPath": destDirPath,
|
||||
themeDirPath: destDirPath,
|
||||
buildOptions
|
||||
});
|
||||
}
|
||||
@ -76,12 +91,14 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
|
||||
// prettier-ignore
|
||||
[
|
||||
"This is just a test folder that helps develop",
|
||||
"the login and register page without having to run a Keycloak container"
|
||||
"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(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
|
||||
fs.writeFileSync(
|
||||
keycloakifyBuildinfoFilePath,
|
||||
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
|
||||
);
|
||||
}
|
||||
|
@ -1,203 +0,0 @@
|
||||
import { createHash } from "crypto";
|
||||
import { mkdir, writeFile, unlink } from "fs/promises";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { unzip, zip } from "../tools/unzip";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import * as child_process from "child_process";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import type { BuildOptions } from "./buildOptions";
|
||||
import { getProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function downloadAndUnzip(params: {
|
||||
url: string;
|
||||
destDirPath: string;
|
||||
specificDirsToExtract?: string[];
|
||||
preCacheTransform?: {
|
||||
actionCacheId: string;
|
||||
action: (params: { destDirPath: string }) => Promise<void>;
|
||||
};
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
|
||||
|
||||
const { extractDirPath, zipFilePath } = (() => {
|
||||
const zipFileBasenameWithoutExt = generateFileNameFromURL({
|
||||
url,
|
||||
"preCacheTransform":
|
||||
preCacheTransform === undefined
|
||||
? undefined
|
||||
: {
|
||||
"actionCacheId": preCacheTransform.actionCacheId,
|
||||
"actionFootprint": preCacheTransform.action.toString()
|
||||
}
|
||||
});
|
||||
|
||||
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
|
||||
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
|
||||
|
||||
return { zipFilePath, extractDirPath };
|
||||
})();
|
||||
|
||||
download_zip_and_transform: {
|
||||
if (await existsAsync(zipFilePath)) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
const { response, isFromRemoteCache } = await (async () => {
|
||||
const proxyFetchOptions = await getProxyFetchOptions({
|
||||
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
|
||||
proxyFetchOptions
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
response,
|
||||
"isFromRemoteCache": true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"response": await fetch(url, proxyFetchOptions),
|
||||
"isFromRemoteCache": false
|
||||
};
|
||||
})();
|
||||
|
||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||
|
||||
/**
|
||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||
* octokit upgrades.
|
||||
*/
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
|
||||
await writeFile(zipFilePath, response.body);
|
||||
|
||||
if (isFromRemoteCache) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
||||
break download_zip_and_transform;
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
||||
|
||||
try {
|
||||
await preCacheTransform?.action({
|
||||
"destDirPath": extractDirPath
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await unlink(zipFilePath);
|
||||
|
||||
await zip(extractDirPath, zipFilePath);
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
|
||||
upload_to_remot_cache_if_admin: {
|
||||
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
||||
|
||||
if (githubToken === undefined) {
|
||||
break upload_to_remot_cache_if_admin;
|
||||
}
|
||||
|
||||
console.log("uploading to remote cache");
|
||||
|
||||
try {
|
||||
child_process.execSync(`which putasset`);
|
||||
} catch {
|
||||
child_process.execSync(`npm install -g putasset`);
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execFileSync("putasset", [
|
||||
"--owner",
|
||||
"keycloakify",
|
||||
"--repo",
|
||||
"keycloakify",
|
||||
"--tag",
|
||||
"v0.0.1",
|
||||
"--filename",
|
||||
zipFilePath,
|
||||
"--token",
|
||||
githubToken
|
||||
]);
|
||||
} catch {
|
||||
console.log("upload failed, asset probably already exists in remote cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await unzip(zipFilePath, extractDirPath);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
function generateFileNameFromURL(params: {
|
||||
url: string;
|
||||
preCacheTransform:
|
||||
| {
|
||||
actionCacheId: string;
|
||||
actionFootprint: string;
|
||||
}
|
||||
| undefined;
|
||||
}): string {
|
||||
const { preCacheTransform } = params;
|
||||
|
||||
// Parse the URL
|
||||
const url = new URL(params.url);
|
||||
|
||||
// Extract pathname and remove leading slashes
|
||||
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
||||
|
||||
// Optionally, add query parameters replacing special characters
|
||||
if (url.search) {
|
||||
fileName += url.search.replace(/[&=?]/g, "-");
|
||||
}
|
||||
|
||||
// Replace any characters that are not valid in filenames
|
||||
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
|
||||
// Trim or pad the fileName to a specific length
|
||||
fileName = fileName.substring(0, 50);
|
||||
|
||||
add_pre_cache_transform: {
|
||||
if (preCacheTransform === undefined) {
|
||||
break add_pre_cache_transform;
|
||||
}
|
||||
|
||||
// Sanitize actionCacheId the same way as other components
|
||||
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
||||
|
||||
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadAndUnzip } from "./downloadAndUnzip";
|
||||
import { type BuildOptions } from "./buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
import { lastKeycloakVersionWithAccountV1 } from "../shared/constants";
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
|
||||
const { keycloakVersion, destDirPath, buildOptions } = params;
|
||||
|
||||
await downloadAndUnzip({
|
||||
destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
||||
buildOptions,
|
||||
"preCacheTransform": {
|
||||
"actionCacheId": "npm install and build",
|
||||
"action": async ({ destDirPath }) => {
|
||||
install_common_node_modules: {
|
||||
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
|
||||
|
||||
if (!fs.existsSync(commonResourcesDirPath)) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
if (fs.existsSync(pathJoin(commonResourcesDirPath, "node_modules"))) {
|
||||
break install_common_node_modules;
|
||||
}
|
||||
|
||||
child_process.execSync("npm install --omit=dev", {
|
||||
"cwd": commonResourcesDirPath,
|
||||
"stdio": "ignore"
|
||||
});
|
||||
}
|
||||
|
||||
repatriate_common_resources_from_base_login_theme: {
|
||||
const baseLoginThemeResourceDir = pathJoin(destDirPath, "base", "login", "resources");
|
||||
|
||||
if (!fs.existsSync(baseLoginThemeResourceDir)) {
|
||||
break repatriate_common_resources_from_base_login_theme;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": baseLoginThemeResourceDir,
|
||||
"destDirPath": pathJoin(destDirPath, "keycloak", "login", "resources")
|
||||
});
|
||||
}
|
||||
|
||||
install_and_move_to_common_resources_generated_in_keycloak_v2: {
|
||||
if (!fs.readFileSync(pathJoin(destDirPath, "keycloak", "login", "theme.properties")).toString("utf8").includes("web_modules")) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
|
||||
|
||||
if (!fs.existsSync(accountV2DirSrcDirPath)) {
|
||||
break install_and_move_to_common_resources_generated_in_keycloak_v2;
|
||||
}
|
||||
|
||||
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
||||
|
||||
if (packageManager === "pnpm") {
|
||||
try {
|
||||
child_process.execSync(`which pnpm`);
|
||||
} catch {
|
||||
console.log(`Installing pnpm globally`);
|
||||
child_process.execSync(`npm install -g pnpm`);
|
||||
}
|
||||
}
|
||||
|
||||
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
|
||||
|
||||
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
|
||||
|
||||
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
|
||||
|
||||
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
|
||||
.replace(`${packageManager} run check-types`, "true")
|
||||
.replace(`${packageManager} run babel`, "true");
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
|
||||
|
||||
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
|
||||
|
||||
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
|
||||
|
||||
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
|
||||
}
|
||||
|
||||
remove_keycloak_v2: {
|
||||
const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
|
||||
|
||||
if (!fs.existsSync(keycloakV2DirPath)) {
|
||||
break remove_keycloak_v2;
|
||||
}
|
||||
|
||||
rmSync(keycloakV2DirPath, { "recursive": true });
|
||||
}
|
||||
|
||||
// Note, this is an optimization for reducing the size of the jar
|
||||
remove_unused_node_modules: {
|
||||
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
|
||||
|
||||
if (!fs.existsSync(nodeModuleDirPath)) {
|
||||
break remove_unused_node_modules;
|
||||
}
|
||||
|
||||
const toDeletePerfixes = [
|
||||
"angular",
|
||||
"bootstrap",
|
||||
"rcue",
|
||||
"font-awesome",
|
||||
"ng-file-upload",
|
||||
pathJoin("patternfly", "dist", "sass"),
|
||||
pathJoin("patternfly", "dist", "less"),
|
||||
pathJoin("patternfly", "dist", "js"),
|
||||
"d3",
|
||||
pathJoin("jquery", "src"),
|
||||
"c3",
|
||||
"core-js",
|
||||
"eonasdan-bootstrap-datetimepicker",
|
||||
"moment",
|
||||
"react",
|
||||
"patternfly-bootstrap-treeview",
|
||||
"popper.js",
|
||||
"tippy.js",
|
||||
"jquery-match-height",
|
||||
"google-code-prettify",
|
||||
"patternfly-bootstrap-combobox",
|
||||
"focus-trap",
|
||||
"tabbable",
|
||||
"scheduler",
|
||||
"@types",
|
||||
"datatables.net",
|
||||
"datatables.net-colreorder",
|
||||
"tslib",
|
||||
"prop-types",
|
||||
"file-selector",
|
||||
"datatables.net-colreorder-bs",
|
||||
"object-assign",
|
||||
"warning",
|
||||
"js-tokens",
|
||||
"loose-envify",
|
||||
"prop-types-extra",
|
||||
"attr-accept",
|
||||
"datatables.net-select",
|
||||
"drmonty-datatables-colvis",
|
||||
"datatables.net-bs",
|
||||
pathJoin("@patternfly", "react"),
|
||||
pathJoin("@patternfly", "patternfly", "docs")
|
||||
];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": nodeModuleDirPath,
|
||||
"destDirPath": nodeModuleDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (fileRelativePath.endsWith(".map")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
|
||||
if (
|
||||
!fileRelativePath.endsWith(".woff2") &&
|
||||
!fileRelativePath.endsWith(".woff") &&
|
||||
!fileRelativePath.endsWith(".ttf")
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Just like node_modules
|
||||
remove_unused_lib: {
|
||||
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
|
||||
|
||||
if (!fs.existsSync(libDirPath)) {
|
||||
break remove_unused_lib;
|
||||
}
|
||||
|
||||
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace"];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": libDirPath,
|
||||
"destDirPath": libDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (fileRelativePath.endsWith(".map")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
{
|
||||
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
|
||||
|
||||
fs.writeFileSync(
|
||||
accountCssFilePath,
|
||||
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
// Note, this is an optimization for reducing the size of the jar,
|
||||
// For this version we know exactly which resources are used.
|
||||
{
|
||||
const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
|
||||
|
||||
const toKeepPrefixes = [
|
||||
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
|
||||
pathJoin("patternfly", "dist", "css", fileBasename)
|
||||
),
|
||||
pathJoin("patternfly", "dist", "fonts")
|
||||
];
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": nodeModulesDirPath,
|
||||
"destDirPath": nodeModulesDirPath,
|
||||
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
|
||||
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
207
src/bin/shared/downloadKeycloakDefaultTheme.ts
Normal file
207
src/bin/shared/downloadKeycloakDefaultTheme.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { type BuildOptions } from "./buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { lastKeycloakVersionWithAccountV1 } from "./constants";
|
||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
import { isInside } from "../tools/isInside";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function downloadKeycloakDefaultTheme(params: {
|
||||
keycloakVersion: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<{ defaultThemeDirPath: string }> {
|
||||
const { keycloakVersion, buildOptions } = params;
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: buildOptions.cacheDirPath,
|
||||
npmWorkspaceRootDirPath: buildOptions.npmWorkspaceRootDirPath,
|
||||
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||
onArchiveFile: async params => {
|
||||
if (!isInside({ dirPath: "theme", filePath: params.fileRelativePath })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = params;
|
||||
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
skip_keycloak_v2: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin("keycloak.v2"),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
break skip_keycloak_v2;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
patch_account_css: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "account", "resources", "css", "account.css")
|
||||
) {
|
||||
break patch_account_css;
|
||||
}
|
||||
|
||||
await writeFile({
|
||||
fileRelativePath,
|
||||
modifiedData: Buffer.from(
|
||||
(await readFile())
|
||||
.toString("utf8")
|
||||
.replace("top: -34px;", "top: -34px !important;"),
|
||||
"utf8"
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_web_modules: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"web_modules"
|
||||
),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
break skip_web_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_unused_node_modules: {
|
||||
const nodeModulesDirPath = pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules"
|
||||
);
|
||||
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: nodeModulesDirPath,
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
break skip_unused_node_modules;
|
||||
}
|
||||
|
||||
const toKeepPrefixes = [
|
||||
...[
|
||||
"patternfly.min.css",
|
||||
"patternfly-additions.min.css",
|
||||
"patternfly-additions.min.css"
|
||||
].map(fileBasename =>
|
||||
pathJoin(
|
||||
nodeModulesDirPath,
|
||||
"patternfly",
|
||||
"dist",
|
||||
"css",
|
||||
fileBasename
|
||||
)
|
||||
),
|
||||
pathJoin(nodeModulesDirPath, "patternfly", "dist", "fonts")
|
||||
];
|
||||
|
||||
if (
|
||||
toKeepPrefixes.find(prefix =>
|
||||
fileRelativePath.startsWith(prefix)
|
||||
) !== undefined
|
||||
) {
|
||||
break skip_unused_node_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
skip_unused_resources: {
|
||||
if (keycloakVersion !== "24.0.4") {
|
||||
break skip_unused_resources;
|
||||
}
|
||||
|
||||
for (const dirBasename of [
|
||||
"@patternfly-v5",
|
||||
"@rollup",
|
||||
"rollup",
|
||||
"react",
|
||||
"react-dom",
|
||||
"shx",
|
||||
".pnpm"
|
||||
]) {
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules",
|
||||
dirBasename
|
||||
),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const dirBasename of ["react", "react-dom"]) {
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"vendor",
|
||||
dirBasename
|
||||
),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isInside({
|
||||
dirPath: pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules",
|
||||
"@patternfly",
|
||||
"react-core"
|
||||
),
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile({ fileRelativePath });
|
||||
}
|
||||
});
|
||||
|
||||
return { defaultThemeDirPath: extractedDirPath };
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadBuiltinKeycloakTheme } from "./downloadBuiltinKeycloakTheme";
|
||||
import {
|
||||
downloadKeycloakDefaultTheme,
|
||||
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakDefaultTheme
|
||||
} from "./downloadKeycloakDefaultTheme";
|
||||
import { resources_common, type ThemeType } from "./constants";
|
||||
import type { BuildOptions } from "./buildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as crypto from "crypto";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakDefaultTheme & {};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
@ -22,28 +21,33 @@ export async function downloadKeycloakStaticResources(params: {
|
||||
}) {
|
||||
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(
|
||||
buildOptions.cacheDirPath,
|
||||
`downloadKeycloakStaticResources_tmp_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
|
||||
);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
const resourcesPath = pathJoin(themeDirPath, themeType, "resources");
|
||||
const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources");
|
||||
|
||||
repatriate_base_resources: {
|
||||
const srcDirPath = pathJoin(defaultThemeDirPath, "base", themeType, "resources");
|
||||
|
||||
if (!(await existsAsync(srcDirPath))) {
|
||||
break repatriate_base_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath,
|
||||
destDirPath: resourcesDirPath
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||
"destDirPath": resourcesPath
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", themeType, "resources"),
|
||||
destDirPath: resourcesDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(resourcesPath, resources_common)
|
||||
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(resourcesDirPath, resources_common)
|
||||
});
|
||||
|
||||
rmSync(tmpDirPath, { "recursive": true });
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||
|
||||
export function getJarFileBasename(params: { keycloakVersionRange: KeycloakVersionRange }) {
|
||||
export function getJarFileBasename(params: {
|
||||
keycloakVersionRange: KeycloakVersionRange;
|
||||
}) {
|
||||
const { keycloakVersionRange } = params;
|
||||
|
||||
const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`;
|
||||
|
@ -12,7 +12,10 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
|
||||
const srcDirPath = pathJoin(reactAppRootDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
|
||||
const themeSrcDirPath: string | undefined = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of themeSrcDirBasenames) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
@ -32,7 +35,7 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
||||
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
return { "themeSrcDirPath": srcDirPath };
|
||||
return { themeSrcDirPath: srcDirPath };
|
||||
}
|
||||
|
||||
console.error(
|
||||
|
@ -6,29 +6,56 @@ export type MetaInfKeycloakTheme = {
|
||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
||||
};
|
||||
|
||||
export function getMetaInfKeycloakThemesJsonPath(params: { keycloakifyBuildDirPath: string }) {
|
||||
export function getMetaInfKeycloakThemesJsonFilePath(params: {
|
||||
keycloakifyBuildDirPath: string;
|
||||
}) {
|
||||
const { keycloakifyBuildDirPath } = params;
|
||||
|
||||
return pathJoin(keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
|
||||
return pathJoin(
|
||||
keycloakifyBuildDirPath === "." ? "" : keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"META-INF",
|
||||
"keycloak-themes.json"
|
||||
);
|
||||
}
|
||||
|
||||
export function readMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string }): MetaInfKeycloakTheme {
|
||||
export function readMetaInfKeycloakThemes(params: {
|
||||
keycloakifyBuildDirPath: string;
|
||||
}): MetaInfKeycloakTheme {
|
||||
const { keycloakifyBuildDirPath } = params;
|
||||
|
||||
return JSON.parse(fs.readFileSync(getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath })).toString("utf8")) as MetaInfKeycloakTheme;
|
||||
return JSON.parse(
|
||||
fs
|
||||
.readFileSync(
|
||||
getMetaInfKeycloakThemesJsonFilePath({
|
||||
keycloakifyBuildDirPath
|
||||
})
|
||||
)
|
||||
.toString("utf8")
|
||||
) as MetaInfKeycloakTheme;
|
||||
}
|
||||
|
||||
export function writeMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string; metaInfKeycloakThemes: MetaInfKeycloakTheme }) {
|
||||
export function writeMetaInfKeycloakThemes(params: {
|
||||
keycloakifyBuildDirPath: string;
|
||||
metaInfKeycloakThemes: MetaInfKeycloakTheme;
|
||||
}) {
|
||||
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
|
||||
|
||||
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath });
|
||||
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
|
||||
keycloakifyBuildDirPath
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { "recursive": true });
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(metaInfKeycloakThemesJsonPath, Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8"));
|
||||
fs.writeFileSync(
|
||||
metaInfKeycloakThemesJsonPath,
|
||||
Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8")
|
||||
);
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
export const loginThemePageIds = [
|
||||
"login.ftl",
|
||||
"login-username.ftl",
|
||||
"login-password.ftl",
|
||||
"webauthn-authenticate.ftl",
|
||||
"webauthn-register.ftl",
|
||||
"register.ftl",
|
||||
"info.ftl",
|
||||
"error.ftl",
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-oauth2-device-verify-user-code.ftl",
|
||||
"login-oauth-grant.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-idp-link-email.ftl",
|
||||
"login-page-expired.ftl",
|
||||
"login-config-totp.ftl",
|
||||
"logout-confirm.ftl",
|
||||
"idp-review-user-profile.ftl",
|
||||
"update-email.ftl",
|
||||
"select-authenticator.ftl",
|
||||
"saml-post-form.ftl",
|
||||
"delete-credential.ftl",
|
||||
"code.ftl",
|
||||
"delete-account-confirm.ftl",
|
||||
"frontchannel-logout.ftl",
|
||||
"login-recovery-authn-code-config.ftl",
|
||||
"login-recovery-authn-code-input.ftl",
|
||||
"login-reset-otp.ftl",
|
||||
"login-x509-info.ftl",
|
||||
"webauthn-error.ftl"
|
||||
] as const;
|
||||
|
||||
export const accountThemePageIds = [
|
||||
"password.ftl",
|
||||
"account.ftl",
|
||||
"sessions.ftl",
|
||||
"totp.ftl",
|
||||
"applications.ftl",
|
||||
"log.ftl",
|
||||
"federatedIdentity.ftl"
|
||||
] as const;
|
||||
|
||||
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
@ -1,65 +1,121 @@
|
||||
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import cliSelect from "cli-select";
|
||||
import { lastKeycloakVersionWithAccountV1 } from "./constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as fs from "fs";
|
||||
import type { ReturnType } from "tsafe";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export async function promptKeycloakVersion(params: { startingFromMajor: number | undefined }) {
|
||||
const { startingFromMajor } = params;
|
||||
export async function promptKeycloakVersion(params: {
|
||||
startingFromMajor: number | undefined;
|
||||
cacheDirPath: string;
|
||||
}) {
|
||||
const { startingFromMajor, cacheDirPath } = params;
|
||||
|
||||
const { getLatestsSemVersionedTag } = (() => {
|
||||
const { octokit } = (() => {
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
|
||||
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
|
||||
const octokit = new Octokit(
|
||||
githubToken === undefined ? undefined : { auth: githubToken }
|
||||
);
|
||||
|
||||
return { octokit };
|
||||
})();
|
||||
|
||||
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
|
||||
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
|
||||
octokit
|
||||
});
|
||||
|
||||
return { getLatestsSemVersionedTag };
|
||||
})();
|
||||
|
||||
console.log("Select Keycloak version");
|
||||
|
||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
||||
|
||||
(
|
||||
await getLatestsSemVersionedTag({
|
||||
"count": 50,
|
||||
"owner": "keycloak",
|
||||
"repo": "keycloak"
|
||||
})
|
||||
).forEach(semVersionedTag => {
|
||||
if (startingFromMajor !== undefined && semVersionedTag.version.major < startingFromMajor) {
|
||||
const semVersionedTags = await (async () => {
|
||||
const cacheFilePath = pathJoin(cacheDirPath, "keycloak-versions.json");
|
||||
|
||||
type Cache = {
|
||||
time: number;
|
||||
semVersionedTags: ReturnType<typeof getLatestsSemVersionedTag>;
|
||||
};
|
||||
|
||||
use_cache: {
|
||||
if (!fs.existsSync(cacheFilePath)) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
const cache: Cache = JSON.parse(
|
||||
fs.readFileSync(cacheFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
if (Date.now() - cache.time > 3_600_000) {
|
||||
fs.unlinkSync(cacheFilePath);
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return cache.semVersionedTags;
|
||||
}
|
||||
|
||||
const semVersionedTags = await getLatestsSemVersionedTag({
|
||||
count: 50,
|
||||
owner: "keycloak",
|
||||
repo: "keycloak"
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(cacheFilePath);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
cacheFilePath,
|
||||
JSON.stringify(
|
||||
id<Cache>({
|
||||
time: Date.now(),
|
||||
semVersionedTags
|
||||
}),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
return semVersionedTags;
|
||||
})();
|
||||
|
||||
semVersionedTags.forEach(semVersionedTag => {
|
||||
if (
|
||||
startingFromMajor !== undefined &&
|
||||
semVersionedTag.version.major < startingFromMajor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSemVersionedTag = semVersionedTagByMajor.get(semVersionedTag.version.major);
|
||||
const currentSemVersionedTag = semVersionedTagByMajor.get(
|
||||
semVersionedTag.version.major
|
||||
);
|
||||
|
||||
if (currentSemVersionedTag !== undefined && SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1) {
|
||||
if (
|
||||
currentSemVersionedTag !== undefined &&
|
||||
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
|
||||
});
|
||||
|
||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(({ tag: version }) => {
|
||||
let out = version;
|
||||
|
||||
if (version === lastKeycloakVersionWithAccountV1) {
|
||||
out += " (last version with account v1 built in)";
|
||||
}
|
||||
|
||||
return out;
|
||||
});
|
||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
||||
({ tag }) => tag
|
||||
);
|
||||
|
||||
const { value } = await cliSelect<string>({
|
||||
"values": lastMajorVersions
|
||||
values: lastMajorVersions
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
|
@ -1,141 +0,0 @@
|
||||
import { readBuildOptions } from "./shared/buildOptions";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { readMetaInfKeycloakThemes } from "./shared/metaInfKeycloakThemes";
|
||||
import { accountV1ThemeName } from "./shared/constants";
|
||||
import { SemVer } from "./tools/SemVer";
|
||||
import type { KeycloakVersionRange } from "./shared/KeycloakVersionRange";
|
||||
import { getJarFileBasename } from "./shared/getJarFileBasename";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, posix as pathPosix } from "path";
|
||||
import * as child_process from "child_process";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
|
||||
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
||||
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
|
||||
});
|
||||
|
||||
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(({ name }) => name === accountV1ThemeName);
|
||||
|
||||
const { keycloakVersion, keycloakMajorNumber } = await (async function getKeycloakMajor(): Promise<{
|
||||
keycloakVersion: string;
|
||||
keycloakMajorNumber: number;
|
||||
}> {
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
"startingFromMajor": 17
|
||||
});
|
||||
|
||||
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
|
||||
|
||||
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
|
||||
console.log([
|
||||
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
|
||||
"Please select any other Keycloak version"
|
||||
]);
|
||||
return getKeycloakMajor();
|
||||
}
|
||||
|
||||
return { keycloakVersion, keycloakMajorNumber };
|
||||
})();
|
||||
|
||||
const keycloakVersionRange: KeycloakVersionRange = (() => {
|
||||
if (doesImplementAccountTheme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (keycloakMajorNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
assert(keycloakMajorNumber !== 22);
|
||||
|
||||
if (keycloakMajorNumber === 23) {
|
||||
return "23" as const;
|
||||
}
|
||||
|
||||
return "24-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (keycloakMajorNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
return "22-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme>>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
}
|
||||
})();
|
||||
|
||||
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
|
||||
|
||||
const mountTargets = buildOptions.themeNames
|
||||
.map(themeName => {
|
||||
const themeEntry = metaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
|
||||
|
||||
assert(themeEntry !== undefined);
|
||||
|
||||
return themeEntry.types
|
||||
.map(themeType => {
|
||||
const localPathDirname = pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
themeType
|
||||
);
|
||||
|
||||
return fs
|
||||
.readdirSync(localPathDirname)
|
||||
.filter(fileOrDirectoryBasename => !fileOrDirectoryBasename.endsWith(".properties"))
|
||||
.map(fileOrDirectoryBasename => ({
|
||||
"localPath": pathJoin(localPathDirname, fileOrDirectoryBasename),
|
||||
"containerPath": pathPosix.join("/", "opt", "keycloak", "themes", themeName, themeType, fileOrDirectoryBasename)
|
||||
}));
|
||||
})
|
||||
.flat();
|
||||
})
|
||||
.flat();
|
||||
|
||||
const containerName = "keycloak-keycloakify";
|
||||
|
||||
try {
|
||||
child_process.execSync(`docker rm ${containerName}`, { "stdio": "ignore" });
|
||||
} catch {}
|
||||
|
||||
const child = child_process.spawn(
|
||||
"docker",
|
||||
[
|
||||
"run",
|
||||
...["-p", "8080:8080"],
|
||||
...["--name", containerName],
|
||||
...["-e", "KEYCLOAK_ADMIN=admin"],
|
||||
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
|
||||
...["-v", `"${pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)}":"/opt/keycloak/providers/keycloak-theme.jar"`],
|
||||
...(keycloakMajorNumber <= 20 ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] : []),
|
||||
...mountTargets.map(({ localPath, containerPath }) => ["-v", `"${localPath}":"${containerPath}":rw`]).flat(),
|
||||
...["-it", `quay.io/keycloak/keycloak:${keycloakVersion}`],
|
||||
"start-dev",
|
||||
...(21 <= keycloakMajorNumber && keycloakMajorNumber < 24 ? ["--features=declarative-user-profile"] : [])
|
||||
],
|
||||
{
|
||||
"cwd": buildOptions.keycloakifyBuildDirPath
|
||||
}
|
||||
);
|
||||
|
||||
child.stdout.on("data", data => console.log(data.toString("utf8")));
|
||||
|
||||
child.stderr.on("data", data => console.error(data.toString("utf8")));
|
||||
}
|
129
src/bin/start-keycloak/appBuild.ts
Normal file
129
src/bin/start-keycloak/appBuild.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import * as child_process from "child_process";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import type { BuildOptions } from "../shared/buildOptions";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
reactAppRootDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
npmWorkspaceRootDirPath: string;
|
||||
reactAppBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function appBuild(params: {
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildOptions } = params;
|
||||
|
||||
const { bundler } = buildOptions;
|
||||
|
||||
const { command, args, cwd } = (() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return {
|
||||
command: "npx",
|
||||
args: ["vite", "build"],
|
||||
cwd: buildOptions.reactAppRootDirPath
|
||||
};
|
||||
case "webpack": {
|
||||
for (const dirPath of [
|
||||
buildOptions.reactAppRootDirPath,
|
||||
buildOptions.npmWorkspaceRootDirPath
|
||||
]) {
|
||||
try {
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(dirPath, "package.json"))
|
||||
.toString("utf8")
|
||||
);
|
||||
|
||||
const [scriptName] =
|
||||
Object.entries(parsedPackageJson.scripts).find(
|
||||
([, scriptValue]) => {
|
||||
assert(is<string>(scriptValue));
|
||||
if (
|
||||
scriptValue.includes("webpack") &&
|
||||
scriptValue.includes("--mode production")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
scriptValue.includes("react-scripts") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
scriptValue.includes("react-app-rewired") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
scriptValue.includes("craco") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
scriptValue.includes("ng") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
) ?? [];
|
||||
|
||||
if (scriptName === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npm",
|
||||
args: ["run", scriptName],
|
||||
cwd: dirPath
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Keycloakify was unable to determine which script is responsible for building the app."
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||
|
||||
const child = child_process.spawn(command, args, { cwd });
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
if (data.toString("utf8").includes("gzip:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 }));
|
||||
|
||||
const { isSuccess } = await dResult.pr;
|
||||
|
||||
return { isAppBuildSuccess: isSuccess };
|
||||
}
|
1
src/bin/start-keycloak/index.ts
Normal file
1
src/bin/start-keycloak/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./start-keycloak";
|
41
src/bin/start-keycloak/keycloakifyBuild.ts
Normal file
41
src/bin/start-keycloak/keycloakifyBuild.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { skipBuildJarsEnvName } from "../shared/constants";
|
||||
import * as child_process from "child_process";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildOptions } from "../shared/buildOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
reactAppRootDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function keycloakifyBuild(params: {
|
||||
doSkipBuildJars: boolean;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
|
||||
const { buildOptions, doSkipBuildJars } = params;
|
||||
|
||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||
|
||||
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
||||
cwd: buildOptions.reactAppRootDirPath,
|
||||
env: {
|
||||
...process.env,
|
||||
...(doSkipBuildJars ? { [skipBuildJarsEnvName]: "true" } : {})
|
||||
}
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 }));
|
||||
|
||||
const { isSuccess } = await dResult.pr;
|
||||
|
||||
return { isKeycloakifyBuildSuccess: isSuccess };
|
||||
}
|
2142
src/bin/start-keycloak/myrealm-realm-23.json
Normal file
2142
src/bin/start-keycloak/myrealm-realm-23.json
Normal file
File diff suppressed because it is too large
Load Diff
2318
src/bin/start-keycloak/myrealm-realm-24.json
Normal file
2318
src/bin/start-keycloak/myrealm-realm-24.json
Normal file
File diff suppressed because it is too large
Load Diff
480
src/bin/start-keycloak/start-keycloak.ts
Normal file
480
src/bin/start-keycloak/start-keycloak.ts
Normal file
@ -0,0 +1,480 @@
|
||||
import { readBuildOptions } from "../shared/buildOptions";
|
||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
|
||||
import { accountV1ThemeName, containerName } from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
|
||||
import { getJarFileBasename } from "../shared/getJarFileBasename";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
import chokidar from "chokidar";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import cliSelect from "cli-select";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { extractArchive } from "../tools/extractArchive";
|
||||
import { appBuild } from "./appBuild";
|
||||
import { keycloakifyBuild } from "./keycloakifyBuild";
|
||||
|
||||
export type CliCommandOptions = CliCommandOptions_common & {
|
||||
port: number;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
exit_if_docker_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
||||
try {
|
||||
commandOutput = child_process.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
});
|
||||
} catch {}
|
||||
|
||||
if (commandOutput?.toString("utf8").includes("Docker")) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
|
||||
console.log(
|
||||
[
|
||||
`${chalk.red("Docker required.")}`,
|
||||
`Install it with Docker Desktop: ${chalk.bold.underline(
|
||||
"https://www.docker.com/products/docker-desktop/"
|
||||
)}`,
|
||||
`(or any other way)`
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
exit_if_docker_not_running: {
|
||||
let isDockerRunning: boolean;
|
||||
|
||||
try {
|
||||
child_process.execSync("docker info", { stdio: "ignore" });
|
||||
isDockerRunning = true;
|
||||
} catch {
|
||||
isDockerRunning = false;
|
||||
}
|
||||
|
||||
if (isDockerRunning) {
|
||||
break exit_if_docker_not_running;
|
||||
}
|
||||
|
||||
console.log(
|
||||
[
|
||||
`${chalk.red("Docker daemon is not running.")}`,
|
||||
`Please start Docker Desktop and try again.`
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
||||
|
||||
{
|
||||
const { isAppBuildSuccess } = await appBuild({
|
||||
buildOptions
|
||||
});
|
||||
|
||||
if (!isAppBuildSuccess) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`App build failed, exiting. Try running 'yarn build-keycloak-theme' and see what's wrong.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
||||
doSkipBuildJars: false,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
if (!isKeycloakifyBuildSuccess) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`Keycloakify build failed, exiting. Try running 'yarn build-keycloak-theme' and see what's wrong.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
||||
});
|
||||
|
||||
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(
|
||||
({ name }) => name === accountV1ThemeName
|
||||
);
|
||||
|
||||
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
|
||||
await (async function getKeycloakMajor(): Promise<{
|
||||
keycloakVersion: string;
|
||||
keycloakMajorNumber: number;
|
||||
}> {
|
||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||
return {
|
||||
keycloakVersion: cliCommandOptions.keycloakVersion,
|
||||
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion)
|
||||
.major
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.cyan("On which version of Keycloak do you want to test your theme?")
|
||||
);
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
startingFromMajor: 17,
|
||||
cacheDirPath: buildOptions.cacheDirPath
|
||||
});
|
||||
|
||||
console.log(`→ ${keycloakVersion}`);
|
||||
|
||||
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
|
||||
|
||||
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
|
||||
console.log(
|
||||
[
|
||||
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
|
||||
"Please select any other Keycloak version"
|
||||
].join(" ")
|
||||
);
|
||||
return getKeycloakMajor();
|
||||
}
|
||||
|
||||
return { keycloakVersion, keycloakMajorNumber };
|
||||
})();
|
||||
|
||||
const keycloakVersionRange: KeycloakVersionRange = (() => {
|
||||
if (doesImplementAccountTheme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (keycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
assert(keycloakMajorVersionNumber !== 22);
|
||||
|
||||
if (keycloakMajorVersionNumber === 23) {
|
||||
return "23" as const;
|
||||
}
|
||||
|
||||
return "24-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
} else {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (keycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
}
|
||||
|
||||
return "22-and-above" as const;
|
||||
})();
|
||||
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountTheme
|
||||
>
|
||||
>();
|
||||
|
||||
return keycloakVersionRange;
|
||||
}
|
||||
})();
|
||||
|
||||
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
|
||||
|
||||
console.log(`Using Keycloak ${chalk.bold(jarFileBasename)}`);
|
||||
|
||||
const realmJsonFilePath = await (async () => {
|
||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
|
||||
)
|
||||
);
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
}
|
||||
|
||||
const dirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak"
|
||||
);
|
||||
|
||||
const filePath = pathJoin(
|
||||
dirPath,
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${chalk.yellow(
|
||||
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
|
||||
)}`
|
||||
);
|
||||
|
||||
console.log(chalk.cyan("Select what configuration to use:"));
|
||||
|
||||
const { value } = await cliSelect<string>({
|
||||
values: [
|
||||
...fs
|
||||
.readdirSync(dirPath)
|
||||
.filter(fileBasename => fileBasename.endsWith(".json")),
|
||||
"none"
|
||||
]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
if (value === "none") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return pathJoin(dirPath, value);
|
||||
})();
|
||||
|
||||
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename);
|
||||
|
||||
const { doUseBuiltInAccountV1Theme } = await (async () => {
|
||||
let doUseBuiltInAccountV1Theme = false;
|
||||
|
||||
await extractArchive({
|
||||
archiveFilePath: jarFilePath,
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
|
||||
for (const themeName of buildOptions.themeNames) {
|
||||
if (
|
||||
relativeFilePathInArchive ===
|
||||
pathJoin("theme", themeName, "account", "theme.properties")
|
||||
) {
|
||||
if (
|
||||
(await readFile())
|
||||
.toString("utf8")
|
||||
.includes("parent=keycloak")
|
||||
) {
|
||||
doUseBuiltInAccountV1Theme = true;
|
||||
}
|
||||
|
||||
earlyExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { doUseBuiltInAccountV1Theme };
|
||||
})();
|
||||
|
||||
const accountThemePropertyPatch = !doUseBuiltInAccountV1Theme
|
||||
? undefined
|
||||
: () => {
|
||||
for (const themeName of buildOptions.themeNames) {
|
||||
const filePath = pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
"account",
|
||||
"theme.properties"
|
||||
);
|
||||
|
||||
const sourceCode = fs.readFileSync(filePath);
|
||||
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
||||
|
||||
fs.writeFileSync(filePath, modifiedSourceCode);
|
||||
}
|
||||
};
|
||||
|
||||
accountThemePropertyPatch?.();
|
||||
|
||||
try {
|
||||
child_process.execSync(`docker rm --force ${containerName}`, {
|
||||
stdio: "ignore"
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const spawnArgs = [
|
||||
"docker",
|
||||
[
|
||||
"run",
|
||||
...["-p", `${cliCommandOptions.port}:8080`],
|
||||
...["--name", containerName],
|
||||
...["-e", "KEYCLOAK_ADMIN=admin"],
|
||||
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
|
||||
...(realmJsonFilePath === undefined
|
||||
? []
|
||||
: [
|
||||
"-v",
|
||||
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
|
||||
]),
|
||||
...["-v", `${jarFilePath}:/opt/keycloak/providers/keycloak-theme.jar`],
|
||||
...(keycloakMajorVersionNumber <= 20
|
||||
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
||||
: []),
|
||||
...[
|
||||
...buildOptions.themeNames,
|
||||
...(doUseBuiltInAccountV1Theme ? [] : [accountV1ThemeName])
|
||||
]
|
||||
.map(themeName => ({
|
||||
localDirPath: pathJoin(
|
||||
buildOptions.keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName
|
||||
),
|
||||
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
||||
}))
|
||||
.map(({ localDirPath, containerDirPath }) => [
|
||||
"-v",
|
||||
`${localDirPath}:${containerDirPath}:rw`
|
||||
])
|
||||
.flat(),
|
||||
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
||||
"start-dev",
|
||||
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
||||
? ["--features=declarative-user-profile"]
|
||||
: []),
|
||||
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
|
||||
],
|
||||
{
|
||||
cwd: buildOptions.keycloakifyBuildDirPath
|
||||
}
|
||||
] as const;
|
||||
|
||||
const child = child_process.spawn(...spawnArgs);
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", process.exit);
|
||||
|
||||
const srcDirPath = pathJoin(buildOptions.reactAppRootDirPath, "src");
|
||||
|
||||
{
|
||||
const handler = async (data: Buffer) => {
|
||||
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
|
||||
return;
|
||||
}
|
||||
|
||||
child.stdout.off("data", handler);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1_000));
|
||||
|
||||
console.log(
|
||||
[
|
||||
"",
|
||||
`Keycloak Admin console: ${chalk.cyan.bold(
|
||||
`http://localhost:${cliCommandOptions.port}`
|
||||
)}`,
|
||||
`- user: ${chalk.cyan.bold("admin")}`,
|
||||
`- password: ${chalk.cyan.bold("admin")}`,
|
||||
"",
|
||||
"",
|
||||
`${chalk.green("Your theme is accessible at:")}`,
|
||||
`${chalk.green("➜")} ${chalk.cyan.bold(
|
||||
`https://my-theme.keycloakify.dev${cliCommandOptions.port === 8080 ? "" : `?port=${cliCommandOptions.port}`}`
|
||||
)}`,
|
||||
"",
|
||||
"You can login with the following credentials:",
|
||||
`- username: ${chalk.cyan.bold("testuser")}`,
|
||||
`- password: ${chalk.cyan.bold("password123")}`,
|
||||
"",
|
||||
`Watching for changes in ${chalk.bold(
|
||||
`.${pathSep}${pathRelative(process.cwd(), buildOptions.reactAppRootDirPath)}`
|
||||
)}`
|
||||
].join("\n")
|
||||
);
|
||||
};
|
||||
|
||||
child.stdout.on("data", handler);
|
||||
}
|
||||
|
||||
{
|
||||
const runFullBuild = runExclusive.build(async () => {
|
||||
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
||||
|
||||
const { isAppBuildSuccess } = await appBuild({
|
||||
buildOptions
|
||||
});
|
||||
|
||||
if (!isAppBuildSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
||||
doSkipBuildJars: true,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
if (!isKeycloakifyBuildSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountThemePropertyPatch?.();
|
||||
|
||||
console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
|
||||
});
|
||||
|
||||
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
|
||||
|
||||
chokidar
|
||||
.watch(
|
||||
[
|
||||
srcDirPath,
|
||||
buildOptions.publicDirPath,
|
||||
pathJoin(buildOptions.reactAppRootDirPath, "package.json"),
|
||||
pathJoin(buildOptions.reactAppRootDirPath, "vite.config.ts"),
|
||||
pathJoin(buildOptions.reactAppRootDirPath, "vite.config.js"),
|
||||
pathJoin(buildOptions.reactAppRootDirPath, "index.html"),
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src")
|
||||
],
|
||||
{
|
||||
ignoreInitial: true
|
||||
}
|
||||
)
|
||||
.on("all", async (...[, filePath]) => {
|
||||
console.log(`Detected changes in ${filePath}`);
|
||||
|
||||
await waitForDebounce();
|
||||
|
||||
runFullBuild();
|
||||
});
|
||||
}
|
||||
}
|
@ -12,35 +12,39 @@ export namespace SemVer {
|
||||
export type BumpType = (typeof bumpTypes)[number];
|
||||
|
||||
export function parse(versionStr: string): SemVer {
|
||||
const match = versionStr.match(/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/);
|
||||
const match = versionStr.match(
|
||||
/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`${versionStr} is not a valid semantic version`);
|
||||
}
|
||||
|
||||
const semVer: Omit<SemVer, "parsedFrom"> = {
|
||||
"major": parseInt(match[1]),
|
||||
"minor": parseInt(match[2]),
|
||||
"patch": (() => {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: (() => {
|
||||
const str = match[3];
|
||||
|
||||
return str === undefined ? 0 : parseInt(str);
|
||||
})(),
|
||||
...(() => {
|
||||
const str = match[4];
|
||||
return str === undefined ? {} : { "rc": parseInt(str) };
|
||||
return str === undefined ? {} : { rc: parseInt(str) };
|
||||
})()
|
||||
};
|
||||
|
||||
const initialStr = stringify(semVer);
|
||||
|
||||
Object.defineProperty(semVer, "parsedFrom", {
|
||||
"enumerable": true,
|
||||
"get": function () {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
const currentStr = stringify(this);
|
||||
|
||||
if (currentStr !== initialStr) {
|
||||
throw new Error(`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`);
|
||||
throw new Error(
|
||||
`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`
|
||||
);
|
||||
}
|
||||
|
||||
return versionStr;
|
||||
@ -51,7 +55,9 @@ export namespace SemVer {
|
||||
}
|
||||
|
||||
export function stringify(v: Omit<SemVer, "parsedFrom">): string {
|
||||
return `${v.major}.${v.minor}.${v.patch}${v.rc === undefined ? "" : `-rc.${v.rc}`}`;
|
||||
return `${v.major}.${v.minor}.${v.patch}${
|
||||
v.rc === undefined ? "" : `-rc.${v.rc}`
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,12 +86,25 @@ export namespace SemVer {
|
||||
console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 )
|
||||
*/
|
||||
|
||||
export function bumpType(params: { versionBehind: string | SemVer; versionAhead: string | SemVer }): BumpType | "no bump" {
|
||||
const versionAhead = typeof params.versionAhead === "string" ? parse(params.versionAhead) : params.versionAhead;
|
||||
const versionBehind = typeof params.versionBehind === "string" ? parse(params.versionBehind) : params.versionBehind;
|
||||
export function bumpType(params: {
|
||||
versionBehind: string | SemVer;
|
||||
versionAhead: string | SemVer;
|
||||
}): BumpType | "no bump" {
|
||||
const versionAhead =
|
||||
typeof params.versionAhead === "string"
|
||||
? parse(params.versionAhead)
|
||||
: params.versionAhead;
|
||||
const versionBehind =
|
||||
typeof params.versionBehind === "string"
|
||||
? parse(params.versionBehind)
|
||||
: params.versionBehind;
|
||||
|
||||
if (compare(versionBehind, versionAhead) === 1) {
|
||||
throw new Error(`Version regression ${stringify(versionBehind)} -> ${stringify(versionAhead)}`);
|
||||
throw new Error(
|
||||
`Version regression ${stringify(versionBehind)} -> ${stringify(
|
||||
versionAhead
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const level of ["major", "minor", "patch", "rc"] as const) {
|
||||
|
@ -1,4 +1,8 @@
|
||||
export function replaceAll(string: string, searchValue: string | RegExp, replaceValue: string): string {
|
||||
export function replaceAll(
|
||||
string: string,
|
||||
searchValue: string | RegExp,
|
||||
replaceValue: string
|
||||
): string {
|
||||
if ((string as any).replaceAll !== undefined) {
|
||||
return (string as any).replaceAll(searchValue, replaceValue);
|
||||
}
|
||||
@ -24,7 +28,10 @@ export function replaceAll(string: string, searchValue: string | RegExp, replace
|
||||
|
||||
// Convert searchValue to string if it's not a string or RegExp
|
||||
var searchString = String(searchValue);
|
||||
var regexFromString = new RegExp(searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
||||
var regexFromString = new RegExp(
|
||||
searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
||||
"g"
|
||||
);
|
||||
|
||||
return string.replace(regexFromString, replaceValue);
|
||||
}
|
||||
|
@ -16,7 +16,10 @@ const crawlRec = (dirPath: string, filePaths: string[]) => {
|
||||
};
|
||||
|
||||
/** List all files in a given directory return paths relative to the dir_path */
|
||||
export function crawl(params: { dirPath: string; returnedPathsType: "absolute" | "relative to dirPath" }): string[] {
|
||||
export function crawl(params: {
|
||||
dirPath: string;
|
||||
returnedPathsType: "absolute" | "relative to dirPath";
|
||||
}): string[] {
|
||||
const { dirPath, returnedPathsType } = params;
|
||||
|
||||
const filePaths: string[] = [];
|
||||
|
@ -1,27 +1,42 @@
|
||||
import { Readable } from "stream";
|
||||
|
||||
const crc32tab = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
||||
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
||||
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
||||
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
||||
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
||||
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
|
||||
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
|
||||
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
|
||||
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
||||
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
|
||||
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
|
||||
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
|
||||
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
|
||||
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
|
||||
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
|
||||
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
|
||||
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
|
||||
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
|
||||
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
|
||||
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
|
||||
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
|
||||
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
|
||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||
];
|
||||
|
||||
@ -33,11 +48,13 @@ const crc32tab = [
|
||||
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||
if (typeof input === "string") {
|
||||
let crc = ~0;
|
||||
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||
for (let i = 0; i < input.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||
return Promise.resolve((crc ^ -1) >>> 0);
|
||||
} else if (input instanceof Buffer) {
|
||||
let crc = ~0;
|
||||
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||
for (let i = 0; i < input.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||
return Promise.resolve((crc ^ -1) >>> 0);
|
||||
} else if (input instanceof Readable) {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
@ -46,7 +63,8 @@ export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
||||
input.on("error", e => reject(e));
|
||||
input.on("data", (chunk: Buffer) => {
|
||||
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||
for (let i = 0; i < chunk.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
@ -0,0 +1,262 @@
|
||||
import fetch from "make-fetch-happen";
|
||||
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
|
||||
import { dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { extractArchive } from "../extractArchive";
|
||||
import { existsAsync } from "../fs.existsAsync";
|
||||
import { getProxyFetchOptions } from "./fetchProxyOptions";
|
||||
import * as crypto from "crypto";
|
||||
import { rm } from "../fs.rm";
|
||||
|
||||
export async function downloadAndExtractArchive(params: {
|
||||
url: string;
|
||||
uniqueIdOfOnOnArchiveFile: string;
|
||||
onArchiveFile: (params: {
|
||||
fileRelativePath: string;
|
||||
readFile: () => Promise<Buffer>;
|
||||
writeFile: (params: {
|
||||
fileRelativePath: string;
|
||||
modifiedData?: Buffer;
|
||||
}) => Promise<void>;
|
||||
}) => Promise<void>;
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
}): Promise<{ extractedDirPath: string }> {
|
||||
const {
|
||||
url,
|
||||
uniqueIdOfOnOnArchiveFile,
|
||||
onArchiveFile,
|
||||
cacheDirPath,
|
||||
npmWorkspaceRootDirPath
|
||||
} = params;
|
||||
|
||||
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
|
||||
|
||||
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
|
||||
|
||||
download: {
|
||||
if (await existsAsync(archiveFilePath)) {
|
||||
const isDownloaded = await SuccessTracker.getIsDownloaded({
|
||||
cacheDirPath,
|
||||
archiveFileBasename
|
||||
});
|
||||
|
||||
if (isDownloaded) {
|
||||
break download;
|
||||
}
|
||||
|
||||
await unlink(archiveFilePath);
|
||||
|
||||
await SuccessTracker.removeFromDownloaded({
|
||||
cacheDirPath,
|
||||
archiveFileBasename
|
||||
});
|
||||
}
|
||||
|
||||
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
||||
|
||||
const response = await fetch(
|
||||
url,
|
||||
await getProxyFetchOptions({ npmWorkspaceRootDirPath })
|
||||
);
|
||||
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
|
||||
await writeFile(archiveFilePath, response.body);
|
||||
|
||||
await SuccessTracker.markAsDownloaded({
|
||||
cacheDirPath,
|
||||
archiveFileBasename
|
||||
});
|
||||
}
|
||||
|
||||
const extractDirBasename = `${archiveFileBasename.split(".")[0]}_${uniqueIdOfOnOnArchiveFile}_${crypto
|
||||
.createHash("sha256")
|
||||
.update(onArchiveFile.toString())
|
||||
.digest("hex")
|
||||
.substring(0, 5)}`;
|
||||
|
||||
await Promise.all(
|
||||
(await readdir(cacheDirPath))
|
||||
.filter(
|
||||
(() => {
|
||||
const prefix = extractDirBasename
|
||||
.split("_")
|
||||
.reverse()
|
||||
.slice(1)
|
||||
.reverse()
|
||||
.join("_");
|
||||
|
||||
return basename =>
|
||||
basename !== extractDirBasename && basename.startsWith(prefix);
|
||||
})()
|
||||
)
|
||||
.map(async extractDirBasename => {
|
||||
await rm(pathJoin(cacheDirPath, extractDirBasename), { recursive: true });
|
||||
await SuccessTracker.removeFromExtracted({
|
||||
cacheDirPath,
|
||||
extractDirBasename
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const extractedDirPath = pathJoin(cacheDirPath, extractDirBasename);
|
||||
|
||||
extract_and_transform: {
|
||||
if (await existsAsync(extractedDirPath)) {
|
||||
const isExtracted = await SuccessTracker.getIsExtracted({
|
||||
cacheDirPath,
|
||||
extractDirBasename
|
||||
});
|
||||
|
||||
if (isExtracted) {
|
||||
break extract_and_transform;
|
||||
}
|
||||
|
||||
await rm(extractedDirPath, { recursive: true });
|
||||
|
||||
await SuccessTracker.removeFromExtracted({
|
||||
cacheDirPath,
|
||||
extractDirBasename
|
||||
});
|
||||
}
|
||||
|
||||
await extractArchive({
|
||||
archiveFilePath,
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, readFile, writeFile }) =>
|
||||
onArchiveFile({
|
||||
fileRelativePath: relativeFilePathInArchive,
|
||||
readFile,
|
||||
writeFile: ({ fileRelativePath, modifiedData }) =>
|
||||
writeFile({
|
||||
filePath: pathJoin(extractedDirPath, fileRelativePath),
|
||||
modifiedData
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
await SuccessTracker.markAsExtracted({
|
||||
cacheDirPath,
|
||||
extractDirBasename
|
||||
});
|
||||
}
|
||||
|
||||
return { extractedDirPath };
|
||||
}
|
||||
|
||||
type SuccessTracker = {
|
||||
archiveFileBasenames: string[];
|
||||
extractDirBasenames: string[];
|
||||
};
|
||||
|
||||
namespace SuccessTracker {
|
||||
async function read(params: { cacheDirPath: string }): Promise<SuccessTracker> {
|
||||
const { cacheDirPath } = params;
|
||||
|
||||
const filePath = pathJoin(cacheDirPath, "downloadAndExtractArchive.json");
|
||||
|
||||
if (!(await existsAsync(filePath))) {
|
||||
return { archiveFileBasenames: [], extractDirBasenames: [] };
|
||||
}
|
||||
|
||||
return JSON.parse((await readFile(filePath)).toString("utf8"));
|
||||
}
|
||||
|
||||
async function write(params: {
|
||||
cacheDirPath: string;
|
||||
successTracker: SuccessTracker;
|
||||
}) {
|
||||
const { cacheDirPath, successTracker } = params;
|
||||
|
||||
const filePath = pathJoin(cacheDirPath, "downloadAndExtractArchive.json");
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(filePath);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(filePath, JSON.stringify(successTracker));
|
||||
}
|
||||
|
||||
export async function markAsDownloaded(params: {
|
||||
cacheDirPath: string;
|
||||
archiveFileBasename: string;
|
||||
}) {
|
||||
const { cacheDirPath, archiveFileBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
successTracker.archiveFileBasenames.push(archiveFileBasename);
|
||||
|
||||
await write({ cacheDirPath, successTracker });
|
||||
}
|
||||
|
||||
export async function getIsDownloaded(params: {
|
||||
cacheDirPath: string;
|
||||
archiveFileBasename: string;
|
||||
}): Promise<boolean> {
|
||||
const { cacheDirPath, archiveFileBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
return successTracker.archiveFileBasenames.includes(archiveFileBasename);
|
||||
}
|
||||
|
||||
export async function removeFromDownloaded(params: {
|
||||
cacheDirPath: string;
|
||||
archiveFileBasename: string;
|
||||
}) {
|
||||
const { cacheDirPath, archiveFileBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
successTracker.archiveFileBasenames = successTracker.archiveFileBasenames.filter(
|
||||
basename => basename !== archiveFileBasename
|
||||
);
|
||||
|
||||
await write({ cacheDirPath, successTracker });
|
||||
}
|
||||
|
||||
export async function markAsExtracted(params: {
|
||||
cacheDirPath: string;
|
||||
extractDirBasename: string;
|
||||
}) {
|
||||
const { cacheDirPath, extractDirBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
successTracker.extractDirBasenames.push(extractDirBasename);
|
||||
|
||||
await write({ cacheDirPath, successTracker });
|
||||
}
|
||||
|
||||
export async function getIsExtracted(params: {
|
||||
cacheDirPath: string;
|
||||
extractDirBasename: string;
|
||||
}): Promise<boolean> {
|
||||
const { cacheDirPath, extractDirBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
return successTracker.extractDirBasenames.includes(extractDirBasename);
|
||||
}
|
||||
|
||||
export async function removeFromExtracted(params: {
|
||||
cacheDirPath: string;
|
||||
extractDirBasename: string;
|
||||
}) {
|
||||
const { cacheDirPath, extractDirBasename } = params;
|
||||
|
||||
const successTracker = await read({ cacheDirPath });
|
||||
|
||||
successTracker.extractDirBasenames = successTracker.extractDirBasenames.filter(
|
||||
basename => basename !== extractDirBasename
|
||||
);
|
||||
|
||||
await write({ cacheDirPath, successTracker });
|
||||
}
|
||||
}
|
96
src/bin/tools/downloadAndExtractArchive/fetchProxyOptions.ts
Normal file
96
src/bin/tools/downloadAndExtractArchive/fetchProxyOptions.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { exec as execCallback } from "child_process";
|
||||
import { readFile } from "fs/promises";
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import { promisify } from "util";
|
||||
|
||||
function ensureArray<T>(arg0: T | T[]) {
|
||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||
}
|
||||
|
||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error(
|
||||
"Illegal configuration, expected a single value but found multiple: " +
|
||||
arg0.map(String).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
type NPMConfig = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Get npm configuration as map
|
||||
*/
|
||||
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
const stdout = await exec("npm config get", {
|
||||
encoding: "utf8",
|
||||
cwd: npmWorkspaceRootDirPath
|
||||
}).then(({ stdout }) => stdout);
|
||||
|
||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||
key in cfg
|
||||
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
||||
: { ...cfg, [key]: value };
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2) as [string, string])
|
||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||
}
|
||||
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
|
||||
export async function getProxyFetchOptions(params: {
|
||||
npmWorkspaceRootDirPath: string;
|
||||
}): Promise<ProxyFetchOptions> {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
|
||||
|
||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||
|
||||
function maybeBoolean(arg0: string | undefined) {
|
||||
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
||||
}
|
||||
|
||||
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
||||
const cert = cfg["cert"];
|
||||
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
||||
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
||||
|
||||
if (typeof cafile !== "undefined" && cafile !== "null") {
|
||||
ca.push(
|
||||
...(await (async () => {
|
||||
function chunks<T>(arr: T[], size: number = 2) {
|
||||
return arr
|
||||
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
|
||||
.filter(Boolean) as T[][];
|
||||
}
|
||||
|
||||
const cafileContent = await readFile(cafile, "utf-8");
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
|
||||
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n")
|
||||
);
|
||||
})())
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
proxy,
|
||||
noProxy,
|
||||
strictSSL,
|
||||
cert,
|
||||
ca: ca.length === 0 ? undefined : ca
|
||||
};
|
||||
}
|
1
src/bin/tools/downloadAndExtractArchive/index.ts
Normal file
1
src/bin/tools/downloadAndExtractArchive/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./downloadAndExtractArchive";
|
140
src/bin/tools/extractArchive.ts
Normal file
140
src/bin/tools/extractArchive.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import fs from "fs/promises";
|
||||
import fsSync from "fs";
|
||||
import yauzl from "yauzl";
|
||||
import stream from "stream";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { dirname as pathDirname, sep as pathSep } from "path";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
|
||||
export async function extractArchive(params: {
|
||||
archiveFilePath: string;
|
||||
onArchiveFile: (params: {
|
||||
relativeFilePathInArchive: string;
|
||||
readFile: () => Promise<Buffer>;
|
||||
/** NOTE: Will create the directory if it does not exist */
|
||||
writeFile: (params: { filePath: string; modifiedData?: Buffer }) => Promise<void>;
|
||||
earlyExit: () => void;
|
||||
}) => Promise<void>;
|
||||
}) {
|
||||
const { archiveFilePath, onArchiveFile } = params;
|
||||
|
||||
const zipFile = await new Promise<yauzl.ZipFile>((resolve, reject) => {
|
||||
yauzl.open(archiveFilePath, { lazyEntries: true }, async (error, zipFile) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(zipFile);
|
||||
});
|
||||
});
|
||||
|
||||
const dDone = new Deferred<void>();
|
||||
|
||||
zipFile.once("end", () => {
|
||||
zipFile.close();
|
||||
dDone.resolve();
|
||||
});
|
||||
|
||||
const writeFile = async (
|
||||
entry: yauzl.Entry,
|
||||
params: {
|
||||
filePath: string;
|
||||
modifiedData?: Buffer;
|
||||
}
|
||||
): Promise<void> => {
|
||||
const { filePath, modifiedData } = params;
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(filePath);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiedData !== undefined) {
|
||||
await fs.writeFile(filePath, modifiedData);
|
||||
return;
|
||||
}
|
||||
|
||||
const readStream = await new Promise<stream.Readable>(resolve =>
|
||||
zipFile.openReadStream(entry, async (error, readStream) => {
|
||||
if (error) {
|
||||
dDone.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(readStream);
|
||||
})
|
||||
);
|
||||
|
||||
const dDoneWithFile = new Deferred<void>();
|
||||
|
||||
stream.pipeline(readStream, fsSync.createWriteStream(filePath), error => {
|
||||
if (error) {
|
||||
dDone.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dDoneWithFile.resolve();
|
||||
});
|
||||
|
||||
await dDoneWithFile.pr;
|
||||
};
|
||||
|
||||
const readFile = (entry: yauzl.Entry) =>
|
||||
new Promise<Buffer>(resolve =>
|
||||
zipFile.openReadStream(entry, async (error, readStream) => {
|
||||
if (error) {
|
||||
dDone.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
readStream.on("data", chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
readStream.on("end", () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
|
||||
readStream.on("error", error => {
|
||||
dDone.reject(error);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
zipFile.on("entry", async (entry: yauzl.Entry) => {
|
||||
handle_file: {
|
||||
// NOTE: Skip directories
|
||||
if (entry.fileName.endsWith(pathSep)) {
|
||||
break handle_file;
|
||||
}
|
||||
|
||||
let hasEarlyExitBeenCalled = false;
|
||||
|
||||
await onArchiveFile({
|
||||
relativeFilePathInArchive: entry.fileName.split("/").join(pathSep),
|
||||
readFile: () => readFile(entry),
|
||||
writeFile: params => writeFile(entry, params),
|
||||
earlyExit: () => {
|
||||
hasEarlyExitBeenCalled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEarlyExitBeenCalled) {
|
||||
zipFile.close();
|
||||
dDone.resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.readEntry();
|
||||
});
|
||||
|
||||
zipFile.readEntry();
|
||||
|
||||
await dDone.pr;
|
||||
}
|
@ -11,7 +11,10 @@ function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
|
||||
throw new Error(
|
||||
"Illegal configuration, expected a single value but found multiple: " +
|
||||
arg0.map(String).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
type NPMConfig = Record<string, string | string[]>;
|
||||
@ -24,10 +27,15 @@ async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
|
||||
const stdout = await exec("npm config get", {
|
||||
encoding: "utf8",
|
||||
cwd: npmWorkspaceRootDirPath
|
||||
}).then(({ stdout }) => stdout);
|
||||
|
||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
|
||||
key in cfg
|
||||
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
||||
: { ...cfg, [key]: value };
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
@ -37,9 +45,14 @@ async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||
}
|
||||
|
||||
export type ProxyFetchOptions = Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca">;
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
|
||||
export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise<ProxyFetchOptions> {
|
||||
export async function getProxyFetchOptions(params: {
|
||||
npmWorkspaceRootDirPath: string;
|
||||
}): Promise<ProxyFetchOptions> {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
|
||||
@ -60,14 +73,24 @@ export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: st
|
||||
ca.push(
|
||||
...(await (async () => {
|
||||
function chunks<T>(arr: T[], size: number = 2) {
|
||||
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
|
||||
return arr
|
||||
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
|
||||
.filter(Boolean) as T[][];
|
||||
}
|
||||
|
||||
const cafileContent = await readFile(cafile, "utf-8");
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
|
||||
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n")
|
||||
);
|
||||
})())
|
||||
);
|
||||
}
|
||||
|
||||
return { proxy, noProxy, strictSSL, cert, "ca": ca.length === 0 ? undefined : ca };
|
||||
return {
|
||||
proxy,
|
||||
noProxy,
|
||||
strictSSL,
|
||||
cert,
|
||||
ca: ca.length === 0 ? undefined : ca
|
||||
};
|
||||
}
|
||||
|
@ -1,17 +1,32 @@
|
||||
import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin } from "path";
|
||||
import {
|
||||
isAbsolute as pathIsAbsolute,
|
||||
sep as pathSep,
|
||||
join as pathJoin,
|
||||
resolve as pathResolve
|
||||
} from "path";
|
||||
import * as os from "os";
|
||||
|
||||
export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: string }): string {
|
||||
export function getAbsoluteAndInOsFormatPath(params: {
|
||||
pathIsh: string;
|
||||
cwd: string;
|
||||
}): string {
|
||||
const { pathIsh, cwd } = params;
|
||||
|
||||
let pathOut = pathIsh;
|
||||
|
||||
pathOut = pathOut.replace(/\//g, pathSep);
|
||||
|
||||
if (pathOut.startsWith("~")) {
|
||||
pathOut = pathOut.replace("~", os.homedir());
|
||||
}
|
||||
|
||||
pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut;
|
||||
|
||||
if (!pathIsAbsolute(pathOut)) {
|
||||
pathOut = pathJoin(cwd, pathOut);
|
||||
}
|
||||
|
||||
pathOut = pathResolve(pathOut);
|
||||
|
||||
return pathOut;
|
||||
}
|
||||
|
@ -1,25 +1,71 @@
|
||||
import * as child_process from "child_process";
|
||||
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
|
||||
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
|
||||
const { reactAppRootDirPath } = params;
|
||||
export function getNpmWorkspaceRootDirPath(params: {
|
||||
reactAppRootDirPath: string;
|
||||
dependencyExpected: string;
|
||||
}) {
|
||||
const { reactAppRootDirPath, dependencyExpected } = params;
|
||||
|
||||
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
||||
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
|
||||
const cwd = pathResolve(
|
||||
pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")])
|
||||
);
|
||||
|
||||
assert(cwd !== pathSep, "NPM workspace not found");
|
||||
|
||||
try {
|
||||
child_process.execSync("npm config get", { cwd, "stdio": "ignore" });
|
||||
child_process.execSync("npm config get", {
|
||||
cwd,
|
||||
stdio: "ignore"
|
||||
});
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
assert(cwd !== pathSep, "NPM workspace not found");
|
||||
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const packageJsonFilePath = pathJoin(cwd, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageJsonFilePath)) {
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
assert(fs.existsSync(packageJsonFilePath));
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
let isExpectedDependencyFound = false;
|
||||
|
||||
for (const dependenciesOrDevDependencies of [
|
||||
"dependencies",
|
||||
"devDependencies"
|
||||
] as const) {
|
||||
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
|
||||
|
||||
if (dependencies === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(dependencies instanceof Object);
|
||||
|
||||
if (dependencies[dependencyExpected] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isExpectedDependencyFound = true;
|
||||
}
|
||||
|
||||
if (!isExpectedDependencyFound && parsedPackageJson.name !== dependencyExpected) {
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
return cwd;
|
||||
})(0);
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
type LoggerOpts = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
type Logger = {
|
||||
log: (message: string, opts?: LoggerOpts) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
|
||||
return {
|
||||
log: (message, { force } = {}) => {
|
||||
if (isSilent && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(message);
|
||||
},
|
||||
warn: message => {
|
||||
console.warn(message);
|
||||
},
|
||||
error: message => {
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
};
|
@ -5,7 +5,11 @@ import { SemVer } from "../SemVer";
|
||||
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
|
||||
const { octokit } = params;
|
||||
|
||||
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; count: number }): Promise<
|
||||
async function getLatestsSemVersionedTag(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
count: number;
|
||||
}): Promise<
|
||||
{
|
||||
tag: string;
|
||||
version: SemVer;
|
||||
@ -33,7 +37,9 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
|
||||
semVersionedTags.push({ tag, version });
|
||||
}
|
||||
|
||||
return semVersionedTags.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX)).slice(0, count);
|
||||
return semVersionedTags
|
||||
.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX))
|
||||
.slice(0, count);
|
||||
}
|
||||
|
||||
return { getLatestsSemVersionedTag };
|
||||
|
@ -5,11 +5,19 @@ const per_page = 99;
|
||||
export function listTagsFactory(params: { octokit: Octokit }) {
|
||||
const { octokit } = params;
|
||||
|
||||
const octokit_repo_listTags = async (params: { owner: string; repo: string; per_page: number; page: number }) => {
|
||||
const octokit_repo_listTags = async (params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
per_page: number;
|
||||
page: number;
|
||||
}) => {
|
||||
return octokit.repos.listTags(params);
|
||||
};
|
||||
|
||||
async function* listTags(params: { owner: string; repo: string }): AsyncGenerator<string> {
|
||||
async function* listTags(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
}): AsyncGenerator<string> {
|
||||
const { owner, repo } = params;
|
||||
|
||||
let page = 1;
|
||||
@ -19,7 +27,7 @@ export function listTagsFactory(params: { octokit: Octokit }) {
|
||||
owner,
|
||||
repo,
|
||||
per_page,
|
||||
"page": page++
|
||||
page: page++
|
||||
});
|
||||
|
||||
for (const branch of resp.data.map(({ name }) => name)) {
|
||||
@ -33,7 +41,10 @@ export function listTagsFactory(params: { octokit: Octokit }) {
|
||||
}
|
||||
|
||||
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
|
||||
async function getLatestTag(params: { owner: string; repo: string }): Promise<string | undefined> {
|
||||
async function getLatestTag(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
}): Promise<string | undefined> {
|
||||
const { owner, repo } = params;
|
||||
|
||||
const itRes = await listTags({ owner, repo }).next();
|
||||
|
@ -2,10 +2,19 @@ export type PromiseSettledAndPartitioned<T> = [T[], any[]];
|
||||
|
||||
export function partitionPromiseSettledResults<T>() {
|
||||
return [
|
||||
([successes, failures]: PromiseSettledAndPartitioned<T>, item: PromiseSettledResult<T>) =>
|
||||
(
|
||||
[successes, failures]: PromiseSettledAndPartitioned<T>,
|
||||
item: PromiseSettledResult<T>
|
||||
) =>
|
||||
item.status === "rejected"
|
||||
? ([successes, [item.reason, ...failures]] as PromiseSettledAndPartitioned<T>)
|
||||
: ([[item.value, ...successes], failures] as PromiseSettledAndPartitioned<T>),
|
||||
? ([
|
||||
successes,
|
||||
[item.reason, ...failures]
|
||||
] as PromiseSettledAndPartitioned<T>)
|
||||
: ([
|
||||
[item.value, ...successes],
|
||||
failures
|
||||
] as PromiseSettledAndPartitioned<T>),
|
||||
[[], []] as PromiseSettledAndPartitioned<T>
|
||||
] as const;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user