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 packagesRoot = path.join(repoRoot, "packages");
const tempPrefix = path.join(os.tmpdir(), "cadence-ui-package-consumer-");
const keepArtifacts = process.env.CADENCE_KEEP_PACKAGE_SMOKE === "1";
const skipBuild = process.env.CADENCE_SKIP_PACKAGE_BUILD === "1";
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
function toPosixPath(value) {
return value.split(path.sep).join(path.posix.sep);
}
async function readJson(filePath) {
return JSON.parse(await fs.readFile(filePath, "utf8"));
}
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 packPackage(packageDir, outputDir) {
const before = new Set(await fs.readdir(outputDir));
await run(
pnpmCommand,
["pack", "--pack-destination", outputDir],
{
cwd: packageDir,
env: {
CI: "1"
}
}
);
const after = await fs.readdir(outputDir);
const created = after.filter((entry) => !before.has(entry) && entry.endsWith(".tgz"));
if (created.length !== 1) {
throw new Error(
`Expected exactly one tarball from ${packageDir}, received ${created.length}.`
);
}
return path.join(outputDir, created[0]);
}
function toFileDependency(projectDir, absolutePath) {
let relativePath = toPosixPath(path.relative(projectDir, absolutePath));
if (!relativePath.startsWith(".")) {
relativePath = `./${relativePath}`;
}
return `file:${relativePath}`;
}
async function writeConsumerFixture({
projectDir,
tokensTarball,
uiTarball,
rootPackage,
uiPackage
}) {
const packageJson = {
name: "cadence-ui-package-consumer-smoke",
private: true,
type: "module",
dependencies: {
"@ai-ui/tokens": toFileDependency(projectDir, tokensTarball),
"@ai-ui/ui": toFileDependency(projectDir, uiTarball),
react: uiPackage.devDependencies.react,
"react-dom": uiPackage.devDependencies["react-dom"]
},
devDependencies: {
"@tailwindcss/vite": rootPackage.devDependencies["@tailwindcss/vite"],
"@types/react": rootPackage.devDependencies["@types/react"],
"@types/react-dom": rootPackage.devDependencies["@types/react-dom"],
tailwindcss: rootPackage.devDependencies.tailwindcss,
typescript: rootPackage.devDependencies.typescript,
vite: rootPackage.devDependencies.vite
},
pnpm: {
overrides: {
"@ai-ui/tokens": toFileDependency(projectDir, tokensTarball)
}
}
};
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", "vite.config.ts"]
};
const viteConfig = `import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()]
});
`;
const indexHtml = `
Cadence UI Package Smoke
`;
const stylesSource = `@import "tailwindcss";
@import "@ai-ui/ui/styles.css";
@source "../node_modules/@ai-ui/ui/src";
`;
const mainSource = `import { createRoot } from "react-dom/client";
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
Input
} from "@ai-ui/ui";
import { setTheme } from "@ai-ui/tokens";
import "./styles.css";
setTheme("light");
function App() {
return (
);
}
const root = document.getElementById("root");
if (!root) {
throw new Error("Missing root element.");
}
createRoot(root).render();
`;
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, "vite.config.ts"), viteConfig);
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", "styles.css"), stylesSource);
await fs.writeFile(path.join(projectDir, "src", "main.tsx"), mainSource);
}
async function main() {
const [rootPackage, uiPackage, tokensPackage] = await Promise.all([
readJson(path.join(repoRoot, "package.json")),
readJson(path.join(packagesRoot, "ui", "package.json")),
readJson(path.join(packagesRoot, "tokens", "package.json"))
]);
if (uiPackage.version !== tokensPackage.version) {
throw new Error(
`Expected @ai-ui/ui and @ai-ui/tokens to share a version. Received ${uiPackage.version} and ${tokensPackage.version}.`
);
}
const projectDir = await fs.mkdtemp(tempPrefix);
const packDir = path.join(projectDir, "packed");
let succeeded = false;
const cleanup = async () => {
if (!succeeded || keepArtifacts) {
console.log(`Keeping package smoke fixture at ${projectDir}`);
return;
}
await fs.rm(projectDir, { force: true, recursive: true });
};
try {
if (!skipBuild) {
console.log("Building workspace packages for package smoke");
await run(pnpmCommand, ["build"], {
cwd: repoRoot,
env: {
CI: "1"
}
});
}
await fs.mkdir(packDir, { recursive: true });
console.log("Packing @ai-ui/tokens");
const tokensTarball = await packPackage(path.join(packagesRoot, "tokens"), packDir);
console.log("Packing @ai-ui/ui");
const uiTarball = await packPackage(path.join(packagesRoot, "ui"), packDir);
console.log(`Creating package consumer fixture in ${projectDir}`);
await writeConsumerFixture({
projectDir,
tokensTarball,
uiTarball,
rootPackage,
uiPackage
});
console.log("Installing package consumer dependencies");
await run(pnpmCommand, ["install", "--ignore-workspace"], {
cwd: projectDir,
env: {
CI: "1"
}
});
console.log("Verifying CommonJS package entrypoints");
await run(process.execPath, ["-e", 'const ui=require("@ai-ui/ui"); const tokens=require("@ai-ui/tokens"); if(!ui.Button||!tokens.setTheme){throw new Error("Missing CommonJS export.");}'], {
cwd: projectDir
});
console.log("Verifying ESM package entrypoints");
await run(process.execPath, ["--input-type=module", "-e", 'const ui=await import("@ai-ui/ui"); const tokens=await import("@ai-ui/tokens"); if(!ui.Button||!tokens.setTheme){throw new Error("Missing ESM export.");}'], {
cwd: projectDir
});
console.log("Typechecking the package consumer fixture");
await run(pnpmCommand, ["exec", "tsc", "-p", "tsconfig.json", "--noEmit"], {
cwd: projectDir
});
console.log("Building the package consumer fixture");
await run(pnpmCommand, ["exec", "vite", "build"], {
cwd: projectDir,
env: {
CI: "1"
}
});
succeeded = true;
console.log("Package consumer smoke test passed.");
} catch (error) {
console.error(`Package consumer smoke test failed. Fixture preserved at ${projectDir}`);
throw error;
} finally {
await cleanup();
}
}
await main();