Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
7d6476c1b5 | |||
e892a0e7e6 | |||
ca5b41e730 | |||
9b18234112 | |||
5274368f47 | |||
1415c24028 | |||
4a084f5859 | |||
a30c9eb0cd | |||
85d3b40b8e | |||
335afec230 | |||
69fa49848a | |||
7a09051127 | |||
07ee0ecb8b | |||
6f133428f8 | |||
4f733736db | |||
d96ff13a67 | |||
2c1351ce47 | |||
96cd56ec77 | |||
e5b2096d65 | |||
3aa140335f | |||
4cafaa2492 | |||
9c633a7521 | |||
e27845ba91 | |||
2a8708a45b | |||
6874fa4c24 | |||
ba531a4927 | |||
20175b57cf | |||
ad275e4c34 | |||
060b9fe0de | |||
17b24d14ed | |||
2d278b0680 | |||
fb5975e4f1 | |||
24fccaf513 | |||
293953aa1b | |||
1049e312f9 |
40
CHANGELOG.md
40
CHANGELOG.md
@ -1,3 +1,43 @@
|
|||||||
|
### **0.3.14** (2021-03-28)
|
||||||
|
|
||||||
|
- Fix standalone mode: imports from js
|
||||||
|
|
||||||
|
### **0.3.13** (2021-03-26)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### **0.3.12** (2021-03-26)
|
||||||
|
|
||||||
|
- Fix mocksContext
|
||||||
|
|
||||||
|
### **0.3.11** (2021-03-26)
|
||||||
|
|
||||||
|
- Fix previous build, improve README
|
||||||
|
|
||||||
|
### **0.3.10** (2021-03-26)
|
||||||
|
|
||||||
|
- Handle <style> tag, improve documentation
|
||||||
|
|
||||||
|
### **0.3.9** (2021-03-25)
|
||||||
|
|
||||||
|
- Update readme
|
||||||
|
- Document --external-assets
|
||||||
|
- Update README.md
|
||||||
|
- Update README.md
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
### **0.3.8** (2021-03-22)
|
||||||
|
|
||||||
|
- Make standalone mode the default
|
||||||
|
|
||||||
|
### **0.3.7** (2021-03-22)
|
||||||
|
|
||||||
|
- (test) external asset mode by default
|
||||||
|
|
||||||
|
### **0.3.6** (2021-03-22)
|
||||||
|
|
||||||
|
- Fix previous release
|
||||||
|
|
||||||
### **0.3.5** (2021-03-22)
|
### **0.3.5** (2021-03-22)
|
||||||
|
|
||||||
- support homepage with urlPath
|
- support homepage with urlPath
|
||||||
|
144
README.md
144
README.md
@ -20,8 +20,10 @@
|
|||||||
|
|
||||||
The problem:
|
The problem:
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<i>Without keycloakify:</i><br>
|
||||||
|
<img src="https://user-images.githubusercontent.com/6702424/108838381-dbbbcf80-75d3-11eb-8ae8-db41563ef9db.gif">
|
||||||
|
</p>
|
||||||
When we redirected to Keycloak the user suffers from a harsh context switch.
|
When we redirected to Keycloak the user suffers from a harsh context switch.
|
||||||
Keycloak does offer a way to customize theses pages but it requires a lot of raw HTML/CSS hacking
|
Keycloak does offer a way to customize theses pages but it requires a lot of raw HTML/CSS hacking
|
||||||
to reproduce the look and feel of a specific app. Not mentioning the maintenance cost of such an endeavour.
|
to reproduce the look and feel of a specific app. Not mentioning the maintenance cost of such an endeavour.
|
||||||
@ -29,54 +31,105 @@ to reproduce the look and feel of a specific app. Not mentioning the maintenance
|
|||||||
Wouldn't it be great if we could just design the login and register pages as if they where part of our app?
|
Wouldn't it be great if we could just design the login and register pages as if they where part of our app?
|
||||||
Here is `yarn add keycloakify` for you 🍸
|
Here is `yarn add keycloakify` for you 🍸
|
||||||
|
|
||||||
TODO: Insert video after.
|
<p align="center">
|
||||||
|
<i>With keycloakify:</i><br>
|
||||||
|
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
|
||||||
|
</p>
|
||||||
|
|
||||||
Tested with the following Keycloak versions:
|
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
|
||||||
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
|
|
||||||
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
|
|
||||||
|
|
||||||
**Disclaimer**: This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
|
|
||||||
(before you customize it) will always be the ones of the Keycloak v11.
|
|
||||||
|
|
||||||
- [Motivations](#motivations)
|
- [Motivations](#motivations)
|
||||||
- [How to use](#how-to-use)
|
- [How to use](#how-to-use)
|
||||||
- [Setting up the build tool](#setting-up-the-build-tool)
|
- [Setting up the build tool](#setting-up-the-build-tool)
|
||||||
|
- [Specify from where the resources should be downloaded.](#specify-from-where-the-resources-should-be-downloaded)
|
||||||
- [Developing your login and register pages in your React app](#developing-your-login-and-register-pages-in-your-react-app)
|
- [Developing your login and register pages in your React app](#developing-your-login-and-register-pages-in-your-react-app)
|
||||||
- [Just changing the look](#just-changing-the-look)
|
- [Just changing the look](#just-changing-the-look)
|
||||||
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
||||||
- [Hot reload](#hot-reload)
|
- [Hot reload](#hot-reload)
|
||||||
- [GitHub Actions](#github-actions)
|
- [GitHub Actions](#github-actions)
|
||||||
- [REQUIREMENTS](#requirements)
|
- [Requirements](#requirements)
|
||||||
|
- [Limitations](#limitations)
|
||||||
|
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported)
|
||||||
|
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-thesrc-dir)
|
||||||
|
- [Example of setup that **won't** work](#example-of-setup-that-wont-work)
|
||||||
|
- [Workarounds](#workarounds)
|
||||||
|
- [Implement context persistence (optional)](#implement-context-persistence-optional)
|
||||||
- [API Reference](#api-reference)
|
- [API Reference](#api-reference)
|
||||||
- [The build tool](#the-build-tool)
|
- [The build tool](#the-build-tool)
|
||||||
- [Implement context persistance (optional)](#implement-context-persistance-optional)
|
|
||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
## Setting up the build tool
|
## Setting up the build tool
|
||||||
|
|
||||||
Add `keycloakify` to the dev dependencies of your project `npm install --save-dev keycloakify` or `yarn add --dev keycloakify`
|
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
|
||||||
then configure your `package.json` build's script to build the keycloak's theme by adding `&& build-keycloak-theme`.
|
|
||||||
|
|
||||||
Typically you will get:
|
|
||||||
|
|
||||||
`package.json`
|
|
||||||
```json
|
```json
|
||||||
"devDependencies": {
|
"homepage": "https://URL.OF/YOUR-APP"
|
||||||
|
"dependencies": {
|
||||||
"keycloakify": "^0.0.10"
|
"keycloakify": "^0.0.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"keycloak": "yarn build && build-keycloak-theme",
|
"keycloak": "yarn build && build-keycloak-theme",
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
`"homepage"` must be specified only if the theme is build using
|
||||||
|
`--external-assets`(#specify-from-where-the-resources-should-be-downloaded) or if
|
||||||
|
the url path is not `/` (only the url path will be considered so it doesn't matter if the
|
||||||
|
base url is wrong)
|
||||||
|
|
||||||
|
It is mandatory that you specify the url where your app will be available
|
||||||
|
using the `homepage` field.
|
||||||
|
|
||||||
|
Once you've edited your `package.json` you can install your new
|
||||||
|
dependency with `yarn install` and build the keycloak theme with
|
||||||
|
`yarn keycloak`.
|
||||||
|
|
||||||
|
Once the build is complete instructions about how to load
|
||||||
|
the theme into Keycloak are printed in the console.
|
||||||
|
|
||||||
|
### Specify from where the resources should be downloaded.
|
||||||
|
|
||||||
|
*TL;DR*: Building the theme with the `--external-assets` option enables the login
|
||||||
|
page to load faster for first time users but it also implies that:
|
||||||
|
- If the app is down, your Keycloak login and register pages are down as well.
|
||||||
|
- Each time the app is updated, the theme must be updated.
|
||||||
|
- CORS must be enabled for fonts.
|
||||||
|
- You must know at build time what will be the url of your app (`"homepage"` in `package.json`).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
When you run `npx build-keycloak-theme` without arguments, Keycloakify will build
|
||||||
|
a standalone version of the Keycloak theme. That is to say even if your app, the
|
||||||
|
one hosted at the url specified as `homepage`, is down the Keycloak theme will still work.
|
||||||
|
It also mean that you won't have to update your theme on your Keycloak server each time
|
||||||
|
your app is updated.
|
||||||
|
In this mode, the default, every asset are served by the keycloak server.
|
||||||
|
The drawback of this approach is that when users access the login page for the first time
|
||||||
|
they have to download the whole app again.
|
||||||
|
You probably have [long-term asset caching](https://create-react-app.dev/docs/production-build/#static-file-caching)
|
||||||
|
enabled in the server that host your app ([example](https://github.com/garronej/keycloakify-demo-app/blob/224c43383548635a463fa68e8909c147ac189f0e/nginx.conf#L14))
|
||||||
|
so it can be interesting to only serve the html from Keycloak server and everything
|
||||||
|
else, your JS bundles, your CSS ect from the server that host your app.
|
||||||
|
|
||||||
|
To enable this behavior you car run:
|
||||||
|
```bash
|
||||||
|
npx build-keycloak-theme --external-assets
|
||||||
|
```
|
||||||
|
(instead of `npx build-keycloak-theme`)
|
||||||
|
|
||||||
|
This is something you probably want to do in your CI pipeline. [Example](https://github.com/garronej/keycloakify-demo-app/blob/224c43383548635a463fa68e8909c147ac189f0e/.github/workflows/ci.yaml#L112)
|
||||||
|
|
||||||
|
Also note that there is [a same-origin policy exception for fonts](https://en.wikipedia.org/wiki/Same-origin_policy#cite_note-3) so you must enabled
|
||||||
|
CORS for fonts on the server hosting your app. Concretely this mean that your server should add a `Access-Control-Allow-Origin: *` response header to
|
||||||
|
GET request on *.woff2?. [Example with Nginx](https://github.com/garronej/keycloakify-demo-app/blob/224c43383548635a463fa68e8909c147ac189f0e/nginx.conf#L18-L20)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
Then run `yarn keycloak` or `npm run keycloak`, you will be provided with instructions
|
|
||||||
about how to load the theme into Keycloak.
|
|
||||||
|
|
||||||
## Developing your login and register pages in your React app
|
## Developing your login and register pages in your React app
|
||||||
|
|
||||||
### Just changing the look
|
### Just changing the look
|
||||||
|
|
||||||
The fist approach is to only arr/replace the default class names by your
|
The first approach is to only arr/replace the default class names by your
|
||||||
own.
|
own.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@ -159,30 +212,52 @@ Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-ap
|
|||||||
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
||||||
the building and publishing of the theme (the .jar file).
|
the building and publishing of the theme (the .jar file).
|
||||||
|
|
||||||
# REQUIREMENTS
|
# Requirements
|
||||||
|
|
||||||
|
Tested with the following Keycloak versions:
|
||||||
|
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
|
||||||
|
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
|
||||||
|
|
||||||
|
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
|
||||||
|
(before you customize it) will always be the ones of the Keycloak v11.
|
||||||
|
|
||||||
This tools assumes you are bundling your app with Webpack (tested with 4.44.2) .
|
This tools assumes you are bundling your app with Webpack (tested with 4.44.2) .
|
||||||
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
|
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
|
||||||
and a `static/` directory generated by webpack.
|
and a `build/static/` directory generated by webpack.
|
||||||
|
|
||||||
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3=)
|
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3=)
|
||||||
|
|
||||||
- For building the theme: `mvn` (Maven) must be installed
|
- For building the theme: `mvn` (Maven) must be installed
|
||||||
- For development, (testing the theme in a local container ): `rm`, `mkdir`, `wget`, `unzip` are assumed to be available
|
- For development (testing the theme in a local container ): `rm`, `mkdir`, `wget`, `unzip` are assumed to be available
|
||||||
and `docker` up and running.
|
and `docker` up and running.
|
||||||
|
|
||||||
NOTE: This build tool has only be tested on MacOS.
|
NOTE: This build tool has only be tested on MacOS.
|
||||||
|
|
||||||
# API Reference
|
# Limitations
|
||||||
|
|
||||||
## The build tool
|
## `process.env.PUBLIC_URL` not supported.
|
||||||
|
|
||||||
Part of the lib that runs with node, at build time.
|
You won't be able to [import things from your public directory in your JavaScript code](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system). (This isn't recommended anyway).
|
||||||
|
|
||||||
- `npx build-keycloak-theme`: Builds the theme, the CWD is assumed to be the root of your react project.
|
## `@font-face` importing fonts from the `src/` dir
|
||||||
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
|
|
||||||
|
|
||||||
# Implement context persistance (optional)
|
**If you are building the theme with `--external-assets` this limitation doesn't apply.**
|
||||||
|
### Example of setup that **won't** work
|
||||||
|
|
||||||
|
- We have a `fonts/` directory in `src/`
|
||||||
|
- We import the font like this [`src: url("/fonts/my-font.woff2") format("woff2");`(https://github.com/garronej/keycloakify-demo-app/blob/07d54a3012ef354ee12b1374c6f7ad1cb125d56b/src/fonts.scss#L4) in a `.scss` a file.
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
If it is possible, use Google Fonts or any other font provider.
|
||||||
|
|
||||||
|
If you want to host your font recommended approach is to move your fonts into the `public`
|
||||||
|
directory and to place your `@font-face` statements in the `public/index.html`.
|
||||||
|
Example [here]().
|
||||||
|
|
||||||
|
You can also [use your explicit url](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/src/fonts.scss#L16) but don't forget [`Access-Control-Allow-Origin`](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/nginx.conf#L17-L19).
|
||||||
|
|
||||||
|
# Implement context persistence (optional)
|
||||||
|
|
||||||
If, before logging in, a user has selected a specific language
|
If, before logging in, a user has selected a specific language
|
||||||
you don't want it to be reset to default when the user gets redirected to
|
you don't want it to be reset to default when the user gets redirected to
|
||||||
@ -240,3 +315,12 @@ keycloakInstance.init({
|
|||||||
If you really want to go the extra miles and avoid having the white
|
If you really want to go the extra miles and avoid having the white
|
||||||
flash of the blank html before the js bundle have been evaluated
|
flash of the blank html before the js bundle have been evaluated
|
||||||
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
||||||
|
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
## The build tool
|
||||||
|
|
||||||
|
Part of the lib that runs with node, at build time.
|
||||||
|
|
||||||
|
- `npx build-keycloak-theme [--external-assets]`: Builds the theme, the CWD is assumed to be the root of your react project.
|
||||||
|
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "0.3.5",
|
"version": "0.3.14",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import cheerio from "cheerio";
|
import cheerio from "cheerio";
|
||||||
import {
|
import {
|
||||||
replaceImportFromStaticInJsCode,
|
replaceImportsFromStaticInJsCode,
|
||||||
|
replaceImportsInInlineCssCode,
|
||||||
generateCssCodeToDefineGlobals
|
generateCssCodeToDefineGlobals
|
||||||
} from "../replaceImportFromStatic";
|
} from "../replaceImportFromStatic";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@ -29,13 +30,19 @@ function loadFtlFile(ftlFileBasename: PageId | "template.ftl") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(
|
export function generateFtlFilesCodeFactory(
|
||||||
params: {
|
params: {
|
||||||
ftlValuesGlobalName: string;
|
ftlValuesGlobalName: string;
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
indexHtmlCode: string;
|
indexHtmlCode: string;
|
||||||
urlPathname: string;
|
urlPathname: string;
|
||||||
}
|
} & ({
|
||||||
|
mode: "standalone";
|
||||||
|
} | {
|
||||||
|
mode: "external assets";
|
||||||
|
urlOrigin: string;
|
||||||
|
})
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode, urlPathname } = params;
|
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode, urlPathname } = params;
|
||||||
@ -44,15 +51,49 @@ export function generateFtlFilesCodeFactory(
|
|||||||
|
|
||||||
$("script:not([src])").each((...[, element]) => {
|
$("script:not([src])").each((...[, element]) => {
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
ftlValuesGlobalName,
|
ftlValuesGlobalName,
|
||||||
"jsCode": $(element).html()!
|
"jsCode": $(element).html()!,
|
||||||
|
...(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "standalone": return {
|
||||||
|
"mode": params.mode
|
||||||
|
};
|
||||||
|
case "external assets": return {
|
||||||
|
"mode": params.mode,
|
||||||
|
"urlOrigin": params.urlOrigin,
|
||||||
|
"urlPathname": params.urlPathname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
});
|
});
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
$(element).text(fixedJsCode);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("style").each((...[, element]) => {
|
||||||
|
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
"cssCode": $(element).html()!,
|
||||||
|
"urlPathname": params.urlPathname,
|
||||||
|
...(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "standalone": return {
|
||||||
|
"mode": params.mode
|
||||||
|
};
|
||||||
|
case "external assets": return {
|
||||||
|
"mode": params.mode,
|
||||||
|
"urlOrigin": params.urlOrigin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedCssCode);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
([
|
([
|
||||||
["link", "href"],
|
["link", "href"],
|
||||||
["script", "src"],
|
["script", "src"],
|
||||||
@ -65,13 +106,24 @@ export function generateFtlFilesCodeFactory(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(element).attr(
|
switch (params.mode) {
|
||||||
attrName,
|
case "external assets":
|
||||||
href.replace(
|
$(element).attr(
|
||||||
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
|
attrName,
|
||||||
"${url.resourcesPath}/build/"
|
href.replace(/^\//, `${params.urlOrigin}/`)
|
||||||
)
|
);
|
||||||
);
|
break;
|
||||||
|
case "standalone":
|
||||||
|
$(element).attr(
|
||||||
|
attrName,
|
||||||
|
href.replace(
|
||||||
|
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
|
||||||
|
"${url.resourcesPath}/build/"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -96,7 +148,7 @@ export function generateFtlFilesCodeFactory(
|
|||||||
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
|
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
|
||||||
'',
|
'',
|
||||||
'<style>',
|
'<style>',
|
||||||
generateCssCodeToDefineGlobals({
|
generateCssCodeToDefineGlobals({
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
urlPathname
|
urlPathname
|
||||||
}).cssCodeToPrependInHead,
|
}).cssCodeToPrependInHead,
|
||||||
|
@ -3,24 +3,30 @@ import { transformCodebase } from "../tools/transformCodebase";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import {
|
import {
|
||||||
replaceImportFromStaticInCssCode,
|
replaceImportsInCssCode,
|
||||||
replaceImportFromStaticInJsCode
|
replaceImportsFromStaticInJsCode
|
||||||
} from "./replaceImportFromStatic";
|
} from "./replaceImportFromStatic";
|
||||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
||||||
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
|
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
|
||||||
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
||||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/kcContextMocks/urlResourcesPath";
|
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/kcContextMocks/urlResourcesPath";
|
||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
|
|
||||||
|
|
||||||
export function generateKeycloakThemeResources(
|
export function generateKeycloakThemeResources(
|
||||||
params: {
|
params: {
|
||||||
urlPathname: string;
|
|
||||||
themeName: string;
|
themeName: string;
|
||||||
reactAppBuildDirPath: string;
|
reactAppBuildDirPath: string;
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
}
|
urlPathname: string;
|
||||||
|
} & ({
|
||||||
|
mode: "standalone";
|
||||||
|
} | {
|
||||||
|
mode: "external assets";
|
||||||
|
urlOrigin: string;
|
||||||
|
})
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname } = params;
|
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname } = params;
|
||||||
@ -44,27 +50,42 @@ export function generateKeycloakThemeResources(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.mode === "standalone") {
|
||||||
|
|
||||||
if (/\.css?$/i.test(filePath)) {
|
if (/\.css?$/i.test(filePath)) {
|
||||||
|
|
||||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode(
|
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode(
|
||||||
{ "cssCode": sourceCode.toString("utf8") }
|
{ "cssCode": sourceCode.toString("utf8") }
|
||||||
);
|
);
|
||||||
|
|
||||||
allCssGlobalsToDefine = {
|
allCssGlobalsToDefine = {
|
||||||
...allCssGlobalsToDefine,
|
...allCssGlobalsToDefine,
|
||||||
...cssGlobalsToDefine
|
...cssGlobalsToDefine
|
||||||
};
|
};
|
||||||
|
|
||||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.js?$/i.test(filePath)) {
|
if (/\.js?$/i.test(filePath)) {
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": sourceCode.toString("utf8"),
|
"jsCode": sourceCode.toString("utf8"),
|
||||||
ftlValuesGlobalName
|
ftlValuesGlobalName,
|
||||||
|
...(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "external assets": return {
|
||||||
|
"mode": params.mode,
|
||||||
|
"urlOrigin": params.urlOrigin,
|
||||||
|
"urlPathname": params.urlPathname
|
||||||
|
};
|
||||||
|
case "standalone": return {
|
||||||
|
"mode": params.mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
});
|
});
|
||||||
|
|
||||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||||
@ -82,7 +103,18 @@ export function generateKeycloakThemeResources(
|
|||||||
"indexHtmlCode": fs.readFileSync(
|
"indexHtmlCode": fs.readFileSync(
|
||||||
pathJoin(reactAppBuildDirPath, "index.html")
|
pathJoin(reactAppBuildDirPath, "index.html")
|
||||||
).toString("utf8"),
|
).toString("utf8"),
|
||||||
urlPathname
|
urlPathname,
|
||||||
|
...(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "external assets": return {
|
||||||
|
"mode": params.mode,
|
||||||
|
"urlOrigin": params.urlOrigin
|
||||||
|
};
|
||||||
|
case "standalone": return {
|
||||||
|
"mode": params.mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
});
|
});
|
||||||
|
|
||||||
pageIds.forEach(pageId => {
|
pageIds.forEach(pageId => {
|
||||||
|
@ -6,11 +6,13 @@ import type { ParsedPackageJson } from "./generateJavaStackFiles";
|
|||||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
|
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
|
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
||||||
|
|
||||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
||||||
|
|
||||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||||
@ -24,15 +26,48 @@ if (require.main === module) {
|
|||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||||
"themeName": parsedPackageJson.name,
|
"themeName": parsedPackageJson.name,
|
||||||
"urlPathname": (()=>{
|
...(() => {
|
||||||
|
|
||||||
const { homepage } = parsedPackageJson;
|
|
||||||
|
|
||||||
return homepage === undefined ?
|
const url = (() => {
|
||||||
"/" :
|
|
||||||
new URL(homepage).pathname.replace(/([^/])$/, "$1/");
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
return homepage === undefined ?
|
||||||
|
undefined :
|
||||||
|
new URL(homepage);
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
const urlPathname =
|
||||||
|
url === undefined ?
|
||||||
|
"/" :
|
||||||
|
url.pathname.replace(/([^/])$/, "$1/");
|
||||||
|
|
||||||
|
return !doUseExternalAssets ?
|
||||||
|
{
|
||||||
|
"mode": "standalone",
|
||||||
|
urlPathname
|
||||||
|
} as const
|
||||||
|
:
|
||||||
|
{
|
||||||
|
"mode": "external assets",
|
||||||
|
urlPathname,
|
||||||
|
"urlOrigin": (() => {
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
console.error("ERROR: You must specify 'homepage' in your package.json");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.origin;
|
||||||
|
|
||||||
|
})()
|
||||||
|
|
||||||
|
} as const;
|
||||||
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
|
@ -1,25 +1,69 @@
|
|||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
export function replaceImportFromStaticInJsCode(
|
export function replaceImportsFromStaticInJsCode(
|
||||||
params: {
|
params: {
|
||||||
ftlValuesGlobalName: string;
|
ftlValuesGlobalName: string;
|
||||||
jsCode: string;
|
jsCode: string;
|
||||||
}
|
} & ({
|
||||||
|
mode: "standalone";
|
||||||
|
} | {
|
||||||
|
mode: "external assets";
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string;
|
||||||
|
})
|
||||||
): { fixedJsCode: string; } {
|
): { fixedJsCode: string; } {
|
||||||
|
|
||||||
const { jsCode, ftlValuesGlobalName } = params;
|
const { jsCode, ftlValuesGlobalName } = params;
|
||||||
|
|
||||||
const fixedJsCode = jsCode!.replace(
|
const fixedJsCode = jsCode.replace(
|
||||||
/ [^ ]+"static\//g,
|
/[a-z]+\.[a-z]+\+"static\//g,
|
||||||
` window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "standalone":
|
||||||
|
return `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`;
|
||||||
|
case "external assets":
|
||||||
|
return `"${params.urlOrigin}${params.urlPathname}static/`;
|
||||||
|
}
|
||||||
|
})()
|
||||||
);
|
);
|
||||||
|
|
||||||
return { fixedJsCode };
|
return { fixedJsCode };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceImportFromStaticInCssCode(
|
export function replaceImportsInInlineCssCode(
|
||||||
|
params: {
|
||||||
|
cssCode: string;
|
||||||
|
urlPathname: string;
|
||||||
|
} & ({
|
||||||
|
mode: "standalone";
|
||||||
|
} | {
|
||||||
|
mode: "external assets";
|
||||||
|
urlOrigin: string;
|
||||||
|
})
|
||||||
|
): { fixedCssCode: string; } {
|
||||||
|
|
||||||
|
const { cssCode, urlPathname } = params;
|
||||||
|
|
||||||
|
const fixedCssCode = cssCode.replace(
|
||||||
|
urlPathname === "/" ?
|
||||||
|
/url\(\/([^/][^)]+)\)/g :
|
||||||
|
new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"),
|
||||||
|
(...[, group]) => `url(${(() => {
|
||||||
|
switch (params.mode) {
|
||||||
|
case "standalone": return "${url.resourcesPath}/build/" + group;
|
||||||
|
case "external assets": return params.urlOrigin + urlPathname + group
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInCssCode(
|
||||||
params: {
|
params: {
|
||||||
cssCode: string;
|
cssCode: string;
|
||||||
}
|
}
|
||||||
@ -32,7 +76,7 @@ export function replaceImportFromStaticInCssCode(
|
|||||||
|
|
||||||
const cssGlobalsToDefine: Record<string, string> = {};
|
const cssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
new Set(cssCode.match(/url\(\/[^)]+\)[^;}]*/g) ?? [])
|
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? [])
|
||||||
.forEach(match =>
|
.forEach(match =>
|
||||||
cssGlobalsToDefine[
|
cssGlobalsToDefine[
|
||||||
"url" + crypto
|
"url" + crypto
|
||||||
@ -60,7 +104,7 @@ export function replaceImportFromStaticInCssCode(
|
|||||||
export function generateCssCodeToDefineGlobals(
|
export function generateCssCodeToDefineGlobals(
|
||||||
params: {
|
params: {
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
urlPathname: string;
|
urlPathname: string;
|
||||||
}
|
}
|
||||||
): {
|
): {
|
||||||
cssCodeToPrependInHead: string;
|
cssCodeToPrependInHead: string;
|
||||||
@ -75,7 +119,7 @@ export function generateCssCodeToDefineGlobals(
|
|||||||
.map(cssVariableName => [
|
.map(cssVariableName => [
|
||||||
`--${cssVariableName}:`,
|
`--${cssVariableName}:`,
|
||||||
cssGlobalsToDefine[cssVariableName]
|
cssGlobalsToDefine[cssVariableName]
|
||||||
.replace(new RegExp(`url\\(${urlPathname.replace(/\//g,"\\/")}`, "g"),"url(${url.resourcesPath}/build/")
|
.replace(new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"), "url(${url.resourcesPath}/build/")
|
||||||
].join(" "))
|
].join(" "))
|
||||||
.map(line => ` ${line};`),
|
.map(line => ` ${line};`),
|
||||||
"}"
|
"}"
|
||||||
|
@ -9,8 +9,8 @@ import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
|||||||
export const kcTemplateContext: KcContext.Template = {
|
export const kcTemplateContext: KcContext.Template = {
|
||||||
"url": {
|
"url": {
|
||||||
"loginAction": "#",
|
"loginAction": "#",
|
||||||
"resourcesPath": "/" + resourcesPath,
|
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
|
||||||
"resourcesCommonPath": "/" + resourcesCommonPath,
|
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
|
||||||
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
||||||
import {
|
import {
|
||||||
setupSampleReactProject,
|
setupSampleReactProject,
|
||||||
sampleReactProjectDirPath
|
sampleReactProjectDirPath
|
||||||
} from "./setupSampleReactProject";
|
} from "./setupSampleReactProject";
|
||||||
@ -12,6 +12,7 @@ generateKeycloakThemeResources({
|
|||||||
"themeName": "keycloakify-demo-app",
|
"themeName": "keycloakify-demo-app",
|
||||||
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
||||||
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
||||||
|
"mode": "standalone",
|
||||||
"urlPathname": "/keycloakify-demo-app/"
|
"urlPathname": "/keycloakify-demo-app/"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
replaceImportFromStaticInJsCode,
|
replaceImportsFromStaticInJsCode,
|
||||||
replaceImportFromStaticInCssCode,
|
replaceImportsInCssCode,
|
||||||
generateCssCodeToDefineGlobals
|
generateCssCodeToDefineGlobals
|
||||||
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
|
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"ftlValuesGlobalName": "keycloakFtlValues",
|
"ftlValuesGlobalName": "keycloakFtlValues",
|
||||||
"jsCode": `
|
"jsCode": `
|
||||||
function f() {
|
function f() {
|
||||||
@ -19,12 +19,13 @@ const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
|||||||
3: "0664cdc0"
|
3: "0664cdc0"
|
||||||
}[e] + ".chunk.js"
|
}[e] + ".chunk.js"
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
|
"mode": "standalone"
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({ fixedJsCode });
|
console.log({ fixedJsCode });
|
||||||
|
|
||||||
const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({
|
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
|
||||||
"cssCode": `
|
"cssCode": `
|
||||||
|
|
||||||
.my-div {
|
.my-div {
|
||||||
|
Reference in New Issue
Block a user