diff --git a/scripts/link-in-starter.ts b/scripts/link-in-starter.ts index ed794a88..06fa0abe 100644 --- a/scripts/link-in-starter.ts +++ b/scripts/link-in-starter.ts @@ -10,7 +10,7 @@ fs.rmSync(".yarn_home", { recursive: true, force: true }); run("yarn install"); run("yarn build"); -const starterName = "keycloakify-starter"; +const starterName = "keycloakify-starter-webpack"; fs.rmSync(join("..", starterName, "node_modules"), { recursive: true, diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index 3dd77cb1..e3af9b68 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -1,6 +1,12 @@ import cheerio from "cheerio"; -import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; -import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; +import { + replaceImportsInJsCode, + BuildContextLike as BuildContextLike_replaceImportsInJsCode +} from "../replacers/replaceImportsInJsCode"; +import { + replaceImportsInCssCode, + BuildContextLike as BuildContextLike_replaceImportsInCssCode +} from "../replacers/replaceImportsInCssCode"; import * as fs from "fs"; import { join as pathJoin } from "path"; import type { BuildContext } from "../../shared/buildContext"; @@ -12,14 +18,12 @@ import { } from "../../shared/constants"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; -export type BuildContextLike = { - bundler: "vite" | "webpack"; - themeVersion: string; - urlPathname: string | undefined; - projectBuildDirPath: string; - assetsDirPath: string; - kcContextExclusionsFtlCode: string | undefined; -}; +export type BuildContextLike = BuildContextLike_replaceImportsInJsCode & + BuildContextLike_replaceImportsInCssCode & { + urlPathname: string | undefined; + themeVersion: string; + kcContextExclusionsFtlCode: string | undefined; + }; assert(); diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 4e0ba155..a748a9ba 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -53,6 +53,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & environmentVariables: { name: string; default: string }[]; recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; themeSrcDirPath: string; + bundler: { type: "vite" } | { type: "webpack" }; }; assert(); @@ -113,7 +114,7 @@ export async function generateResourcesForMainTheme(params: { ); if (fs.existsSync(dirPath)) { - assert(buildContext.bundler === "webpack"); + assert(buildContext.bundler.type === "webpack"); throw new Error( [ diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index 5de98472..a21a9a51 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) }); run_post_build_script: { - if (buildContext.bundler !== "vite") { + if (buildContext.bundler.type !== "vite") { break run_post_build_script; } diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts index 06e08a2c..0317cf33 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts @@ -8,7 +8,7 @@ export type BuildContextLike = { projectBuildDirPath: string; assetsDirPath: string; urlPathname: string | undefined; - bundler: "vite" | "webpack"; + bundler: { type: "vite" } | { type: "webpack" }; }; assert(); @@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: { const { jsCode, buildContext } = params; const { fixedJsCode } = (() => { - switch (buildContext.bundler) { + switch (buildContext.bundler.type) { case "vite": return replaceImportsInJsCode_vite({ jsCode, diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index ba4b5b10..b2498c52 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -1,7 +1,12 @@ import { parse as urlParse } from "url"; -import { join as pathJoin, sep as pathSep, relative as pathRelative } from "path"; +import { + join as pathJoin, + sep as pathSep, + relative as pathRelative, + resolve as pathResolve, + dirname as pathDirname +} from "path"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; -import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath"; import type { CliCommandOptions } from "../main"; import { z } from "zod"; import * as fs from "fs"; @@ -21,9 +26,9 @@ import { type ThemeType } from "./constants"; import { id } from "tsafe/id"; import { symToStr } from "tsafe/symToStr"; import chalk from "chalk"; +import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions"; export type BuildContext = { - bundler: "vite" | "webpack"; themeVersion: string; themeNames: [string, ...string[]]; extraThemeProperties: string[] | undefined; @@ -40,7 +45,7 @@ export type BuildContext = { * In this case the urlPathname will be "/my-app/" */ urlPathname: string | undefined; assetsDirPath: string; - npmWorkspaceRootDirPath: string; + fetchOptions: ProxyFetchOptions; kcContextExclusionsFtlCode: string | undefined; environmentVariables: { name: string; default: string }[]; themeSrcDirPath: string; @@ -49,6 +54,15 @@ export type BuildContext = { keycloakVersionRange: KeycloakVersionRange; jarFileBasename: string; }[]; + bundler: + | { + type: "vite"; + } + | { + type: "webpack"; + packageJsonDirPath: string; + packageJsonScripts: Record; + }; }; export type BuildOptions = { @@ -174,6 +188,40 @@ export function getBuildContext(params: { return { resolvedViteConfig }; })(); + const packageJsonFilePath = (function getPackageJSonDirPath(upCount: number): string { + const dirPath = pathResolve( + pathJoin(...[projectDirPath, ...Array(upCount).fill("..")]) + ); + + assert(dirPath !== pathSep, "Root package.json not found"); + + success: { + const packageJsonFilePath = pathJoin(dirPath, "package.json"); + + if (!fs.existsSync(packageJsonFilePath)) { + break success; + } + + const parsedPackageJson = z + .object({ + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional() + }) + .parse(JSON.parse(fs.readFileSync(packageJsonFilePath).toString("utf8"))); + + if ( + parsedPackageJson.dependencies?.keycloakify === undefined && + parsedPackageJson.devDependencies?.keycloakify === undefined + ) { + break success; + } + + return packageJsonFilePath; + } + + return getPackageJSonDirPath(upCount + 1); + })(0); + const parsedPackageJson = (() => { type BuildOptions_packageJson = BuildOptions & { projectBuildDirPath?: string; @@ -182,14 +230,14 @@ export function getBuildContext(params: { }; type ParsedPackageJson = { - name: string; + name?: string; version?: string; homepage?: string; keycloakify?: BuildOptions_packageJson; }; const zParsedPackageJson = z.object({ - name: z.string(), + name: z.string().optional(), version: z.string().optional(), homepage: z.string().optional(), keycloakify: id>( @@ -267,10 +315,16 @@ export function getBuildContext(params: { assert>(); } + const configurationPackageJsonFilePath = (() => { + const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json"); + + return fs.existsSync(rootPackageJsonFilePath) + ? rootPackageJsonFilePath + : packageJsonFilePath; + })(); + return zParsedPackageJson.parse( - JSON.parse( - fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8") - ) + JSON.parse(fs.readFileSync(configurationPackageJsonFilePath).toString("utf8")) ); })(); @@ -288,12 +342,14 @@ export function getBuildContext(params: { const themeNames = ((): [string, ...string[]] => { if (buildOptions.themeName === undefined) { - return [ - parsedPackageJson.name - .replace(/^@(.*)/, "$1") - .split("/") - .join("-") - ]; + return parsedPackageJson.name === undefined + ? ["keycloakify"] + : [ + parsedPackageJson.name + .replace(/^@(.*)/, "$1") + .split("/") + .join("-") + ]; } if (typeof buildOptions.themeName === "string") { @@ -326,15 +382,29 @@ export function getBuildContext(params: { return pathJoin(projectDirPath, resolvedViteConfig.buildDir); })(); - const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ - projectDirPath, - dependencyExpected: "keycloakify" - }); - const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack"; return { - bundler, + bundler: + resolvedViteConfig !== undefined + ? { type: "vite" } + : (() => { + const { scripts } = z + .object({ + scripts: z.record(z.string()).optional() + }) + .parse( + JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ) + ); + + return { + type: "webpack", + packageJsonDirPath: pathDirname(packageJsonFilePath), + packageJsonScripts: scripts ?? {} + }; + })(), themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0", themeNames, extraThemeProperties: buildOptions.extraThemeProperties, @@ -411,7 +481,11 @@ export function getBuildContext(params: { }); } - return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache"); + return pathJoin( + pathDirname(packageJsonFilePath), + "node_modules", + ".cache" + ); })(), "keycloakify" ); @@ -460,7 +534,6 @@ export function getBuildContext(params: { return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir); })(), - npmWorkspaceRootDirPath, kcContextExclusionsFtlCode: (() => { if (buildOptions.kcContextExclusionsFtl === undefined) { return undefined; @@ -480,6 +553,33 @@ export function getBuildContext(params: { environmentVariables: buildOptions.environmentVariables ?? [], recordIsImplementedByThemeType, themeSrcDirPath, + fetchOptions: getProxyFetchOptions({ + npmConfigGetCwd: (function callee(upCount: number): string { + const dirPath = pathResolve( + pathJoin(...[projectDirPath, ...Array(upCount).fill("..")]) + ); + + assert( + dirPath !== pathSep, + "Couldn't find a place to run 'npm config get'" + ); + + try { + child_process.execSync("npm config get", { + cwd: dirPath, + stdio: "ignore" + }); + } catch (error) { + if (String(error).includes("ENOWORKSPACES")) { + return callee(upCount + 1); + } + + throw error; + } + + return dirPath; + })(0) + }), jarTargets: (() => { const getDefaultJarFileBasename = (range: string) => `keycloak-theme-for-kc-${range}.jar`; diff --git a/src/bin/shared/copyKeycloakResourcesToPublic.ts b/src/bin/shared/copyKeycloakResourcesToPublic.ts index 22c863b9..a5aa4060 100644 --- a/src/bin/shared/copyKeycloakResourcesToPublic.ts +++ b/src/bin/shared/copyKeycloakResourcesToPublic.ts @@ -37,10 +37,7 @@ export async function copyKeycloakResourcesToPublic(params: { buildContext: { loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(), cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath), - npmWorkspaceRootDirPath: pathRelative( - destDirPath, - buildContext.npmWorkspaceRootDirPath - ) + fetchOptions: buildContext.fetchOptions } }, null, diff --git a/src/bin/shared/downloadKeycloakDefaultTheme.ts b/src/bin/shared/downloadKeycloakDefaultTheme.ts index 7fd8bdd3..f7feb661 100644 --- a/src/bin/shared/downloadKeycloakDefaultTheme.ts +++ b/src/bin/shared/downloadKeycloakDefaultTheme.ts @@ -6,7 +6,7 @@ import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; export type BuildContextLike = { cacheDirPath: string; - npmWorkspaceRootDirPath: string; + fetchOptions: BuildContext["fetchOptions"]; }; assert(); @@ -23,7 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: { const { extractedDirPath } = await downloadAndExtractArchive({ url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, cacheDirPath: buildContext.cacheDirPath, - npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath, + fetchOptions: buildContext.fetchOptions, uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme", onArchiveFile: async params => { const fileRelativePath = pathRelative("theme", params.fileRelativePath); diff --git a/src/bin/start-keycloak/appBuild.ts b/src/bin/start-keycloak/appBuild.ts index 2c998520..bfeb277a 100644 --- a/src/bin/start-keycloak/appBuild.ts +++ b/src/bin/start-keycloak/appBuild.ts @@ -1,16 +1,14 @@ import * as child_process from "child_process"; import { Deferred } from "evt/tools/Deferred"; import { assert } from "tsafe/assert"; -import { is } from "tsafe/is"; import type { BuildContext } from "../shared/buildContext"; -import * as fs from "fs"; -import { join as pathJoin } from "path"; +import chalk from "chalk"; +import { sep as pathSep, join as pathJoin } from "path"; export type BuildContextLike = { projectDirPath: string; keycloakifyBuildDirPath: string; - bundler: "vite" | "webpack"; - npmWorkspaceRootDirPath: string; + bundler: BuildContext["bundler"]; projectBuildDirPath: string; }; @@ -21,95 +19,27 @@ export async function appBuild(params: { }): Promise<{ isAppBuildSuccess: boolean }> { const { buildContext } = params; - const { bundler } = buildContext; + switch (buildContext.bundler.type) { + case "vite": + return appBuild_vite({ buildContext }); + case "webpack": + return appBuild_webpack({ buildContext }); + } +} - const { command, args, cwd } = (() => { - switch (bundler) { - case "vite": - return { - command: "npx", - args: ["vite", "build"], - cwd: buildContext.projectDirPath - }; - case "webpack": { - for (const dirPath of [ - buildContext.projectDirPath, - buildContext.npmWorkspaceRootDirPath - ]) { - try { - const parsedPackageJson = JSON.parse( - fs - .readFileSync(pathJoin(dirPath, "package.json")) - .toString("utf8") - ); +async function appBuild_vite(params: { + buildContext: BuildContextLike; +}): Promise<{ isAppBuildSuccess: boolean }> { + const { buildContext } = params; - const [scriptName] = - Object.entries(parsedPackageJson.scripts).find( - ([, scriptValue]) => { - assert(is(scriptValue)); - if ( - scriptValue.includes("webpack") && - scriptValue.includes("--mode production") - ) { - return true; - } - - if ( - scriptValue.includes("react-scripts") && - scriptValue.includes("build") - ) { - return true; - } - - if ( - scriptValue.includes("react-app-rewired") && - scriptValue.includes("build") - ) { - return true; - } - - if ( - scriptValue.includes("craco") && - scriptValue.includes("build") - ) { - return true; - } - - if ( - scriptValue.includes("ng") && - scriptValue.includes("build") - ) { - return true; - } - - return false; - } - ) ?? []; - - if (scriptName === undefined) { - continue; - } - - return { - command: "npm", - args: ["run", scriptName], - cwd: dirPath - }; - } catch { - continue; - } - } - - throw new Error( - "Keycloakify was unable to determine which script is responsible for building the app." - ); - } - } - })(); + assert(buildContext.bundler.type === "vite"); const dResult = new Deferred<{ isSuccess: boolean }>(); - const child = child_process.spawn(command, args, { cwd, shell: true }); + const child = child_process.spawn("npx", ["vite", "build"], { + cwd: buildContext.projectDirPath, + shell: true + }); child.stdout.on("data", data => { if (data.toString("utf8").includes("gzip:")) { @@ -127,3 +57,113 @@ export async function appBuild(params: { return { isAppBuildSuccess: isSuccess }; } + +async function appBuild_webpack(params: { + buildContext: BuildContextLike; +}): Promise<{ isAppBuildSuccess: boolean }> { + const { buildContext } = params; + + assert(buildContext.bundler.type === "webpack"); + + const entries = Object.entries(buildContext.bundler.packageJsonScripts).filter( + ([, scriptCommand]) => scriptCommand.includes("keycloakify build") + ); + + if (entries.length === 0) { + console.log( + chalk.red( + [ + `You should have a script in your package.json at ${buildContext.bundler.packageJsonDirPath}`, + `that includes the 'keycloakify build' command` + ].join(" ") + ) + ); + process.exit(-1); + } + + const entry = + entries.length === 1 + ? entries[0] + : entries.find(([scriptName]) => scriptName === "build-keycloak-theme"); + + if (entry === undefined) { + console.log( + chalk.red( + "There's multiple candidate script for building your app, name one 'build-keycloak-theme'" + ) + ); + process.exit(-1); + } + + const [scriptName, scriptCommand] = entry; + + const { appBuildSubCommands } = (() => { + const appBuildSubCommands: string[] = []; + + for (const subCmd of scriptCommand.split("&&").map(s => s.trim())) { + if (subCmd.includes("keycloakify build")) { + break; + } + + appBuildSubCommands.push(subCmd); + } + + return { appBuildSubCommands }; + })(); + + if (appBuildSubCommands.length === 0) { + console.log( + chalk.red( + `Your ${scriptName} script should look like "... && keycloakify build ..."` + ) + ); + process.exit(-1); + } + + for (const subCommand of appBuildSubCommands) { + const dIsSuccess = new Deferred(); + + child_process.exec( + subCommand, + { + cwd: buildContext.bundler.packageJsonDirPath, + env: { + ...process.env, + PATH: (() => { + const separator = pathSep === "/" ? ":" : ";"; + + return [ + pathJoin( + buildContext.bundler.packageJsonDirPath, + "node_modules", + ".bin" + ), + ...(process.env.PATH ?? "").split(separator) + ].join(separator); + })() + } + }, + (error, stdout, stderr) => { + if (error) { + dIsSuccess.resolve(false); + + console.log(chalk.red(`Error running: '${subCommand}'`)); + console.log(stdout); + console.log(stderr); + + return; + } + + dIsSuccess.resolve(true); + } + ); + + const isSuccess = await dIsSuccess.pr; + + if (!isSuccess) { + return { isAppBuildSuccess: false }; + } + } + + return { isAppBuildSuccess: true }; +} diff --git a/src/bin/start-keycloak/keycloakifyBuild.ts b/src/bin/start-keycloak/keycloakifyBuild.ts index 10ab77f9..b64fa179 100644 --- a/src/bin/start-keycloak/keycloakifyBuild.ts +++ b/src/bin/start-keycloak/keycloakifyBuild.ts @@ -7,7 +7,6 @@ import type { BuildContext } from "../shared/buildContext"; export type BuildContextLike = { projectDirPath: string; keycloakifyBuildDirPath: string; - bundler: "vite" | "webpack"; }; assert(); diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index 2ab65eb8..61aed440 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -121,7 +121,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) if (!isAppBuildSuccess) { console.log( chalk.red( - `App build failed, exiting. Try running 'npm run build' and see what's wrong.` + `App build failed, exiting. Try building your app (e.g 'npm run build') and see what's wrong.` ) ); process.exit(1); diff --git a/src/bin/tools/downloadAndExtractArchive/downloadAndExtractArchive.ts b/src/bin/tools/downloadAndExtractArchive.ts similarity index 93% rename from src/bin/tools/downloadAndExtractArchive/downloadAndExtractArchive.ts rename to src/bin/tools/downloadAndExtractArchive.ts index 72353000..ad22b8d8 100644 --- a/src/bin/tools/downloadAndExtractArchive/downloadAndExtractArchive.ts +++ b/src/bin/tools/downloadAndExtractArchive.ts @@ -1,12 +1,12 @@ -import fetch from "make-fetch-happen"; +import fetch, { type FetchOptions } from "make-fetch-happen"; import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises"; import { dirname as pathDirname, join as pathJoin } from "path"; import { assert } from "tsafe/assert"; -import { extractArchive } from "../extractArchive"; -import { existsAsync } from "../fs.existsAsync"; -import { getProxyFetchOptions } from "./fetchProxyOptions"; +import { extractArchive } from "./extractArchive"; +import { existsAsync } from "./fs.existsAsync"; + import * as crypto from "crypto"; -import { rm } from "../fs.rm"; +import { rm } from "./fs.rm"; export async function downloadAndExtractArchive(params: { url: string; @@ -20,15 +20,10 @@ export async function downloadAndExtractArchive(params: { }) => Promise; }) => Promise; cacheDirPath: string; - npmWorkspaceRootDirPath: string; + fetchOptions: FetchOptions | undefined; }): Promise<{ extractedDirPath: string }> { - const { - url, - uniqueIdOfOnOnArchiveFile, - onArchiveFile, - cacheDirPath, - npmWorkspaceRootDirPath - } = params; + const { url, uniqueIdOfOnOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } = + params; const archiveFileBasename = url.split("?")[0].split("/").reverse()[0]; @@ -55,10 +50,7 @@ export async function downloadAndExtractArchive(params: { await mkdir(pathDirname(archiveFilePath), { recursive: true }); - const response = await fetch( - url, - await getProxyFetchOptions({ npmWorkspaceRootDirPath }) - ); + const response = await fetch(url, fetchOptions); response.body?.setMaxListeners(Number.MAX_VALUE); assert(typeof response.body !== "undefined" && response.body != null); diff --git a/src/bin/tools/downloadAndExtractArchive/index.ts b/src/bin/tools/downloadAndExtractArchive/index.ts deleted file mode 100644 index 2590e230..00000000 --- a/src/bin/tools/downloadAndExtractArchive/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./downloadAndExtractArchive"; diff --git a/src/bin/tools/downloadAndExtractArchive/fetchProxyOptions.ts b/src/bin/tools/fetchProxyOptions.ts similarity index 57% rename from src/bin/tools/downloadAndExtractArchive/fetchProxyOptions.ts rename to src/bin/tools/fetchProxyOptions.ts index d9a19424..c4e0d90c 100644 --- a/src/bin/tools/downloadAndExtractArchive/fetchProxyOptions.ts +++ b/src/bin/tools/fetchProxyOptions.ts @@ -1,61 +1,40 @@ -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(arg0: T | T[]) { - return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0]; -} - -function ensureSingleOrNone(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; - -/** - * 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); -} +import * as child_process from "child_process"; +import * as fs from "fs"; export type ProxyFetchOptions = Pick< FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca" >; -export async function getProxyFetchOptions(params: { - npmWorkspaceRootDirPath: string; -}): Promise { - const { npmWorkspaceRootDirPath } = params; +export function getProxyFetchOptions(params: { + npmConfigGetCwd: string; +}): ProxyFetchOptions { + const { npmConfigGetCwd } = params; - const cfg = await getNmpConfig({ npmWorkspaceRootDirPath }); + const cfg = (() => { + const output = child_process + .execSync("npm config get", { + cwd: npmConfigGetCwd + }) + .toString("utf8"); + + return output + .split("\n") + .filter(line => !line.startsWith(";")) + .map(line => line.trim()) + .map(line => line.split("=", 2) as [string, string]) + .reduce( + ( + cfg: Record, + [key, value]: [string, string] + ) => + key in cfg + ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } + : { ...cfg, [key]: value }, + {} + ); + })(); const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]); const noProxy = cfg["noproxy"] ?? cfg["no-proxy"]; @@ -71,17 +50,16 @@ export async function getProxyFetchOptions(params: { if (typeof cafile !== "undefined" && cafile !== "null") { ca.push( - ...(await (async () => { - function chunks(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"); + ...(() => { + const cafileContent = fs.readFileSync(cafile).toString("utf8"); const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt"; + const chunks = (arr: T[], size: number = 2) => + arr + .map((_, i) => i % size == 0 && arr.slice(i, i + size)) + .filter(Boolean) as T[][]; + return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map( ca => ca @@ -90,7 +68,7 @@ export async function getProxyFetchOptions(params: { .replace(new RegExp(`^${newLinePlaceholder}`), "") .replace(new RegExp(newLinePlaceholder, "g"), "\\n") ); - })()) + })() ); } @@ -102,3 +80,17 @@ export async function getProxyFetchOptions(params: { ca: ca.length === 0 ? undefined : ca }; } + +function ensureArray(arg0: T | T[]) { + return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0]; +} + +function ensureSingleOrNone(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(", ") + ); +} diff --git a/src/bin/tools/getNpmWorkspaceRootDirPath.ts b/src/bin/tools/getNpmWorkspaceRootDirPath.ts deleted file mode 100644 index 87a610c7..00000000 --- a/src/bin/tools/getNpmWorkspaceRootDirPath.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as child_process from "child_process"; -import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path"; -import { assert } from "tsafe/assert"; -import * as fs from "fs"; - -export function getNpmWorkspaceRootDirPath(params: { - projectDirPath: string; - dependencyExpected: string; -}) { - const { projectDirPath, dependencyExpected } = params; - - const npmWorkspaceRootDirPath = (function callee(depth: number): string { - const cwd = pathResolve( - pathJoin(...[projectDirPath, ...Array(depth).fill("..")]) - ); - - assert(cwd !== pathSep, "NPM workspace not found"); - - try { - child_process.execSync("npm config get", { - cwd, - stdio: "ignore" - }); - } catch (error) { - if (String(error).includes("ENOWORKSPACES")) { - return callee(depth + 1); - } - - throw error; - } - - const packageJsonFilePath = pathJoin(cwd, "package.json"); - - if (!fs.existsSync(packageJsonFilePath)) { - return callee(depth + 1); - } - - assert(fs.existsSync(packageJsonFilePath)); - - const parsedPackageJson = JSON.parse( - fs.readFileSync(packageJsonFilePath).toString("utf8") - ); - - let isExpectedDependencyFound = false; - - for (const dependenciesOrDevDependencies of [ - "dependencies", - "devDependencies" - ] as const) { - const dependencies = parsedPackageJson[dependenciesOrDevDependencies]; - - if (dependencies === undefined) { - continue; - } - - assert(dependencies instanceof Object); - - if (dependencies[dependencyExpected] === undefined) { - continue; - } - - isExpectedDependencyFound = true; - } - - if (!isExpectedDependencyFound && parsedPackageJson.name !== dependencyExpected) { - return callee(depth + 1); - } - - return cwd; - })(0); - - return { npmWorkspaceRootDirPath }; -}