Files

243 lines
7.0 KiB
JavaScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const registryInstallerPath = path.join(repoRoot, "scripts", "registry-install.mjs");
const tscBinPath = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc");
const viteBinPath = path.join(repoRoot, "node_modules", "vite", "bin", "vite.js");
const tempPrefix = path.join(os.tmpdir(), "cadence-ui-registry-consumer-");
const keepArtifacts = process.env.CADENCE_KEEP_REGISTRY_SMOKE === "1";
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const registryItems = ["button", "dialog", "input", "form"];
const packageJson = {
name: "cadence-ui-registry-consumer-smoke",
private: true,
type: "module",
dependencies: {
react: "^18.3.1",
"react-dom": "^18.3.1"
},
devDependencies: {
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7"
}
};
const tsconfig = {
compilerOptions: {
target: "ES2022",
module: "ESNext",
moduleResolution: "Bundler",
jsx: "react-jsx",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
noEmit: true,
types: ["react", "react-dom"]
},
include: ["src"]
};
const indexHtml = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cadence UI Registry Smoke</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
`;
const mainSource = `import { useForm } from "react-hook-form";
import { createRoot } from "react-dom/client";
import "./cadence-ui/tokens/styles.css";
import { Button } from "./cadence-ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger
} from "./cadence-ui/components/dialog";
import { Form, FormControl, FormItem, FormLabel, FormMessage } from "./cadence-ui/components/form";
import { Input } from "./cadence-ui/components/input";
type FormValues = {
email: string;
};
function App() {
const form = useForm<FormValues>({
defaultValues: {
email: "team@cadence.dev"
}
});
return (
<main style={{ display: "grid", gap: "1.5rem", padding: "2rem" }}>
<Form {...form}>
<FormItem name="email">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" />
</FormControl>
<FormMessage />
</FormItem>
</Form>
<Dialog>
<DialogTrigger asChild>
<Button>Open registry smoke dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Registry smoke</DialogTitle>
<DialogDescription>
Verifies copied components typecheck and build in a consumer app.
</DialogDescription>
</DialogContent>
</Dialog>
</main>
);
}
const root = document.getElementById("root");
if (!root) {
throw new Error("Missing root element.");
}
createRoot(root).render(<App />);
`;
function formatCommand(command, args) {
return [command, ...args].join(" ");
}
async function run(command, args, options = {}) {
await new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd ?? repoRoot,
env: {
...process.env,
...(options.env ?? {})
},
stdio: "inherit"
});
child.once("error", reject);
child.once("exit", (code, signal) => {
if (code === 0) {
resolve();
return;
}
reject(
new Error(
`${formatCommand(command, args)} failed with code ${code ?? "null"} and signal ${
signal ?? "null"
}.`
)
);
});
});
}
async function writeConsumerFixture(projectDir) {
await fs.mkdir(path.join(projectDir, "src"), { recursive: true });
await fs.writeFile(path.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
await fs.writeFile(path.join(projectDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`);
await fs.writeFile(path.join(projectDir, "index.html"), indexHtml);
await fs.writeFile(path.join(projectDir, "src", "global.d.ts"), 'declare module "*.css";\n');
await fs.writeFile(path.join(projectDir, "src", "main.tsx"), mainSource);
}
async function assertFileExists(projectDir, relativePath) {
const absolutePath = path.join(projectDir, ...relativePath.split("/"));
try {
await fs.stat(absolutePath);
} catch {
throw new Error(`Expected ${relativePath} to exist in the consumer fixture.`);
}
}
async function verifyManifest(projectDir) {
const manifestPath = path.join(projectDir, "src", "cadence-ui", ".install-manifest.json");
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
for (const item of registryItems) {
if (!manifest.items.includes(item)) {
throw new Error(`Registry manifest is missing "${item}".`);
}
}
if (!manifest.items.includes("tokens")) {
throw new Error('Registry manifest is missing the required "tokens" item.');
}
}
async function main() {
const projectDir = await fs.mkdtemp(tempPrefix);
let succeeded = false;
const cleanup = async () => {
if (!succeeded || keepArtifacts) {
console.log(`Keeping registry smoke fixture at ${projectDir}`);
return;
}
await fs.rm(projectDir, { force: true, recursive: true });
};
try {
console.log(`Creating registry consumer fixture in ${projectDir}`);
await writeConsumerFixture(projectDir);
console.log("Installing registry items into the consumer fixture");
await run(process.execPath, [registryInstallerPath, "--project", projectDir, ...registryItems]);
await assertFileExists(projectDir, "src/cadence-ui/components/button.tsx");
await assertFileExists(projectDir, "src/cadence-ui/components/dialog.tsx");
await assertFileExists(projectDir, "src/cadence-ui/components/form.tsx");
await assertFileExists(projectDir, "src/cadence-ui/tokens/styles.css");
await verifyManifest(projectDir);
console.log("Installing consumer dependencies");
await run(pnpmCommand, ["install", "--ignore-workspace"], {
cwd: projectDir,
env: {
CI: "1"
}
});
console.log("Re-running registry install from the saved manifest");
await run(process.execPath, [registryInstallerPath, "--project", projectDir, "--dry-run"]);
console.log("Typechecking the consumer fixture");
await run(process.execPath, [tscBinPath, "-p", path.join(projectDir, "tsconfig.json"), "--noEmit"]);
console.log("Building the consumer fixture");
await run(process.execPath, [viteBinPath, "build"], {
cwd: projectDir
});
succeeded = true;
console.log("Registry consumer smoke test passed.");
} catch (error) {
console.error(`Registry consumer smoke test failed. Fixture preserved at ${projectDir}`);
throw error;
} finally {
await cleanup();
}
}
await main();