322 lines
9.1 KiB
JavaScript
322 lines
9.1 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 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 = `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Cadence UI Package Smoke</title>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="module" src="/src/main.tsx"></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
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 { setDynamicColor, setTheme } from "@ai-ui/tokens";
|
|
|
|
import "./styles.css";
|
|
|
|
setTheme("violet");
|
|
setDynamicColor("#6750A4");
|
|
|
|
function App() {
|
|
return (
|
|
<main className="min-h-screen bg-[var(--color-background)] p-8 text-[var(--color-foreground)]">
|
|
<div className="mx-auto grid max-w-xl gap-6">
|
|
<div className="grid gap-3">
|
|
<h1 className="text-2xl font-semibold">Cadence UI package smoke</h1>
|
|
<Input defaultValue="team@cadence.dev" placeholder="Email" />
|
|
</div>
|
|
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button>Open package smoke dialog</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogTitle>Package consumer</DialogTitle>
|
|
<DialogDescription>
|
|
Verifies published package exports typecheck and build in a consumer app.
|
|
</DialogDescription>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const root = document.getElementById("root");
|
|
|
|
if (!root) {
|
|
throw new Error("Missing root element.");
|
|
}
|
|
|
|
createRoot(root).render(<App />);
|
|
`;
|
|
|
|
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||!tokens.setDynamicColor){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||!tokens.setDynamicColor){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();
|