Checkpoint

This commit is contained in:
Joseph Garrone 2024-11-09 14:02:19 +01:00
parent a60a0d0696
commit a73281d46d
16 changed files with 224 additions and 104 deletions

View File

@ -115,7 +115,7 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
}
run(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join(
"dist",
"ncc_out"
)}`

View File

@ -14,7 +14,7 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { runFormat } from "./tools/runFormat";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
@ -119,7 +119,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
}
const componentCode = fs
let sourceCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
@ -133,6 +133,17 @@ export async function command(params: { buildContext: BuildContext }) {
.replace('import React from "react";\n', "")
.replace(/from "[./]+dist\//, 'from "keycloakify/');
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetFilePath,
sourceCode: sourceCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
@ -141,11 +152,7 @@ export async function command(params: { buildContext: BuildContext }) {
}
}
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
console.log(
[

View File

@ -22,7 +22,7 @@ import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runFormat } from "./tools/runFormat";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -217,7 +217,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
}
const componentCode = fs
let componentCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
@ -229,6 +229,17 @@ export async function command(params: { buildContext: BuildContext }) {
)
.toString("utf8");
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
componentCode = await runPrettier({
filePath: targetFilePath,
sourceCode: componentCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
@ -239,10 +250,6 @@ export async function command(params: { buildContext: BuildContext }) {
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
console.log(
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))

View File

@ -197,20 +197,6 @@ program
}
});
program
.command({
name: "initialize-admin-theme",
description: "Initialize the admin theme."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-admin-theme");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
program
.command({
name: "copy-keycloak-resources-to-public",
@ -241,6 +227,20 @@ program
}
});
program
.command({
name: "postinstall",
description: "Initialize all the Keycloakify UI modules installed in the project."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./postinstall");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
// Fallback to build command if no command is provided
{
const [, , ...rest] = process.argv;

View File

@ -3,6 +3,7 @@ import * as fsPr from "fs/promises";
import { join as pathJoin, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import { KEYCLOAK_THEME } from "../shared/constants";
export type BuildContextLike = {
themeSrcDirPath: string;
@ -10,27 +11,32 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getSourceCodeToCopyInUserCodebase(params: {
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
buildContext: BuildContextLike;
relativeFromDirPath: string;
fileRelativePath: string;
commentData: {
isForEjection: boolean;
uiModuleDirPath: string;
uiModuleName: string;
uiModuleVersion: string;
};
}): Promise<string> {
const { buildContext, relativeFromDirPath, fileRelativePath, commentData } = params;
}): Promise<Buffer> {
const {
buildContext,
uiModuleDirPath,
fileRelativePath,
isForEjection,
uiModuleName,
uiModuleVersion
} = params;
let sourceCode = (
await fsPr.readFile(pathJoin(relativeFromDirPath, fileRelativePath))
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
).toString("utf8");
const comment = (() => {
if (commentData.isForEjection) {
if (isForEjection) {
return [
`/*`,
` This file was ejected from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`,
` This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`,
`*/`
].join("\n");
} else {
@ -39,7 +45,7 @@ export async function getSourceCodeToCopyInUserCodebase(params: {
` WARNING: Before modifying this file run the following command:`,
` \`npx keycloakify eject-file ${fileRelativePath.split(pathSep).join("/")}\``,
` `,
` This file comes from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`,
` This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
`*/`
];
}
@ -47,16 +53,18 @@ export async function getSourceCodeToCopyInUserCodebase(params: {
sourceCode = [comment, ``, sourceCode].join("\n");
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
sourceCode = await runPrettier({
filePath: pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
filePath: destFilePath,
sourceCode
});
}
return sourceCode;
return Buffer.from(sourceCode, "utf8");
}

View File

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

View File

@ -0,0 +1,79 @@
import type { BuildContext } from "../shared/buildContext";
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
import {
readManagedGitignoreFile,
writeManagedGitignoreFile
} from "./managedGitignoreFile";
import { dirname as pathDirname } from "path";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import * as fsPr from "fs/promises";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const uiModuleMetas = await getUiModuleMetas({ buildContext });
await installUiModulesPeerDependencies({
buildContext,
uiModuleMetas
});
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
buildContext
});
await writeManagedGitignoreFile({
buildContext,
ejectedFilesRelativePaths,
uiModuleMetas
});
await Promise.all(
uiModuleMetas
.map(uiModuleMeta =>
Promise.all(
uiModuleMeta.files.map(
async ({ fileRelativePath, copyableFilePath, hash }) => {
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
return;
}
const destFilePath = pathJoin(
buildContext.themeSrcDirPath,
fileRelativePath
);
skip_condition: {
if (!(await existsAsync(destFilePath))) {
break skip_condition;
}
const destFileHash = computeHash(
await fsPr.readFile(destFilePath)
);
if (destFileHash !== hash) {
break skip_condition;
}
return;
}
{
const dirName = pathDirname(copyableFilePath);
if (!(await existsAsync(dirName))) {
await fsPr.mkdir(dirName, { recursive: true });
}
}
await fsPr.copyFile(copyableFilePath, destFilePath);
}
)
)
)
.flat()
);
}

View File

@ -1,7 +1,7 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin, sep as pathSep, dirname as pathDirname } from "path";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fsPr from "fs/promises";
import type { BuildContext } from "../shared/buildContext";
import { is } from "tsafe/is";
@ -11,10 +11,11 @@ import { crawlAsync } from "../tools/crawlAsync";
import { getIsPrettierAvailable, getPrettierAndConfig } from "../tools/runPrettier";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import {
getSourceCodeToCopyInUserCodebase,
type BuildContextLike as BuildContextLike_getSourceCodeToCopyInUserCodebase
} from "./getSourceCodeToCopyInUserCodebase";
getUiModuleFileSourceCodeReadyToBeCopied,
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
import * as crypto from "crypto";
import { KEYCLOAK_THEME } from "../shared/constants";
export type UiModuleMeta = {
moduleName: string;
@ -22,6 +23,7 @@ export type UiModuleMeta = {
files: {
fileRelativePath: string;
hash: string;
copyableFilePath: string;
}[];
peerDependencies: Record<string, string>;
};
@ -35,7 +37,8 @@ const zUiModuleMeta = (() => {
files: z.array(
z.object({
fileRelativePath: z.string(),
hash: z.string()
hash: z.string(),
copyableFilePath: z.string()
})
),
peerDependencies: z.record(z.string())
@ -51,7 +54,7 @@ const zUiModuleMeta = (() => {
type ParsedCacheFile = {
keycloakifyVersion: string;
prettierConfigHash: string | null;
pathSep: string;
thisFilePath: string;
uiModuleMetas: UiModuleMeta[];
};
@ -61,7 +64,7 @@ const zParsedCacheFile = (() => {
const zTargetType = z.object({
keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]),
pathSep: z.string(),
thisFilePath: z.string(),
uiModuleMetas: z.array(zUiModuleMeta)
});
@ -72,13 +75,14 @@ const zParsedCacheFile = (() => {
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
const CACHE_FILE_BASENAME = "uiModulesMeta.json";
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebase & {
export type BuildContextLike =
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
cacheDirPath: string;
packageJsonFilePath: string;
projectDirPath: string;
};
};
assert<BuildContext extends BuildContextLike ? true : false>();
@ -87,7 +91,7 @@ export async function getUiModuleMetas(params: {
}): Promise<UiModuleMeta[]> {
const { buildContext } = params;
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_BASENAME);
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
const keycloakifyVersion = readThisNpmPackageVersion();
@ -101,13 +105,21 @@ export async function getUiModuleMetas(params: {
return crypto.createHash("sha256").update(JSON.stringify(config)).digest("hex");
})();
const installedUiModules = await listInstalledModules({
const installedUiModules = await (async () => {
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName.includes("keycloakify") && moduleName.endsWith("-ui")
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
});
return Promise.all(
installedModulesWithKeycloakifyInTheName.filter(async ({ dirPath }) =>
existsAsync(pathJoin(dirPath, KEYCLOAK_THEME))
)
);
})();
const cacheContent = await (async () => {
if (!(await existsAsync(cacheFilePath))) {
return undefined;
@ -155,7 +167,7 @@ export async function getUiModuleMetas(params: {
return [];
}
if (parsedCacheFile.pathSep !== pathSep) {
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
return [];
}
@ -200,31 +212,44 @@ export async function getUiModuleMetas(params: {
const files: UiModuleMeta["files"] = [];
{
const srcDirPath = pathJoin(dirPath, "src");
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
await crawlAsync({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => {
const sourceCode = await getSourceCodeToCopyInUserCodebase({
const sourceCode =
await getUiModuleFileSourceCodeReadyToBeCopied({
buildContext,
relativeFromDirPath: srcDirPath,
fileRelativePath,
commentData: {
isForEjection: false,
uiModuleDirPath: dirPath,
uiModuleName: moduleName,
uiModuleVersion: version
}
});
const hash = crypto
.createHash("sha256")
.update(sourceCode)
.digest("hex");
const hash = computeHash(sourceCode);
const copyableFilePath = pathJoin(
pathDirname(cacheFilePath),
KEYCLOAK_THEME,
fileRelativePath
);
{
const dirPath = pathDirname(copyableFilePath);
if (!(await existsAsync(dirPath))) {
await fsPr.mkdir(dirPath, { recursive: true });
}
}
fsPr.writeFile(copyableFilePath, sourceCode);
files.push({
fileRelativePath,
hash
hash,
copyableFilePath
});
}
});
@ -244,7 +269,7 @@ export async function getUiModuleMetas(params: {
const parsedCacheFile = id<ParsedCacheFile>({
keycloakifyVersion,
prettierConfigHash,
pathSep,
thisFilePath: cacheFilePath,
uiModuleMetas
});
@ -272,3 +297,7 @@ export async function getUiModuleMetas(params: {
return uiModuleMetas;
}
export function computeHash(data: Buffer) {
return crypto.createHash("sha256").update(data).digest("hex");
}

View File

@ -18,9 +18,8 @@ import {
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { THEME_TYPES } from "./constants";
import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants";
import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
@ -147,7 +146,10 @@ export function getBuildContext(params: {
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
for (const themeSrcDirBasename of [
KEYCLOAK_THEME,
KEYCLOAK_THEME.replace(/-/g, "_")
]) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
@ -173,7 +175,7 @@ export function getBuildContext(params: {
[
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
`If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory`
].join("\n")
)
);

View File

@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};
export const KEYCLOAK_THEME = "keycloak-theme";

View File

@ -1 +0,0 @@
export * from "./sync-ui-modules";

View File

@ -1,13 +0,0 @@
import type { BuildContext } from "./shared/buildContext";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { is } from "tsafe/is";
import { z } from "zod";
import { join as pathJoin } from "path";
import { existsAsync } from "./tools/fs.existsAsync";
import * as fsPr from "fs/promises";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
}

View File

@ -1,16 +1,14 @@
import { dirname as pathDirname, join as pathJoin } from "path";
import { join as pathJoin } from "path";
import { existsAsync } from "./fs.existsAsync";
import * as child_process from "child_process";
import { assert } from "tsafe/assert";
export async function getInstalledModuleDirPath(params: {
moduleName: string;
packageJsonFilePath: string;
packageJsonDirPath: string;
projectDirPath: string;
}) {
const { moduleName, packageJsonFilePath, projectDirPath } = params;
const packageJsonDirPath = pathDirname(packageJsonFilePath);
const { moduleName, packageJsonDirPath, projectDirPath } = params;
common_case: {
const dirPath = pathJoin(

View File

@ -1,10 +1,11 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin } from "path";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fsPr from "fs/promises";
import { is } from "tsafe/is";
import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath";
import { exclude } from "tsafe/exclude";
export async function listInstalledModules(params: {
packageJsonFilePath: string;
@ -27,7 +28,7 @@ export async function listInstalledModules(params: {
const uiModuleNames = (
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
)
.filter(obj => obj !== undefined)
.filter(exclude(undefined))
.map(obj => Object.keys(obj))
.flat()
.filter(moduleName => filter({ moduleName }));
@ -36,7 +37,7 @@ export async function listInstalledModules(params: {
uiModuleNames.map(async moduleName => {
const dirPath = await getInstalledModuleDirPath({
moduleName,
packageJsonFilePath,
packageJsonDirPath: pathDirname(packageJsonFilePath),
projectDirPath
});