Improve monorepo support
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "./downloadAndUnzip";
|
||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
import { getLogger } from "./tools/logger";
|
import { getLogger } from "./tools/logger";
|
||||||
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
|
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
|
||||||
@ -13,6 +13,7 @@ import { transformCodebase } from "./tools/transformCodebase";
|
|||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
@ -21,11 +22,10 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
|
|||||||
const { keycloakVersion, destDirPath, buildOptions } = params;
|
const { keycloakVersion, destDirPath, buildOptions } = params;
|
||||||
|
|
||||||
await downloadAndUnzip({
|
await downloadAndUnzip({
|
||||||
"doUseCache": true,
|
|
||||||
"cacheDirPath": buildOptions.cacheDirPath,
|
|
||||||
destDirPath,
|
destDirPath,
|
||||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||||
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
|
||||||
|
buildOptions,
|
||||||
"preCacheTransform": {
|
"preCacheTransform": {
|
||||||
"actionCacheId": "npm install and build",
|
"actionCacheId": "npm install and build",
|
||||||
"action": async ({ destDirPath }) => {
|
"action": async ({ destDirPath }) => {
|
||||||
|
199
src/bin/downloadAndUnzip.ts
Normal file
199
src/bin/downloadAndUnzip.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { createHash } from "crypto";
|
||||||
|
import { mkdir, writeFile, unlink } from "fs/promises";
|
||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
|
import { unzip, zip } from "./tools/unzip";
|
||||||
|
import { rm } from "./tools/fs.rm";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { existsAsync } from "./tools/fs.existsAsync";
|
||||||
|
import type { BuildOptions } from "./keycloakify/buildOptions";
|
||||||
|
import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
cacheDirPath: string;
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
|
export async function downloadAndUnzip(params: {
|
||||||
|
url: string;
|
||||||
|
destDirPath: string;
|
||||||
|
specificDirsToExtract?: string[];
|
||||||
|
preCacheTransform?: {
|
||||||
|
actionCacheId: string;
|
||||||
|
action: (params: { destDirPath: string }) => Promise<void>;
|
||||||
|
};
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}) {
|
||||||
|
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
|
||||||
|
|
||||||
|
const zipFileBasename = generateFileNameFromURL({
|
||||||
|
url,
|
||||||
|
"preCacheTransform":
|
||||||
|
preCacheTransform === undefined
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
"actionCacheId": preCacheTransform.actionCacheId,
|
||||||
|
"actionFootprint": preCacheTransform.action.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasename}.zip`);
|
||||||
|
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasename}`);
|
||||||
|
|
||||||
|
download_zip_and_transform: {
|
||||||
|
if (await existsAsync(zipFilePath)) {
|
||||||
|
break download_zip_and_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response, isFromRemoteCache } = await (async () => {
|
||||||
|
const proxyFetchOptions = await getProxyFetchOptions({
|
||||||
|
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
|
||||||
|
proxyFetchOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
"isFromRemoteCache": true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"response": await fetch(url, proxyFetchOptions),
|
||||||
|
"isFromRemoteCache": false
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||||
|
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||||
|
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||||
|
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||||
|
* octokit upgrades.
|
||||||
|
*/
|
||||||
|
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||||
|
assert(typeof response.body !== "undefined" && response.body != null);
|
||||||
|
|
||||||
|
await writeFile(zipFilePath, response.body);
|
||||||
|
|
||||||
|
if (isFromRemoteCache) {
|
||||||
|
break download_zip_and_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
||||||
|
break download_zip_and_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await preCacheTransform?.action({
|
||||||
|
"destDirPath": extractDirPath
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await unlink(zipFilePath);
|
||||||
|
|
||||||
|
await zip(extractDirPath, zipFilePath);
|
||||||
|
|
||||||
|
await rm(extractDirPath, { "recursive": true });
|
||||||
|
|
||||||
|
upload_to_remot_cache_if_admin: {
|
||||||
|
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
||||||
|
|
||||||
|
if (githubToken === undefined) {
|
||||||
|
break upload_to_remot_cache_if_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("uploading to remote cache");
|
||||||
|
|
||||||
|
try {
|
||||||
|
child_process.execSync(`which putasset`);
|
||||||
|
} catch {
|
||||||
|
child_process.execSync(`npm install -g putasset`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
child_process.execFileSync("putasset", [
|
||||||
|
"--owner",
|
||||||
|
"keycloakify",
|
||||||
|
"--repo",
|
||||||
|
"keycloakify",
|
||||||
|
"--tag",
|
||||||
|
"v0.0.1",
|
||||||
|
"--filename",
|
||||||
|
zipFilePath,
|
||||||
|
"--token",
|
||||||
|
githubToken
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
console.log("upload failed, asset probably already exists in remote cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await unzip(zipFilePath, extractDirPath);
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": extractDirPath,
|
||||||
|
"destDirPath": destDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await rm(extractDirPath, { "recursive": true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFileNameFromURL(params: {
|
||||||
|
url: string;
|
||||||
|
preCacheTransform:
|
||||||
|
| {
|
||||||
|
actionCacheId: string;
|
||||||
|
actionFootprint: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}): string {
|
||||||
|
const { preCacheTransform } = params;
|
||||||
|
|
||||||
|
// Parse the URL
|
||||||
|
const url = new URL(params.url);
|
||||||
|
|
||||||
|
// Extract pathname and remove leading slashes
|
||||||
|
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
||||||
|
|
||||||
|
// Optionally, add query parameters replacing special characters
|
||||||
|
if (url.search) {
|
||||||
|
fileName += url.search.replace(/[&=?]/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace any characters that are not valid in filenames
|
||||||
|
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||||
|
|
||||||
|
// Trim or pad the fileName to a specific length
|
||||||
|
fileName = fileName.substring(0, 50);
|
||||||
|
|
||||||
|
add_pre_cache_transform: {
|
||||||
|
if (preCacheTransform === undefined) {
|
||||||
|
break add_pre_cache_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize actionCacheId the same way as other components
|
||||||
|
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
||||||
|
|
||||||
|
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { readResolvedViteConfig } from "./resolvedViteConfig";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { getCacheDirPath } from "./getCacheDirPath";
|
import { getCacheDirPath } from "./getCacheDirPath";
|
||||||
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
|
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
|
||||||
|
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
|
||||||
|
|
||||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
export type BuildOptions = {
|
export type BuildOptions = {
|
||||||
@ -30,6 +31,7 @@ export type BuildOptions = {
|
|||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
doBuildRetrocompatAccountTheme: boolean;
|
doBuildRetrocompatAccountTheme: boolean;
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
|
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
|
||||||
@ -85,6 +87,8 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
|||||||
|
|
||||||
const argv = parseArgv(processArgv);
|
const argv = parseArgv(processArgv);
|
||||||
|
|
||||||
|
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
||||||
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||||
@ -175,6 +179,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
|
|||||||
|
|
||||||
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
||||||
})(),
|
})(),
|
||||||
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
|
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true,
|
||||||
|
npmWorkspaceRootDirPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
|
||||||
|
|
||||||
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
|
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
|
||||||
const { reactAppRootDirPath } = params;
|
const { reactAppRootDirPath } = params;
|
||||||
|
|
||||||
|
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
|
||||||
|
|
||||||
const cacheDirPath = pathJoin(
|
const cacheDirPath = pathJoin(
|
||||||
(() => {
|
(() => {
|
||||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||||
@ -13,8 +16,7 @@ export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Recursively look up
|
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
|
||||||
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
|
|
||||||
})(),
|
})(),
|
||||||
"keycloakify"
|
"keycloakify"
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import * as child_process from "child_process";
|
||||||
|
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
|
||||||
|
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
|
||||||
|
const { reactAppRootDirPath } = params;
|
||||||
|
|
||||||
|
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
||||||
|
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
child_process.execSync("npm config get", { cwd: cwd });
|
||||||
|
} catch (error) {
|
||||||
|
if (String(error).includes("ENOWORKSPACES")) {
|
||||||
|
assert(cwd !== pathSep, "NPM workspace not found");
|
||||||
|
|
||||||
|
return callee(depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cwd;
|
||||||
|
})(0);
|
||||||
|
|
||||||
|
return { npmWorkspaceRootDirPath };
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { rmSync } from "../../tools/fs.rmSync";
|
|||||||
type BuildOptionsLike = {
|
type BuildOptionsLike = {
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -9,6 +9,7 @@ import { rmSync } from "../../tools/fs.rmSync";
|
|||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
@ -33,6 +33,7 @@ export type BuildOptionsLike = {
|
|||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
doBuildRetrocompatAccountTheme: boolean;
|
doBuildRetrocompatAccountTheme: boolean;
|
||||||
themeNames: string[];
|
themeNames: string[];
|
||||||
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
@ -1,301 +0,0 @@
|
|||||||
import { exec as execCallback } from "child_process";
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
import { mkdir, readFile, stat, writeFile, unlink } from "fs/promises";
|
|
||||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
|
||||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep, basename as pathBasename } from "path";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { transformCodebase } from "./transformCodebase";
|
|
||||||
import { unzip, zip } from "./unzip";
|
|
||||||
import { rm } from "../tools/fs.rm";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
|
|
||||||
const exec = promisify(execCallback);
|
|
||||||
|
|
||||||
function generateFileNameFromURL(params: {
|
|
||||||
url: string;
|
|
||||||
preCacheTransform:
|
|
||||||
| {
|
|
||||||
actionCacheId: string;
|
|
||||||
actionFootprint: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
}): string {
|
|
||||||
const { preCacheTransform } = params;
|
|
||||||
|
|
||||||
// Parse the URL
|
|
||||||
const url = new URL(params.url);
|
|
||||||
|
|
||||||
// Extract pathname and remove leading slashes
|
|
||||||
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
|
|
||||||
|
|
||||||
// Optionally, add query parameters replacing special characters
|
|
||||||
if (url.search) {
|
|
||||||
fileName += url.search.replace(/[&=?]/g, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace any characters that are not valid in filenames
|
|
||||||
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
|
|
||||||
|
|
||||||
// Trim or pad the fileName to a specific length
|
|
||||||
fileName = fileName.substring(0, 50);
|
|
||||||
|
|
||||||
add_pre_cache_transform: {
|
|
||||||
if (preCacheTransform === undefined) {
|
|
||||||
break add_pre_cache_transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize actionCacheId the same way as other components
|
|
||||||
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
||||||
|
|
||||||
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exists(path: string) {
|
|
||||||
try {
|
|
||||||
await stat(path);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureArray<T>(arg0: T | T[]) {
|
|
||||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
|
||||||
if (!Array.isArray(arg0)) return arg0;
|
|
||||||
if (arg0.length === 0) return undefined;
|
|
||||||
if (arg0.length === 1) return arg0[0];
|
|
||||||
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
type NPMConfig = Record<string, string | string[]>;
|
|
||||||
|
|
||||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
|
||||||
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get npm configuration as map
|
|
||||||
*/
|
|
||||||
async function getNmpConfig() {
|
|
||||||
return readNpmConfig().then(parseNpmConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readNpmConfig(): Promise<string> {
|
|
||||||
return (async function callee(depth: number): Promise<string> {
|
|
||||||
const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
|
|
||||||
|
|
||||||
let stdout: string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
|
|
||||||
} catch (error) {
|
|
||||||
if (String(error).includes("ENOWORKSPACES")) {
|
|
||||||
assert(cwd !== pathSep);
|
|
||||||
|
|
||||||
return callee(depth + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stdout;
|
|
||||||
})(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseNpmConfig(stdout: string) {
|
|
||||||
return stdout
|
|
||||||
.split("\n")
|
|
||||||
.filter(line => !line.startsWith(";"))
|
|
||||||
.map(line => line.trim())
|
|
||||||
.map(line => line.split("=", 2) as [string, string])
|
|
||||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeBoolean(arg0: string | undefined) {
|
|
||||||
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function chunks<T>(arr: T[], size: number = 2) {
|
|
||||||
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readCafile(cafile: string) {
|
|
||||||
const cafileContent = await readFile(cafile, "utf-8");
|
|
||||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get proxy and ssl configuration from npm config files. Note that we don't care about
|
|
||||||
* proxy config in env vars, because make-fetch-happen will do that for us.
|
|
||||||
*
|
|
||||||
* @returns proxy configuration
|
|
||||||
*/
|
|
||||||
async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "ca" | "cert">> {
|
|
||||||
const cfg = await getNmpConfig();
|
|
||||||
|
|
||||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
|
||||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
|
||||||
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
|
||||||
const cert = cfg["cert"];
|
|
||||||
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
|
||||||
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
|
||||||
|
|
||||||
if (typeof cafile !== "undefined" && cafile !== "null") ca.push(...(await readCafile(cafile)));
|
|
||||||
|
|
||||||
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadAndUnzip(
|
|
||||||
params: {
|
|
||||||
url: string;
|
|
||||||
destDirPath: string;
|
|
||||||
specificDirsToExtract?: string[];
|
|
||||||
preCacheTransform?: {
|
|
||||||
actionCacheId: string;
|
|
||||||
action: (params: { destDirPath: string }) => Promise<void>;
|
|
||||||
};
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
doUseCache: true;
|
|
||||||
cacheDirPath: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
doUseCache: false;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
|
|
||||||
|
|
||||||
const zipFileBasename = generateFileNameFromURL({
|
|
||||||
url,
|
|
||||||
"preCacheTransform":
|
|
||||||
preCacheTransform === undefined
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
"actionCacheId": preCacheTransform.actionCacheId,
|
|
||||||
"actionFootprint": preCacheTransform.action.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
|
|
||||||
|
|
||||||
const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
|
|
||||||
const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
|
|
||||||
|
|
||||||
download_zip_and_transform: {
|
|
||||||
if (await exists(zipFilePath)) {
|
|
||||||
break download_zip_and_transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = await getFetchOptions();
|
|
||||||
|
|
||||||
const { response, isFromRemoteCache } = await (async () => {
|
|
||||||
const response = await fetch(`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`, opts);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
return {
|
|
||||||
response,
|
|
||||||
"isFromRemoteCache": true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"response": await fetch(url, opts),
|
|
||||||
"isFromRemoteCache": false
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
|
||||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
|
||||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
|
||||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
|
||||||
* octokit upgrades.
|
|
||||||
*/
|
|
||||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
|
||||||
assert(typeof response.body !== "undefined" && response.body != null);
|
|
||||||
|
|
||||||
await writeFile(zipFilePath, response.body);
|
|
||||||
|
|
||||||
if (isFromRemoteCache) {
|
|
||||||
break download_zip_and_transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
|
|
||||||
break download_zip_and_transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await preCacheTransform?.action({
|
|
||||||
"destDirPath": extractDirPath
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
await unlink(zipFilePath);
|
|
||||||
|
|
||||||
await zip(extractDirPath, zipFilePath);
|
|
||||||
|
|
||||||
await rm(extractDirPath, { "recursive": true });
|
|
||||||
|
|
||||||
upload_to_remot_cache_if_admin: {
|
|
||||||
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
|
|
||||||
|
|
||||||
if (githubToken === undefined) {
|
|
||||||
break upload_to_remot_cache_if_admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("uploading to remote cache");
|
|
||||||
|
|
||||||
try {
|
|
||||||
child_process.execSync(`which putasset`);
|
|
||||||
} catch {
|
|
||||||
child_process.execSync(`npm install -g putasset`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
child_process.execFileSync("putasset", [
|
|
||||||
"--owner",
|
|
||||||
"keycloakify",
|
|
||||||
"--repo",
|
|
||||||
"keycloakify",
|
|
||||||
"--tag",
|
|
||||||
"v0.0.1",
|
|
||||||
"--filename",
|
|
||||||
zipFilePath,
|
|
||||||
"--token",
|
|
||||||
githubToken
|
|
||||||
]);
|
|
||||||
} catch {
|
|
||||||
console.log("upload failed, asset probably already exists in remote cache");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await unzip(zipFilePath, extractDirPath);
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": extractDirPath,
|
|
||||||
"destDirPath": destDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!rest.doUseCache) {
|
|
||||||
await rm(cacheDirPath, { "recursive": true });
|
|
||||||
} else {
|
|
||||||
await rm(extractDirPath, { "recursive": true });
|
|
||||||
}
|
|
||||||
}
|
|
73
src/bin/tools/fetchProxyOptions.ts
Normal file
73
src/bin/tools/fetchProxyOptions.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { exec as execCallback } from "child_process";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { type FetchOptions } from "make-fetch-happen";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
function ensureArray<T>(arg0: T | T[]) {
|
||||||
|
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||||
|
if (!Array.isArray(arg0)) return arg0;
|
||||||
|
if (arg0.length === 0) return undefined;
|
||||||
|
if (arg0.length === 1) return arg0[0];
|
||||||
|
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
type NPMConfig = Record<string, string | string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get npm configuration as map
|
||||||
|
*/
|
||||||
|
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||||
|
const { npmWorkspaceRootDirPath } = params;
|
||||||
|
|
||||||
|
const exec = promisify(execCallback);
|
||||||
|
|
||||||
|
const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
|
||||||
|
|
||||||
|
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||||
|
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
|
||||||
|
|
||||||
|
return stdout
|
||||||
|
.split("\n")
|
||||||
|
.filter(line => !line.startsWith(";"))
|
||||||
|
.map(line => line.trim())
|
||||||
|
.map(line => line.split("=", 2) as [string, string])
|
||||||
|
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProxyFetchOptions = Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca">;
|
||||||
|
|
||||||
|
export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise<ProxyFetchOptions> {
|
||||||
|
const { npmWorkspaceRootDirPath } = params;
|
||||||
|
|
||||||
|
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
|
||||||
|
|
||||||
|
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||||
|
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||||
|
|
||||||
|
function maybeBoolean(arg0: string | undefined) {
|
||||||
|
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
||||||
|
const cert = cfg["cert"];
|
||||||
|
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
||||||
|
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
||||||
|
|
||||||
|
if (typeof cafile !== "undefined" && cafile !== "null") {
|
||||||
|
ca.push(
|
||||||
|
...(await (async () => {
|
||||||
|
function chunks<T>(arr: T[], size: number = 2) {
|
||||||
|
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cafileContent = await readFile(cafile, "utf-8");
|
||||||
|
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
|
||||||
|
})())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { proxy, noProxy, strictSSL, cert, "ca": ca.length === 0 ? undefined : ca };
|
||||||
|
}
|
11
src/bin/tools/fs.existsAsync.ts
Normal file
11
src/bin/tools/fs.existsAsync.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as fs from "fs/promises";
|
||||||
|
|
||||||
|
export async function existsAsync(path: string) {
|
||||||
|
try {
|
||||||
|
await fs.stat(path);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user