Compare commits

...

9 Commits

9 changed files with 206 additions and 93 deletions

View File

@ -1,3 +1,11 @@
### **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

View File

@ -49,9 +49,13 @@ 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)
- [Workarounds](#workarounds)
- [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
@ -66,6 +70,10 @@ Here is `yarn add keycloakify` for you 🍸
"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.
@ -79,18 +87,28 @@ 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` 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 +122,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)
</details>
## Developing your login and register pages in your React app
### Just changing the look
@ -214,19 +235,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, wont 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 +315,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)

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "0.3.9",
"version": "0.3.11",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",

View File

@ -2,7 +2,8 @@
import cheerio from "cheerio";
import {
replaceImportFromStaticInJsCode,
replaceImportsFromStaticInJsCode,
replaceImportsInInlineCssCode,
generateCssCodeToDefineGlobals
} from "../replaceImportFromStatic";
import fs from "fs";
@ -29,40 +30,70 @@ function loadFtlFile(ftlFileBasename: PageId | "template.ftl") {
}
}
export type Mode = {
type: "standalone";
urlPathname: string;
} | {
type: "external assets";
urlPathname: string;
urlOrigin: string;
}
export function generateFtlFilesCodeFactory(
params: {
ftlValuesGlobalName: string;
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
mode: Mode;
}
urlPathname: string;
} & ({
mode: "standalone";
} | {
mode: "external assets";
urlOrigin: string;
})
) {
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode, mode } = params;
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode, urlPathname } = params;
const $ = cheerio.load(indexHtmlCode);
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
ftlValuesGlobalName,
"jsCode": $(element).html()!,
mode
...(() => {
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);
});
$("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"],
["script", "src"],
@ -75,18 +106,18 @@ export function generateFtlFilesCodeFactory(
return;
}
switch (mode.type) {
switch (params.mode) {
case "external assets":
$(element).attr(
attrName,
href.replace(/^\//, `${mode.urlOrigin}/`)
href.replace(/^\//, `${params.urlOrigin}/`)
);
break;
case "standalone":
$(element).attr(
attrName,
href.replace(
new RegExp(`^${mode.urlPathname.replace(/\//g, "\\/")}`),
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
"${url.resourcesPath}/build/"
)
);
@ -119,7 +150,7 @@ export function generateFtlFilesCodeFactory(
'<style>',
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
"urlPathname": mode.urlPathname
urlPathname
}).cssCodeToPrependInHead,
'</style>',
''

View File

@ -3,10 +3,10 @@ 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 { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
import * as child_process from "child_process";
@ -20,11 +20,16 @@ export function generateKeycloakThemeResources(
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
mode: Mode;
}
urlPathname: string;
} & ({
mode: "standalone";
} | {
mode: "external assets";
urlOrigin: string;
})
) {
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, mode } = params;
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname } = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
@ -45,11 +50,11 @@ export function generateKeycloakThemeResources(
return undefined;
}
if (mode.type === "standalone") {
if (params.mode === "standalone") {
if (/\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode(
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode(
{ "cssCode": sourceCode.toString("utf8") }
);
@ -64,10 +69,10 @@ export function generateKeycloakThemeResources(
if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
ftlValuesGlobalName,
mode
"mode": params.mode
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
@ -87,7 +92,18 @@ export function generateKeycloakThemeResources(
"indexHtmlCode": fs.readFileSync(
pathJoin(reactAppBuildDirPath, "index.html")
).toString("utf8"),
mode
urlPathname,
...(() => {
switch (params.mode) {
case "external assets": return {
"mode": params.mode,
"urlOrigin": params.urlOrigin
};
case "standalone": return {
"mode": params.mode
};
}
})()
});
pageIds.forEach(pageId => {

View File

@ -26,7 +26,7 @@ if (require.main === module) {
keycloakThemeBuildingDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
"themeName": parsedPackageJson.name,
"mode": (() => {
...(() => {
const url = (() => {
@ -44,16 +44,14 @@ if (require.main === module) {
"/" :
url.pathname.replace(/([^/])$/, "$1/");
return !doUseExternalAssets ?
{
"type": "standalone",
"mode": "standalone",
urlPathname
} as const
:
{
"type": "external assets",
"mode": "external assets",
urlPathname,
"urlOrigin": (() => {

View File

@ -1,45 +1,69 @@
import * as crypto from "crypto";
type Mode = {
type: "standalone";
} | {
type: "external assets";
urlOrigin: string;
urlPathname: string;
}
export function replaceImportFromStaticInJsCode(
export function replaceImportsFromStaticInJsCode(
params: {
ftlValuesGlobalName: string;
jsCode: string;
mode: Mode;
}
} & ({
mode: "standalone";
} | {
mode: "external assets";
urlOrigin: string;
urlPathname: string;
})
): { fixedJsCode: string; } {
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 { jsCode, ftlValuesGlobalName } = params;
const fixedJsCode = jsCode.replace(
/[a-z]+\.[a-z]+\+"static\//g,
(() => {
switch (params.mode) {
case "standalone":
return `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`;
case "external assets":
return `"${params.urlOrigin}${params.urlPathname}static/`;
}
})()
);
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: {
cssCode: string;
}
@ -52,7 +76,7 @@ export function replaceImportFromStaticInCssCode(
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(\/[^)]+\)[^;}]*/g) ?? [])
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? [])
.forEach(match =>
cssGlobalsToDefine[
"url" + crypto

View File

@ -12,9 +12,7 @@ generateKeycloakThemeResources({
"themeName": "keycloakify-demo-app",
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
"mode": {
"type": "standalone",
"urlPathname": "/keycloakify-demo-app/"
}
"mode": "standalone",
"urlPathname": "/keycloakify-demo-app/"
});

View File

@ -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() {
@ -20,12 +20,12 @@ const { fixedJsCode } = replaceImportFromStaticInJsCode({
}[e] + ".chunk.js"
}
`,
"mode": { "type": "standalone" }
"mode": "standalone"
});
console.log({ fixedJsCode });
const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
"cssCode": `
.my-div {