checkpoint

This commit is contained in:
Joseph Garrone 2024-11-02 22:39:03 +01:00
parent db37320280
commit af7a45d125
16 changed files with 677 additions and 115 deletions

View File

@ -40,7 +40,9 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
);
}
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
run(
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
);
transformCodebase({
srcDirPath: join("dist", "ncc_out"),

View File

@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};
export const KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR = "kc-gen.tsx";

View File

@ -8,7 +8,7 @@ import {
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { sep as pathSep } from "path";
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
@ -19,32 +19,7 @@ export function maybeDelegateCommandToCustomHandler(params: {
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
const nodeModulesBinDirPath = (() => {
const binPath = process.argv[1];
const segments: string[] = [".bin"];
let foundNodeModules = false;
for (const segment of binPath.split(pathSep).reverse()) {
skip_segment: {
if (foundNodeModules) {
break skip_segment;
}
if (segment === "node_modules") {
foundNodeModules = true;
break skip_segment;
}
continue;
}
segments.unshift(segment);
}
return segments.join(pathSep);
})();
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false };

View File

@ -0,0 +1,62 @@
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
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";
export type BuildContextLike = {
themeSrcDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getSourceCodeToCopyInUserCodebase(params: {
buildContext: BuildContextLike;
relativeFromDirPath: string;
fileRelativePath: string;
commentData: {
isForEjection: boolean;
uiModuleName: string;
uiModuleVersion: string;
};
}): Promise<string> {
const { buildContext, relativeFromDirPath, fileRelativePath, commentData } = params;
let sourceCode = (
await fsPr.readFile(pathJoin(relativeFromDirPath, fileRelativePath))
).toString("utf8");
const comment = (() => {
if (commentData.isForEjection) {
return [
`/*`,
` This file was ejected from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`,
`*/`
].join("\n");
} else {
return [
`/*`,
` 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}.`,
`*/`
];
}
})();
sourceCode = [comment, ``, sourceCode].join("\n");
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
sourceCode = await runPrettier({
filePath: pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
sourceCode
});
}
return sourceCode;
}

View File

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

View File

@ -0,0 +1,3 @@
export async function getListOfEjectedFiles(params: {}): Promise<string[]> {}
export async function writeListOfEjectedFiles(params: {}) {}

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,225 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin } from "path";
import * as fsPr from "fs/promises";
import type { BuildContext } from "../shared/buildContext";
import { is } from "tsafe/is";
import { existsAsync } from "../tools/fs.existsAsync";
import { listInstalledModules } from "../tools/listInstalledModules";
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";
import * as crypto from "crypto";
export type UiModulesMeta = {
keycloakifyVersion: string;
prettierConfigHash: string | null;
entries: UiModulesMeta.Entry[];
};
export namespace UiModulesMeta {
export type Entry = {
moduleName: string;
version: string;
files: {
fileRelativePath: string;
hash: string;
}[];
};
}
const zUiModuleMetasEntry = (() => {
type ExpectedType = UiModulesMeta.Entry;
const zTargetType = z.object({
moduleName: z.string(),
version: z.string(),
files: z.array(
z.object({
fileRelativePath: z.string(),
hash: z.string()
})
)
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
const zUiModulesMeta = (() => {
type ExpectedType = UiModulesMeta;
const zTargetType = z.object({
keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]),
entries: z.array(zUiModuleMetasEntry)
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
const RELATIVE_FILE_PATH = pathJoin("uiModulesMeta.json");
export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebase & {
cacheDirPath: string;
packageJsonFilePath: string;
projectDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function readOrCreateUiModulesMeta(params: {
buildContext: BuildContextLike;
}): Promise<UiModulesMeta> {
const { buildContext } = params;
const filePath = pathJoin(buildContext.cacheDirPath, RELATIVE_FILE_PATH);
const keycloakifyVersion = readThisNpmPackageVersion();
const prettierConfigHash = await (async () => {
if (!(await getIsPrettierAvailable())) {
return null;
}
const { config } = await getPrettierAndConfig();
return crypto.createHash("sha256").update(JSON.stringify(config)).digest("hex");
})();
const installedUiModules = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName.includes("keycloakify") && moduleName.endsWith("-ui")
});
const upToDateEntries: UiModulesMeta.Entry[] = await (async () => {
const uiModulesMeta_cache: UiModulesMeta | undefined = await (async () => {
if (!(await existsAsync(filePath))) {
return undefined;
}
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
let uiModuleMeta: unknown;
try {
uiModuleMeta = JSON.parse(contentStr);
} catch {
return undefined;
}
try {
zUiModulesMeta.parse(uiModuleMeta);
} catch {
return undefined;
}
assert(is<UiModulesMeta>(uiModuleMeta));
return uiModuleMeta;
})();
if (uiModulesMeta_cache === undefined) {
return [];
}
if (uiModulesMeta_cache.keycloakifyVersion !== keycloakifyVersion) {
return [];
}
if (uiModulesMeta_cache.prettierConfigHash !== prettierConfigHash) {
return [];
}
const upToDateEntries = uiModulesMeta_cache.entries.filter(entry => {
const correspondingInstalledUiModule = installedUiModules.find(
installedUiModule => installedUiModule.moduleName === entry.moduleName
);
if (correspondingInstalledUiModule === undefined) {
return false;
}
return correspondingInstalledUiModule.version === entry.version;
});
return upToDateEntries;
})();
const entries = await Promise.all(
installedUiModules.map(
async ({ moduleName, version, dirPath }): Promise<UiModulesMeta.Entry> => {
use_cache: {
const cachedEntry = upToDateEntries.find(
entry => entry.moduleName === moduleName
);
if (cachedEntry === undefined) {
break use_cache;
}
return cachedEntry;
}
const files: UiModulesMeta.Entry["files"] = [];
{
const srcDirPath = pathJoin(dirPath, "src");
await crawlAsync({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => {
const sourceCode = await getSourceCodeToCopyInUserCodebase({
buildContext,
relativeFromDirPath: srcDirPath,
fileRelativePath,
commentData: {
isForEjection: false,
uiModuleName: moduleName,
uiModuleVersion: version
}
});
const hash = crypto
.createHash("sha256")
.update(sourceCode)
.digest("hex");
files.push({
fileRelativePath,
hash
});
}
});
}
return id<UiModulesMeta.Entry>({
files,
moduleName,
version
});
}
)
);
return id<UiModulesMeta>({
keycloakifyVersion,
prettierConfigHash,
entries
});
}

View File

@ -0,0 +1,51 @@
import * as fsPr from "fs/promises";
import { join as pathJoin, relative as pathRelative } from "path";
import { assert, type Equals } from "tsafe/assert";
/** List all files in a given directory return paths relative to the dir_path */
export async function crawlAsync(params: {
dirPath: string;
returnedPathsType: "absolute" | "relative to dirPath";
onFileFound: (filePath: string) => void;
}) {
const { dirPath, returnedPathsType, onFileFound } = params;
await crawlAsyncRec({
dirPath,
onFileFound: ({ filePath }) => {
switch (returnedPathsType) {
case "absolute":
onFileFound(filePath);
return;
case "relative to dirPath":
onFileFound(pathRelative(dirPath, filePath));
return;
}
assert<Equals<typeof returnedPathsType, never>>();
}
});
}
async function crawlAsyncRec(params: {
dirPath: string;
onFileFound: (params: { filePath: string }) => void;
}) {
const { dirPath, onFileFound } = params;
await Promise.all(
(await fsPr.readdir(dirPath)).map(async basename => {
const fileOrDirPath = pathJoin(dirPath, basename);
const isDirectory = await fsPr
.lstat(fileOrDirPath)
.then(stat => stat.isDirectory());
if (isDirectory) {
await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound });
return;
}
onFileFound({ filePath: fileOrDirPath });
})
);
}

View File

@ -0,0 +1,53 @@
import { dirname as pathDirname, 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;
projectDirPath: string;
}) {
const { moduleName, packageJsonFilePath, projectDirPath } = params;
const packageJsonDirPath = pathDirname(packageJsonFilePath);
common_case: {
const dirPath = pathJoin(
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
);
if (!(await existsAsync(dirPath))) {
break common_case;
}
return dirPath;
}
node_modules_at_root_case: {
if (projectDirPath === packageJsonDirPath) {
break node_modules_at_root_case;
}
const dirPath = pathJoin(
...[projectDirPath, "node_modules", ...moduleName.split("/")]
);
if (!(await existsAsync(dirPath))) {
break node_modules_at_root_case;
}
return dirPath;
}
const dirPath = child_process
.execSync(`npm list ${moduleName}`, {
cwd: packageJsonDirPath
})
.toString("utf8")
.trim();
assert(dirPath !== "");
return dirPath;
}

View File

@ -0,0 +1,114 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin } from "path";
import * as fsPr from "fs/promises";
import { is } from "tsafe/is";
import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath";
export async function listInstalledModules(params: {
packageJsonFilePath: string;
projectDirPath: string;
filter: (params: { moduleName: string }) => boolean;
}): Promise<{ moduleName: string; version: string; dirPath: string }[]> {
const { packageJsonFilePath, projectDirPath, filter } = params;
const parsedPackageJson = await readPackageJsonDependencies({
packageJsonFilePath
});
const uiModuleNames = (
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
)
.filter(obj => obj !== undefined)
.map(obj => Object.keys(obj))
.flat()
.filter(moduleName => filter({ moduleName }));
const result = await Promise.all(
uiModuleNames.map(async moduleName => {
const dirPath = await getInstalledModuleDirPath({
moduleName,
packageJsonFilePath,
projectDirPath
});
const { version } = await readPackageJsonVersion({
packageJsonFilePath: pathJoin(dirPath, "package.json")
});
return { moduleName, version, dirPath } as const;
})
);
return result;
}
const { readPackageJsonDependencies } = (() => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
async function readPackageJsonDependencies(params: { packageJsonFilePath: string }) {
const { packageJsonFilePath } = params;
const parsedPackageJson = JSON.parse(
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
}
return { readPackageJsonDependencies };
})();
const { readPackageJsonVersion } = (() => {
type ParsedPackageJson = {
version: string;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
version: z.string()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
async function readPackageJsonVersion(params: { packageJsonFilePath: string }) {
const { packageJsonFilePath } = params;
const parsedPackageJson = JSON.parse(
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
}
return { readPackageJsonVersion };
})();

View File

@ -0,0 +1,38 @@
import { sep as pathSep } from "path";
let cache: string | undefined = undefined;
export function getNodeModulesBinDirPath() {
if (cache !== undefined) {
return cache;
}
const binPath = process.argv[1];
const segments: string[] = [".bin"];
let foundNodeModules = false;
for (const segment of binPath.split(pathSep).reverse()) {
skip_segment: {
if (foundNodeModules) {
break skip_segment;
}
if (segment === "node_modules") {
foundNodeModules = true;
break skip_segment;
}
continue;
}
segments.unshift(segment);
}
const nodeModulesBinDirPath = segments.join(pathSep);
cache = nodeModulesBinDirPath;
return nodeModulesBinDirPath;
}

View File

@ -3,7 +3,13 @@ import { assert } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin } from "path";
let cache: string | undefined = undefined;
export function readThisNpmPackageVersion(): string {
if (cache !== undefined) {
return cache;
}
const version = JSON.parse(
fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
@ -12,5 +18,7 @@ export function readThisNpmPackageVersion(): string {
assert(typeof version === "string");
cache = version;
return version;
}

View File

@ -1,71 +0,0 @@
import * as fs from "fs";
import { dirname as pathDirname } from "path";
import { assert, Equals } from "tsafe/assert";
import chalk from "chalk";
import { id } from "tsafe/id";
import { z } from "zod";
import { is } from "tsafe/is";
import * as child_process from "child_process";
export function runFormat(params: { packageJsonFilePath: string }) {
const { packageJsonFilePath } = params;
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
const { scripts } = parsedPackageJson;
if (scripts === undefined) {
return;
}
for (const scriptName of ["format", "lint"]) {
if (!(scriptName in scripts)) {
continue;
}
const command = `npm run ${scriptName}`;
console.log(chalk.grey(`$ ${command}`));
try {
child_process.execSync(`npm run ${scriptName}`, {
stdio: "inherit",
cwd: pathDirname(packageJsonFilePath)
});
} catch {
console.log(
chalk.yellow(
`\`${command}\` failed, it does not matter, please format your code manually, continuing...`
)
);
}
return;
}
}

View File

@ -0,0 +1,77 @@
import { getNodeModulesBinDirPath } from "./nodeModulesBinDirPath";
import { join as pathJoin } from "path";
import * as fsPr from "fs/promises";
import { id } from "tsafe/id";
import { assert } from "tsafe/assert";
import chalk from "chalk";
getIsPrettierAvailable.cache = id<boolean | undefined>(undefined);
export async function getIsPrettierAvailable(): Promise<boolean> {
if (getIsPrettierAvailable.cache !== undefined) {
return getIsPrettierAvailable.cache;
}
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
const stats = await fsPr.stat(prettierBinPath).catch(() => undefined);
const isPrettierAvailable = stats?.isFile() ?? false;
getIsPrettierAvailable.cache = isPrettierAvailable;
return isPrettierAvailable;
}
type PrettierAndConfig = {
prettier: typeof import("prettier");
config: import("prettier").Options | null;
};
getPrettierAndConfig.cache = id<PrettierAndConfig | undefined>(undefined);
export async function getPrettierAndConfig(): Promise<PrettierAndConfig> {
assert(getIsPrettierAvailable());
if (getPrettierAndConfig.cache !== undefined) {
return getPrettierAndConfig.cache;
}
const prettier = await import("prettier");
const prettierAndConfig: PrettierAndConfig = {
prettier,
config: await prettier.resolveConfig(pathJoin(getNodeModulesBinDirPath(), ".."))
};
getPrettierAndConfig.cache = prettierAndConfig;
return prettierAndConfig;
}
export async function runPrettier(params: {
sourceCode: string;
filePath: string;
}): Promise<string> {
const { sourceCode, filePath } = params;
let formattedSourceCode: string;
try {
const { prettier, config } = await getPrettierAndConfig();
formattedSourceCode = await prettier.format(sourceCode, { ...config, filePath });
} catch (error) {
console.log(
chalk.red(
`You probably need to upgrade the version of prettier in your project`
)
);
throw error;
}
return formattedSourceCode;
}

View File

@ -1,10 +1,11 @@
import type { BuildContext } from "./shared/buildContext";
import { KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR } from "./shared/constants";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "./tools/fs.existsAsync";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runFormat } from "./tools/runFormat";
import * as crypto from "crypto";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -18,13 +19,16 @@ export async function command(params: { buildContext: BuildContext }) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const filePath = pathJoin(
buildContext.themeSrcDirPath,
KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR
);
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
const newContent = [
let newContent = [
``,
`/* eslint-disable */`,
``,
@ -114,20 +118,25 @@ export async function command(params: { buildContext: BuildContext }) {
return;
}
await fs.writeFile(
filePath,
Buffer.from(
[
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n"),
"utf8"
)
);
newContent = [
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n");
runFormat({ packageJsonFilePath: buildContext.packageJsonFilePath });
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
newContent = await runPrettier({
filePath,
sourceCode: newContent
});
}
await fs.writeFile(filePath, Buffer.from(newContent, "utf8"));
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");