Compare commits

..

207 Commits

Author SHA1 Message Date
82f34c38f6 Bump version 2024-02-01 06:22:54 +01:00
694b4c8027 Reintroduce doBuildRetrocompatAccountTheme (for now) and fix multiple things in the account default theme 2024-02-01 06:22:33 +01:00
33b7bb6184 Bump version 2024-01-26 04:09:54 +01:00
7d9130b2af Remove the doBuildRetrocompatAccountTheme and the retrocompat_ option in dropdown 2024-01-26 03:39:48 +01:00
482d71743b Bump version 2024-01-26 02:03:06 +01:00
1db37a4727 Do not include retrocompat_ in META-INF when using doBuildRetrocompatAccountTheme false 2024-01-26 02:02:45 +01:00
194d16ff91 Bump version 2024-01-26 00:56:45 +01:00
b1e2284c0e Fix condition for displaying info in login page 2024-01-26 00:55:50 +01:00
70d1aa70a3 Bump version 2024-01-18 18:09:59 +01:00
3b17d6e0ab Merge pull request #496 from keycloakify/all-contributors/add-Moulyy
docs: add Moulyy as a contributor for code
2024-01-18 18:09:10 +01:00
9a5819b93b docs: update .all-contributorsrc [skip ci] 2024-01-18 16:45:36 +00:00
a260cd67b0 docs: update README.md [skip ci] 2024-01-18 16:45:35 +00:00
64111fb0ec Merge pull request #495 from Moulyy/fix/add_password_visibility_classkey
Add "kcInputGroup" in ClassKey type definition and useGetClassName
2024-01-18 17:43:48 +01:00
faf2be23d9 Add "kcInputGroup" in ClassKey type definition and the patternfly class associated in useGetClassName 2024-01-18 17:31:16 +01:00
0eb4a6a315 Merge pull request #494 from alexted/patch-1
Update README.md
2024-01-18 16:59:17 +01:00
85673250ed Update README.md
extra "v" deleted  in several places of changelog highlights
2024-01-18 17:48:10 +02:00
09daa741ce Install pnpm if needed 2024-01-18 01:02:14 +01:00
55e2379aab Bump version 2024-01-18 00:57:02 +01:00
9937977203 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-18 00:56:26 +01:00
c897e7491a Use message of Keycloak version 23.0.4 #493 2024-01-18 00:54:09 +01:00
0a74a95283 Bump version 2024-01-15 11:02:00 +01:00
74ef2c3dff Merge pull request #489 from law108000/main
refactor: point the import statement to keycloakify instead of internal
2024-01-15 11:01:14 +01:00
9976dfacc0 Merge pull request #491 from keycloakify/all-contributors/add-law108000
docs: add law108000 as a contributor for code
2024-01-15 10:59:17 +01:00
659f8ddc7a docs: update .all-contributorsrc [skip ci] 2024-01-15 09:56:30 +00:00
9e4cc2ae57 docs: update README.md [skip ci] 2024-01-15 09:56:29 +00:00
a27d78fcdf refactor: point the import statement to keycloakify instead of internal
align import statement with other page templates
2024-01-15 14:03:01 +08:00
e507435bcb Bump version 2024-01-10 13:01:23 +01:00
d5f234909f Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-10 12:43:11 +01:00
c17f721625 Bump version 2024-01-10 12:43:04 +01:00
600705130f #483: Enable the Template prop not to be lazy 2024-01-10 12:42:48 +01:00
5c5dce1422 Bump version 2023-12-18 13:19:54 +01:00
53585bf2f0 Merge pull request #476 from BlackVoid/feature/document-title
Fixes #473: Set document title using the "loginTitle" translation
2023-12-18 13:18:51 +01:00
116f88a503 Fixes #473: Set document title using the "loginTitle" translation 2023-12-18 11:56:37 +01:00
aaba8cd2c7 Bump version 2023-12-14 15:34:13 +01:00
b67aeb0d3a Fix tests #471 2023-12-14 15:33:57 +01:00
f620562d68 docs: update .all-contributorsrc [skip ci] 2023-12-14 15:09:10 +01:00
5231d0eaa1 docs: update README.md [skip ci] 2023-12-14 15:09:09 +01:00
cb470e3573 Handle CSS with multiple urls 2023-12-12 14:51:09 +00:00
0a0f90aa2e Bump version 2023-12-04 14:10:25 +01:00
635207d12c Generate a README alongside the jars to indicate why there's two jar file 2023-12-04 14:10:10 +01:00
5e4a829413 Bump version 2023-12-01 00:54:36 +01:00
b13b3fd92e Mention the nessesity to use retrocompat- with older Keycloak versions 2023-12-01 00:54:19 +01:00
564dc8e6f1 Bump version 2023-12-01 00:03:58 +01:00
6e4cced8c6 Rename original- to retrocompat- 2023-12-01 00:03:44 +01:00
29a4a5027c Bump version 2023-11-30 22:37:13 +01:00
ee327448b4 Delete original- jar 2023-11-30 22:36:58 +01:00
d078960c5c Bump version 2023-11-30 18:53:17 +01:00
2e8cd375fc Merge pull request #462 from BlackVoid/fix/client-attributes
Fixes #460: Fixes KcContext to contain attributes for client object
2023-11-30 18:52:26 +01:00
1f6751cb01 Fixes #460: Fixes KcContext to contain attributes for client object 2023-11-30 17:17:58 +01:00
3cca4e31cd Merge pull request #463 from keycloakify/all-contributors/add-BlackVoid
docs: add BlackVoid as a contributor for code
2023-11-30 17:10:29 +01:00
b93902800c docs: update .all-contributorsrc [skip ci] 2023-11-30 16:06:55 +00:00
70f6bb3fda docs: update README.md [skip ci] 2023-11-30 16:06:54 +00:00
c075cb6311 Merge pull request #461 from BlackVoid/fix/template
Fixes #459: Use Template from props
2023-11-30 17:03:36 +01:00
d7db85b062 Fixes #459: Use Template from props 2023-11-30 16:29:53 +01:00
b442e7d958 Merge pull request #457 from keycloakify/all-contributors/add-xgp
docs: add xgp as a contributor for code
2023-11-28 13:06:04 +01:00
a495ae637f docs: update .all-contributorsrc [skip ci] 2023-11-28 12:05:49 +00:00
94748a96a9 docs: update README.md [skip ci] 2023-11-28 12:05:48 +00:00
7657429054 Release v9 2023-11-28 12:53:37 +01:00
2ff6dbf975 Update README 2023-11-28 12:53:10 +01:00
4f34628c14 Merge pull request #414 from keycloakify/v9
v9: Support Keycloak 22 and decoupling account / login builtin resources
2023-11-28 12:18:07 +01:00
6ff2111cee #389 https://github.com/p2-inc/keycloak-account-v1/issues/3 2023-11-28 12:17:16 +01:00
85957980f6 update CI 2023-11-26 16:24:57 +01:00
a6dcfe2c87 Release candidate 2023-11-26 16:14:25 +01:00
c32d590fbb #389 https://github.com/xgp/keycloak-account-v1/issues/3 2023-11-26 16:10:34 +01:00
ab41462f71 Actually resolve conflict with main 2023-11-26 15:07:27 +01:00
951f16b1a5 Merge pull request #456 from keycloakify/v9_tmp
Rebase v9 to main
2023-11-26 14:46:28 +01:00
b5818888bb Merge branch 'v9' into v9_tmp 2023-11-26 14:45:23 +01:00
1a326bf7e4 Bump version 2023-11-22 18:58:04 +01:00
e1afc1cf7a Add themeVersion in KcContext type 2023-11-22 18:57:43 +01:00
bb007ddce5 fmt 2023-11-22 11:44:58 +01:00
b5dd0317c7 Update README.md 2023-11-22 11:39:10 +01:00
3c54541a73 Bump version 2023-11-19 03:27:54 +01:00
2657f01135 Enable to ignore part of the HTML 2023-11-19 03:27:40 +01:00
7223409eb1 Bump version 2023-11-07 16:33:33 +01:00
c41eae63e7 Fix info.ftl page rendering in storybook 2023-11-07 16:33:19 +01:00
c8b85c43aa Bump version 2023-11-04 16:47:58 +01:00
e918788c3f Reverse previous change, it breaks cra build 2023-11-04 16:47:13 +01:00
b53f4f997c Bump version 2023-11-04 16:29:36 +01:00
481d93ebc4 Create symlink to build in keycloak-resource for test env that better reflect prod 2023-11-04 16:29:09 +01:00
1ce666f136 Bump version 2023-11-04 15:50:51 +01:00
49a8e702bc Merge pull request #447 from celinepelletier/add-oauth2-device-flow-pages
feat: add login-oauth2-device-verify-user-code and login-oauth-grant pages
2023-11-04 00:14:52 +01:00
5d59e652d7 feat: add login-oauth2-device-verify-user-code and login-oauth-grant in storybook 2023-11-03 16:48:40 -04:00
02af8c7311 Merge pull request #448 from keycloakify/all-contributors/add-celinepelletier
docs: add celinepelletier as a contributor for code
2023-11-03 21:31:55 +01:00
fadf4e867c docs: update .all-contributorsrc [skip ci] 2023-11-03 20:29:27 +00:00
0839859fef docs: update README.md [skip ci] 2023-11-03 20:29:26 +00:00
c122b48e35 feat: add login-oauth2-device-verify-user-code and login-oauth-grant pages 2023-11-03 16:06:46 -04:00
cebb297bbf Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-10-26 12:56:47 +02:00
2e31b796f7 Bump version 2023-10-26 12:52:06 +02:00
e0a61b51cb Little fix on LoginConfigTotp.tsx 2023-10-26 12:51:43 +02:00
46e50e622b Bump version 2023-10-24 16:23:28 +02:00
7cfa1df0b2 Merge pull request #440 from keycloakify/discussion_432
Discussion 432
2023-10-24 16:22:57 +02:00
8a63648339 Release candidate 2023-10-22 16:39:34 +02:00
bb6b026720 https://github.com/keycloakify/keycloakify/discussions/432#discussioncomment-7332729 2023-10-22 16:39:15 +02:00
2a13b314dc Bump version 2023-10-22 16:05:09 +02:00
4506b3f6d4 Remove some dead code 2023-10-22 16:04:47 +02:00
804abef0de #433 2023-10-22 15:58:11 +02:00
7e932b920e Bump version 2023-10-20 15:06:29 +02:00
46fdfbc507 Merge pull request #438 from keycloakify/all-contributors/add-rome-user
docs: add rome-user as a contributor for code
2023-10-20 15:05:48 +02:00
a4ff8607c5 docs: update .all-contributorsrc [skip ci] 2023-10-20 13:05:34 +00:00
7fe4eeda57 docs: update README.md [skip ci] 2023-10-20 13:05:33 +00:00
9f25cddaa4 Allow "keycloak_theme" as a theme source directory
Fixes #429.
2023-10-08 18:12:57 -07:00
eb64fe60d0 Bump version 2023-10-09 00:49:50 +02:00
36f404e17d Preserve css order 2023-10-09 00:49:35 +02:00
5398590939 https://github.com/keycloakify/keycloakify/discussions/371#discussioncomment-7223711 2023-10-09 00:49:18 +02:00
96d5cfea14 Bump version 2023-10-02 22:49:31 +02:00
79007ebd55 #427 2023-10-02 22:49:04 +02:00
fcb519dac3 Bump version 2023-09-27 04:08:13 +02:00
2b487aa959 Merge pull request #423 from zavoloklom/fix/js-code-path-replacer
fix: change JS path transformation for static resources
2023-09-27 04:07:41 +02:00
733feadcb2 Merge pull request #425 from keycloakify/all-contributors/add-zavoloklom
docs: add zavoloklom as a contributor for test, and code
2023-09-27 03:59:00 +02:00
5ae568f19c docs: update .all-contributorsrc [skip ci] 2023-09-27 01:58:05 +00:00
0e51807856 docs: update README.md [skip ci] 2023-09-27 01:58:04 +00:00
b6eb165207 https://github.com/keycloakify/keycloakify/discussions/422 2023-09-25 13:41:51 +02:00
d26dbf4b3d fix: change JS path transformation for static resources
- Handle both arrow functions and traditional function expressions
- Add tests to ensure correctness of transformations
2023-09-24 23:42:10 +02:00
e06ef01f72 Merge branch 'main' into v9 2023-09-22 15:52:23 +02:00
a722582709 Bump version 2023-09-22 15:51:52 +02:00
de64deb5c5 #421 2023-09-22 15:51:18 +02:00
7de54a2cc4 Fix tests 2023-09-04 03:26:30 +02:00
c788b8cc82 Release candidate 2023-09-04 02:49:58 +02:00
cb8db1a541 Fix build 2023-09-04 02:49:32 +02:00
8a7a551c3b Fix mock path error in account 2023-09-04 02:38:19 +02:00
84d180b810 Fix bug with asset paths 2023-09-04 02:34:10 +02:00
de261a27ca Do not display that the jar have been created if we don't create it. 2023-09-04 02:29:16 +02:00
28288a8f7b Build retrocompatible account theme 2023-09-04 02:16:55 +02:00
cd8548fc32 Remove extraThemeNames option in favor of extending themeName to accept array 2023-09-04 01:19:21 +02:00
37dbd49589 Rename extraThemeNames to themeVariantNames 2023-09-04 00:53:57 +02:00
5af8d67b62 Refactor and update docker script 2023-09-04 00:25:36 +02:00
72e6309c4a Fix warning 2023-09-03 23:32:21 +02:00
18f0f3cce1 Refactor build option managment 2023-09-03 23:26:34 +02:00
8c3e9ff192 Remove inhouse bundler, we actually need Maven to build now 2023-09-03 21:10:20 +02:00
21d6d27435 Rename build option, update readme 2023-09-03 21:02:51 +02:00
39ff7913d6 https://github.com/xgp/keycloak-account-v1/issues/3 2023-09-03 07:14:57 +02:00
402c6fc64a Fix log message when prompting which version to download 2023-09-03 01:38:38 +02:00
a1f934466c Update resolution with Keycloak 22 and up 2023-09-01 17:45:33 +02:00
15aa114579 Release v8 2023-08-28 20:11:32 +02:00
b9cc82e37d Show how to persist cache between builds 2023-08-28 20:09:48 +02:00
8af9c8b150 Release candidate 2023-08-28 19:25:31 +02:00
7dcc985222 Remove debug log 2023-08-28 19:25:15 +02:00
9c2bc19897 Give futher instruction for migrating to v8 2023-08-28 19:17:32 +02:00
801b08359a Release candidate 2023-08-28 18:35:55 +02:00
c469dee158 Accomodate https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 and #406 2023-08-28 18:35:37 +02:00
2aa7eda1e9 Fix typo and formatting 2023-08-25 08:42:00 +02:00
f1246c9e00 Add version note 2023-08-24 09:44:00 +02:00
2749cbe4d1 Disable test with starter for now 2023-08-24 09:17:12 +02:00
d2a9280ab3 Update CI 2023-08-24 09:07:09 +02:00
8e25ee0fc9 Release candidate for v8 2023-08-24 09:06:42 +02:00
55026f913b Remove debug console.log 2023-08-24 09:05:58 +02:00
7cc40e2453 Merge pull request #404 from keycloakify/smaller_jar_size
Smaller jar size
2023-08-24 09:02:03 +02:00
cb6b19952d Merge pull request #396 from ddubrava/remove-message-from-kc-context-mock
feat: remove message from kcContextCommonMock
2023-08-24 09:00:16 +02:00
983af57842 Actually remove non used resources 2023-08-24 08:58:00 +02:00
3c2820dc31 Meta progaming for detecting static assets usage a build time 2023-08-23 08:13:09 +02:00
1c25b69160 Remove --external-assets option 2023-08-21 05:54:17 +02:00
641cc38ae4 Remove unused parameter 2023-08-21 04:29:32 +02:00
cd68b07e19 Build keycloak static assets and improve cache mechanism to keep build time in check https://github.com/xgp/keycloak-account-v1/issues/3 2023-08-21 04:26:58 +02:00
2b252c9abb npm install missing resources 2023-08-20 03:00:45 +02:00
e2e8370bb9 Bump version 2023-08-20 02:58:29 +02:00
e9e31394c4 #380 2023-08-20 02:58:10 +02:00
2825ccbcd5 Update README.md 2023-08-19 19:44:49 +02:00
377a14ff72 Update README.md 2023-08-19 19:44:13 +02:00
a83997b9b4 Update README.md 2023-08-19 08:53:29 +02:00
3e155d8e80 Restore starter test build step 2023-08-15 20:27:09 +02:00
6953b72ee6 Temporarely comment the test with the starter App in order to be able to release 2023-08-15 19:50:10 +02:00
ab370a1dda Merge pull request #400 from keycloakify/fix/usernameEditDisabled-usernameHidden
fix: update WithImmutablePresetUsername story parameters
2023-08-15 19:20:05 +02:00
20845e5860 fix: update WithImmutablePresetUsername story parameters 2023-08-15 08:59:00 +02:00
9ed3257006 Bump version 2023-08-14 21:57:37 +02:00
2221e30c0a fmt 2023-08-14 21:57:19 +02:00
ce43dca23b Merge pull request #399 from keycloakify/fix/usernameEditDisabled-usernameHidden
fix: usernameEditDisabled renamed to usernameHidden
2023-08-14 21:56:03 +02:00
4acf5d0931 fix: usernameEditDisabled renamed to usernameHidden
- update KcContext definition for login.ftl
- update the username field rendering conditions in Login.tsx

