Improve monorepo project support, work if there only a package.json at the root (like NX)

This commit is contained in:
Joseph Garrone 2024-06-23 21:23:06 +02:00
parent cf6bc8666b
commit 3878e28b56
15 changed files with 336 additions and 285 deletions

View File

@ -10,7 +10,7 @@ fs.rmSync(".yarn_home", { recursive: true, force: true });
run("yarn install"); run("yarn install");
run("yarn build"); run("yarn build");
const starterName = "keycloakify-starter"; const starterName = "keycloakify-starter-webpack";
fs.rmSync(join("..", starterName, "node_modules"), { fs.rmSync(join("..", starterName, "node_modules"), {
recursive: true, recursive: true,

View File

@ -1,6 +1,12 @@
import cheerio from "cheerio"; import cheerio from "cheerio";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import {
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; replaceImportsInJsCode,
BuildContextLike as BuildContextLike_replaceImportsInJsCode
} from "../replacers/replaceImportsInJsCode";
import {
replaceImportsInCssCode,
BuildContextLike as BuildContextLike_replaceImportsInCssCode
} from "../replacers/replaceImportsInCssCode";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
@ -12,14 +18,12 @@ import {
} from "../../shared/constants"; } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = { export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
bundler: "vite" | "webpack"; BuildContextLike_replaceImportsInCssCode & {
themeVersion: string; urlPathname: string | undefined;
urlPathname: string | undefined; themeVersion: string;
projectBuildDirPath: string; kcContextExclusionsFtlCode: string | undefined;
assetsDirPath: string; };
kcContextExclusionsFtlCode: string | undefined;
};
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();

View File

@ -53,6 +53,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
themeSrcDirPath: string; themeSrcDirPath: string;
bundler: { type: "vite" } | { type: "webpack" };
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -113,7 +114,7 @@ export async function generateResourcesForMainTheme(params: {
); );
if (fs.existsSync(dirPath)) { if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack"); assert(buildContext.bundler.type === "webpack");
throw new Error( throw new Error(
[ [

View File

@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
run_post_build_script: { run_post_build_script: {
if (buildContext.bundler !== "vite") { if (buildContext.bundler.type !== "vite") {
break run_post_build_script; break run_post_build_script;
} }

View File

@ -8,7 +8,7 @@ export type BuildContextLike = {
projectBuildDirPath: string; projectBuildDirPath: string;
assetsDirPath: string; assetsDirPath: string;
urlPathname: string | undefined; urlPathname: string | undefined;
bundler: "vite" | "webpack"; bundler: { type: "vite" } | { type: "webpack" };
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: {
const { jsCode, buildContext } = params; const { jsCode, buildContext } = params;
const { fixedJsCode } = (() => { const { fixedJsCode } = (() => {
switch (buildContext.bundler) { switch (buildContext.bundler.type) {
case "vite": case "vite":
return replaceImportsInJsCode_vite({ return replaceImportsInJsCode_vite({
jsCode, jsCode,

View File

@ -1,7 +1,12 @@
import { parse as urlParse } from "url"; 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 { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; import * as fs from "fs";
@ -21,9 +26,9 @@ import { type ThemeType } from "./constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import chalk from "chalk"; import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
export type BuildContext = { export type BuildContext = {
bundler: "vite" | "webpack";
themeVersion: string; themeVersion: string;
themeNames: [string, ...string[]]; themeNames: [string, ...string[]];
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
@ -40,7 +45,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */ * In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined; urlPathname: string | undefined;
assetsDirPath: string; assetsDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: ProxyFetchOptions;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
@ -49,6 +54,15 @@ export type BuildContext = {
keycloakVersionRange: KeycloakVersionRange; keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string; jarFileBasename: string;
}[]; }[];
bundler:
| {
type: "vite";
}
| {
type: "webpack";
packageJsonDirPath: string;
packageJsonScripts: Record<string, string>;
};
}; };
export type BuildOptions = { export type BuildOptions = {
@ -174,6 +188,40 @@ export function getBuildContext(params: {
return { resolvedViteConfig }; 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 = (() => { const parsedPackageJson = (() => {
type BuildOptions_packageJson = BuildOptions & { type BuildOptions_packageJson = BuildOptions & {
projectBuildDirPath?: string; projectBuildDirPath?: string;
@ -182,14 +230,14 @@ export function getBuildContext(params: {
}; };
type ParsedPackageJson = { type ParsedPackageJson = {
name: string; name?: string;
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: BuildOptions_packageJson; keycloakify?: BuildOptions_packageJson;
}; };
const zParsedPackageJson = z.object({ const zParsedPackageJson = z.object({
name: z.string(), name: z.string().optional(),
version: z.string().optional(), version: z.string().optional(),
homepage: z.string().optional(), homepage: z.string().optional(),
keycloakify: id<z.ZodType<BuildOptions_packageJson>>( keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
@ -267,10 +315,16 @@ export function getBuildContext(params: {
assert<Equals<Got, Expected>>(); assert<Equals<Got, Expected>>();
} }
const configurationPackageJsonFilePath = (() => {
const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json");
return fs.existsSync(rootPackageJsonFilePath)
? rootPackageJsonFilePath
: packageJsonFilePath;
})();
return zParsedPackageJson.parse( return zParsedPackageJson.parse(
JSON.parse( JSON.parse(fs.readFileSync(configurationPackageJsonFilePath).toString("utf8"))
fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")
)
); );
})(); })();
@ -288,12 +342,14 @@ export function getBuildContext(params: {
const themeNames = ((): [string, ...string[]] => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return parsedPackageJson.name === undefined
parsedPackageJson.name ? ["keycloakify"]
.replace(/^@(.*)/, "$1") : [
.split("/") parsedPackageJson.name
.join("-") .replace(/^@(.*)/, "$1")
]; .split("/")
.join("-")
];
} }
if (typeof buildOptions.themeName === "string") { if (typeof buildOptions.themeName === "string") {
@ -326,15 +382,29 @@ export function getBuildContext(params: {
return pathJoin(projectDirPath, resolvedViteConfig.buildDir); return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
})(); })();
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
projectDirPath,
dependencyExpected: "keycloakify"
});
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack"; const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
return { 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", themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
extraThemeProperties: buildOptions.extraThemeProperties, 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" "keycloakify"
); );
@ -460,7 +534,6 @@ export function getBuildContext(params: {
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir); return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
})(), })(),
npmWorkspaceRootDirPath,
kcContextExclusionsFtlCode: (() => { kcContextExclusionsFtlCode: (() => {
if (buildOptions.kcContextExclusionsFtl === undefined) { if (buildOptions.kcContextExclusionsFtl === undefined) {
return undefined; return undefined;
@ -480,6 +553,33 @@ export function getBuildContext(params: {
environmentVariables: buildOptions.environmentVariables ?? [], environmentVariables: buildOptions.environmentVariables ?? [],
recordIsImplementedByThemeType, recordIsImplementedByThemeType,
themeSrcDirPath, 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: (() => { jarTargets: (() => {
const getDefaultJarFileBasename = (range: string) => const getDefaultJarFileBasename = (range: string) =>
`keycloak-theme-for-kc-${range}.jar`; `keycloak-theme-for-kc-${range}.jar`;

View File

@ -37,10 +37,7 @@ export async function copyKeycloakResourcesToPublic(params: {
buildContext: { buildContext: {
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(), loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath), cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
npmWorkspaceRootDirPath: pathRelative( fetchOptions: buildContext.fetchOptions
destDirPath,
buildContext.npmWorkspaceRootDirPath
)
} }
}, },
null, null,

View File

@ -6,7 +6,7 @@ import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: BuildContext["fetchOptions"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -23,7 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: {
const { extractedDirPath } = await downloadAndExtractArchive({ const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath, fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme", uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
onArchiveFile: async params => { onArchiveFile: async params => {
const fileRelativePath = pathRelative("theme", params.fileRelativePath); const fileRelativePath = pathRelative("theme", params.fileRelativePath);

View File

@ -1,16 +1,14 @@
import * as child_process from "child_process"; import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs"; import chalk from "chalk";
import { join as pathJoin } from "path"; import { sep as pathSep, join as pathJoin } from "path";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
bundler: "vite" | "webpack"; bundler: BuildContext["bundler"];
npmWorkspaceRootDirPath: string;
projectBuildDirPath: string; projectBuildDirPath: string;
}; };
@ -21,95 +19,27 @@ export async function appBuild(params: {
}): Promise<{ isAppBuildSuccess: boolean }> { }): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params; 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 } = (() => { async function appBuild_vite(params: {
switch (bundler) { buildContext: BuildContextLike;
case "vite": }): Promise<{ isAppBuildSuccess: boolean }> {
return { const { buildContext } = params;
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")
);
const [scriptName] = assert(buildContext.bundler.type === "vite");
Object.entries(parsedPackageJson.scripts).find(
([, scriptValue]) => {
assert(is<string>(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."
);
}
}
})();
const dResult = new Deferred<{ isSuccess: boolean }>(); 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 => { child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) { if (data.toString("utf8").includes("gzip:")) {
@ -127,3 +57,113 @@ export async function appBuild(params: {
return { isAppBuildSuccess: isSuccess }; 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<boolean>();
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 };
}

View File

@ -7,7 +7,6 @@ import type { BuildContext } from "../shared/buildContext";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
bundler: "vite" | "webpack";
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();

View File

@ -121,7 +121,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
if (!isAppBuildSuccess) { if (!isAppBuildSuccess) {
console.log( console.log(
chalk.red( 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); process.exit(1);

View File

@ -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 { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
import { dirname as pathDirname, join as pathJoin } from "path"; import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { extractArchive } from "../extractArchive"; import { extractArchive } from "./extractArchive";
import { existsAsync } from "../fs.existsAsync"; import { existsAsync } from "./fs.existsAsync";
import { getProxyFetchOptions } from "./fetchProxyOptions";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { rm } from "../fs.rm"; import { rm } from "./fs.rm";
export async function downloadAndExtractArchive(params: { export async function downloadAndExtractArchive(params: {
url: string; url: string;
@ -20,15 +20,10 @@ export async function downloadAndExtractArchive(params: {
}) => Promise<void>; }) => Promise<void>;
}) => Promise<void>; }) => Promise<void>;
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: FetchOptions | undefined;
}): Promise<{ extractedDirPath: string }> { }): Promise<{ extractedDirPath: string }> {
const { const { url, uniqueIdOfOnOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
url, params;
uniqueIdOfOnOnArchiveFile,
onArchiveFile,
cacheDirPath,
npmWorkspaceRootDirPath
} = params;
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0]; const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
@ -55,10 +50,7 @@ export async function downloadAndExtractArchive(params: {
await mkdir(pathDirname(archiveFilePath), { recursive: true }); await mkdir(pathDirname(archiveFilePath), { recursive: true });
const response = await fetch( const response = await fetch(url, fetchOptions);
url,
await getProxyFetchOptions({ npmWorkspaceRootDirPath })
);
response.body?.setMaxListeners(Number.MAX_VALUE); response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null); assert(typeof response.body !== "undefined" && response.body != null);

View File

@ -1 +0,0 @@
export * from "./downloadAndExtractArchive";

View File

@ -1,61 +1,40 @@
import { exec as execCallback } from "child_process";
import { readFile } from "fs/promises";
import { type FetchOptions } from "make-fetch-happen"; import { type FetchOptions } from "make-fetch-happen";
import { promisify } from "util"; import * as child_process from "child_process";
import * as fs from "fs";
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< export type ProxyFetchOptions = Pick<
FetchOptions, FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca" "proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>; >;
export async function getProxyFetchOptions(params: { export function getProxyFetchOptions(params: {
npmWorkspaceRootDirPath: string; npmConfigGetCwd: string;
}): Promise<ProxyFetchOptions> { }): ProxyFetchOptions {
const { npmWorkspaceRootDirPath } = params; 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<string, string | string[]>,
[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 proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"]; const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
@ -71,17 +50,16 @@ export async function getProxyFetchOptions(params: {
if (typeof cafile !== "undefined" && cafile !== "null") { if (typeof cafile !== "undefined" && cafile !== "null") {
ca.push( ca.push(
...(await (async () => { ...(() => {
function chunks<T>(arr: T[], size: number = 2) { const cafileContent = fs.readFileSync(cafile).toString("utf8");
return arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt"; const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt";
const chunks = <T>(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( return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca =>
ca ca
@ -90,7 +68,7 @@ export async function getProxyFetchOptions(params: {
.replace(new RegExp(`^${newLinePlaceholder}`), "") .replace(new RegExp(`^${newLinePlaceholder}`), "")
.replace(new RegExp(newLinePlaceholder, "g"), "\\n") .replace(new RegExp(newLinePlaceholder, "g"), "\\n")
); );
})()) })()
); );
} }
@ -102,3 +80,17 @@ export async function getProxyFetchOptions(params: {
ca: ca.length === 0 ? undefined : ca ca: ca.length === 0 ? undefined : ca
}; };
} }
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(", ")
);
}

View File

@ -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 };
}