import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, ".."); const registryIndexPath = path.join(repoRoot, "registry", "index.json"); function toPosixPath(value) { return value.split(path.sep).join(path.posix.sep); } async function readJson(filePath) { const source = await fs.readFile(filePath, "utf8"); return JSON.parse(source); } function sortObject(input) { return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))); } function parseArgs(argv) { const options = { dryRun: false, project: process.cwd(), skipPackageJson: false, targetDir: null }; const items = []; for (let index = 0; index < argv.length; index += 1) { const current = argv[index]; if (current === "--dry-run") { options.dryRun = true; continue; } if (current === "--skip-package-json") { options.skipPackageJson = true; continue; } if (current === "--project" || current === "--target-dir") { const next = argv[index + 1]; if (!next) { throw new Error(`Expected a value after ${current}.`); } if (current === "--project") { options.project = next; } else { options.targetDir = next; } index += 1; continue; } if (current.startsWith("--")) { throw new Error(`Unknown option: ${current}`); } items.push(current); } return { items, options }; } function mapSourceToTarget(sourcePath, targetDir) { if (sourcePath.startsWith("packages/ui/src/")) { return toPosixPath(path.posix.join(targetDir, sourcePath.replace("packages/ui/src/", ""))); } if (sourcePath.startsWith("packages/tokens/src/")) { return toPosixPath( path.posix.join(targetDir, "tokens", sourcePath.replace("packages/tokens/src/", "")) ); } throw new Error(`Unsupported registry source path: ${sourcePath}`); } function resolveItems(registryIndex, requestedItems) { const itemLookup = new Map(registryIndex.items.map((item) => [item.name, item])); const resolved = new Map(); const stack = [...requestedItems]; while (stack.length > 0) { const name = stack.pop(); const item = itemLookup.get(name); if (!item) { const available = [...itemLookup.keys()].sort().join(", "); throw new Error(`Unknown registry item "${name}". Available items: ${available}.`); } if (resolved.has(name)) { continue; } resolved.set(name, item); for (const dependency of item.requires) { stack.push(dependency); } } return [...resolved.values()].sort((left, right) => left.name.localeCompare(right.name)); } async function readInstallManifest(manifestPath) { try { return await readJson(manifestPath); } catch { return null; } } function collectPackageDependencies(items) { const dependencies = {}; for (const item of items) { for (const [name, range] of Object.entries(item.packageDependencies)) { dependencies[name] = range; } } return sortObject(dependencies); } function upsertDependencySections(packageJson, dependencies) { const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; const added = {}; const conflicts = []; const nextPackageJson = JSON.parse(JSON.stringify(packageJson)); nextPackageJson.dependencies = nextPackageJson.dependencies ?? {}; for (const [name, range] of Object.entries(dependencies)) { let existingSection = null; let existingRange = null; for (const section of sections) { const value = nextPackageJson[section]?.[name]; if (typeof value === "string") { existingSection = section; existingRange = value; break; } } if (!existingSection) { nextPackageJson.dependencies[name] = range; added[name] = range; continue; } if (existingRange !== range) { conflicts.push({ existingRange, name, requiredRange: range, section: existingSection }); } } nextPackageJson.dependencies = sortObject(nextPackageJson.dependencies); return { added, conflicts, packageJson: nextPackageJson }; } async function copyRegistryFiles({ dryRun, items, projectRoot, targetDir }) { const writtenFiles = []; const updatedFiles = []; const unchangedFiles = []; for (const item of items) { for (const sourcePath of item.files) { const sourceFile = path.join(repoRoot, ...sourcePath.split("/")); const targetRelativePath = mapSourceToTarget(sourcePath, targetDir); const targetFile = path.join(projectRoot, ...targetRelativePath.split("/")); const content = await fs.readFile(sourceFile, "utf8"); let existingContent = null; try { existingContent = await fs.readFile(targetFile, "utf8"); } catch { existingContent = null; } if (existingContent === content) { unchangedFiles.push(targetRelativePath); continue; } if (!dryRun) { await fs.mkdir(path.dirname(targetFile), { recursive: true }); await fs.writeFile(targetFile, content); } if (existingContent === null) { writtenFiles.push(targetRelativePath); } else { updatedFiles.push(targetRelativePath); } } } return { unchangedFiles, updatedFiles, writtenFiles }; } async function main() { const { items: requestedItems, options } = parseArgs(process.argv.slice(2)); const registryIndex = await readJson(registryIndexPath); const projectRoot = path.resolve(options.project); const targetDir = toPosixPath(options.targetDir ?? registryIndex.install.defaultTargetDir); const targetRoot = path.join(projectRoot, ...targetDir.split("/")); const manifestPath = path.join(targetRoot, registryIndex.install.manifestFile); let itemNames = requestedItems; if (itemNames.length === 0) { const manifest = await readInstallManifest(manifestPath); if (!manifest?.items?.length) { throw new Error( `No registry items were provided and ${toPosixPath( path.relative(projectRoot, manifestPath) )} does not exist yet.` ); } itemNames = manifest.items; } const resolvedItems = resolveItems(registryIndex, itemNames); const packageDependencies = collectPackageDependencies(resolvedItems); const copyResult = await copyRegistryFiles({ dryRun: options.dryRun, items: resolvedItems, projectRoot, targetDir }); let dependencyResult = { added: {}, conflicts: [] }; if (!options.skipPackageJson) { const packageJsonPath = path.join(projectRoot, "package.json"); const packageJson = await readJson(packageJsonPath); dependencyResult = upsertDependencySections(packageJson, packageDependencies); if (!options.dryRun) { await fs.writeFile( packageJsonPath, `${JSON.stringify(dependencyResult.packageJson, null, 2)}\n` ); } } const installManifest = { items: resolvedItems.map((item) => item.name), packageDependencies, registry: registryIndex.library.name, sourcePackages: registryIndex.library.packages, targetDir }; if (!options.dryRun) { await fs.mkdir(targetRoot, { recursive: true }); await fs.writeFile(manifestPath, `${JSON.stringify(installManifest, null, 2)}\n`); } console.log( `${options.dryRun ? "Planned" : "Installed"} registry items: ${resolvedItems .map((item) => item.name) .join(", ")}` ); console.log( `Files written: ${copyResult.writtenFiles.length}, updated: ${copyResult.updatedFiles.length}, unchanged: ${copyResult.unchangedFiles.length}` ); if (!options.skipPackageJson) { const added = Object.keys(dependencyResult.added); console.log( added.length > 0 ? `Added package dependencies: ${added.join(", ")}` : "Added package dependencies: none" ); } if (dependencyResult.conflicts.length > 0) { console.log("Dependency version conflicts:"); for (const conflict of dependencyResult.conflicts) { console.log( `- ${conflict.name}: keeping ${conflict.section}=${conflict.existingRange}, registry expects ${conflict.requiredRange}` ); } } console.log(`Target directory: ${targetDir}`); console.log(`Tokens import path: ${targetDir}/tokens/styles.css`); console.log( options.dryRun ? "Dry run complete." : "Review the diff, then run your package manager install if package.json changed." ); } await main();