First draft
This commit is contained in:
parent
3af3178d42
commit
83755d1f5f
2019
package-lock.json
generated
2019
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -1,16 +1,21 @@
|
||||
{
|
||||
"name": "keycloak-react-theming",
|
||||
"version": "0.0.2",
|
||||
"description": "Provides a way to customise Keycloak login and register pages with React",
|
||||
"description": "Provides a way to customize Keycloak login and register pages with React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/garronej/keycloak-react-theming.git"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "src/index.js",
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node dist/test/",
|
||||
"build": "tsc",
|
||||
"start": "webpack-dev-server --open",
|
||||
"create": "webpack",
|
||||
"enable_short_import_path": "npm run build && denoify_enable_short_npm_import_path"
|
||||
},
|
||||
"author": "u/garronej",
|
||||
@ -27,8 +32,11 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^10.0.0",
|
||||
"denoify": "^0.6.4",
|
||||
"evt": "^1.8.11",
|
||||
"lint-staged": "^10.5.4",
|
||||
"evt": "beta",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"node-html-parser": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
30
res/index.html
Normal file
30
res/index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>React App</title>
|
||||
<link href="/static/css/main.8c8b27cf.chunk.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/2.0f3a6c43.chunk.js"></script>
|
||||
<script src="/static/js/main.94e9b83c.chunk.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
0
res/static/css/main.8c8b27cf.chunk.css
Normal file
0
res/static/css/main.8c8b27cf.chunk.css
Normal file
13
res/static/js/2.0f3a6c43.chunk.js
Normal file
13
res/static/js/2.0f3a6c43.chunk.js
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
||||
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
13
res/static/js/2.0f3a6c43.chunk_another.js
Normal file
13
res/static/js/2.0f3a6c43.chunk_another.js
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
||||
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
105
src/bin/generateFtl.ts
Normal file
105
src/bin/generateFtl.ts
Normal file
@ -0,0 +1,105 @@
|
||||
|
||||
|
||||
import cheerio from "cheerio";
|
||||
import {
|
||||
replaceImportFromStaticInJsCode,
|
||||
generateCssCodeToDefineGlobals
|
||||
} from "./replaceImportFromStatic";
|
||||
|
||||
export function generateFtlFilesCodeFactory(
|
||||
params: {
|
||||
ftlValuesGlobalName: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
indexHtmlCode: string;
|
||||
}
|
||||
) {
|
||||
|
||||
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode } = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
$("script:not([src])").each((...[, element]) => {
|
||||
|
||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
||||
ftlValuesGlobalName,
|
||||
"jsCode": $(element).html()!
|
||||
});
|
||||
|
||||
$(element).html(fixedJsCode);
|
||||
|
||||
});
|
||||
|
||||
|
||||
([
|
||||
["link", "href"],
|
||||
["script", "src"],
|
||||
] as const).forEach(([selector, attrName]) =>
|
||||
$(selector).each((...[, element]) => {
|
||||
|
||||
const href = $(element).attr(attrName);
|
||||
|
||||
if (!href?.startsWith("/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(element).attr(attrName, "${url.resourcesPath}" + href);
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
'',
|
||||
'<style>',
|
||||
generateCssCodeToDefineGlobals(
|
||||
{ cssGlobalsToDefine }
|
||||
).cssCodeToPrependInHead,
|
||||
'</style>',
|
||||
'',
|
||||
'<script>',
|
||||
' Object.assign(',
|
||||
` window.${ftlValuesGlobalName},`,
|
||||
' {',
|
||||
' "url": {',
|
||||
' "loginAction": "${url.loginAction}",',
|
||||
' "resourcesPath": "${url.resourcesPath}"',
|
||||
' }',
|
||||
' }',
|
||||
' });',
|
||||
'</script>',
|
||||
''
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(
|
||||
params: {
|
||||
pageBasename: "login.ftl" | "register.ftl"
|
||||
}
|
||||
): { ftlCode: string; } {
|
||||
|
||||
const { pageBasename } = params;
|
||||
|
||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
'',
|
||||
'<script>',
|
||||
` window.${ftlValuesGlobalName} = { "pageBasename": "${pageBasename}" };'`,
|
||||
'</script>',
|
||||
''
|
||||
].join("\n"),
|
||||
|
||||
);
|
||||
|
||||
return { "ftlCode": $.html() };
|
||||
|
||||
}
|
||||
|
||||
return { generateFtlFilesCode };
|
||||
|
||||
|
||||
}
|
80
src/bin/main.ts
Normal file
80
src/bin/main.ts
Normal file
@ -0,0 +1,80 @@
|
||||
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "evt/tools/typeSafety/assert";
|
||||
import {
|
||||
replaceImportFromStaticInCssCode,
|
||||
replaceImportFromStaticInJsCode
|
||||
} from "./replaceImportFromStatic";
|
||||
import { generateFtlFilesCodeFactory } from "./generateFtl";
|
||||
|
||||
|
||||
const reactAppBuildDirPath = pathJoin(__dirname, "build");
|
||||
|
||||
assert(
|
||||
fs.existsSync(reactAppBuildDirPath),
|
||||
"Run 'react-script build' first (the build dir should be present)"
|
||||
);
|
||||
|
||||
const keycloakDir = pathJoin(reactAppBuildDirPath, "..", "keycloak_build");
|
||||
|
||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
const ftlValuesGlobalName = "keycloakFtlValues";
|
||||
|
||||
|
||||
transformCodebase({
|
||||
"destDirPath": pathJoin(keycloakDir, "login", "resources"),
|
||||
"srcDirPath": reactAppBuildDirPath,
|
||||
"transformSourceCodeString": ({ filePath, sourceCode }) => {
|
||||
|
||||
if (/\.css?$/i.test(filePath)) {
|
||||
|
||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode(
|
||||
{ "cssCode": sourceCode.toString("utf8") }
|
||||
);
|
||||
|
||||
allCssGlobalsToDefine = {
|
||||
...allCssGlobalsToDefine,
|
||||
...cssGlobalsToDefine
|
||||
};
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
|
||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
ftlValuesGlobalName
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||
ftlValuesGlobalName,
|
||||
"indexHtmlCode": fs.readFileSync(
|
||||
pathJoin(reactAppBuildDirPath, "index.html")
|
||||
).toString("utf8")
|
||||
});
|
||||
|
||||
(["login.ftl", "register.ftl"] as const).forEach(pageBasename => {
|
||||
|
||||
const { ftlCode } = generateFtlFilesCode({ pageBasename });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakDir, "login", pageBasename),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
)
|
||||
|
||||
});
|
91
src/bin/replaceImportFromStatic.ts
Normal file
91
src/bin/replaceImportFromStatic.ts
Normal file
@ -0,0 +1,91 @@
|
||||
|
||||
import * as crypto from "crypto";
|
||||
|
||||
export function replaceImportFromStaticInJsCode(
|
||||
params: {
|
||||
ftlValuesGlobalName: string;
|
||||
jsCode: string;
|
||||
}
|
||||
): { fixedJsCode: string; } {
|
||||
|
||||
const { jsCode, ftlValuesGlobalName } = params;
|
||||
|
||||
const fixedJsCode = jsCode!.replace(
|
||||
/"static\//g,
|
||||
`window.${ftlValuesGlobalName}.url.resourcesPath.replace(/^\//,"") + "/" + "static/`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
|
||||
}
|
||||
|
||||
export function replaceImportFromStaticInCssCode(
|
||||
params: {
|
||||
cssCode: string;
|
||||
}
|
||||
): {
|
||||
fixedCssCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
} {
|
||||
|
||||
const { cssCode } = params;
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/(url\(\/[^)]+\))/g) ?? [])
|
||||
.forEach(match =>
|
||||
cssGlobalsToDefine[
|
||||
"url" + crypto
|
||||
.createHash("sha256")
|
||||
.update(match)
|
||||
.digest("hex")
|
||||
.substring(0, 15)
|
||||
] = match
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
fixedCssCode =
|
||||
fixedCssCode.split(cssGlobalsToDefine[cssVariableName])
|
||||
.join(`var(--${cssVariableName})`)
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(
|
||||
params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
}
|
||||
): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
|
||||
const { cssGlobalsToDefine } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName => [
|
||||
`--${cssVariableName}:`,
|
||||
[
|
||||
"url(",
|
||||
"${url.resourcesPath}" +
|
||||
cssGlobalsToDefine[cssVariableName].match(/^url\(([^)]+)\)$/)![1],
|
||||
")"
|
||||
].join("")
|
||||
].join(" "))
|
||||
.map(line => " " + line),
|
||||
"}"
|
||||
].join("\n")
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
export { myFunction } from "./myFunction";
|
||||
export { myObject } from "./myObject";
|
@ -1,3 +0,0 @@
|
||||
export function myFunction() {
|
||||
return Promise.resolve(["a", "b", "c"]);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
import { toUpperCase } from "./tools/toUpperCase";
|
||||
|
||||
export const myObject = { "p": toUpperCase("foo") };
|
@ -1,5 +0,0 @@
|
||||
import { getProjectRoot } from "../tools/getProjectRoot";
|
||||
|
||||
console.log(
|
||||
`Project root path: ${getProjectRoot()} does it seems right ? If yes then PASS`,
|
||||
);
|
@ -1,41 +0,0 @@
|
||||
//This will not run on deno, we need a separate test runner for Deno (./mod.ts).
|
||||
|
||||
import * as child_process from "child_process";
|
||||
import * as path from "path";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
|
||||
const names = ["myFunction", "myObject", "getProjectRoot"];
|
||||
|
||||
(async () => {
|
||||
if (!!process.env.FORK) {
|
||||
process.once("unhandledRejection", error => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
require(process.env.FORK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
console.log(`Running: ${name}`);
|
||||
|
||||
const dExitCode = new Deferred<number>();
|
||||
|
||||
child_process
|
||||
.fork(__filename, undefined, {
|
||||
"env": { "FORK": path.join(__dirname, name) },
|
||||
})
|
||||
.on("message", console.log)
|
||||
.once("exit", code => dExitCode.resolve(code ?? 1));
|
||||
|
||||
const exitCode = await dExitCode.pr;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log(`${name} exited with error code: ${exitCode}`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
console.log("\n");
|
||||
}
|
||||
})();
|
@ -1,16 +0,0 @@
|
||||
import { myFunction } from "..";
|
||||
import { getPromiseAssertionApi } from "evt/tools/testing";
|
||||
|
||||
const { mustResolve } = getPromiseAssertionApi({
|
||||
"takeIntoAccountArraysOrdering": true,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await mustResolve({
|
||||
"promise": myFunction(),
|
||||
"expectedData": ["a", "b", "c"],
|
||||
"delay": 0,
|
||||
});
|
||||
|
||||
console.log("PASS");
|
||||
})();
|
@ -1,7 +0,0 @@
|
||||
import { assert } from "evt/tools/typeSafety";
|
||||
import * as inDepth from "evt/tools/inDepth";
|
||||
import { myObject } from "..";
|
||||
|
||||
assert(inDepth.same(myObject, { "p": "FOO" }));
|
||||
|
||||
console.log("PASS");
|
50
src/test/replaceImportFromStatic.ts
Normal file
50
src/test/replaceImportFromStatic.ts
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
import {
|
||||
replaceImportFromStaticInJsCode,
|
||||
replaceImportFromStaticInCssCode,
|
||||
generateCssCodeToDefineGlobals
|
||||
} from "../bin/replaceImportFromStatic";
|
||||
|
||||
const { fixedJsCode } = replaceImportFromStaticInJsCode({
|
||||
"ftlValuesGlobalName": "keycloakFtlValues",
|
||||
"jsCode": `
|
||||
function f() {
|
||||
return a.p + "static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
||||
|
||||
function f2() {
|
||||
return a.p +"static/js/" + ({}[e] || e) + "." + {
|
||||
3: "0664cdc0"
|
||||
}[e] + ".chunk.js"
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
console.log({ fixedJsCode });
|
||||
|
||||
const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({
|
||||
"cssCode": `
|
||||
|
||||
.my-div {
|
||||
background: url(/logo192.png) no-repeat center center;
|
||||
}
|
||||
|
||||
.my-div2 {
|
||||
background: url(/logo192.png) no-repeat center center;
|
||||
}
|
||||
|
||||
.my-div {
|
||||
background-image: url(/static/media/something.svg);
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
console.log({ fixedCssCode, cssGlobalsToDefine });
|
||||
|
||||
|
||||
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ cssGlobalsToDefine });
|
||||
|
||||
console.log({ cssCodeToPrependInHead });
|
@ -1,19 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
function getProjectRootRec(dirPath: string): string {
|
||||
if (fs.existsSync(path.join(dirPath, "package.json"))) {
|
||||
return dirPath;
|
||||
}
|
||||
return getProjectRootRec(path.join(dirPath, ".."));
|
||||
}
|
||||
|
||||
let result: string | undefined = undefined;
|
||||
|
||||
export function getProjectRoot(): string {
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return (result = getProjectRootRec(__dirname));
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export function toUpperCase(str: string): string {
|
||||
return str.toUpperCase();
|
||||
}
|
62
src/tools/transformCodebase.ts
Normal file
62
src/tools/transformCodebase.ts
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { crawl } from "denoify/tools/crawl";
|
||||
import { createDirectoryIfNotExistsRecursive } from "denoify/tools/createDirectoryIfNotExistsRecursive";
|
||||
|
||||
/** Apply a transformation function to every file of directory */
|
||||
export async function transformCodebase(
|
||||
params: {
|
||||
srcDirPath: string;
|
||||
destDirPath: string;
|
||||
transformSourceCodeString: (params: {
|
||||
sourceCode: Buffer;
|
||||
filePath: string;
|
||||
}) => {
|
||||
modifiedSourceCode: Buffer;
|
||||
newFileName?: string;
|
||||
} | undefined;
|
||||
}
|
||||
) {
|
||||
|
||||
const { srcDirPath, destDirPath, transformSourceCodeString } = params;
|
||||
|
||||
for (const file_relative_path of crawl(srcDirPath)) {
|
||||
|
||||
const filePath = path.join(srcDirPath, file_relative_path);
|
||||
|
||||
const transformSourceCodeStringResult = transformSourceCodeString({
|
||||
"sourceCode": fs.readFileSync(filePath),
|
||||
"filePath": path.join(srcDirPath, file_relative_path)
|
||||
});
|
||||
|
||||
if (transformSourceCodeStringResult === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await createDirectoryIfNotExistsRecursive(
|
||||
path.dirname(
|
||||
path.join(
|
||||
destDirPath,
|
||||
file_relative_path
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const { newFileName, modifiedSourceCode } = transformSourceCodeStringResult;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(
|
||||
path.dirname(path.join(destDirPath, file_relative_path)),
|
||||
newFileName ?? path.basename(file_relative_path)
|
||||
),
|
||||
modifiedSourceCode
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user