Closes #397
2023-08-14 15:53:05 +02:00
b742ed73aa feat: remove message from kcContextCommonMock 2023-08-11 12:54:09 +02:00
5156b2e0cc Bump version 2023-08-07 15:21:43 +02:00
6b81cf4a24 #391 2023-08-07 15:21:22 +02:00
cca3a68fe4 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-08-07 15:10:48 +02:00
adb2904872 Bump version 2023-08-07 15:10:43 +02:00
d68b8d03dd #392 2023-08-07 15:10:25 +02:00
e7afb88f22 Update README.md 2023-08-02 12:55:08 +02:00
48cbfc64c0 Add warning about Keycloak 22 2023-08-02 09:15:10 +02:00
0b067858bc Bump version 2023-08-02 09:04:05 +02:00
2d44d98f17 Test with Keycloak 21.1.2 by default, use --features=declarative-user-profile for enabling User Profile features 2023-08-02 08:56:47 +02:00
74ef3096ae Can't support Account theme in Keycloak 22 2023-08-02 08:48:38 +02:00
8f1163fd75 Release candidate 2023-08-01 19:10:25 +02:00
a240d503c5 Hotfix for #388 2023-08-01 19:09:18 +02:00
e331a641b2 Bump version 2023-07-29 03:45:50 +02:00
85db4b8e0a Export the default PageId type 2023-07-29 03:45:33 +02:00
0aa139cf4a Bump version 2023-07-26 19:33:55 +02:00
4140ca6fbd #385 2023-07-26 19:33:38 +02:00
a8ce9da9ee Bump version 2023-07-24 23:22:51 +02:00
476a33c0ab #380 2023-07-24 23:22:16 +02:00
8e868c9fda Bump version 2023-07-24 22:39:16 +02:00
17c8b1a172 When no path PUBLIC_URL is empty string 2023-07-24 22:38:53 +02:00
b374c04d73 Bump version 2023-07-24 20:45:31 +02:00
e750d824ad #47 #384 2023-07-24 20:45:02 +02:00
dd4c50c3eb Bump version 2023-07-24 01:11:40 +02:00
20cc869299 #375 2023-07-24 01:11:34 +02:00
7214dbccdb Bump version 2023-07-07 17:03:03 +02:00
e6cebdd546 #377 2023-07-07 17:02:18 +02:00
0301003ccf Bump version 2023-06-27 17:52:18 +02:00
de2efe0c01 #369 2023-06-27 17:51:59 +02:00
90d765d7f6 #366: fix tests 2023-06-21 18:23:12 +02:00
3e0a1721ce Merge pull request #366 from Gravity-Software-srl/main
prevent crawlRec from crashing when dir_path does not exist
2023-06-21 18:17:11 +02:00
7214fbcd4c Bump version 2023-06-21 18:08:02 +02:00
4b8aecfe91 #364 2023-06-21 18:06:12 +02:00
387c71c0aa prevent crawlRec from crashing when dir_path does not exist 2023-06-21 16:24:58 +02:00
8d5ce21df4 Note about compatibility 2023-06-21 14:19:28 +02:00
f6dfcfbae9 Fix scripts build 2023-06-21 04:01:11 +02:00
69e9595db9 Bump version 2023-06-21 03:56:30 +02:00
de390678fd Deprecate the extraPages options, analyze the code to detect extra pages 2023-06-21 03:54:43 +02:00
cf9a7b8c60 Update json-schema 2023-06-21 02:56:55 +02:00
73e9c16a8d Fix some types approximations 2023-06-21 02:55:44 +02:00
83 changed files with 2585 additions and 1917 deletions

View File

