diff --git a/README.md b/README.md index eec470c1..0a62f358 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,12 @@ Here is `yarn add keycloakify` for you 🍸 - [GitHub Actions](#github-actions) - [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) +- [Implement context persistence (optional)](#implement-context-persistence-optional) - [API Reference](#api-reference) - [The build tool](#the-build-tool) -- [Implement context persistence (optional)](#implement-context-persistence-optional) # How to use ## Setting up the build tool @@ -79,18 +82,27 @@ 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 as well. +- CORS must be enabled for fonts. + +
+ Click to expand + 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` in your package.json, is down the -Keycloak theme will still work. -In this mode (the default) every asset are served by the keycloak server. It is -convergent for debugging but it production you probably want the assets to be -fetched from your app. -Indeed in the default mode your users have to download again the whole app just -to access the login page. You probably have [long-term asset caching](https://create-react-app.dev/docs/production-build/#static-file-caching) +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's better if only the html is served by the Keycloak server and everything -else, your JS bundles, your CSS ect point to your app. +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 @@ -104,6 +116,9 @@ Also note that there is [a same-origin policy exception for fonts](https://en.wi 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) +
+ + ## Developing your login and register pages in your React app ### Just changing the look @@ -214,19 +229,27 @@ NOTE: This build tool has only be tested on MacOS. # Limitations -In the standalone mode (when you run `npx build-keycloak-theme` without `--external-assets`) the fonts won't work if you are self -hosting them. This, for example, won’t work: [`src: url("/assets/worksans-bold-webfont.woff2") format("woff2")`](https://github.com/InseeFrLab/onyxia-ui/blob/b24df3a9b34b505ce00619bb8ec0174223ecfaca/src/app/theme/fonts.scss#L5-L6) -you will have to [host them externally](https://github.com/InseeFrLab/onyxia-ui/blob/43bf4a508419072a4ae202698e59d20b69feb9c0/src/app/theme/fonts.scss#L8-L9) -on a server that has CORS enabled. -Again this apply ony if you are not building your theme with `--external-assets` which is advised against in production. -# API Reference +## `process.env.PUBLIC_URL` not supported. -## The build tool +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). -Part of the lib that runs with node, at build time. +## `@font-face` importing fonts from the `src/` dir -- `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) +**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) @@ -286,3 +309,12 @@ keycloakInstance.init({ 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 [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) \ No newline at end of file diff --git a/src/bin/build-keycloak-theme/generateFtl/index.ts b/src/bin/build-keycloak-theme/generateFtl/index.ts index 9ad33323..c6e8d019 100644 --- a/src/bin/build-keycloak-theme/generateFtl/index.ts +++ b/src/bin/build-keycloak-theme/generateFtl/index.ts @@ -2,7 +2,8 @@ import cheerio from "cheerio"; import { - replaceImportFromStaticInJsCode, + replaceImportsFromStaticInJsCode, + replaceImportsInInlineCssCode, generateCssCodeToDefineGlobals } from "../replaceImportFromStatic"; import fs from "fs"; @@ -53,7 +54,7 @@ export function generateFtlFilesCodeFactory( $("script:not([src])").each((...[, element]) => { - const { fixedJsCode } = replaceImportFromStaticInJsCode({ + const { fixedJsCode } = replaceImportsFromStaticInJsCode({ ftlValuesGlobalName, "jsCode": $(element).html()!, mode @@ -63,6 +64,17 @@ export function generateFtlFilesCodeFactory( }); + $("style").each((...[, element]) => { + + const { fixedCssCode } = replaceImportsInInlineCssCode({ + "cssCode": $(element).html()!, + mode + }); + + $(element).text(fixedCssCode); + + }); + ([ ["link", "href"], ["script", "src"], diff --git a/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts b/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts index dbf1872c..5e0ab31b 100644 --- a/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts +++ b/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts @@ -3,8 +3,8 @@ import { transformCodebase } from "../tools/transformCodebase"; import * as fs from "fs"; import { join as pathJoin } from "path"; import { - replaceImportFromStaticInCssCode, - replaceImportFromStaticInJsCode + replaceImportsInCssCode, + replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic"; import { generateFtlFilesCodeFactory, pageIds, Mode } from "./generateFtl"; import { builtinThemesUrl } from "../install-builtin-keycloak-themes"; @@ -49,7 +49,7 @@ export function generateKeycloakThemeResources( if (/\.css?$/i.test(filePath)) { - const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode( + const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode( { "cssCode": sourceCode.toString("utf8") } ); @@ -64,7 +64,7 @@ export function generateKeycloakThemeResources( if (/\.js?$/i.test(filePath)) { - const { fixedJsCode } = replaceImportFromStaticInJsCode({ + const { fixedJsCode } = replaceImportsFromStaticInJsCode({ "jsCode": sourceCode.toString("utf8"), ftlValuesGlobalName, mode diff --git a/src/bin/build-keycloak-theme/replaceImportFromStatic.ts b/src/bin/build-keycloak-theme/replaceImportFromStatic.ts index 463343f0..e1630e77 100644 --- a/src/bin/build-keycloak-theme/replaceImportFromStatic.ts +++ b/src/bin/build-keycloak-theme/replaceImportFromStatic.ts @@ -9,7 +9,7 @@ type Mode = { urlPathname: string; } -export function replaceImportFromStaticInJsCode( +export function replaceImportsFromStaticInJsCode( params: { ftlValuesGlobalName: string; jsCode: string; @@ -19,27 +19,48 @@ export function replaceImportFromStaticInJsCode( const { jsCode, ftlValuesGlobalName, mode } = params; - const fixedJsCode = (() => { - switch (mode.type) { - case "standalone": - return jsCode!.replace( - /[a-z]+\.[a-z]+\+"static\//g, - `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` - ); - case "external assets": - return jsCode!.replace( - /[a-z]+\.[a-z]+\+"static\//g, - `"${mode.urlOrigin}${mode.urlPathname}static/` - ); - } - })(); - + const fixedJsCode = jsCode.replace( + /[a-z]+\.[a-z]+\+"static\//g, + (() => { + switch (mode.type) { + case "standalone": + return `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`; + case "external assets": + return `"${mode.urlOrigin}${mode.urlPathname}static/`; + } + })() + ); return { fixedJsCode }; } -export function replaceImportFromStaticInCssCode( +export function replaceImportsInInlineCssCode( + params: { + cssCode: string; + mode: Mode; + } +): { fixedCssCode: string; } { + + const { cssCode, mode } = params; + + const fixedCssCode = cssCode.replace( + /url\((\/[^/][^)]+)\)/g, + (...[,group])=> `url(${ + (()=>{ + switch(mode.type){ + case "standalone": return "${url.resourcesPath}/build" + group; + case "external assets": return mode.urlOrigin + group + } + })() + })` + ); + + return { fixedCssCode }; + +} + +export function replaceImportsInCssCode( params: { cssCode: string; } @@ -52,7 +73,7 @@ export function replaceImportFromStaticInCssCode( const cssGlobalsToDefine: Record = {}; - new Set(cssCode.match(/url\(\/[^)]+\)[^;}]*/g) ?? []) + new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? []) .forEach(match => cssGlobalsToDefine[ "url" + crypto diff --git a/src/test/replaceImportFromStatic.ts b/src/test/replaceImportFromStatic.ts index 5121b3ac..40bb34f4 100644 --- a/src/test/replaceImportFromStatic.ts +++ b/src/test/replaceImportFromStatic.ts @@ -1,11 +1,11 @@ import {  - replaceImportFromStaticInJsCode, - replaceImportFromStaticInCssCode, + replaceImportsFromStaticInJsCode, + replaceImportsInCssCode, generateCssCodeToDefineGlobals } from "../bin/build-keycloak-theme/replaceImportFromStatic"; -const { fixedJsCode } = replaceImportFromStaticInJsCode({ +const { fixedJsCode } = replaceImportsFromStaticInJsCode({ "ftlValuesGlobalName": "keycloakFtlValues", "jsCode": ` function f() { @@ -25,7 +25,7 @@ const { fixedJsCode } = replaceImportFromStaticInJsCode({ console.log({ fixedJsCode }); -const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({ +const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({ "cssCode": ` .my-div {