keycloak_theme/src/bin/postinstall/uiModuleMeta.ts

304 lines
9.3 KiB
TypeScript
Raw Normal View History

2024-11-02 22:39:03 +01:00
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
2024-11-09 14:02:19 +01:00
import { join as pathJoin, dirname as pathDirname } from "path";
2024-11-02 22:39:03 +01:00
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";
2024-11-17 19:22:34 +01:00
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
2024-11-02 22:39:03 +01:00
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import {
2024-11-09 14:02:19 +01:00
getUiModuleFileSourceCodeReadyToBeCopied,
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
2024-11-02 22:39:03 +01:00
import * as crypto from "crypto";
2024-11-09 14:02:19 +01:00
import { KEYCLOAK_THEME } from "../shared/constants";
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
export type UiModuleMeta = {
moduleName: string;
version: string;
files: {
fileRelativePath: string;
hash: string;
2024-11-09 14:02:19 +01:00
copyableFilePath: string;
2024-11-03 01:56:41 +01:00
}[];
peerDependencies: Record<string, string>;
2024-11-02 22:39:03 +01:00
};
2024-11-03 01:56:41 +01:00
const zUiModuleMeta = (() => {
type ExpectedType = UiModuleMeta;
2024-11-02 22:39:03 +01:00
const zTargetType = z.object({
moduleName: z.string(),
version: z.string(),
files: z.array(
z.object({
fileRelativePath: z.string(),
2024-11-09 14:02:19 +01:00
hash: z.string(),
copyableFilePath: z.string()
2024-11-02 22:39:03 +01:00
})
2024-11-03 01:56:41 +01:00
),
peerDependencies: z.record(z.string())
2024-11-02 22:39:03 +01:00
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
2024-11-03 01:56:41 +01:00
type ParsedCacheFile = {
keycloakifyVersion: string;
prettierConfigHash: string | null;
2024-11-09 14:02:19 +01:00
thisFilePath: string;
2024-11-03 01:56:41 +01:00
uiModuleMetas: UiModuleMeta[];
};
const zParsedCacheFile = (() => {
type ExpectedType = ParsedCacheFile;
2024-11-02 22:39:03 +01:00
const zTargetType = z.object({
keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]),
2024-11-09 14:02:19 +01:00
thisFilePath: z.string(),
2024-11-03 01:56:41 +01:00
uiModuleMetas: z.array(zUiModuleMeta)
2024-11-02 22:39:03 +01:00
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
2024-11-09 14:02:19 +01:00
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
2024-11-02 22:39:03 +01:00
2024-11-09 14:02:19 +01:00
export type BuildContextLike =
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
cacheDirPath: string;
packageJsonFilePath: string;
projectDirPath: string;
};
2024-11-02 22:39:03 +01:00
assert<BuildContext extends BuildContextLike ? true : false>();
2024-11-03 01:56:41 +01:00
export async function getUiModuleMetas(params: {
2024-11-02 22:39:03 +01:00
buildContext: BuildContextLike;
2024-11-03 01:56:41 +01:00
}): Promise<UiModuleMeta[]> {
2024-11-02 22:39:03 +01:00
const { buildContext } = params;
2024-11-09 14:02:19 +01:00
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
2024-11-02 22:39:03 +01:00
const keycloakifyVersion = readThisNpmPackageVersion();
const prettierConfigHash = await (async () => {
if (!(await getIsPrettierAvailable())) {
return null;
}
2024-11-17 19:22:34 +01:00
const { configHash } = await getPrettier();
2024-11-02 22:39:03 +01:00
2024-11-17 19:22:34 +01:00
return configHash;
2024-11-02 22:39:03 +01:00
})();
2024-11-09 14:02:19 +01:00
const installedUiModules = await (async () => {
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
});
return Promise.all(
installedModulesWithKeycloakifyInTheName.filter(async ({ dirPath }) =>
existsAsync(pathJoin(dirPath, KEYCLOAK_THEME))
)
);
})();
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
const cacheContent = await (async () => {
if (!(await existsAsync(cacheFilePath))) {
return undefined;
}
return await fsPr.readFile(cacheFilePath);
})();
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
if (cacheContent === undefined) {
2024-11-02 22:39:03 +01:00
return undefined;
}
2024-11-03 01:56:41 +01:00
const cacheContentStr = cacheContent.toString("utf8");
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
let parsedCacheFile: unknown;
2024-11-02 22:39:03 +01:00
try {
2024-11-03 01:56:41 +01:00
parsedCacheFile = JSON.parse(cacheContentStr);
2024-11-02 22:39:03 +01:00
} catch {
return undefined;
}
try {
2024-11-03 01:56:41 +01:00
zParsedCacheFile.parse(parsedCacheFile);
2024-11-02 22:39:03 +01:00
} catch {
return undefined;
}
2024-11-03 01:56:41 +01:00
assert(is<ParsedCacheFile>(parsedCacheFile));
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
return parsedCacheFile;
2024-11-02 22:39:03 +01:00
})();
2024-11-03 01:56:41 +01:00
if (parsedCacheFile === undefined) {
2024-11-02 22:39:03 +01:00
return [];
}
2024-11-03 01:56:41 +01:00
if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
2024-11-02 22:39:03 +01:00
return [];
}
2024-11-03 01:56:41 +01:00
if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
2024-11-02 22:39:03 +01:00
return [];
}
2024-11-09 14:02:19 +01:00
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
2024-11-03 01:56:41 +01:00
return [];
}
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
uiModuleMeta => {
const correspondingInstalledUiModule = installedUiModules.find(
installedUiModule =>
installedUiModule.moduleName === uiModuleMeta.moduleName
);
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
if (correspondingInstalledUiModule === undefined) {
return false;
}
return correspondingInstalledUiModule.version === uiModuleMeta.version;
}
);
2024-11-02 22:39:03 +01:00
2024-11-03 01:56:41 +01:00
return uiModuleMetas_cacheUpToDate;
2024-11-02 22:39:03 +01:00
})();
2024-11-03 01:56:41 +01:00
const uiModuleMetas = await Promise.all(
2024-11-02 22:39:03 +01:00
installedUiModules.map(
2024-11-03 01:56:41 +01:00
async ({
moduleName,
version,
peerDependencies,
dirPath
}): Promise<UiModuleMeta> => {
2024-11-02 22:39:03 +01:00
use_cache: {
2024-11-03 01:56:41 +01:00
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
uiModuleMeta => uiModuleMeta.moduleName === moduleName
2024-11-02 22:39:03 +01:00
);
2024-11-03 01:56:41 +01:00
if (uiModuleMeta_cache === undefined) {
2024-11-02 22:39:03 +01:00
break use_cache;
}
2024-11-03 01:56:41 +01:00
return uiModuleMeta_cache;
2024-11-02 22:39:03 +01:00
}
2024-11-03 01:56:41 +01:00
const files: UiModuleMeta["files"] = [];
2024-11-02 22:39:03 +01:00
{
2024-11-09 14:02:19 +01:00
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
2024-11-02 22:39:03 +01:00
await crawlAsync({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => {
2024-11-09 14:02:19 +01:00
const sourceCode =
await getUiModuleFileSourceCodeReadyToBeCopied({
buildContext,
fileRelativePath,
2024-11-02 22:39:03 +01:00
isForEjection: false,
2024-11-09 14:02:19 +01:00
uiModuleDirPath: dirPath,
2024-11-02 22:39:03 +01:00
uiModuleName: moduleName,
uiModuleVersion: version
2024-11-09 14:02:19 +01:00
});
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 });
2024-11-02 22:39:03 +01:00
}
2024-11-09 14:02:19 +01:00
}
2024-11-02 22:39:03 +01:00
2024-11-09 14:02:19 +01:00
fsPr.writeFile(copyableFilePath, sourceCode);
2024-11-02 22:39:03 +01:00
files.push({
fileRelativePath,
2024-11-09 14:02:19 +01:00
hash,
copyableFilePath
2024-11-02 22:39:03 +01:00
});
}
});
}
2024-11-03 01:56:41 +01:00
return id<UiModuleMeta>({
2024-11-02 22:39:03 +01:00
moduleName,
2024-11-03 01:56:41 +01:00
version,
files,
peerDependencies
2024-11-02 22:39:03 +01:00
});
}
)
);
2024-11-03 01:56:41 +01:00
update_cache: {
const parsedCacheFile = id<ParsedCacheFile>({
keycloakifyVersion,
prettierConfigHash,
2024-11-09 14:02:19 +01:00
thisFilePath: cacheFilePath,
2024-11-03 01:56:41 +01:00
uiModuleMetas
});
const cacheContent_new = Buffer.from(
JSON.stringify(parsedCacheFile, null, 2),
"utf8"
);
if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) {
break update_cache;
}
create_dir: {
const dirPath = pathDirname(cacheFilePath);
if (await existsAsync(dirPath)) {
break create_dir;
}
await fsPr.mkdir(dirPath, { recursive: true });
}
await fsPr.writeFile(cacheFilePath, cacheContent_new);
}
return uiModuleMetas;
2024-11-02 22:39:03 +01:00
}
2024-11-09 14:02:19 +01:00
export function computeHash(data: Buffer) {
return crypto.createHash("sha256").update(data).digest("hex");
}