@ -158,6 +158,79 @@
"contributions": [
"code"
]
},
{
"login": "zavoloklom",
"name": "Sergey Kupletsky",
"avatar_url": "https://avatars.githubusercontent.com/u/4151869?v=4",
"profile": "https://github.com/zavoloklom",
"contributions": [
"test",
"code"
]
},
{
"login": "rome-user",
"name": "rome-user",
"avatar_url": "https://avatars.githubusercontent.com/u/114131048?v=4",
"profile": "https://github.com/rome-user",
"contributions": [
"code"
]
},
{
"login": "celinepelletier",
"name": "Céline Pelletier",
"avatar_url": "https://avatars.githubusercontent.com/u/82821620?v=4",
"profile": "https://github.com/celinepelletier",
"contributions": [
"code"
]
},
{
"login": "xgp",
"name": "Garth",
"avatar_url": "https://avatars.githubusercontent.com/u/244253?v=4",
"profile": "https://github.com/xgp",
"contributions": [
"code"
]
},
{
"login": "BlackVoid",
"name": "Felix Gustavsson",
"avatar_url": "https://avatars.githubusercontent.com/u/673720?v=4",
"profile": "https://github.com/BlackVoid",
"contributions": [
"code"
]
},
{
"login": "msiemens",
"name": "Markus Siemens",
"avatar_url": "https://avatars.githubusercontent.com/u/1873922?v=4",
"profile": "https://m-siemens.de/",
"contributions": [
"code"
]
},
{
"login": "law108000",
"name": "Rlok",
"avatar_url": "https://avatars.githubusercontent.com/u/8112024?v=4",
"profile": "https://github.com/law108000",
"contributions": [
"code"
]
},
{
"login": "Moulyy",
"name": "Moulyy",
"avatar_url": "https://avatars.githubusercontent.com/u/115405804?v=4",
"profile": "https://github.com/Moulyy",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
@ -165,5 +238,6 @@
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "keycloakify",
"projectOwner": "keycloakify"
"projectOwner": "keycloakify",
"commitType": "docs"
}

View File

@ -3,6 +3,9 @@ on:
push:
branches:
- main
- v5
- v6
- v7
pull_request:
branches:
- main
@ -34,7 +37,7 @@ jobs:
- uses: bahmutov/npm-install@v1
- run: yarn build
- run: yarn test
- run: yarn test:keycloakify-starter
#- run: yarn test:keycloakify-starter
storybook:
runs-on: ubuntu-latest
@ -74,7 +77,6 @@ jobs:
id: step1
with:
action_name: is_package_json_version_upgraded
branch: ${{ github.head_ref || github.ref }}
create_github_release:
runs-on: ubuntu-latest

View File

@ -36,14 +36,12 @@
<p align="center">
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
<br/>
<br/>
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
</p>
> Whether or not React is your preferred framework, Keycloakify
> offers a solid option for building Keycloak themes.
> It's not just a convenient way to create a Keycloak theme
> when using React; it's a well-regarded solution that many
> developers appreciate.
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
## Sponsor 👼
@ -106,6 +104,16 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xgp"><img src="https://avatars.githubusercontent.com/u/244253?v=4?s=100" width="100px;" alt="Garth"/><br /><sub><b>Garth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=xgp" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BlackVoid"><img src="https://avatars.githubusercontent.com/u/673720?v=4?s=100" width="100px;" alt="Felix Gustavsson"/><br /><sub><b>Felix Gustavsson</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=BlackVoid" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@ -117,14 +125,45 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
## 9.0
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
### Breaking changes
Very few. Check them out [here](https://docs.keycloakify.dev/migration-guides/v8-greater-than-v9).
## 8.0
- Much smaller .jar size. 70.2 MB -> 7.8 MB.
Keycloakify now detects which of the static resources from the default theme are actually used by your theme and only include those in the .jar.
- Build time: The first build is slowed but the subsequent build are faster. [Update your CI so that the cache is persisted across CI build](https://github.com/keycloakify/keycloakify-starter/commit/bc378d5afb67e796f520afbc348185f3e319d9d0).
### Breaking changes
There are very few breaking changes in this major version. [Check them out](https://docs.keycloakify.dev/migration-guides/v7-greater-than-v8).
## 7.15
- The i18n messages you defines in your theme are now also maid available to Keycloak.
In practice this mean that you can now customize the `kcContext.message.summary` that
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
are used to display specific error on some field of the form.
[See video](https://youtu.be/D6tZcemReTI)
## 7.14
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
## 7.13
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
It's now mandatory to [adopt the new directory structure](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
## 7.12
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.
There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.extrathemenames).
There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.themeVariantNames).
## 7.9
@ -219,79 +258,79 @@ Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
## v5.6.0
## 5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## v5.3.0
## 5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
## v5.0.0
## 5.0.0
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
New i18n system.
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
## v4.10.0
## 4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## v4.8.0
## 4.8.0
[Email template customization.](#email-template-customization)
## v4.7.4
## 4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## v4.7.2
## 4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
## 4.7.0
Register with user profile enabled: Out of the box `options` validator support.
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
## v4.6.0
## 4.6.0
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
## v4.5.3
## 4.5.3
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
with this new method your theme wont break on minor Keycloakify update.
## v4.3.0
## 4.3.0
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
Every time a page is added it's a breaking change for non CSS-only theme.
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4
## 4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## v3
## 3
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
## v2.5
## 2.5
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2
## 2
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "7.13.2",
"version": "9.3.0",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -13,7 +13,7 @@
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
"test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit",
@ -79,12 +79,14 @@
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"@types/babel__generator": "^7.6.4",
"@types/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@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",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"eslint-plugin-storybook": "^0.6.7",
@ -106,8 +108,10 @@
"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",
"@types/yazl": "^2.4.2",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.18",
@ -116,6 +120,7 @@
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",

View File

@ -17,7 +17,7 @@ const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "21.0.1";
const keycloakVersion = "23.0.4";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
@ -26,7 +26,9 @@ async function main() {
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
"buildOptions": {
"cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
}
});
type Dictionary = { [idiomId: string]: string };
@ -37,7 +39,10 @@ async function main() {
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl(baseThemeDirPath).forEach(filePath => {
crawl({
"dirPath": baseThemeDirPath,
"returnedPathsType": "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re);
if (match === null) {

View File

@ -17,10 +17,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
url,
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
"styles": ["css/account.css"],
"htmlClassName": undefined,
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
});

View File

@ -11,4 +11,4 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
children: ReactNode;
};
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";

View File

@ -4,6 +4,7 @@ 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/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -1,11 +1,13 @@
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { type ThemeType } from "keycloakify/bin/constants";
export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
themeVersion: string;
keycloakifyVersion: string;
themeType: "account";
themeName: string;

View File

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

View File

@ -1,18 +1,21 @@
import "minimal-polyfills/Object.fromEntries";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"themeName": "my-theme-name",
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
@ -132,10 +135,6 @@ export const kcContextCommonMock: KcContext.Common = {
],
"currentLanguageTag": "en"
},
"message": {
"type": "success",
"summary": "This is a test message"
},
"features": {
"authorization": true,
"identityFederation": true,

View File

@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/account/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcHtmlClass": undefined,
"kcBodyClass": undefined,
"kcButtonClass": "btn",
"kcButtonPrimaryClass": "btn-primary",

View File

@ -51,7 +51,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
id="username"
name="username"
disabled={!realm.editUsernameAllowed}
value={account.username ?? ""}
defaultValue={account.username ?? ""}
/>
</div>
</div>
@ -66,7 +66,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
<input type="text" className="form-control" id="email" name="email" autoFocus defaultValue={account.email ?? ""} />
</div>
</div>
@ -79,7 +79,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
<input type="text" className="form-control" id="firstName" name="firstName" defaultValue={account.firstName ?? ""} />
</div>
</div>
@ -92,7 +92,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
<input type="text" className="form-control" id="lastName" name="lastName" defaultValue={account.lastName ?? ""} />
</div>
</div>

View File

@ -1,9 +1,9 @@
import type { LazyExoticComponent } from "react";
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
export type PageProps<KcContext, I18nExtended extends I18n> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

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

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

View File

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

View File

@ -4,24 +4,141 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process";
import * as fs from "fs";
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath } = params;
export type BuildOptionsLike = {
cacheDirPath: string;
};
await Promise.all(
["", "-community"].map(ext =>
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
})
)
);
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
"doUseCache": true,
"cacheDirPath": buildOptions.cacheDirPath,
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
fix_account_css: {
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
if (!fs.existsSync(accountCssFilePath)) {
break fix_account_css;
}
fs.writeFileSync(
accountCssFilePath,
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
);
}
fix_account_topt: {
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
if (!fs.existsSync(totpFtlFilePath)) {
break fix_account_topt;
}
fs.writeFileSync(
totpFtlFilePath,
Buffer.from(
fs
.readFileSync(totpFtlFilePath)
.toString("utf8")
.replace(
[
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>"
].join("\n"),
[
" <#if totp.policy.supportedApplications?has_content>",
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>",
" </#if>"
].join("\n")
),
"utf8"
)
);
}
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"
});
}
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 });
}
}
}
});
}
async function main() {
const buildOptions = readBuildOptions({
"projectDirPath": process.cwd(),
"reactAppRootDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
@ -35,7 +152,7 @@ async function main() {
await downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
"isSilent": buildOptions.isSilent
buildOptions
});
}

View File

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

View File

@ -2,32 +2,46 @@ import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./constants";
const themeSrcDirBasename = "keycloak-theme";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const srcDirPath = pathJoin(reactAppRootDirPath, "src");
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
.map(fileRelativePath => {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length !== 2) {
return undefined;
for (const themeSrcDirBasename of themeSrcDirBasenames) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath === undefined) {
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
return { "themeSrcDirPath": srcDirPath };
}
return { "themeSrcDirPath": undefined };
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
return { themeSrcDirPath };
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { "themeSrcDirPath": srcDirPath };
}
console.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme or src/keycloak_theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
process.exit(-1);
}

View File

