Merge pull request #717 from keycloakify/admin_theme_support
Run prettier of a file per file basis (and prepare for extension support)
This commit is contained in:
commit
2a126d65c5
@ -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({
|
transformCodebase({
|
||||||
srcDirPath: join("dist", "ncc_out"),
|
srcDirPath: join("dist", "ncc_out"),
|
||||||
@ -113,7 +115,7 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
|||||||
}
|
}
|
||||||
|
|
||||||
run(
|
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",
|
"dist",
|
||||||
"ncc_out"
|
"ncc_out"
|
||||||
)}`
|
)}`
|
||||||
|
@ -37,7 +37,7 @@ async function generateI18nMessages() {
|
|||||||
|
|
||||||
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
for (const themeType of THEME_TYPES) {
|
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
|
||||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersionId: (() => {
|
keycloakVersionId: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
|
@ -125,7 +125,54 @@ if (testAppPaths.length === 0) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
testAppPaths.forEach(testAppPath => {
|
||||||
|
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
|
||||||
|
|
||||||
|
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hasPostInstallOrPrepareScript = false;
|
||||||
|
|
||||||
|
if (parsedPackageJson.scripts !== undefined) {
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
if (parsedPackageJson.scripts[scriptName] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPostInstallOrPrepareScript = true;
|
||||||
|
|
||||||
|
delete parsedPackageJson.scripts[scriptName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPostInstallOrPrepareScript) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonFilePath,
|
||||||
|
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const restorePackageJson = () => {
|
||||||
|
if (!hasPostInstallOrPrepareScript) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync("yarn install", { cwd: testAppPath });
|
||||||
|
} catch (error) {
|
||||||
|
restorePackageJson();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
restorePackageJson();
|
||||||
|
});
|
||||||
|
|
||||||
console.log("=== Linking common dependencies ===");
|
console.log("=== Linking common dependencies ===");
|
||||||
|
|
||||||
@ -172,4 +219,20 @@ testAppPaths.forEach(testAppPath =>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath => {
|
||||||
|
const { scripts = {} } = JSON.parse(
|
||||||
|
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
|
||||||
|
) as {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
if (scripts[scriptName] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -5,8 +5,7 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -15,7 +14,7 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
|||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import type { BuildContext } from "./shared/buildContext";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { runFormat } from "./tools/runFormat";
|
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
@ -39,6 +38,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -49,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -68,6 +69,16 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType === "admin") {
|
||||||
|
console.log(
|
||||||
|
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
@ -108,7 +119,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentCode = fs
|
let sourceCode = fs
|
||||||
.readFileSync(
|
.readFileSync(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
@ -122,6 +133,17 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
.replace('import React from "react";\n', "")
|
.replace('import React from "react";\n', "")
|
||||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||||
|
|
||||||
|
run_prettier: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break run_prettier;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
filePath: targetFilePath,
|
||||||
|
sourceCode: sourceCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const targetDirPath = pathDirname(targetFilePath);
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
@ -130,11 +152,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
|
||||||
|
|
||||||
runFormat({
|
|
||||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
|
68
src/bin/eject-file.ts
Normal file
68
src/bin/eject-file.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { getUiModuleMetas } from "./postinstall/uiModuleMeta";
|
||||||
|
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import {
|
||||||
|
readManagedGitignoreFile,
|
||||||
|
writeManagedGitignoreFile
|
||||||
|
} from "./postinstall/managedGitignoreFile";
|
||||||
|
|
||||||
|
export async function command(params: {
|
||||||
|
buildContext: BuildContext;
|
||||||
|
cliCommandOptions: {
|
||||||
|
file: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const { buildContext, cliCommandOptions } = params;
|
||||||
|
|
||||||
|
const fileRelativePath = pathRelative(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
getAbsoluteAndInOsFormatPath({
|
||||||
|
cwd: buildContext.themeSrcDirPath,
|
||||||
|
pathIsh: cliCommandOptions.file
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
||||||
|
|
||||||
|
const uiModuleMeta = uiModuleMetas.find(({ files }) =>
|
||||||
|
files.map(({ fileRelativePath }) => fileRelativePath).includes(fileRelativePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uiModuleMeta) {
|
||||||
|
throw new Error(`No UI module found for the file ${fileRelativePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiModuleDirPath = await getInstalledModuleDirPath({
|
||||||
|
moduleName: uiModuleMeta.moduleName,
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath),
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({
|
||||||
|
buildContext,
|
||||||
|
fileRelativePath,
|
||||||
|
isForEjection: true,
|
||||||
|
uiModuleName: uiModuleMeta.moduleName,
|
||||||
|
uiModuleDirPath,
|
||||||
|
uiModuleVersion: uiModuleMeta.version
|
||||||
|
});
|
||||||
|
|
||||||
|
await fsPr.writeFile(
|
||||||
|
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
||||||
|
sourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
uiModuleMetas,
|
||||||
|
ejectedFilesRelativePaths: [...ejectedFilesRelativePaths, fileRelativePath]
|
||||||
|
});
|
||||||
|
}
|
@ -7,8 +7,7 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -23,7 +22,7 @@ import { assert, Equals } from "tsafe/assert";
|
|||||||
import type { BuildContext } from "./shared/buildContext";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
import { runFormat } from "./tools/runFormat";
|
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
@ -46,6 +45,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -56,7 +57,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -65,6 +66,14 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return value;
|
return value;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
if (themeType === "admin") {
|
||||||
|
console.log(
|
||||||
|
"Use `npx keycloakify eject-file` command instead, see documentation"
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeType === "account" &&
|
themeType === "account" &&
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
@ -74,13 +83,13 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
pathDirname(buildContext.packageJsonFilePath),
|
pathDirname(buildContext.packageJsonFilePath),
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"@keycloakify",
|
"@keycloakify",
|
||||||
"keycloak-account-ui",
|
`keycloak-account-ui`,
|
||||||
"src"
|
"src"
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
|
`There isn't an interactive CLI to eject components of the Account SPA UI.`,
|
||||||
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
||||||
``,
|
``,
|
||||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||||
@ -89,41 +98,44 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
eject_entrypoint: {
|
eject_entrypoint: {
|
||||||
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const;
|
||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(
|
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||||
buildContext.themeSrcDirPath,
|
|
||||||
"account"
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetFilePath = pathJoin(
|
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath);
|
||||||
accountThemeSrcDirPath,
|
|
||||||
kcAccountUiTsxFileRelativePath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(targetFilePath)) {
|
if (fs.existsSync(targetFilePath)) {
|
||||||
break eject_entrypoint;
|
break eject_entrypoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.cpSync(
|
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath);
|
||||||
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
|
|
||||||
targetFilePath
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
|
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx");
|
||||||
|
|
||||||
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
||||||
|
|
||||||
const componentName = pathBasename(
|
const componentName = pathBasename(kcUiTsxFileRelativePath).replace(
|
||||||
kcAccountUiTsxFileRelativePath
|
/.tsx$/,
|
||||||
).replace(/.tsx$/, "");
|
""
|
||||||
|
);
|
||||||
|
|
||||||
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
let modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
`@keycloakify/keycloak-account-ui/${componentName}`,
|
||||||
`./${componentName}`
|
`./${componentName}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
run_prettier: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break run_prettier;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedKcPageTsxCode = await runPrettier({
|
||||||
|
filePath: kcPageTsxFilePath,
|
||||||
|
sourceCode: modifiedKcPageTsxCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
kcPageTsxFilePath,
|
kcPageTsxFilePath,
|
||||||
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
||||||
@ -139,13 +151,14 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
[
|
[
|
||||||
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
||||||
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
||||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
|
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``,
|
||||||
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
|
`then update the import of routes in ${kcUiTsxFileRelativePath}.`
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
@ -222,7 +235,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentCode = fs
|
let componentCode = fs
|
||||||
.readFileSync(
|
.readFileSync(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
@ -234,6 +247,17 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
)
|
)
|
||||||
.toString("utf8");
|
.toString("utf8");
|
||||||
|
|
||||||
|
run_prettier: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break run_prettier;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentCode = await runPrettier({
|
||||||
|
filePath: targetFilePath,
|
||||||
|
sourceCode: componentCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const targetDirPath = pathDirname(targetFilePath);
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
@ -244,10 +268,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||||
|
|
||||||
runFormat({
|
|
||||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.green("✓")} ${chalk.bold(
|
`${chalk.green("✓")} ${chalk.bold(
|
||||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import cliSelect from "cli-select";
|
import cliSelect from "cli-select";
|
||||||
import child_process from "child_process";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||||
|
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
@ -38,37 +38,9 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_if_uncommitted_changes: {
|
exitIfUncommittedChanges({
|
||||||
let hasUncommittedChanges: boolean | undefined = undefined;
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
try {
|
|
||||||
hasUncommittedChanges =
|
|
||||||
child_process
|
|
||||||
.execSync(`git status --porcelain`, {
|
|
||||||
cwd: buildContext.projectDirPath
|
|
||||||
})
|
|
||||||
.toString()
|
|
||||||
.trim() !== "";
|
|
||||||
} catch {
|
|
||||||
// Probably not a git repository
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUncommittedChanges) {
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
[
|
|
||||||
chalk.red(
|
|
||||||
"Please commit or stash your changes before running this command.\n"
|
|
||||||
),
|
|
||||||
"This command will modify your project's files so it's better to have a clean working directory",
|
|
||||||
"so that you can easily see what has been changed and revert if needed."
|
|
||||||
].join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value: accountThemeType } = await cliSelect({
|
const { value: accountThemeType } = await cliSelect({
|
||||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
import { relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@ -14,7 +14,6 @@ import { is } from "tsafe/is";
|
|||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import { npmInstall } from "../tools/npmInstall";
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
import { copyBoilerplate } from "./copyBoilerplate";
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
||||||
|
|
||||||
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
@ -121,20 +120,7 @@ export async function initializeAccountTheme_singlePage(params: {
|
|||||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||||
);
|
);
|
||||||
|
|
||||||
run_npm_install: {
|
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
||||||
if (
|
|
||||||
JSON.parse(
|
|
||||||
fs
|
|
||||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
|
||||||
.toString("utf8")
|
|
||||||
)["version"] === "0.0.0"
|
|
||||||
) {
|
|
||||||
//NOTE: Linked version
|
|
||||||
break run_npm_install;
|
|
||||||
}
|
|
||||||
|
|
||||||
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
|
||||||
}
|
|
||||||
|
|
||||||
copyBoilerplate({
|
copyBoilerplate({
|
||||||
accountThemeType: "Single-Page",
|
accountThemeType: "Single-Page",
|
||||||
|
@ -22,7 +22,7 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
|||||||
|
|
||||||
export function generateMessageProperties(params: {
|
export function generateMessageProperties(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
themeType: ThemeType;
|
themeType: Exclude<ThemeType, "admin">;
|
||||||
}): {
|
}): {
|
||||||
languageTags: string[];
|
languageTags: string[];
|
||||||
writeMessagePropertiesFiles: (params: {
|
writeMessagePropertiesFiles: (params: {
|
||||||
|
@ -19,7 +19,8 @@ import {
|
|||||||
type ThemeType,
|
type ThemeType,
|
||||||
LOGIN_THEME_PAGE_IDS,
|
LOGIN_THEME_PAGE_IDS,
|
||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME
|
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||||
|
THEME_TYPES
|
||||||
} from "../../shared/constants";
|
} from "../../shared/constants";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||||
@ -78,15 +79,29 @@ export async function generateResources(params: {
|
|||||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const themeType of ["login", "account"] as const) {
|
for (const themeType of THEME_TYPES) {
|
||||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForAccountSpa =
|
const getAccountThemeType = () => {
|
||||||
themeType === "account" &&
|
assert(themeType === "account");
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||||
|
|
||||||
|
return buildContext.implementedThemeTypes.account.type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSpa = (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return false;
|
||||||
|
case "account":
|
||||||
|
return getAccountThemeType() === "Single-Page";
|
||||||
|
case "admin":
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||||
|
|
||||||
@ -101,7 +116,7 @@ export async function generateResources(params: {
|
|||||||
rmSync(destDirPath, { recursive: true, force: true });
|
rmSync(destDirPath, { recursive: true, force: true });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeType === "account" &&
|
themeType !== "login" &&
|
||||||
buildContext.implementedThemeTypes.login.isImplemented
|
buildContext.implementedThemeTypes.login.isImplemented
|
||||||
) {
|
) {
|
||||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||||
@ -182,10 +197,13 @@ export async function generateResources(params: {
|
|||||||
buildContext,
|
buildContext,
|
||||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
themeType,
|
themeType,
|
||||||
fieldNames: readFieldNameUsage({
|
fieldNames: isSpa
|
||||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
? []
|
||||||
themeType
|
: (assert(themeType !== "admin"),
|
||||||
})
|
readFieldNameUsage({
|
||||||
|
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||||
|
themeType
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
@ -194,10 +212,14 @@ export async function generateResources(params: {
|
|||||||
case "login":
|
case "login":
|
||||||
return LOGIN_THEME_PAGE_IDS;
|
return LOGIN_THEME_PAGE_IDS;
|
||||||
case "account":
|
case "account":
|
||||||
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
return getAccountThemeType() === "Single-Page"
|
||||||
|
? ["index.ftl"]
|
||||||
|
: ACCOUNT_THEME_PAGE_IDS;
|
||||||
|
case "admin":
|
||||||
|
return ["index.ftl"];
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
...(isForAccountSpa
|
...(isSpa
|
||||||
? []
|
? []
|
||||||
: readExtraPagesNames({
|
: readExtraPagesNames({
|
||||||
themeType,
|
themeType,
|
||||||
@ -215,10 +237,12 @@ export async function generateResources(params: {
|
|||||||
let languageTags: string[] | undefined = undefined;
|
let languageTags: string[] | undefined = undefined;
|
||||||
|
|
||||||
i18n_messages_generation: {
|
i18n_messages_generation: {
|
||||||
if (isForAccountSpa) {
|
if (isSpa) {
|
||||||
break i18n_messages_generation;
|
break i18n_messages_generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "admin");
|
||||||
|
|
||||||
const wrap = generateMessageProperties({
|
const wrap = generateMessageProperties({
|
||||||
buildContext,
|
buildContext,
|
||||||
themeType
|
themeType
|
||||||
@ -231,16 +255,15 @@ export async function generateResources(params: {
|
|||||||
writeMessagePropertiesFiles;
|
writeMessagePropertiesFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
bring_in_account_v3_i18n_messages: {
|
bring_in_spas_messages: {
|
||||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
if (!isSpa) {
|
||||||
break bring_in_account_v3_i18n_messages;
|
break bring_in_spas_messages;
|
||||||
}
|
|
||||||
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
|
|
||||||
break bring_in_account_v3_i18n_messages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "login");
|
||||||
|
|
||||||
const accountUiDirPath = child_process
|
const accountUiDirPath = child_process
|
||||||
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
|
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
||||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||||
})
|
})
|
||||||
.toString("utf8")
|
.toString("utf8")
|
||||||
@ -255,7 +278,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messagesDirPath_dest = pathJoin(
|
const messagesDirPath_dest = pathJoin(
|
||||||
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
getThemeTypeDirPath({ themeName, themeType }),
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -267,7 +290,7 @@ export async function generateResources(params: {
|
|||||||
apply_theme_changes: {
|
apply_theme_changes: {
|
||||||
const messagesDirPath_theme = pathJoin(
|
const messagesDirPath_theme = pathJoin(
|
||||||
buildContext.themeSrcDirPath,
|
buildContext.themeSrcDirPath,
|
||||||
"account",
|
themeType,
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -316,7 +339,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keycloak_static_resources: {
|
keycloak_static_resources: {
|
||||||
if (isForAccountSpa) {
|
if (isSpa) {
|
||||||
break keycloak_static_resources;
|
break keycloak_static_resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,13 +362,22 @@ export async function generateResources(params: {
|
|||||||
`parent=${(() => {
|
`parent=${(() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
case "account":
|
case "account":
|
||||||
return isForAccountSpa ? "base" : "account-v1";
|
switch (getAccountThemeType()) {
|
||||||
|
case "Multi-Page":
|
||||||
|
return "account-v1";
|
||||||
|
case "Single-Page":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
case "login":
|
case "login":
|
||||||
return "keycloak";
|
return "keycloak";
|
||||||
|
case "admin":
|
||||||
|
return "base";
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()}`,
|
})()}`,
|
||||||
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
|
...(themeType === "account" && getAccountThemeType() === "Single-Page"
|
||||||
|
? ["deprecatedMode=false"]
|
||||||
|
: []),
|
||||||
...(buildContext.extraThemeProperties ?? []),
|
...(buildContext.extraThemeProperties ?? []),
|
||||||
...buildContext.environmentVariables.map(
|
...buildContext.environmentVariables.map(
|
||||||
({ name, default: defaultValue }) =>
|
({ name, default: defaultValue }) =>
|
||||||
|
@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
|
|||||||
/** Assumes the theme type exists */
|
/** Assumes the theme type exists */
|
||||||
export function readFieldNameUsage(params: {
|
export function readFieldNameUsage(params: {
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
themeType: ThemeType;
|
themeType: Exclude<ThemeType, "admin">;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const { themeSrcDirPath, themeType } = params;
|
const { themeSrcDirPath, themeType } = params;
|
||||||
|
|
||||||
|
@ -227,6 +227,56 @@ 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 }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command<{
|
||||||
|
file: string;
|
||||||
|
}>({
|
||||||
|
name: "eject-file",
|
||||||
|
description: [
|
||||||
|
"WARNING: Not usable yet, will be used for future features",
|
||||||
|
"Take ownership over a given file"
|
||||||
|
].join(" ")
|
||||||
|
})
|
||||||
|
.option({
|
||||||
|
key: "file",
|
||||||
|
name: (() => {
|
||||||
|
const name = "file";
|
||||||
|
|
||||||
|
optionsKeys.push(name);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
})(),
|
||||||
|
description: [
|
||||||
|
"Relative path of the file relative to the directory of your keycloak theme source",
|
||||||
|
"Example `--file src/login/page/Login.tsx`"
|
||||||
|
].join(" ")
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath, file }) => {
|
||||||
|
const { command } = await import("./eject-file");
|
||||||
|
|
||||||
|
await command({
|
||||||
|
buildContext: getBuildContext({ projectDirPath }),
|
||||||
|
cliCommandOptions: { file }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fallback to build command if no command is provided
|
// Fallback to build command if no command is provided
|
||||||
{
|
{
|
||||||
const [, , ...rest] = process.argv;
|
const [, , ...rest] = process.argv;
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
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";
|
||||||
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
fileRelativePath: string;
|
||||||
|
isForEjection: boolean;
|
||||||
|
uiModuleDirPath: string;
|
||||||
|
uiModuleName: string;
|
||||||
|
uiModuleVersion: string;
|
||||||
|
}): Promise<Buffer> {
|
||||||
|
const {
|
||||||
|
buildContext,
|
||||||
|
uiModuleDirPath,
|
||||||
|
fileRelativePath,
|
||||||
|
isForEjection,
|
||||||
|
uiModuleName,
|
||||||
|
uiModuleVersion
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let sourceCode = (
|
||||||
|
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
|
||||||
|
).toString("utf8");
|
||||||
|
|
||||||
|
const comment = (() => {
|
||||||
|
if (isForEjection) {
|
||||||
|
return [
|
||||||
|
`/*`,
|
||||||
|
`This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`,
|
||||||
|
`*/`
|
||||||
|
].join("\n");
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
`/*`,
|
||||||
|
`WARNING: Before modifying this file run the following command:`,
|
||||||
|
``,
|
||||||
|
`npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}\``,
|
||||||
|
``,
|
||||||
|
`This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
|
||||||
|
`*/`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
sourceCode = [comment, ``, sourceCode].join("\n");
|
||||||
|
|
||||||
|
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
||||||
|
|
||||||
|
format: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break format;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
filePath: destFilePath,
|
||||||
|
sourceCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(sourceCode, "utf8");
|
||||||
|
}
|
1
src/bin/postinstall/index.ts
Normal file
1
src/bin/postinstall/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./postinstall";
|
157
src/bin/postinstall/installUiModulesPeerDependencies.ts
Normal file
157
src/bin/postinstall/installUiModulesPeerDependencies.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import { same } from "evt/tools/inDepth/same";
|
||||||
|
import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
|
||||||
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export type UiModuleMetaLike = {
|
||||||
|
moduleName: string;
|
||||||
|
peerDependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
|
||||||
|
|
||||||
|
export async function installUiModulesPeerDependencies(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
uiModuleMetas: UiModuleMetaLike[];
|
||||||
|
}): Promise<void | never> {
|
||||||
|
const { buildContext, uiModuleMetas } = params;
|
||||||
|
|
||||||
|
const { uiModulesPerDependencies } = (() => {
|
||||||
|
const uiModulesPerDependencies: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const { peerDependencies } of uiModuleMetas) {
|
||||||
|
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||||
|
peerDependencies
|
||||||
|
)) {
|
||||||
|
const versionRange = (() => {
|
||||||
|
const versionRange_current =
|
||||||
|
uiModulesPerDependencies[peerDependencyName];
|
||||||
|
|
||||||
|
if (versionRange_current === undefined) {
|
||||||
|
return versionRange_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionRange_current === "*") {
|
||||||
|
return versionRange_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionRange_candidate === "*") {
|
||||||
|
return versionRange_current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { versionRange } = [
|
||||||
|
versionRange_current,
|
||||||
|
versionRange_candidate
|
||||||
|
]
|
||||||
|
.map(versionRange => ({
|
||||||
|
versionRange,
|
||||||
|
semVer: SemVer.parse(
|
||||||
|
(() => {
|
||||||
|
if (
|
||||||
|
versionRange.startsWith("^") ||
|
||||||
|
versionRange.startsWith("~")
|
||||||
|
) {
|
||||||
|
return versionRange.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionRange;
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0];
|
||||||
|
|
||||||
|
return versionRange;
|
||||||
|
})();
|
||||||
|
|
||||||
|
uiModulesPerDependencies[peerDependencyName] = versionRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { uiModulesPerDependencies };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = await (async () => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
dependencies?: Record<string, string>;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zParsedPackageJson = z.object({
|
||||||
|
dependencies: z.record(z.string()).optional(),
|
||||||
|
devDependencies: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zParsedPackageJson>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zParsedPackageJson);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
(await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
||||||
|
|
||||||
|
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
|
||||||
|
if (moduleName.startsWith("@types/")) {
|
||||||
|
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
(parsedPackageJson.dependencies ??= {})[moduleName] = versionRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (same(parsedPackageJson, parsedPackageJson_before)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2);
|
||||||
|
|
||||||
|
format: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break format;
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJsonContentStr = await runPrettier({
|
||||||
|
sourceCode: packageJsonContentStr,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
||||||
|
|
||||||
|
npmInstall({
|
||||||
|
packageJsonDirPath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
135
src/bin/postinstall/managedGitignoreFile.ts
Normal file
135
src/bin/postinstall/managedGitignoreFile.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import {
|
||||||
|
join as pathJoin,
|
||||||
|
sep as pathSep,
|
||||||
|
dirname as pathDirname,
|
||||||
|
relative as pathRelative
|
||||||
|
} from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
const DELIMITER_START = `# === Ejected files start ===`;
|
||||||
|
const DELIMITER_END = `# === Ejected files end =====`;
|
||||||
|
|
||||||
|
export async function writeManagedGitignoreFile(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
uiModuleMetas: UiModuleMeta[];
|
||||||
|
ejectedFilesRelativePaths: string[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
|
||||||
|
|
||||||
|
if (uiModuleMetas.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||||
|
|
||||||
|
const content_new = Buffer.from(
|
||||||
|
[
|
||||||
|
`# This file is managed by Keycloakify, do not edit it manually.`,
|
||||||
|
``,
|
||||||
|
DELIMITER_START,
|
||||||
|
...ejectedFilesRelativePaths.map(fileRelativePath =>
|
||||||
|
fileRelativePath.split(pathSep).join("/")
|
||||||
|
),
|
||||||
|
DELIMITER_END,
|
||||||
|
``,
|
||||||
|
...uiModuleMetas
|
||||||
|
.map(uiModuleMeta => [
|
||||||
|
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
|
||||||
|
...uiModuleMeta.files
|
||||||
|
.map(({ fileRelativePath }) => fileRelativePath)
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ejectedFilesRelativePaths.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
fileRelativePath =>
|
||||||
|
`/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}`
|
||||||
|
),
|
||||||
|
|
||||||
|
``
|
||||||
|
])
|
||||||
|
.flat()
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const content_current = await (async () => {
|
||||||
|
if (!(await existsAsync(filePath))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fsPr.readFile(filePath);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (content_current !== undefined && content_current.equals(content_new)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dir: {
|
||||||
|
const dirPath = pathDirname(filePath);
|
||||||
|
|
||||||
|
if (await existsAsync(dirPath)) {
|
||||||
|
break create_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(filePath, content_new);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readManagedGitignoreFile(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<{
|
||||||
|
ejectedFilesRelativePaths: string[];
|
||||||
|
}> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||||
|
|
||||||
|
if (!(await existsAsync(filePath))) {
|
||||||
|
return { ejectedFilesRelativePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
||||||
|
|
||||||
|
const payload = (() => {
|
||||||
|
const index_start = contentStr.indexOf(DELIMITER_START);
|
||||||
|
const index_end = contentStr.indexOf(DELIMITER_END);
|
||||||
|
|
||||||
|
if (index_start === -1 || index_end === -1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (payload === undefined) {
|
||||||
|
return { ejectedFilesRelativePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ejectedFilesRelativePaths = payload
|
||||||
|
.split("\n")
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line !== "")
|
||||||
|
.map(line =>
|
||||||
|
getAbsoluteAndInOsFormatPath({
|
||||||
|
cwd: buildContext.themeSrcDirPath,
|
||||||
|
pathIsh: line
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
||||||
|
|
||||||
|
return { ejectedFilesRelativePaths };
|
||||||
|
}
|
79
src/bin/postinstall/postinstall.ts
Normal file
79
src/bin/postinstall/postinstall.ts
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
303
src/bin/postinstall/uiModuleMeta.ts
Normal file
303
src/bin/postinstall/uiModuleMeta.ts
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { z } from "zod";
|
||||||
|
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";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { listInstalledModules } from "../tools/listInstalledModules";
|
||||||
|
import { crawlAsync } from "../tools/crawlAsync";
|
||||||
|
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
||||||
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
|
import {
|
||||||
|
getUiModuleFileSourceCodeReadyToBeCopied,
|
||||||
|
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
|
||||||
|
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
|
|
||||||
|
export type UiModuleMeta = {
|
||||||
|
moduleName: string;
|
||||||
|
version: string;
|
||||||
|
files: {
|
||||||
|
fileRelativePath: string;
|
||||||
|
hash: string;
|
||||||
|
copyableFilePath: string;
|
||||||
|
}[];
|
||||||
|
peerDependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zUiModuleMeta = (() => {
|
||||||
|
type ExpectedType = UiModuleMeta;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
moduleName: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
files: z.array(
|
||||||
|
z.object({
|
||||||
|
fileRelativePath: z.string(),
|
||||||
|
hash: z.string(),
|
||||||
|
copyableFilePath: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
peerDependencies: z.record(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, ExpectedType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
type ParsedCacheFile = {
|
||||||
|
keycloakifyVersion: string;
|
||||||
|
prettierConfigHash: string | null;
|
||||||
|
thisFilePath: string;
|
||||||
|
uiModuleMetas: UiModuleMeta[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedCacheFile = (() => {
|
||||||
|
type ExpectedType = ParsedCacheFile;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
keycloakifyVersion: z.string(),
|
||||||
|
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||||
|
thisFilePath: z.string(),
|
||||||
|
uiModuleMetas: z.array(zUiModuleMeta)
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<InferredType, ExpectedType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
|
||||||
|
|
||||||
|
export type BuildContextLike =
|
||||||
|
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
|
||||||
|
cacheDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
projectDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function getUiModuleMetas(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<UiModuleMeta[]> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
||||||
|
|
||||||
|
const keycloakifyVersion = readThisNpmPackageVersion();
|
||||||
|
|
||||||
|
const prettierConfigHash = await (async () => {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { configHash } = await getPrettier();
|
||||||
|
|
||||||
|
return configHash;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheContentStr = cacheContent.toString("utf8");
|
||||||
|
|
||||||
|
let parsedCacheFile: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedCacheFile = JSON.parse(cacheContentStr);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
zParsedCacheFile.parse(parsedCacheFile);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(is<ParsedCacheFile>(parsedCacheFile));
|
||||||
|
|
||||||
|
return parsedCacheFile;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (parsedCacheFile === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
|
||||||
|
uiModuleMeta => {
|
||||||
|
const correspondingInstalledUiModule = installedUiModules.find(
|
||||||
|
installedUiModule =>
|
||||||
|
installedUiModule.moduleName === uiModuleMeta.moduleName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (correspondingInstalledUiModule === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return correspondingInstalledUiModule.version === uiModuleMeta.version;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return uiModuleMetas_cacheUpToDate;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const uiModuleMetas = await Promise.all(
|
||||||
|
installedUiModules.map(
|
||||||
|
async ({
|
||||||
|
moduleName,
|
||||||
|
version,
|
||||||
|
peerDependencies,
|
||||||
|
dirPath
|
||||||
|
}): Promise<UiModuleMeta> => {
|
||||||
|
use_cache: {
|
||||||
|
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
|
||||||
|
uiModuleMeta => uiModuleMeta.moduleName === moduleName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uiModuleMeta_cache === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uiModuleMeta_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: UiModuleMeta["files"] = [];
|
||||||
|
|
||||||
|
{
|
||||||
|
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||||
|
|
||||||
|
await crawlAsync({
|
||||||
|
dirPath: srcDirPath,
|
||||||
|
returnedPathsType: "relative to dirPath",
|
||||||
|
onFileFound: async fileRelativePath => {
|
||||||
|
const sourceCode =
|
||||||
|
await getUiModuleFileSourceCodeReadyToBeCopied({
|
||||||
|
buildContext,
|
||||||
|
fileRelativePath,
|
||||||
|
isForEjection: false,
|
||||||
|
uiModuleDirPath: dirPath,
|
||||||
|
uiModuleName: moduleName,
|
||||||
|
uiModuleVersion: version
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
copyableFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<UiModuleMeta>({
|
||||||
|
moduleName,
|
||||||
|
version,
|
||||||
|
files,
|
||||||
|
peerDependencies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
update_cache: {
|
||||||
|
const parsedCacheFile = id<ParsedCacheFile>({
|
||||||
|
keycloakifyVersion,
|
||||||
|
prettierConfigHash,
|
||||||
|
thisFilePath: cacheFilePath,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeHash(data: Buffer) {
|
||||||
|
return crypto.createHash("sha256").update(data).digest("hex");
|
||||||
|
}
|
@ -18,9 +18,8 @@ import {
|
|||||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||||
import { exclude } from "tsafe";
|
import { exclude } from "tsafe";
|
||||||
import { crawl } from "../tools/crawl";
|
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 { objectEntries } from "tsafe/objectEntries";
|
||||||
import { type ThemeType } from "./constants";
|
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
|
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
|
||||||
@ -52,6 +51,7 @@ export type BuildContext = {
|
|||||||
account:
|
account:
|
||||||
| { isImplemented: false }
|
| { isImplemented: false }
|
||||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||||
|
admin: { isImplemented: boolean };
|
||||||
};
|
};
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
@ -146,7 +146,10 @@ export function getBuildContext(params: {
|
|||||||
returnedPathsType: "relative to dirPath"
|
returnedPathsType: "relative to dirPath"
|
||||||
})
|
})
|
||||||
.map(fileRelativePath => {
|
.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);
|
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||||
if (split.length === 2) {
|
if (split.length === 2) {
|
||||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||||
@ -172,7 +175,7 @@ export function getBuildContext(params: {
|
|||||||
[
|
[
|
||||||
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
|
`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`,
|
`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")
|
].join("\n")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -448,7 +451,10 @@ export function getBuildContext(params: {
|
|||||||
isImplemented: true,
|
isImplemented: true,
|
||||||
type: buildOptions.accountThemeImplementation
|
type: buildOptions.accountThemeImplementation
|
||||||
};
|
};
|
||||||
})()
|
})(),
|
||||||
|
admin: {
|
||||||
|
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -4,7 +4,7 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
|
|||||||
DIST: "dist"
|
DIST: "dist"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const THEME_TYPES = ["login", "account"] as const;
|
export const THEME_TYPES = ["login", "account", "admin"] as const;
|
||||||
|
|
||||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||||
|
|
||||||
@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
|
|||||||
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
|
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
|
||||||
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
|
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const KEYCLOAK_THEME = "keycloak-theme";
|
||||||
|
@ -11,6 +11,7 @@ export type CommandName =
|
|||||||
| "eject-page"
|
| "eject-page"
|
||||||
| "add-story"
|
| "add-story"
|
||||||
| "initialize-account-theme"
|
| "initialize-account-theme"
|
||||||
|
| "initialize-admin-theme"
|
||||||
| "initialize-email-theme"
|
| "initialize-email-theme"
|
||||||
| "copy-keycloak-resources-to-public";
|
| "copy-keycloak-resources-to-public";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ApiVersion
|
ApiVersion
|
||||||
} from "./customHandler";
|
} from "./customHandler";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { sep as pathSep } from "path";
|
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
assert<Equals<ApiVersion, "v1">>();
|
assert<Equals<ApiVersion, "v1">>();
|
||||||
@ -19,32 +19,7 @@ export function maybeDelegateCommandToCustomHandler(params: {
|
|||||||
}): { hasBeenHandled: boolean } {
|
}): { hasBeenHandled: boolean } {
|
||||||
const { commandName, buildContext } = params;
|
const { commandName, buildContext } = params;
|
||||||
|
|
||||||
const nodeModulesBinDirPath = (() => {
|
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||||
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);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||||
return { hasBeenHandled: false };
|
return { hasBeenHandled: false };
|
||||||
|
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import child_process from "child_process";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
|
let hasUncommittedChanges: boolean | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hasUncommittedChanges =
|
||||||
|
child_process
|
||||||
|
.execSync(`git status --porcelain`, {
|
||||||
|
cwd: projectDirPath
|
||||||
|
})
|
||||||
|
.toString()
|
||||||
|
.trim() !== "";
|
||||||
|
} catch {
|
||||||
|
// Probably not a git repository
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasUncommittedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
[
|
||||||
|
chalk.red(
|
||||||
|
"Please commit or stash your changes before running this command.\n"
|
||||||
|
),
|
||||||
|
"This command will modify your project's files so it's better to have a clean working directory",
|
||||||
|
"so that you can easily see what has been changed and revert if needed."
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
51
src/bin/tools/crawlAsync.ts
Normal file
51
src/bin/tools/crawlAsync.ts
Normal 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 });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
51
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
51
src/bin/tools/getInstalledModuleDirPath.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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;
|
||||||
|
packageJsonDirPath: string;
|
||||||
|
projectDirPath: string;
|
||||||
|
}) {
|
||||||
|
const { moduleName, packageJsonDirPath, projectDirPath } = params;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
131
src/bin/tools/listInstalledModules.ts
Normal file
131
src/bin/tools/listInstalledModules.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { z } from "zod";
|
||||||
|
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;
|
||||||
|
projectDirPath: string;
|
||||||
|
filter: (params: { moduleName: string }) => boolean;
|
||||||
|
}): Promise<
|
||||||
|
{
|
||||||
|
moduleName: string;
|
||||||
|
version: string;
|
||||||
|
dirPath: string;
|
||||||
|
peerDependencies: Record<string, string>;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const { packageJsonFilePath, projectDirPath, filter } = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = await readPackageJsonDependencies({
|
||||||
|
packageJsonFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiModuleNames = (
|
||||||
|
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
||||||
|
)
|
||||||
|
.filter(exclude(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,
|
||||||
|
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
||||||
|
projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const { version, peerDependencies } =
|
||||||
|
await readPackageJsonVersionAndPeerDependencies({
|
||||||
|
packageJsonFilePath: pathJoin(dirPath, "package.json")
|
||||||
|
});
|
||||||
|
|
||||||
|
return { moduleName, version, peerDependencies, 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 { readPackageJsonVersionAndPeerDependencies } = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
version: string;
|
||||||
|
peerDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
version: z.string(),
|
||||||
|
peerDependencies: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function readPackageJsonVersionAndPeerDependencies(params: {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
}): Promise<{ version: string; peerDependencies: Record<string, string> }> {
|
||||||
|
const { packageJsonFilePath } = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: parsedPackageJson.version,
|
||||||
|
peerDependencies: parsedPackageJson.peerDependencies ?? {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { readPackageJsonVersionAndPeerDependencies };
|
||||||
|
})();
|
38
src/bin/tools/nodeModulesBinDirPath.ts
Normal file
38
src/bin/tools/nodeModulesBinDirPath.ts
Normal 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;
|
||||||
|
}
|
@ -1,7 +1,15 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { rmSync } from "./fs.rmSync";
|
||||||
|
|
||||||
export function npmInstall(params: { packageJsonDirPath: string }) {
|
export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||||
const { packageJsonDirPath } = params;
|
const { packageJsonDirPath } = params;
|
||||||
@ -23,6 +31,10 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
|||||||
{
|
{
|
||||||
binName: "bun",
|
binName: "bun",
|
||||||
lockFileBasename: "bun.lockdb"
|
lockFileBasename: "bun.lockdb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binName: "deno",
|
||||||
|
lockFileBasename: "deno.lock"
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@ -37,27 +49,411 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
throw new Error(
|
||||||
|
"No lock file found, cannot tell which package manager to use for installing dependencies."
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
install_dependencies: {
|
console.log(`Installing the new dependencies...`);
|
||||||
if (packageManagerBinName === undefined) {
|
|
||||||
break install_dependencies;
|
install_without_breaking_links: {
|
||||||
|
if (packageManagerBinName !== "yarn") {
|
||||||
|
break install_without_breaking_links;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Installing the new dependencies...`);
|
const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath });
|
||||||
|
|
||||||
try {
|
if (garronejLinkInfos === undefined) {
|
||||||
child_process.execSync(`${packageManagerBinName} install`, {
|
break install_without_breaking_links;
|
||||||
cwd: packageJsonDirPath,
|
|
||||||
stdio: "inherit"
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green("Installing in a way that won't break the links..."));
|
||||||
|
|
||||||
|
installWithoutBreakingLinks({
|
||||||
|
packageJsonDirPath,
|
||||||
|
garronejLinkInfos
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
child_process.execSync(`${packageManagerBinName} install`, {
|
||||||
|
cwd: packageJsonDirPath,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGarronejLinkInfos(params: {
|
||||||
|
packageJsonDirPath: string;
|
||||||
|
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
||||||
|
const { packageJsonDirPath } = params;
|
||||||
|
|
||||||
|
const nodeModuleDirPath = pathJoin(packageJsonDirPath, "node_modules");
|
||||||
|
|
||||||
|
if (!fs.existsSync(nodeModuleDirPath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedModuleNames: string[] = [];
|
||||||
|
|
||||||
|
let yarnHomeDirPath: string | undefined = undefined;
|
||||||
|
|
||||||
|
const getIsLinkedByGarronejScript = (path: string) => {
|
||||||
|
let realPath: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
realPath = fs.readlinkSync(path);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesIncludeYarnHome = realPath.includes(".yarn_home");
|
||||||
|
|
||||||
|
if (!doesIncludeYarnHome) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_yarnHomeDirPath: {
|
||||||
|
if (yarnHomeDirPath !== undefined) {
|
||||||
|
break set_yarnHomeDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstElement] = getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: realPath,
|
||||||
|
cwd: pathDirname(path)
|
||||||
|
}).split(".yarn_home");
|
||||||
|
|
||||||
|
yarnHomeDirPath = pathJoin(firstElement, ".yarn_home");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const basename of fs.readdirSync(nodeModuleDirPath)) {
|
||||||
|
const path = pathJoin(nodeModuleDirPath, basename);
|
||||||
|
|
||||||
|
if (fs.lstatSync(path).isSymbolicLink()) {
|
||||||
|
if (basename.startsWith("@")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getIsLinkedByGarronejScript(path)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedModuleNames.push(basename);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.lstatSync(path).isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename.startsWith("@")) {
|
||||||
|
for (const subBasename of fs.readdirSync(path)) {
|
||||||
|
const subPath = pathJoin(path, subBasename);
|
||||||
|
|
||||||
|
if (!fs.lstatSync(subPath).isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getIsLinkedByGarronejScript(subPath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedModuleNames.push(`${basename}/${subBasename}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yarnHomeDirPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { linkedModuleNames, yarnHomeDirPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function installWithoutBreakingLinks(params: {
|
||||||
|
packageJsonDirPath: string;
|
||||||
|
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
packageJsonDirPath,
|
||||||
|
garronejLinkInfos: { linkedModuleNames, yarnHomeDirPath }
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
const packageJsonFilePath = pathJoin(packageJsonDirPath, "package.json");
|
||||||
|
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||||
|
) as unknown;
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const isImplementedScriptByName = {
|
||||||
|
postinstall: false,
|
||||||
|
prepare: false
|
||||||
|
};
|
||||||
|
|
||||||
|
delete_postinstall_script: {
|
||||||
|
if (parsedPackageJson.scripts === undefined) {
|
||||||
|
break delete_postinstall_script;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const scriptName of objectKeys(isImplementedScriptByName)) {
|
||||||
|
if (parsedPackageJson.scripts[scriptName] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isImplementedScriptByName[scriptName] = true;
|
||||||
|
|
||||||
|
delete parsedPackageJson.scripts[scriptName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject");
|
||||||
|
|
||||||
|
if (fs.existsSync(tmpProjectDirPath)) {
|
||||||
|
rmSync(tmpProjectDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(tmpProjectDirPath, { recursive: true });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(tmpProjectDirPath, "package.json"),
|
||||||
|
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
const YARN_LOCK = "yarn.lock";
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
pathJoin(packageJsonDirPath, YARN_LOCK),
|
||||||
|
pathJoin(tmpProjectDirPath, YARN_LOCK)
|
||||||
|
);
|
||||||
|
|
||||||
|
child_process.execSync(`yarn install`, {
|
||||||
|
cwd: tmpProjectDirPath,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Moving the modules from the tmp project to the actual project
|
||||||
|
// without messing up the links.
|
||||||
|
{
|
||||||
|
const { getAreSameVersions } = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
version: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
function readVersion(params: { moduleDirPath: string }): string {
|
||||||
|
const { moduleDirPath } = params;
|
||||||
|
|
||||||
|
const packageJsonFilePath = pathJoin(moduleDirPath, "package.json");
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(packageJson);
|
||||||
|
assert(is<ParsedPackageJson>(packageJson));
|
||||||
|
|
||||||
|
return packageJson.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAreSameVersions(params: {
|
||||||
|
moduleDirPath_a: string;
|
||||||
|
moduleDirPath_b: string;
|
||||||
|
}): boolean {
|
||||||
|
const { moduleDirPath_a, moduleDirPath_b } = params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
readVersion({ moduleDirPath: moduleDirPath_a }) ===
|
||||||
|
readVersion({ moduleDirPath: moduleDirPath_b })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getAreSameVersions };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const nodeModulesDirPath_tmpProject = pathJoin(tmpProjectDirPath, "node_modules");
|
||||||
|
const nodeModulesDirPath = pathJoin(packageJsonDirPath, "node_modules");
|
||||||
|
|
||||||
|
const modulePaths = fs
|
||||||
|
.readdirSync(nodeModulesDirPath_tmpProject)
|
||||||
|
.map(basename => {
|
||||||
|
if (basename.startsWith(".")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = pathJoin(nodeModulesDirPath_tmpProject, basename);
|
||||||
|
|
||||||
|
if (basename.startsWith("@")) {
|
||||||
|
return fs
|
||||||
|
.readdirSync(path)
|
||||||
|
.map(subBasename => {
|
||||||
|
if (subBasename.startsWith(".")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subPath = pathJoin(path, subBasename);
|
||||||
|
|
||||||
|
if (!fs.lstatSync(subPath).isDirectory()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleName: `${basename}/${subBasename}`,
|
||||||
|
moduleDirPath_tmpProject: subPath,
|
||||||
|
moduleDirPath: pathJoin(
|
||||||
|
nodeModulesDirPath,
|
||||||
|
basename,
|
||||||
|
subBasename
|
||||||
|
)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.lstatSync(path).isDirectory()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
moduleName: basename,
|
||||||
|
moduleDirPath_tmpProject: path,
|
||||||
|
moduleDirPath: pathJoin(nodeModulesDirPath, basename)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
moduleName,
|
||||||
|
moduleDirPath,
|
||||||
|
moduleDirPath_tmpProject
|
||||||
|
} of modulePaths) {
|
||||||
|
if (linkedModuleNames.includes(moduleName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doesTargetModuleExist = false;
|
||||||
|
|
||||||
|
skip_condition: {
|
||||||
|
if (!fs.existsSync(moduleDirPath)) {
|
||||||
|
break skip_condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
doesTargetModuleExist = true;
|
||||||
|
|
||||||
|
const areSameVersions = getAreSameVersions({
|
||||||
|
moduleDirPath_a: moduleDirPath,
|
||||||
|
moduleDirPath_b: moduleDirPath_tmpProject
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!areSameVersions) {
|
||||||
|
break skip_condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesTargetModuleExist) {
|
||||||
|
rmSync(moduleDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = pathDirname(moduleDirPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(moduleDirPath_tmpProject, moduleDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
move_bin: {
|
||||||
|
const binDirPath_tmpProject = pathJoin(nodeModulesDirPath_tmpProject, ".bin");
|
||||||
|
const binDirPath = pathJoin(nodeModulesDirPath, ".bin");
|
||||||
|
|
||||||
|
if (!fs.existsSync(binDirPath_tmpProject)) {
|
||||||
|
break move_bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const basename of fs.readdirSync(binDirPath_tmpProject)) {
|
||||||
|
const path_tmpProject = pathJoin(binDirPath_tmpProject, basename);
|
||||||
|
const path = pathJoin(binDirPath, basename);
|
||||||
|
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(path_tmpProject, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.cpSync(
|
||||||
|
pathJoin(tmpProjectDirPath, YARN_LOCK),
|
||||||
|
pathJoin(packageJsonDirPath, YARN_LOCK)
|
||||||
|
);
|
||||||
|
|
||||||
|
rmSync(tmpProjectDirPath, { recursive: true });
|
||||||
|
|
||||||
|
for (const scriptName of objectKeys(isImplementedScriptByName)) {
|
||||||
|
if (!isImplementedScriptByName[scriptName]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
child_process.execSync(`yarn run ${scriptName}`, {
|
||||||
|
cwd: packageJsonDirPath,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,13 @@ import { assert } from "tsafe/assert";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
|
let cache: string | undefined = undefined;
|
||||||
|
|
||||||
export function readThisNpmPackageVersion(): string {
|
export function readThisNpmPackageVersion(): string {
|
||||||
|
if (cache !== undefined) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
const version = JSON.parse(
|
const version = JSON.parse(
|
||||||
fs
|
fs
|
||||||
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
||||||
@ -12,5 +18,7 @@ export function readThisNpmPackageVersion(): string {
|
|||||||
|
|
||||||
assert(typeof version === "string");
|
assert(typeof version === "string");
|
||||||
|
|
||||||
|
cache = version;
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
106
src/bin/tools/runPrettier.ts
Normal file
106
src/bin/tools/runPrettier.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
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 PrettierAndConfigHash = {
|
||||||
|
prettier: typeof import("prettier");
|
||||||
|
configHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
getPrettier.cache = id<PrettierAndConfigHash | undefined>(undefined);
|
||||||
|
|
||||||
|
export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||||
|
assert(getIsPrettierAvailable());
|
||||||
|
|
||||||
|
if (getPrettier.cache !== undefined) {
|
||||||
|
return getPrettier.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prettier = await import("prettier");
|
||||||
|
|
||||||
|
const configHash = await (async () => {
|
||||||
|
const configFilePath = await prettier.resolveConfigFile(
|
||||||
|
pathJoin(getNodeModulesBinDirPath(), "..")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (configFilePath === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fsPr.readFile(configFilePath);
|
||||||
|
|
||||||
|
return crypto.createHash("sha256").update(data).digest("hex");
|
||||||
|
})();
|
||||||
|
|
||||||
|
const prettierAndConfig: PrettierAndConfigHash = {
|
||||||
|
prettier,
|
||||||
|
configHash
|
||||||
|
};
|
||||||
|
|
||||||
|
getPrettier.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 } = await getPrettier();
|
||||||
|
|
||||||
|
const { ignored, inferredParser } = await prettier.getFileInfo(filePath, {
|
||||||
|
resolveConfig: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ignored) {
|
||||||
|
return sourceCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await prettier.resolveConfig(filePath);
|
||||||
|
|
||||||
|
formattedSourceCode = await prettier.format(sourceCode, {
|
||||||
|
...config,
|
||||||
|
filePath,
|
||||||
|
parser: inferredParser ?? undefined
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
`You probably need to upgrade the version of prettier in your project`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedSourceCode;
|
||||||
|
}
|
@ -3,8 +3,8 @@ import * as fs from "fs/promises";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { existsAsync } from "./tools/fs.existsAsync";
|
import { existsAsync } from "./tools/fs.existsAsync";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
import { runFormat } from "./tools/runFormat";
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
@ -18,12 +18,13 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
|
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx");
|
||||||
|
|
||||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
|
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
|
|
||||||
const newContent = [
|
let newContent = [
|
||||||
``,
|
``,
|
||||||
`/* eslint-disable */`,
|
`/* eslint-disable */`,
|
||||||
``,
|
``,
|
||||||
@ -54,6 +55,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
`type KcContext =`,
|
`type KcContext =`,
|
||||||
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
||||||
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
||||||
|
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
|
||||||
` ;`,
|
` ;`,
|
||||||
``,
|
``,
|
||||||
`declare global {`,
|
`declare global {`,
|
||||||
@ -66,6 +68,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
||||||
hasAccountTheme &&
|
hasAccountTheme &&
|
||||||
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
||||||
|
hasAdminTheme &&
|
||||||
|
`export const KcAdminPage = lazy(() => import("./admin/KcPage"));`,
|
||||||
``,
|
``,
|
||||||
`export function KcPage(`,
|
`export function KcPage(`,
|
||||||
` props: {`,
|
` props: {`,
|
||||||
@ -82,6 +86,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
||||||
hasAccountTheme &&
|
hasAccountTheme &&
|
||||||
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
||||||
|
hasAdminTheme &&
|
||||||
|
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
|
||||||
` }`,
|
` }`,
|
||||||
` })()}`,
|
` })()}`,
|
||||||
` </Suspense>`,
|
` </Suspense>`,
|
||||||
@ -108,20 +114,25 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(
|
newContent = [
|
||||||
filePath,
|
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
|
||||||
Buffer.from(
|
`// Hash: ${hash}`,
|
||||||
[
|
``,
|
||||||
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
|
newContent
|
||||||
`// Hash: ${hash}`,
|
].join("\n");
|
||||||
``,
|
|
||||||
newContent
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
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: {
|
delete_legacy_file: {
|
||||||
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user