328 lines
8.6 KiB
JavaScript
328 lines
8.6 KiB
JavaScript
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();
|