@ -10,23 +10,19 @@ import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getSrcDirPath";
export async function main() {
const { isSilent } = readBuildOptions({
"projectDirPath": process.cwd(),
const reactAppRootDirPath = process.cwd();
const buildOptions = readBuildOptions({
reactAppRootDirPath,
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ isSilent });
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({
"projectDirPath": process.cwd()
reactAppRootDirPath
});
if (themeSrcDirPath === undefined) {
logger.warn("Couldn't locate your theme source directory");
process.exit(-1);
}
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) {
@ -42,7 +38,7 @@ export async function main() {
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
buildOptions
});
transformCodebase({

View File

@ -1,245 +1,157 @@
import { assert } from "tsafe/assert";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
import { typeGuard } from "tsafe/typeGuard";
import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import * as fs from "fs";
import { join as pathJoin, sep as pathSep } from "path";
import { getParsedPackageJson } from "./parsedPackageJson";
import { join as pathJoin } from "path";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
export type BuildOptions = {
isSilent: boolean;
themeVersion: string;
themeNames: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
doCreateJar: boolean;
loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
/** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
publicDirPath: string;
cacheDirPath: string;
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
};
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
themeVersion: string;
themeName: string;
extraThemeNames: string[];
extraLoginPages: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[];
groupId: string;
artifactId: string;
bundler: Bundler;
keycloakVersionDefaultAssets: string;
/** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
};
export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {
const { reactAppRootDirPath, processArgv } = params;
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params;
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
const { isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv);
return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
};
})();
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath });
const url = (() => {
const { homepage } = parsedPackageJson;
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
const { extraThemeProperties, groupId, artifactId, doCreateJar, loginThemeResourcesFromKeycloakVersion } = keycloakify ?? {};
if (homepage !== undefined) {
url = new URL(homepage);
const themeNames = (() => {
if (keycloakify.themeName === undefined) {
return [
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-")
];
}
const CNAME = (() => {
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
if (typeof keycloakify.themeName === "string") {
return [keycloakify.themeName];
}
if (!fs.existsSync(cnameFilePath)) {
return keycloakify.themeName;
})();
return {
reactAppRootDirPath,
themeNames,
"doCreateJar": doCreateJar ?? true,
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeNames[0]}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"loginThemeResourcesFromKeycloakVersion": loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
"publicDirPath": (() => {
let { PUBLIC_DIR_PATH } = process.env;
if (PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": PUBLIC_DIR_PATH,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "public");
})(),
"reactAppBuildDirPath": (() => {
const { reactAppBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build");
})(),
"keycloakifyBuildDirPath": (() => {
const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build_keycloak");
})(),
"cacheDirPath": pathJoin(
(() => {
let { XDG_CACHE_HOME } = process.env;
if (XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
),
"urlPathname": (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (url === undefined) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})();
if (CNAME !== undefined) {
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
}
if (url === undefined) {
return undefined;
}
return {
"origin": url.origin,
"pathname": (() => {
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
})();
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const {
extraPages,
extraLoginPages,
extraAccountPages,
extraThemeProperties,
groupId,
artifactId,
bundler,
keycloakVersionDefaultAssets,
extraThemeNames = []
} = keycloakify ?? {};
const themeName =
keycloakify.themeName ??
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
extraThemeNames,
"bundler": (() => {
const { KEYCLOAKIFY_BUNDLER } = process.env;
assert(
typeGuard<Bundler | undefined>(
KEYCLOAKIFY_BUNDLER,
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
);
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
"reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build");
}
if (pathSep === "\\") {
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
}
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, reactAppBuildDirPath);
}
return reactAppBuildDirPath;
})(),
"keycloakifyBuildDirPath": (() => {
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build_keycloak");
}
if (pathSep === "\\") {
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
}
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
}
return keycloakifyBuildDirPath;
})()
};
})();
if (isExternalAssetsCliParamProvided) {
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
...common,
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
url !== undefined,
[
"Can't compile in external assets mode if we don't know where",
"the app will be hosted.",
"You should provide a homepage field in the package.json (or create a",
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
}
}
return id<BuildOptions.Standalone>({
...common,
"isStandalone": true,
"urlPathname": url?.pathname
});
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})(),
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
};
}

View File

@ -26,64 +26,34 @@
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#else>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
@ -100,107 +70,139 @@
<#else>
<#assign doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>text<#else>undefined</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
</#if>
</#if>
}
</#list>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
},
"existsError": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>true<#else>false</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
<#else>
}
</#list>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"get": function (fieldName) {
@ -208,184 +210,185 @@
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.get in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
return "invalid field";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
</#if>
</#if>
}
</#list>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.exists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>true<#else>false</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
<#else>
}
</#list>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
}
};
@ -454,7 +457,7 @@
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl"]?seq_contains(pageId) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
@ -475,22 +478,27 @@
"error.ftl" == pageId &&
are_same_path(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || (
"applications.ftl" == pageId &&
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
) || (
"applications.ftl" == pageId &&
"masterAdminClient" == key
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#continue>
</#if>
<#recover>
</#attempt>
</#if>
<#attempt>

View File

@ -8,48 +8,17 @@ import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import type { ThemeType } from "../../constants";
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
themeVersion: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = {
isStandalone: false;
};
export type SameDomain = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export type BuildOptionsLike = {
themeVersion: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
@ -58,27 +27,28 @@ export function generateFtlFilesCodeFactory(params: {
themeType: ThemeType;
fieldNames: string[];
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!,
buildOptions
});
const jsCode = $(element).html();
assert(jsCode !== null);
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
$(element).text(fixedJsCode);
});
$("style").each((...[, element]) => {
const cssCode = $(element).html();
assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!,
cssCode,
buildOptions
});
@ -100,9 +70,7 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
buildOptions.isStandalone
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
);
})
);
@ -133,7 +101,7 @@ export function generateFtlFilesCodeFactory(params: {
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", buildOptions.themeName),
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
@ -153,6 +121,27 @@ export function generateFtlFilesCodeFactory(params: {
].join("\n")
);
// Remove part of the document marked as ignored.
{
const startTags = $('meta[name="keycloakify-ignore-start"]');
startTags.each((...[, startTag]) => {
const $startTag = $(startTag);
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
if ($endTag.length) {
let currentNode = $startTag.next();
while (currentNode.length && !currentNode.is($endTag)) {
currentNode.remove();
currentNode = $startTag.next();
}
$startTag.remove();
$endTag.remove();
}
});
}
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params: { pageId: string }): {

View File

@ -10,6 +10,8 @@ export const loginThemePageIds = [
"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",

View File

@ -1,89 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId?: string;
themeVersion: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
implementedThemeTypes
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -0,0 +1,101 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1 } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
cacheDirPath: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
"destDirPath": builtinKeycloakThemeTmpDirPath,
"keycloakVersion": lastKeycloakVersionWithAccountV1,
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1, "account");
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
"destDirPath": accountV1DirPath
});
const commonResourceFilePaths = [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
...[
"OpenSans-Light-webfont.woff2",
"OpenSans-Regular-webfont.woff2",
"OpenSans-Bold-webfont.woff2",
"OpenSans-Semibold-webfont.woff2",
"OpenSans-Bold-webfont.woff",
"OpenSans-Light-webfont.woff",
"OpenSans-Regular-webfont.woff",
"OpenSans-Semibold-webfont.woff",
"OpenSans-Regular-webfont.ttf",
"OpenSans-Light-webfont.ttf",
"OpenSans-Semibold-webfont.ttf",
"OpenSans-Bold-webfont.ttf"
].map(path => `node_modules/patternfly/dist/fonts/${path}`)
];
for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
}
const resourceFilePaths = ["css/account.css", "img/icon-sidebar-active.png", "img/logo.png"];
for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath);
}
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
Buffer.from(
[
"accountResourceProvider=account-v1",
"",
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"",
"styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources-common/${path}`)].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
"kcButtonClass=btn",
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
"kcButtonPrimaryClass=btn-primary",
"kcButtonDefaultClass=btn-default",
"# classes defining size of the button",
"kcButtonLargeClass=btn-lg",
""
].join("\n"),
"utf8"
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,179 @@
import type { ThemeType } from "../../constants";
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import { readFileSync } from "fs";
import { symToStr } from "tsafe/symToStr";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as recast from "recast";
import * as babelParser from "@babel/parser";
import babelGenerate from "@babel/generator";
import * as babelTypes from "@babel/types";
export function generateMessageProperties(params: {
themeSrcDirPath: string;
themeType: ThemeType;
}): { languageTag: string; propertiesFileSource: string }[] {
const { themeSrcDirPath, themeType } = params;
let files = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
});
files = files.filter(file => {
const regex = /\.(js|ts|tsx)$/;
return regex.test(file);
});
files = files.sort((a, b) => {
const regex = /\.i18n\.(ts|js|tsx)$/;
const aIsI18nFile = regex.test(a);
const bIsI18nFile = regex.test(b);
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
});
files = files.sort((a, b) => a.length - b.length);
files = files.filter(file => readFileSync(file).toString("utf8").includes("createUseI18n"));
if (files.length === 0) {
return [];
}
const extraMessages = files
.map(file => {
const root = recast.parse(readFileSync(file).toString("utf8"), {
"parser": {
"parse": (code: string) => babelParser.parse(code, { "sourceType": "module", "plugins": ["typescript"] }),
"generator": babelGenerate,
"types": babelTypes
}
});
const codes: string[] = [];
recast.visit(root, {
"visitCallExpression": function (path) {
if (path.node.callee.type === "Identifier" && path.node.callee.name === "createUseI18n") {
codes.push(babelGenerate(path.node.arguments[0] as any).code);
}
this.traverse(path);
}
});
return codes;
})
.flat()
.map(code => {
let extraMessages: { [languageTag: string]: Record<string, string> } = {};
try {
eval(`${symToStr({ extraMessages })} = ${code}`);
} catch {
console.warn(
[
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript",
"runtime where only the node globals are available.",
"This is important because we need to put your i18n messages in messages_*.properties files",
"or they won't be available server side.",
"\n",
"The following code could not be evaluated:",
"\n",
code
].join(" ")
);
}
return extraMessages;
});
const languageTags = extraMessages
.map(extraMessage => Object.keys(extraMessage))
.flat()
.reduce(...removeDuplicates<string>());
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
for (const languageTag of languageTags) {
const keyValueMap: Record<string, string> = {};
for (const extraMessage of extraMessages) {
const keyValueMap_i = extraMessage[languageTag];
if (keyValueMap_i === undefined) {
continue;
}
for (const [key, value] of Object.entries(keyValueMap_i)) {
if (keyValueMap[key] !== undefined) {
console.warn(
[
"WARNING: The following key is defined multiple times:",
"\n",
key,
"\n",
"The following value will be ignored:",
"\n",
value,
"\n",
"The following value was already defined:",
"\n",
keyValueMap[key]
].join(" ")
);
continue;
}
keyValueMap[key] = value;
}
}
keyValueMapByLanguageTag[languageTag] = keyValueMap;
}
const out: { languageTag: string; propertiesFileSource: string }[] = [];
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
const propertiesFileSource = Object.entries(keyValueMap)
.map(([key, value]) => `${key}=${escapeString(value)}`)
.join("\n");
out.push({
languageTag,
"propertiesFileSource": ["# This file was generated by keycloakify", "", "parent=base", "", propertiesFileSource, ""].join("\n")
});
}
return out;
}
// Convert a JavaScript string to UTF-16 encoding
function toUTF16(codePoint: number): string {
if (codePoint <= 0xffff) {
// BMP character
return "\\u" + codePoint.toString(16).padStart(4, "0");
} else {
// Non-BMP character
codePoint -= 0x10000;
let highSurrogate = (codePoint >> 10) + 0xd800;
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
return "\\u" + highSurrogate.toString(16).padStart(4, "0") + "\\u" + lowSurrogate.toString(16).padStart(4, "0");
}
}
// Escapes special characters and converts unicode to UTF-16 encoding
function escapeString(str: string): string {
let escapedStr = "";
for (const char of [...str]) {
const codePoint = char.codePointAt(0);
if (!codePoint) continue;
if (char === "'") {
escapedStr += "''"; // double single quotes
} else if (codePoint > 0x7f) {
escapedStr += toUTF16(codePoint); // non-ascii characters
} else {
escapedStr += char;
}
}
return escapedStr;
}

View File

@ -1,91 +1,80 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { join as pathJoin, basename as pathBasename, resolve as pathResolve } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl";
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, retrocompatPostfix, accountV1 } from "../../constants";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
themeVersion: string;
keycloakVersionDefaultAssets: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export type BuildOptionsLike = {
extraThemeProperties: string[] | undefined;
themeVersion: string;
loginThemeResourcesFromKeycloakVersion: string;
urlPathname: string | undefined;
keycloakifyBuildDirPath: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
doBuildRetrocompatAccountTheme: boolean;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
themeSrcDirPath: string | undefined;
themeName: string;
themeSrcDirPath: string;
keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}): Promise<void> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email"; isRetrocompat?: true }) => {
const { themeType, isRetrocompat = false } = params;
return pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
`${themeName}${isRetrocompat ? retrocompatPostfix : ""}`,
themeType
);
};
let allCssGlobalsToDefine: Record<string, string> = {};
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
for (const themeType of themeTypes) {
const themeDirPath = getThemeDirPath(themeType);
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0;
if (!isFirstPass && !buildOptions.isStandalone) {
if (!isFirstPass) {
break copy_app_resources_to_theme_path;
}
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"destDirPath": pathJoin(themeTypeDirPath, "resources", "build"),
"srcDirPath": buildOptions.reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
filePath
})
) {
@ -93,10 +82,6 @@ export async function generateTheme(params: {
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
@ -116,43 +101,34 @@ export async function generateTheme(params: {
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
"jsCode": sourceCode.toString("utf8")
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
return { "modifiedSourceCode": sourceCode };
}
});
}
const generateFtlFilesCode = (() => {
if (generateFtlFilesCode_glob !== undefined) {
return generateFtlFilesCode_glob;
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
});
return generateFtlFilesCode;
})();
const generateFtlFilesCode =
generateFtlFilesCode_glob !== undefined
? generateFtlFilesCode_glob
: generateFtlFilesCodeFactory({
themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
}).generateFtlFilesCode;
[
...(() => {
@ -163,74 +139,87 @@ export async function generateTheme(params: {
return accountThemePageIds;
}
})(),
...((() => {
switch (themeType) {
case "login":
return buildOptions.extraLoginPages;
case "account":
return buildOptions.extraAccountPages;
}
})() ?? [])
...readExtraPagesNames({
themeType,
themeSrcDirPath
})
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.mkdirSync(themeTypeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
generateMessageProperties({
themeSrcDirPath,
themeType
}).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
if (fs.existsSync(keycloakDirInPublicDir)) {
break copy_keycloak_resources_to_public;
}
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true });
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir,
themeType
});
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
if (themeType !== themeTypes[0]) {
break copy_keycloak_resources_to_public;
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
}
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
});
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
themeDirPath,
themeType
"keycloakVersion": (() => {
switch (themeType) {
case "account":
return lastKeycloakVersionWithAccountV1;
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
}
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
"usedResources": readStaticResourcesUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
}),
buildOptions
});
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
pathJoin(themeTypeDirPath, "theme.properties"),
Buffer.from(
[
`parent=${(() => {
switch (themeType) {
case "account":
return accountV1;
case "login":
return "keycloak";
}
assert<Equals<typeof themeType, never>>(false);
})()}`,
...(buildOptions.extraThemeProperties ?? [])
].join("\n\n"),
"utf8"
)
);
if (themeType === "account" && buildOptions.doBuildRetrocompatAccountTheme) {
transformCodebase({
"srcDirPath": themeTypeDirPath,
"destDirPath": getThemeTypeDirPath({ themeType, "isRetrocompat": true }),
"transformSourceCode": ({ filePath, sourceCode }) => {
if (pathBasename(filePath) === "theme.properties") {
return {
"modifiedSourceCode": Buffer.from(sourceCode.toString("utf8").replace(`parent=${accountV1}`, "parent=keycloak"), "utf8")
};
}
return { "modifiedSourceCode": sourceCode };
}
});
}
}
email: {
if (themeSrcDirPath === undefined) {
break email;
}
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (!fs.existsSync(emailThemeSrcDirPath)) {
@ -239,7 +228,7 @@ export async function generateTheme(params: {
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email")
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
});
}
}

View File

@ -0,0 +1,39 @@
import { crawl } from "../../tools/crawl";
import { accountThemePageIds, loginThemePageIds } from "../generateFtl";
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 "../../constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;
const filePaths = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
if (candidateFilePaths.length === 0) {
candidateFilePaths.push(...filePaths);
}
const extraPages: string[] = [];
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]));
}
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
switch (themeType) {
case "account":
return !id<readonly string[]>(accountThemePageIds).includes(pageId);
case "login":
return !id<readonly string[]>(loginThemePageIds).includes(pageId);
}
});
}

View File

@ -2,80 +2,16 @@ import { crawl } from "../../tools/crawl";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
import { exclude } from "tsafe/exclude";
import type { ThemeType } from "../../constants";
export function readFieldNameUsage(params: {
keycloakifySrcDirPath: string;
themeSrcDirPath: string | undefined;
themeType: ThemeType | "email";
}): string[] {
/** Assumes the theme type exists */
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const fieldNames: string[] = [];
if (themeSrcDirPath === undefined) {
//If we can't detect the user theme directory we restore the fieldNames we had previously to prevent errors.
fieldNames.push(
...[
"global",
"userLabel",
"username",
"email",
"firstName",
"lastName",
"password",
"password-confirm",
"totp",
"totpSecret",
"SAMLRequest",
"SAMLResponse",
"relayState",
"device_user_code",
"code",
"password-new",
"rememberMe",
"login",
"authenticationExecution",
"cancel-aia",
"clientDataJSON",
"authenticatorData",
"signature",
"credentialId",
"userHandle",
"error",
"authn_use_chk",
"authenticationExecution",
"isSetRetry",
"try-again",
"attestationObject",
"publicKeyCredentialId",
"authenticatorLabel"
]
);
}
for (const srcDirPath of (
[
pathJoin(keycloakifySrcDirPath, themeType),
(() => {
if (themeSrcDirPath === undefined) {
return undefined;
}
const srcDirPath = pathJoin(themeSrcDirPath, themeType);
if (!fs.existsSync(srcDirPath)) {
return undefined;
}
return srcDirPath;
})()
] as const
).filter(exclude(undefined))) {
const filePaths = crawl(srcDirPath)
.filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath))
.map(filePath => pathJoin(srcDirPath, filePath));
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");

View File

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

View File

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

View File

@ -4,27 +4,21 @@ import type { Equals } from "tsafe";
import { z } from "zod";
import { pathJoin } from "../tools/pathJoin";
export const bundlers = ["mvn", "keycloakify", "none"] as const;
export type Bundler = (typeof bundlers)[number];
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
bundler?: Bundler;
keycloakVersionDefaultAssets?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string;
extraThemeNames?: string[];
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
};
@ -34,19 +28,16 @@ export const zParsedPackageJson = z.object({
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraLoginPages": z.array(z.string()).optional(),
"extraAccountPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"bundler": z.enum(bundlers).optional(),
"keycloakVersionDefaultAssets": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.string().optional(),
"extraThemeNames": z.array(z.string()).optional()
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
})
.optional()
});
@ -54,11 +45,11 @@ export const zParsedPackageJson = z.object({
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
export function getParsedPackageJson(params: { projectDirPath: string }) {
const { projectDirPath } = params;
export function getParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;
}
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")));
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
return parsedPackageJson;
}

View File

@ -1,31 +1,6 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
};
export type ExternalAssets = {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): { fixedJsCode: string } {
/*
NOTE:
@ -38,48 +13,38 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
will always run in keycloak context.
*/
const { jsCode, buildOptions } = params;
const { jsCode } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
(...[, n, u, e]) => `
${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
return `
${n}[(function(){
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
? `
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){}
});
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function() {}
});
`
}
}
return "${u}";
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
.replace(/\s+/g, " ")
.trim();
}
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
.replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
.replace(
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
(...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
);
return { fixedJsCode };

View File

@ -1,20 +1,12 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = {
urlPathname: string | undefined;
};
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string;
@ -24,7 +16,7 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
);

View File

@ -1,32 +1,11 @@
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export type BuildOptionsLike = {
urlPathname: string | undefined;
};
export namespace BuildOptionsLike {
export type Common = {
urlPathname: string | undefined;
};
export type Standalone = Common & {
isStandalone: true;
};
export type ExternalAssets = Common & {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
fixedCssCode: string;
@ -37,10 +16,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(${
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
})`
(...[, group]) => `url(\${url.resourcesPath}/build/${group})`
);
return { fixedCssCode };

View File

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

View File

@ -1,6 +1,7 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
export async function promptKeycloakVersion() {
const { getLatestsSemVersionedTag } = (() => {
@ -17,7 +18,7 @@ export async function promptKeycloakVersion() {
return { getLatestsSemVersionedTag };
})();
console.log("Initialize the directory with email template from which keycloak version?");
console.log("Select Keycloak version?");
const tags = [
...(await getLatestsSemVersionedTag({
@ -26,6 +27,7 @@ export async function promptKeycloakVersion() {
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
lastKeycloakVersionWithAccountV1,
"11.0.3"
];

View File

@ -1,27 +1,32 @@
import * as fs from "fs";
import * as path from "path";
/** List all files in a given directory return paths relative to the dir_path */
export const crawl = (() => {
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
continue;
}
paths.push(file_path);
continue;
}
};
return function crawl(dir_path: string): string[] {
const paths: string[] = [];
paths.push(file_path);
}
};
crawlRec(dir_path, paths);
/** 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[] {
const { dirPath, returnedPathsType } = params;
return paths.map(file_path => path.relative(dir_path, file_path));
};
})();
const filePaths: string[] = [];
crawlRec(dirPath, filePaths);
switch (returnedPathsType) {
case "absolute":
return filePaths;
case "relative to dirPath":
return filePaths.map(filePath => path.relative(dirPath, filePath));
}
}

View File

@ -1,18 +1,55 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, readFile, stat, writeFile } from "fs/promises";
import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import { promisify } from "util";
import { getProjectRoot } from "./getProjectRoot";
import { transformCodebase } from "./transformCodebase";
import { unzip } from "./unzip";
import { unzip, zip } from "./unzip";
const exec = promisify(execCallback);
function hash(s: string) {
return createHash("sha256").update(s).digest("hex");
function generateFileNameFromURL(params: {
url: string;
preCacheTransform:
| {
actionCacheId: string;
actionFootprint: string;
}
| undefined;
}): string {
const { preCacheTransform } = params;
// Parse the URL
const url = new URL(params.url);
// Extract pathname and remove leading slashes
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
// Optionally, add query parameters replacing special characters
if (url.search) {
fileName += url.search.replace(/[&=?]/g, "-");
}
// Replace any characters that are not valid in filenames
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
// Trim or pad the fileName to a specific length
fileName = fileName.substring(0, 50);
add_pre_cache_transform: {
if (preCacheTransform === undefined) {
break add_pre_cache_transform;
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
}
return fileName;
}
async function exists(path: string) {
@ -48,9 +85,26 @@ async function getNmpConfig() {
return readNpmConfig().then(parseNpmConfig);
}
async function readNpmConfig() {
const { stdout } = await exec("npm config get", { encoding: "utf8" });
return stdout;
function readNpmConfig(): Promise<string> {
return (async function callee(depth: number): Promise<string> {
const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
let stdout: string;
try {
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep);
return callee(depth + 1);
}
throw error;
}
return stdout;
})(0);
}
function parseNpmConfig(stdout: string) {
@ -96,14 +150,42 @@ async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy"
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
}
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
export async function downloadAndUnzip(
params: {
url: string;
destDirPath: string;
specificDirsToExtract?: string[];
preCacheTransform?: {
actionCacheId: string;
action: (params: { destDirPath: string }) => Promise<void>;
};
} & (
| {
doUseCache: true;
cacheDirPath: string;
}
| {
doUseCache: false;
}
)
) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
const projectRoot = getProjectRoot();
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
const zipFileBasename = generateFileNameFromURL({
url,
"preCacheTransform":
preCacheTransform === undefined
? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
}
});
const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
if (!(await exists(zipFilePath))) {
const opts = await getFetchOptions();
@ -119,12 +201,32 @@ export async function downloadAndUnzip(params: { url: string; destDirPath: strin
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
if (specificDirsToExtract !== undefined || preCacheTransform !== undefined) {
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
}
}
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
await unzip(zipFilePath, extractDirPath);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
if (!rest.doUseCache) {
await rm(cacheDirPath, { "recursive": true });
} else {
await rm(extractDirPath, { "recursive": true });
}
}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import * as path from "path";
import { crawl } from "./crawl";
import { id } from "tsafe/id";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string }) =>
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
| {
modifiedSourceCode: Buffer;
newFileName?: string;
@ -20,26 +20,27 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
}))
} = params;
for (const file_relative_path of crawl(srcDirPath)) {
const filePath = path.join(srcDirPath, file_relative_path);
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, fileRelativePath);
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path)
filePath,
fileRelativePath
});
if (transformSourceCodeResult === undefined) {
continue;
}
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
"recursive": true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
modifiedSourceCode
);
}

View File

@ -2,6 +2,7 @@ import fsp from "node:fs/promises";
import fs from "fs";
import path from "node:path";
import yauzl from "yauzl";
import yazl from "yazl";
import stream from "node:stream";
import { promisify } from "node:util";
@ -19,11 +20,16 @@ async function pathExists(path: string) {
}
}
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
// add trailing slash to unzipSubPath and targetFolder
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
unzipSubPath += "/";
}
// Handlings of non posix path is not implemented correctly
// it work by coincidence. Don't have the time to fix but it should be fixed.
export async function unzip(file: string, targetFolder: string, specificDirsToExtract?: string[]) {
specificDirsToExtract = specificDirsToExtract?.map(dirPath => {
if (!dirPath.endsWith("/") || !dirPath.endsWith("\\")) {
dirPath += "/";
}
return dirPath;
});
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
targetFolder += "/";
@ -42,15 +48,17 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
zipfile.readEntry();
zipfile.on("entry", async entry => {
if (unzipSubPath) {
if (specificDirsToExtract !== undefined) {
const dirPath = specificDirsToExtract.find(dirPath => entry.fileName.startsWith(dirPath));
// Skip files outside of the unzipSubPath
if (!entry.fileName.startsWith(unzipSubPath)) {
if (dirPath === undefined) {
zipfile.readEntry();
return;
}
// Remove the unzipSubPath from the file name
entry.fileName = entry.fileName.substring(unzipSubPath.length);
entry.fileName = entry.fileName.substring(dirPath.length);
}
const target = path.join(targetFolder, entry.fileName);
@ -77,6 +85,8 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
return;
}
await fsp.mkdir(path.dirname(target), { "recursive": true });
await pipeline(readStream, fs.createWriteStream(target));
zipfile.readEntry();
@ -90,3 +100,42 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
});
});
}
// NOTE: This code was directly copied from ChatGPT and appears to function as expected.
// However, confidence in its complete accuracy and robustness is limited.
export async function zip(sourceFolder: string, targetZip: string) {
return new Promise<void>(async (resolve, reject) => {
const zipfile = new yazl.ZipFile();
const files: string[] = [];
// Recursive function to explore directories and their subdirectories
async function exploreDir(dir: string) {
const dirContent = await fsp.readdir(dir);
for (const file of dirContent) {
const filePath = path.join(dir, file);
const stat = await fsp.stat(filePath);
if (stat.isDirectory()) {
await exploreDir(filePath);
} else if (stat.isFile()) {
files.push(filePath);
}
}
}
// Collecting all files to be zipped
await exploreDir(sourceFolder);
// Adding files to zip
for (const file of files) {
const relativePath = path.relative(sourceFolder, file);
zipfile.addFile(file, relativePath);
}
zipfile.outputStream
.pipe(fs.createWriteStream(targetZip))
.on("close", () => resolve())
.on("error", err => reject(err)); // Listen to error events
zipfile.end();
});
}

View File

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

View File

@ -1,21 +1,15 @@
import { useReducer, useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { clsx } from "keycloakify/tools/clsx";
export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean;
stylesCommon?: string[];
styles?: string[];
scripts?: string[];
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
htmlClassName: string | undefined;
bodyClassName: string | undefined;
}) {
const { doFetchDefaultThemeResources, stylesCommon = [], styles = [], url, scripts = [], htmlClassName, bodyClassName } = params;
const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
@ -29,38 +23,30 @@ export function usePrepareTemplate(params: {
const removeArray: (() => void)[] = [];
(async () => {
const prLoadedArray: Promise<void>[] = [];
[
...stylesCommon.map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...styles.map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.forEach(href => {
const { prLoaded, remove } = headInsert({
"type": "css",
"position": "prepend",
href
});
removeArray.push(remove);
prLoadedArray.push(prLoaded);
for (const style of [...styles].reverse()) {
const { prLoaded, remove } = headInsert({
"type": "css",
"position": "prepend",
"href": style
});
await Promise.all(prLoadedArray);
removeArray.push(remove);
if (isUnmounted) {
return;
// TODO: Find a way to do that in parallel (without breaking the order)
await prLoaded;
if (isUnmounted) {
return;
}
}
setReady();
})();
scripts.forEach(relativePath => {
scripts.forEach(src => {
const { remove } = headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
src
});
removeArray.push(remove);

View File

@ -12,6 +12,8 @@ const Error = lazy(() => import("keycloakify/login/pages/Error"));
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail"));
const Terms = lazy(() => import("keycloakify/login/pages/Terms"));
const LoginDeviceVerifyUserCode = lazy(() => import("keycloakify/login/pages/LoginDeviceVerifyUserCode"));
const LoginOauthGrant = lazy(() => import("keycloakify/login/pages/LoginOauthGrant"));
const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
@ -52,6 +54,10 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
case "terms.ftl":
return <Terms kcContext={kcContext} {...rest} />;
case "login-oauth2-device-verify-user-code.ftl":
return <LoginDeviceVerifyUserCode kcContext={kcContext} {...rest} />;
case "login-oauth-grant.ftl":
return <LoginOauthGrant kcContext={kcContext} {...rest} />;
case "login-otp.ftl":
return <LoginOtp kcContext={kcContext} {...rest} />;
case "login-username.ftl":

View File

@ -31,21 +31,22 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
url,
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
`${url.resourcesPath}/css/login.css`
],
"styles": ["css/login.css"],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
"bodyClassName": getClassName("kcBodyClass")
});
if (!isReady) {
return null;
}
document.title = i18n.msgStr("loginTitle", kcContext.realm.displayName);
return (
<div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={getClassName("kcHeaderClass")}>

View File

@ -21,6 +21,7 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
};
export type ClassKey =
| "kcBodyClass"
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
@ -93,4 +94,6 @@ export type ClassKey =
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass";
| "kcFormOptionsWrapperClass"
| "kcFormButtonsWrapperClass"
| "kcInputGroup";

View File

@ -5,6 +5,7 @@ export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
export type { LoginThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -1,4 +1,5 @@
import type { LoginThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { type ThemeType } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
@ -18,6 +19,8 @@ export type KcContext =
| KcContext.LoginResetPassword
| KcContext.LoginVerifyEmail
| KcContext.Terms
| KcContext.LoginDeviceVerifyUserCode
| KcContext.LoginOauthGrant
| KcContext.LoginOtp
| KcContext.LoginUsername
| KcContext.WebauthnAuthenticate
@ -37,6 +40,7 @@ export type KcContext =
export declare namespace KcContext {
export type Common = {
themeVersion: string;
keycloakifyVersion: string;
themeType: "login";
themeName: string;
@ -78,6 +82,7 @@ export declare namespace KcContext {
clientId: string;
name?: string;
description?: string;
attributes: Record<string, string>;
};
isAppInitiatedAction: boolean;
messagesPerField: {
@ -145,7 +150,7 @@ export declare namespace KcContext {
rememberMe?: string;
password?: string;
};
usernameEditDisabled: boolean;
usernameHidden?: boolean;
social: {
displayInfo: boolean;
providers?: {
@ -241,6 +246,27 @@ export declare namespace KcContext {
pageId: "terms.ftl";
};
export type LoginDeviceVerifyUserCode = Common & {
pageId: "login-oauth2-device-verify-user-code.ftl";
url: {
oauth2DeviceVerificationAction: string;
};
};
export type LoginOauthGrant = Common & {
pageId: "login-oauth-grant.ftl";
oauth: {
code: string;
client: string;
clientScopesRequested: {
consentScreenText: string;
}[];
};
url: {
oauthAction: string;
};
};
export type LoginOtp = Common & {
pageId: "login-otp.ftl";
otpLogin: {

View File

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

View File

@ -1,13 +1,11 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const attributes: Attribute[] = [
{
"validators": {
@ -102,14 +100,19 @@ const attributes: Attribute[] = [
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "login",
"themeName": "my-theme-name",
"url": {
"loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
},
@ -231,20 +234,19 @@ export const kcContextCommonMock: KcContext.Common = {
"showTryAnotherWayLink": false
},
"client": {
"clientId": "myApp"
"clientId": "myApp",
"attributes": {}
},
"scripts": [],
"message": {
"type": "success",
"summary": "This is a test message"
},
"isAppInitiatedAction": false
};
const loginUrl = {
...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
"oauth2DeviceVerificationAction": "/auth/realms/myrealm/device",
"oauthAction": "/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
};
export const kcContextMocks = [
@ -264,7 +266,7 @@ export const kcContextMocks = [
"social": {
"displayInfo": true
},
"usernameEditDisabled": false,
"usernameHidden": false,
"login": {},
"registrationDisabled": false
}),
@ -313,7 +315,8 @@ export const kcContextMocks = [
"actionUri": "#",
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
}
}),
id<KcContext.Error>({
@ -321,7 +324,8 @@ export const kcContextMocks = [
"pageId": "error.ftl",
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
},
"message": {
"type": "error",
@ -348,6 +352,25 @@ export const kcContextMocks = [
...kcContextCommonMock,
"pageId": "terms.ftl"
}),
id<KcContext.LoginDeviceVerifyUserCode>({
...kcContextCommonMock,
"pageId": "login-oauth2-device-verify-user-code.ftl",
url: loginUrl
}),
id<KcContext.LoginOauthGrant>({
...kcContextCommonMock,
"pageId": "login-oauth-grant.ftl",
oauth: {
code: "5-1N4CIzfi1aprIQjmylI-9e3spLCWW9i5d-GDcs-Sw",
clientScopesRequested: [
{ consentScreenText: "${profileScopeConsentText}" },
{ consentScreenText: "${rolesScopeConsentText}" },
{ consentScreenText: "${emailScopeConsentText}" }
],
client: "account"
},
url: loginUrl
}),
id<KcContext.LoginOtp>({
...kcContextCommonMock,
"pageId": "login-otp.ftl",
@ -476,7 +499,8 @@ export const kcContextMocks = [
},
"client": {
"clientId": "myApp",
"baseUrl": "#"
"baseUrl": "#",
"attributes": {}
},
"logoutConfirm": { "code": "123", skipLink: false }
}),

View File

@ -25,16 +25,7 @@ export function useFormValidation(params: {
passwordValidators?: Validators;
i18n: I18n;
}) {
const {
kcContext,
passwordValidators = {
"length": {
"ignore.empty.value": true,
"min": "4"
}
},
i18n
} = params;
const { kcContext, passwordValidators = {}, i18n } = params;
const attributesWithPassword = useMemo(
() =>
@ -211,7 +202,7 @@ function useGetErrors(params: {
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
block: {
if (defaultValue !== value) {
if ((defaultValue ?? "") !== value) {
break block;
}

View File

@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcBodyClass": undefined,
"kcHtmlClass": "login-pf",
"kcLoginClass": "login-pf-page",
"kcContentWrapperClass": "row",
@ -44,6 +45,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcInputClass": "form-control",
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormButtonsWrapperClass": undefined,
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormSettingClass": "login-pf-settings",
@ -65,6 +67,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
// css classes for input
"kcInputLargeClass": "input-lg",
"kcInputGroup": "pf-c-input-group",
// css classes for form accessability
"kcSrOnlyClass": "sr-only",

View File

@ -8,7 +8,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined);
assert(
kcContext.message !== undefined,
"No message in kcContext.message, there will always be a message in production context, add it in your mock"
);
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;

View File

@ -14,7 +14,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
classes
});
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { social, realm, url, usernameHidden, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
@ -37,22 +37,18 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
displayInfo={social.displayInfo}
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
infoNode={
realm.password &&
realm.registrationAllowed &&
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
}
>
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && getClassName("kcContentWrapperClass"))}>
@ -66,40 +62,37 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={getClassName("kcFormGroupClass")}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
{!usernameHidden &&
(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return (
<>
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={getClassName("kcInputClass")}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off"
})}
/>
</>
);
})()}
return (
<>
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={getClassName("kcInputClass")}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
autoFocus={true}
autoComplete="off"
/>
</>
);
})()}
</div>
<div className={getClassName("kcFormGroupClass")}>
<label htmlFor="password" className={getClassName("kcLabelClass")}>
@ -116,7 +109,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
</div>
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
<div id="kc-form-options">
{realm.rememberMe && !usernameEditDisabled && (
{realm.rememberMe && !usernameHidden && (
<div className="checkbox">
<label>
<input

View File

@ -32,7 +32,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
<ul id="kc-totp-supported-apps">
{totp.supportedApplications.map(app => (
<li>{msgStr(app as MessageKey, app)}</li>
<li>{msg(app as MessageKey)}</li>
))}
</ul>
</li>

View File

@ -0,0 +1,67 @@
import { clsx } from "keycloakify/tools/clsx";
import { I18n } from "../i18n";
import { KcContext } from "../kcContext";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { PageProps } from "./PageProps";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url } = kcContext;
const { msg, msgStr } = i18n;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("oauth2DeviceVerificationTitle")}>
<form
id="kc-user-verify-device-user-code-form"
className={getClassName("kcFormClass")}
action={url.oauth2DeviceVerificationAction}
method="post"
>
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="device-user-code" className={getClassName("kcLabelClass")}>
{msg("verifyOAuth2DeviceUserCode")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
id="device-user-code"
name="device_user_code"
autoComplete="off"
type="text"
className={getClassName("kcInputClass")}
autoFocus
/>
</div>
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<div className={getClassName("kcFormButtonsWrapperClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
type="submit"
value={msgStr("doSubmit")}
/>
</div>
</div>
</div>
</form>
</Template>
);
}

View File

@ -0,0 +1,72 @@
import { clsx } from "keycloakify/tools/clsx";
import { PageProps } from "./PageProps";
import { KcContext } from "../kcContext";
import { I18n } from "../i18n";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, oauth, client } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={msg("oauthGrantTitle", client.name ? advancedMsgStr(client.name) : client.clientId)}
>
<div id="kc-oauth" className="content-area">
<h3>{msg("oauthGrantRequest")}</h3>
<ul>
{oauth.clientScopesRequested.map(clientScope => (
<li key={clientScope.consentScreenText}>
<span>{advancedMsg(clientScope.consentScreenText)}</span>
</li>
))}
</ul>
<form className="form-actions" action={url.oauthAction} method="POST">
<input type="hidden" name="code" value={oauth.code} />
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options">
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
</div>
<div id="kc-form-buttons">
<div className={getClassName("kcFormButtonsWrapperClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="accept"
id="kc-login"
type="submit"
value={msgStr("doYes")}
/>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass")
)}
name="cancel"
id="kc-cancel"
type="submit"
value={msgStr("doNo")}
/>
</div>
</div>
</div>
</form>
<div className="clearfix"></div>
</div>
</Template>
);
}

View File

@ -1,6 +1,5 @@
import { useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
@ -24,7 +23,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
"src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
});
(async () => {

View File

@ -123,7 +123,7 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
getClassName("kcButtonLargeClass")
)}
type="submit"
defaultValue={msgStr("doSubmit")}
value={msgStr("doSubmit")}
/>
)}
</div>

View File

@ -1,9 +1,9 @@
import type { LazyExoticComponent } from "react";
import type { I18n } from "keycloakify/login/i18n";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
export type PageProps<KcContext, I18nExtended extends I18n> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

3
src/tools/LazyOrNot.ts Normal file
View File

@ -0,0 +1,3 @@
import type { LazyExoticComponent, ComponentType } from "react";
export type LazyOrNot<Component extends ComponentType<any>> = LazyExoticComponent<Component> | Component;

View File

@ -22,10 +22,11 @@ const meta: ComponentMeta<any> = {
export default meta;
export const Default = () => <PageStory />;
export const WithNoMessage = () => (
export const WithMessage = () => (
<PageStory
kcContext={{
message: undefined
message: { type: "success", summary: "This is a test message" }
}}
/>
);

View File

@ -21,4 +21,13 @@ const meta: ComponentMeta<any> = {
export default meta;
export const Default = () => <PageStory />;
export const Default = () => (
<PageStory
kcContext={{
message: {
summary: "This is the server message",
type: "info"
}
}}
/>
);

View File

@ -74,8 +74,12 @@ export const WithPresetUsername = () => (
export const WithImmutablePresetUsername = () => (
<PageStory
kcContext={{
login: { username: "max.mustermann@mail.com" },
usernameEditDisabled: true
auth: {
attemptedUsername: "max.mustermann@mail.com",
showUsername: true
},
usernameHidden: true,
message: { type: "info", summary: "Please re-authenticate to continue" }
}}
/>
);

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-oauth2-device-verify-user-code.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-oauth-grant.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

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

View File

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

View File

@ -32,13 +32,14 @@ describe("bin/js-transforms", () => {
908:"67c9ed2c"
}[e]+".chunk.css"
}
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
it("transforms standalone code properly", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"isStandalone": true
}
"jsCode": jsCodeUntransformed
});
const fixedJsCodeExpected = `
@ -55,11 +56,11 @@ describe("bin/js-transforms", () => {
}
__webpack_require__[(function (){
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
var pd = Object.getOwnPropertyDescriptor(__webpack_require__, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(__webpack_require__, "p", {
get: function() { return window.kcContext.url.resourcesPath; },
set: function (){}
set: function() {}
});
}
return "u";
@ -72,11 +73,11 @@ describe("bin/js-transforms", () => {
}
t[(function (){
var pd= Object.getOwnPropertyDescriptor(t, "p");
var pd = Object.getOwnPropertyDescriptor(t, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(t, "p", {
get: function() { return window.kcContext.url.resourcesPath; },
set: function (){}
set: function() {}
});
}
return "miniCssF";
@ -86,67 +87,28 @@ describe("bin/js-transforms", () => {
908:"67c9ed2c"
} [e] + ".chunk.css"
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("transforms external app code properly", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev"
}
});
const fixedJsCodeExpected = `
function f() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
__webpack_require__[(function (){
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
n[(function(){
var pd = Object.getOwnPropertyDescriptor(n, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(__webpack_require__, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
Object.defineProperty(n, "p", {
get: function() { return window.kcContext.url.resourcesPath; },
set: function() {}
});
}
return "u";
})()] = function(e) {
return "static/js/" + e + "." + {
147: "6c5cee76",
787: "8da10fcf",
922: "be170a73"
} [e] + ".chunk.js"
}
t[(function (){
var pd= Object.getOwnPropertyDescriptor(t, "p");
})()] = e => "/build/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
t[(function(){
var pd = Object.getOwnPropertyDescriptor(t, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(t, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
get: function() { return window.kcContext.url.resourcesPath; },
set: function() {}
});
}
return "miniCssF";
})()] = function(e) {
return "static/css/" + e + "." + {
164:"dcfd7749",
908:"67c9ed2c"
} [e] + ".chunk.css"
}
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
@ -162,7 +124,7 @@ describe("bin/css-transforms", () => {
}
.my-div2 {
background: url(/logo192.png) no-repeat center center;
background: url(/logo192.png) repeat center center;
}
.my-div {
@ -173,11 +135,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = `
.my-div {
background: var(--url1f9ef5a892c104c);
background: var(--urla882a969fd39473) no-repeat center center;
}
.my-div2 {
background: var(--url1f9ef5a892c104c);
background: var(--urla882a969fd39473) repeat center center;
}
.my-div {
@ -188,7 +150,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
"url1f9ef5a892c104c": "url(/logo192.png) no-repeat center center",
"urla882a969fd39473": "url(/logo192.png)",
"urldd75cab58377c19": "url(/static/media/something.svg)"
};
@ -203,7 +165,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = `
:root {
--url1f9ef5a892c104c: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center;
--urla882a969fd39473: url(\${url.resourcesPath}/build/logo192.png);
--urldd75cab58377c19: url(\${url.resourcesPath}/build/static/media/something.svg);
}
`;
@ -229,11 +191,11 @@ describe("bin/css-transforms", () => {
const fixedCssCodeExpected = `
.my-div {
background: var(--urlf8277cddaa2be78);
background: var(--url749a3139386b2c8) no-repeat center center;
}
.my-div2 {
background: var(--urlf8277cddaa2be78);
background: var(--url749a3139386b2c8) no-repeat center center;
}
.my-div {
@ -244,7 +206,7 @@ describe("bin/css-transforms", () => {
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
"urlf8277cddaa2be78": "url(/x/y/z/logo192.png) no-repeat center center",
"url749a3139386b2c8": "url(/x/y/z/logo192.png)",
"url8bdc0887b97ac9a": "url(/x/y/z/static/media/something.svg)"
};
@ -259,7 +221,7 @@ describe("bin/css-transforms", () => {
const cssCodeToPrependInHeadExpected = `
:root {
--urlf8277cddaa2be78: url(\${url.resourcesPath}/build/logo192.png) no-repeat center center;
--url749a3139386b2c8: url(\${url.resourcesPath}/build/logo192.png);
--url8bdc0887b97ac9a: url(\${url.resourcesPath}/build/static/media/something.svg);
}
`;
@ -304,7 +266,6 @@ describe("bin/css-inline-transforms", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": true,
"urlPathname": undefined
}
});
@ -344,53 +305,6 @@ describe("bin/css-inline-transforms", () => {
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
it("transforms css for external app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev",
"urlPathname": undefined
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
});
@ -430,7 +344,6 @@ describe("bin/css-inline-transforms", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": true,
"urlPathname": "/x/y/z/"
}
});
@ -470,53 +383,6 @@ describe("bin/css-inline-transforms", () => {
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
it("transforms css for external app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev",
"urlPathname": "/x/y/z/"
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
});

View File

@ -12,7 +12,8 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
async function setupSampleReactProject(destDir: string) {
await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDir
"destDirPath": destDir,
"doUseCache": false
});
}
let parsedPackageJson: Record<string, unknown> = {};
@ -23,7 +24,7 @@ vi.mock("keycloakify/bin/keycloakify/parsed-package-json", async () => ({
vi.mock("keycloakify/bin/promptKeycloakVersion", async () => ({
...((await vi.importActual("keycloakify/bin/promptKeycloakVersion")) as Record<string, unknown>),
promptKeycloakVersion: () => ({ keycloakVersion: "11.0.3" })
promptKeycloakVersion: () => ({ "keycloakVersion": "11.0.3" })
}));
const nativeCwd = process.cwd;
@ -51,17 +52,25 @@ describe("Sample Project", () => {
await setupSampleReactProject(sampleReactProjectDirPath);
await initializeEmailTheme();
const reactAppRootDirPath = process.cwd();
const destDirPath = pathJoin(
readBuildOptions({
"processArgv": ["--silent"],
"projectDirPath": process.cwd()
reactAppRootDirPath
}).keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", "isSilent": false });
await downloadBuiltinKeycloakTheme({
destDirPath,
"keycloakVersion": "11.0.3",
"buildOptions": {
"cacheDirPath": pathJoin(reactAppRootDirPath, "node_modules", ".cache", "keycloakify")
}
});
},
{ timeout: 90000 }
);
@ -77,17 +86,25 @@ describe("Sample Project", () => {
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
await initializeEmailTheme();
const reactAppRootDirPath = process.cwd();
const destDirPath = pathJoin(
readBuildOptions({
"processArgv": ["--silent"],
"projectDirPath": process.cwd()
reactAppRootDirPath
}).keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", "isSilent": false });
await downloadBuiltinKeycloakTheme({
destDirPath,
"keycloakVersion": "11.0.3",
buildOptions: {
"cacheDirPath": pathJoin(reactAppRootDirPath, "node_modules", ".cache", "keycloakify")
}
});
},
{ timeout: 90000 }
);

View File

@ -3,6 +3,7 @@ import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
export async function setupSampleReactProject(destDirPath: string) {
await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDirPath
"destDirPath": destDirPath,
"doUseCache": false
});
}

View File

@ -0,0 +1,68 @@
import path from "path";
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
import { crawl } from "keycloakify/bin/tools/crawl";
describe("crawl", () => {
describe("crawRec", () => {
beforeAll(() => {
vi.mock("node:fs", async () => {
const mod = await vi.importActual<typeof import("fs")>("fs");
return {
...mod,
readdirSync: vi.fn().mockImplementation((dir_path: string) => {
switch (dir_path) {
case "root_dir":
return ["sub_1_dir", "file_1", "sub_2_dir", "file_2"];
case path.join("root_dir", "sub_1_dir"):
return ["file_3", "sub_3_dir", "file_4"];
case path.join("root_dir", "sub_1_dir", "sub_3_dir"):
return ["file_5"];
case path.join("root_dir", "sub_2_dir"):
return [];
default: {
const enoent = new Error(`ENOENT: no such file or directory, scandir '${dir_path}'`);
// @ts-ignore
enoent.code = "ENOENT";
// @ts-ignore
enoent.syscall = "open";
// @ts-ignore
enoent.path = dir_path;
throw enoent;
}
}
}),
lstatSync: vi.fn().mockImplementation((file_path: string) => {
return { isDirectory: () => file_path.endsWith("_dir") };
})
};
});
});
afterAll(() => {
vi.resetAllMocks();
});
it("returns files under a given dir_path", async () => {
const paths = crawl({ "dirPath": "root_dir/sub_1_dir/sub_3_dir", "returnedPathsType": "absolute" });
expect(paths).toEqual(["root_dir/sub_1_dir/sub_3_dir/file_5"]);
});
it("returns files recursively under a given dir_path", async () => {
const paths = crawl({ "dirPath": "root_dir", "returnedPathsType": "absolute" });
expect(paths).toEqual([
"root_dir/sub_1_dir/file_3",
"root_dir/sub_1_dir/sub_3_dir/file_5",
"root_dir/sub_1_dir/file_4",
"root_dir/file_1",
"root_dir/file_2"
]);
});
it("throw dir_path does not exist", async () => {
try {
crawl({ "dirPath": "404", "returnedPathsType": "absolute" });
} catch {
expect(true);
return;
}
expect(false);
});
});
});

View File

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

124
yarn.lock
View File

@ -75,6 +75,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d"
integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==
dependencies:
"@babel/types" "^7.22.5"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@ -269,11 +279,21 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-string-parser@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
@ -312,6 +332,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17"
integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
"@babel/parser@^7.22.7":
version "7.22.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@ -1120,6 +1145,15 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe"
integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.4.4":
version "7.21.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4"
@ -2763,6 +2797,13 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==
"@types/babel__generator@^7.6.4":
version "7.6.4"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7"
integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==
dependencies:
"@babel/types" "^7.0.0"
"@types/chai-subset@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@ -3816,6 +3857,16 @@ assert@^1.1.1:
object-assign "^4.1.1"
util "0.10.3"
assert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
dependencies:
es6-object-assign "^1.1.0"
is-nan "^1.2.1"
object-is "^1.0.1"
util "^0.12.0"
assertion-error@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
@ -3833,6 +3884,13 @@ ast-types@^0.14.2:
dependencies:
tslib "^2.0.1"
ast-types@^0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2"
integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==
dependencies:
tslib "^2.0.1"
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@ -5739,6 +5797,11 @@ es6-iterator@^2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-object-assign@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
es6-shim@^0.35.5:
version "0.35.8"
resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.8.tgz#89216f6fbf8bacba3f897c8c0e814d2a41c05fb7"
@ -5853,7 +5916,7 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
esprima@^4.0.0, esprima@^4.0.1:
esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@ -7177,7 +7240,7 @@ is-alphanumerical@^1.0.0:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-arguments@^1.1.1:
is-arguments@^1.0.4, is-arguments@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@ -7351,6 +7414,13 @@ is-function@^1.0.2:
resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08"
integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==
is-generator-function@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
dependencies:
has-tostringtag "^1.0.0"
is-glob@^3.0.0, is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@ -7380,6 +7450,14 @@ is-map@^2.0.1, is-map@^2.0.2:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-nan@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
is-negative-zero@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@ -7496,6 +7574,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9:
gopd "^1.0.1"
has-tostringtag "^1.0.0"
is-typed-array@^1.1.3:
version "1.1.12"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a"
integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==
dependencies:
which-typed-array "^1.1.11"
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@ -8769,7 +8854,7 @@ object-inspect@^1.12.3, object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
object-is@^1.1.5:
object-is@^1.0.1, object-is@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
@ -9916,6 +10001,17 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
recast@^0.23.3:
version "0.23.3"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.3.tgz#f205d1f46b2c6f730de413ab18f96c166263d85f"
integrity sha512-HbCVFh2ANP6a09nzD4lx7XthsxMOJWKX5pIcUwtLrmeEIl3I0DwjCoVXDE0Aobk+7k/mS3H50FK4iuYArpcT6Q==
dependencies:
assert "^2.0.0"
ast-types "^0.16.1"
esprima "~4.0.0"
source-map "~0.6.1"
tslib "^2.0.1"
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@ -11750,6 +11846,17 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
util@^0.12.0:
version "0.12.5"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
which-typed-array "^1.1.2"
utila@~0.4:
version "0.4.0"
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
@ -12085,6 +12192,17 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35"
integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==
which-typed-array@^1.1.11, which-typed-array@^1.1.2:
version "1.1.11"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.0"
which-typed-array@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"