import * as semver from "semver";
//#region src/installer.ts
/**
* NPM package installer for virtual file systems.
*
* This module fetches packages from the npm registry and populates
* a virtual node_modules directory structure.
*/
const NPM_REGISTRY = "https://registry.npmjs.org";
const DEFAULT_TIMEOUT_MS = 3e4;
/**
* Fetch with a timeout.
* Throws an error if the request takes longer than the specified timeout.
*/
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
	const controller = new AbortController();
	const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
	try {
		return await fetch(url, {
			...options,
			signal: controller.signal
		});
	} catch (error) {
		if (error instanceof Error && error.name === "AbortError") throw new Error(`Request to ${url} timed out after ${timeoutMs}ms (npm registry slow or unreachable from this Worker)`);
		throw error;
	} finally {
		clearTimeout(timeoutId);
	}
}
/**
* Install npm dependencies into a virtual file system.
*
* Reads the package.json from the files, resolves all dependencies,
* and populates node_modules with the package contents.
*
* @param fileSystem - Virtual file system containing package.json
* @param options - Installation options
* @returns Metadata about the installation
*/
async function installDependencies(fileSystem, options = {}) {
	const { dev = false, registry = NPM_REGISTRY } = options;
	const result = {
		installed: [],
		warnings: []
	};
	const packageJsonContent = fileSystem.read("package.json");
	if (!packageJsonContent) return result;
	let packageJson;
	try {
		packageJson = JSON.parse(packageJsonContent);
	} catch {
		result.warnings.push("Failed to parse package.json");
		return result;
	}
	const depsToInstall = {
		...packageJson.dependencies,
		...dev ? packageJson.devDependencies : {}
	};
	if (Object.keys(depsToInstall).length === 0) return result;
	const installedPackages = /* @__PURE__ */ new Map();
	const inProgress = /* @__PURE__ */ new Map();
	await Promise.all(Object.entries(depsToInstall).map(([name, versionRange]) => installPackage(name, versionRange, result, fileSystem, installedPackages, inProgress, registry)));
	return result;
}
/**
* Install a single package and its dependencies recursively.
*/
async function installPackage(name, versionRange, result, fileSystem, installedPackages, inProgress, registry) {
	if (installedPackages.has(name)) return;
	if (fileSystem.read(`node_modules/${name}/package.json`) !== null) {
		installedPackages.set(name, "existing");
		return;
	}
	const existing = inProgress.get(name);
	if (existing) return existing;
	const installPromise = (async () => {
		try {
			const metadata = await fetchPackageMetadata(name, registry);
			const version = resolveVersion(versionRange, metadata);
			if (!version) {
				result.warnings.push(`Could not resolve version for ${name}@${versionRange}`);
				return;
			}
			const versionMetadata = metadata.versions[version];
			if (!versionMetadata) {
				result.warnings.push(`Version ${version} not found for ${name}`);
				return;
			}
			installedPackages.set(name, version);
			result.installed.push(`${name}@${version}`);
			const packageFiles = await fetchPackageFiles(name, versionMetadata);
			for (const [filePath, content] of Object.entries(packageFiles)) fileSystem.write(`node_modules/${name}/${filePath}`, content);
			const deps = versionMetadata.dependencies ?? {};
			await Promise.all(Object.entries(deps).map(([depName, depVersion]) => installPackage(depName, depVersion, result, fileSystem, installedPackages, inProgress, registry)));
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			result.warnings.push(`Failed to install ${name}: ${message}`);
		}
	})();
	inProgress.set(name, installPromise);
	try {
		await installPromise;
	} finally {
		inProgress.delete(name);
	}
}
/**
* Fetch package metadata from npm registry.
*/
async function fetchPackageMetadata(name, registry) {
	const url = `${registry}/${name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name}`;
	const response = await fetchWithTimeout(url, { headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" } });
	if (!response.ok) {
		const hint = response.status === 404 ? " (package not found — check the name in package.json or set the `registry` option if it lives on a private registry)" : "";
		throw new Error(`Registry returned ${response.status} ${response.statusText} for "${name}" at ${url}${hint}`);
	}
	return await response.json();
}
/**
* Resolve a semver range to a specific version.
*/
function resolveVersion(range, metadata) {
	if (range === "latest" || range === "*") return metadata["dist-tags"]["latest"];
	if (metadata.versions[range]) return range;
	if (metadata["dist-tags"][range]) return metadata["dist-tags"][range];
	const versions = Object.keys(metadata.versions);
	return semver.maxSatisfying(versions, range) ?? void 0;
}
/**
* Fetch and extract package files from npm tarball.
*/
async function fetchPackageFiles(name, metadata) {
	const tarballUrl = metadata.dist?.tarball;
	if (!tarballUrl) throw new Error(`Registry metadata for ${name}@${metadata.version} is missing \`dist.tarball\` — the registry response is likely malformed or the version was unpublished.`);
	const response = await fetchWithTimeout(tarballUrl, {}, DEFAULT_TIMEOUT_MS * 2);
	if (!response.ok) throw new Error(`Failed to fetch tarball for ${name}@${metadata.version}: ${response.status} ${response.statusText} (${tarballUrl})`);
	const buffer = await response.arrayBuffer();
	return extractTarball(new Uint8Array(buffer));
}
/**
* Extract files from a gzipped tarball.
*
* npm packages are distributed as .tgz files (gzipped tar).
* The contents are in a "package/" directory.
*/
async function extractTarball(data) {
	return parseTar(await decompress(data));
}
/**
* Decompress gzip data using DecompressionStream.
*/
async function decompress(data) {
	const ds = new DecompressionStream("gzip");
	const writer = ds.writable.getWriter();
	const reader = ds.readable.getReader();
	writer.write(data).catch(() => {});
	writer.close().catch(() => {});
	const chunks = [];
	let totalLength = 0;
	while (true) {
		const { done, value } = await reader.read();
		if (done) break;
		chunks.push(value);
		totalLength += value.length;
	}
	const result = new Uint8Array(totalLength);
	let offset = 0;
	for (const chunk of chunks) {
		result.set(chunk, offset);
		offset += chunk.length;
	}
	return result;
}
/**
* Parse a tar archive and extract text files.
*
* TAR format:
* - 512-byte header blocks
* - File content (padded to 512 bytes)
* - Two empty blocks at the end
*/
function parseTar(data) {
	const files = {};
	const textDecoder = new TextDecoder();
	let offset = 0;
	while (offset < data.length - 512) {
		const header = data.slice(offset, offset + 512);
		if (header.every((b) => b === 0)) break;
		const name = readString(header, 0, 100);
		const sizeStr = readString(header, 124, 12);
		const typeFlag = header[156];
		const size = parseInt(sizeStr.trim(), 8) || 0;
		offset += 512;
		if ((typeFlag === 48 || typeFlag === 0) && size > 0) {
			const content = data.slice(offset, offset + size);
			let filePath = name;
			if (filePath.startsWith("package/")) filePath = filePath.slice(8);
			if (isTextFile(filePath)) try {
				files[filePath] = textDecoder.decode(content);
			} catch {}
		}
		offset += Math.ceil(size / 512) * 512;
	}
	return files;
}
/**
* Read a null-terminated string from a buffer.
*/
function readString(buffer, offset, length) {
	const bytes = buffer.slice(offset, offset + length);
	const nullIndex = bytes.indexOf(0);
	const relevantBytes = nullIndex >= 0 ? bytes.slice(0, nullIndex) : bytes;
	return new TextDecoder().decode(relevantBytes);
}
/**
* Check if a file path is likely a text file.
*/
function isTextFile(path) {
	const textExtensions = [
		".js",
		".mjs",
		".cjs",
		".ts",
		".mts",
		".cts",
		".tsx",
		".jsx",
		".json",
		".md",
		".txt",
		".css",
		".html",
		".yml",
		".yaml",
		".toml",
		".xml",
		".svg",
		".map",
		".d.ts",
		".d.mts",
		".d.cts"
	];
	const configFiles = [
		"LICENSE",
		"README",
		"CHANGELOG",
		"package.json",
		"tsconfig.json",
		".npmignore",
		".gitignore"
	];
	const fileName = path.split("/").pop() ?? "";
	if (configFiles.some((f) => fileName.toUpperCase().startsWith(f.toUpperCase()))) return true;
	return textExtensions.some((ext) => path.toLowerCase().endsWith(ext));
}
/**
* Check if files contain a package.json with dependencies that need installing.
*/
function hasDependencies(files) {
	const packageJson = files.read("package.json");
	if (!packageJson) return false;
	try {
		const deps = JSON.parse(packageJson).dependencies ?? {};
		return Object.keys(deps).length > 0;
	} catch {
		return false;
	}
}
//#endregion
export { hasDependencies as n, installDependencies as r, fetchPackageFiles as t };

//# sourceMappingURL=installer-Ns4ivVf5.js.map