feat: add package-first release flow
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
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 { setTheme } from "@ai-ui/tokens";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
setTheme("light");
|
||||
|
||||
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){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();
|
||||
Reference in New Issue
Block a user