diff --git a/apps/cli/scripts/buildSharedDeps.contract.test.mjs b/apps/cli/scripts/buildSharedDeps.contract.test.mjs new file mode 100644 index 000000000..d106f5d9d --- /dev/null +++ b/apps/cli/scripts/buildSharedDeps.contract.test.mjs @@ -0,0 +1,112 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { normalize, resolve } from 'node:path'; + +import { + bundledWorkspacePackages, + resolveBundledWorkspaceBuildEntry, + syncBundledWorkspaceDist, +} from './buildSharedDeps.mjs'; +import { createBundledWorkspaceBundles } from './workspaceBundleManifest.mjs'; + +test('buildSharedDeps builds every bundled workspace package needed by the published CLI', () => { + assert.deepEqual(bundledWorkspacePackages, ['agents', 'cli-common', 'protocol', 'release-runtime']); +}); + +test('workspace bundle manifest derives bundle metadata from a single package list', () => { + const repoRoot = resolve('repo'); + const targetRoot = resolve(repoRoot, 'apps', 'cli'); + + assert.deepEqual(createBundledWorkspaceBundles({ repoRoot, targetRoot }), [ + { + packageName: '@happier-dev/agents', + srcDir: resolve(repoRoot, 'packages', 'agents'), + destDir: resolve(targetRoot, 'node_modules', '@happier-dev', 'agents'), + }, + { + packageName: '@happier-dev/cli-common', + srcDir: resolve(repoRoot, 'packages', 'cli-common'), + destDir: resolve(targetRoot, 'node_modules', '@happier-dev', 'cli-common'), + }, + { + packageName: '@happier-dev/protocol', + srcDir: resolve(repoRoot, 'packages', 'protocol'), + destDir: resolve(targetRoot, 'node_modules', '@happier-dev', 'protocol'), + }, + { + packageName: '@happier-dev/release-runtime', + srcDir: resolve(repoRoot, 'packages', 'release-runtime'), + destDir: resolve(targetRoot, 'node_modules', '@happier-dev', 'release-runtime'), + }, + ]); +}); + +test('buildSharedDeps verifies each package using its declared main entry when available', () => { + const repoRoot = resolve('repo'); + + assert.equal( + resolveBundledWorkspaceBuildEntry('release-runtime', { + repoRoot, + readFileSync: () => JSON.stringify({ main: './dist/github.js' }), + }), + resolve(repoRoot, 'packages', 'release-runtime', 'dist', 'github.js'), + ); + + assert.equal( + resolveBundledWorkspaceBuildEntry('release-runtime', { + repoRoot, + readFileSync: () => JSON.stringify({}), + }), + resolve(repoRoot, 'packages', 'release-runtime', 'dist', 'index.js'), + ); +}); + +test('syncBundledWorkspaceDist defaults include release-runtime', () => { + const copyCalls = []; + const writeCalls = []; + const repoRoot = resolve('repo'); + const releaseRuntimeDist = resolve(repoRoot, 'apps', 'cli', 'node_modules', '@happier-dev', 'release-runtime', 'dist'); + const releaseRuntimePackageJson = resolve( + repoRoot, + 'apps', + 'cli', + 'node_modules', + '@happier-dev', + 'release-runtime', + 'package.json', + ); + const releaseRuntimeSourceDist = resolve(repoRoot, 'packages', 'release-runtime', 'dist'); + const toPlatformPath = (path) => normalize(String(path)); + + syncBundledWorkspaceDist({ + repoRoot, + existsSync: (candidate) => + [releaseRuntimeDist, releaseRuntimePackageJson].includes(toPlatformPath(candidate)), + cpSync: (src, dest, opts) => { + copyCalls.push({ src: toPlatformPath(src), dest: toPlatformPath(dest), opts }); + }, + readFileSync: () => + JSON.stringify({ + name: '@happier-dev/release-runtime', + version: '0.0.0', + type: 'module', + exports: { './github': { default: './dist/github.js' } }, + }), + writeFileSync: (path, payload) => { + writeCalls.push({ path: toPlatformPath(path), payload: String(payload) }); + }, + }); + + assert.deepEqual(copyCalls, [ + { + src: releaseRuntimeSourceDist, + dest: releaseRuntimeDist, + opts: { recursive: true, force: true }, + }, + ]); + + assert.equal(writeCalls.length, 1); + assert.equal(writeCalls[0]?.path, releaseRuntimePackageJson); + assert.match(writeCalls[0]?.payload ?? '', /\"name\": \"@happier-dev\/release-runtime\"/); + assert.match(writeCalls[0]?.payload ?? '', /\"private\": true/); +}); diff --git a/apps/cli/scripts/buildSharedDeps.mjs b/apps/cli/scripts/buildSharedDeps.mjs index 97f5c95a5..8411ddfc4 100644 --- a/apps/cli/scripts/buildSharedDeps.mjs +++ b/apps/cli/scripts/buildSharedDeps.mjs @@ -3,7 +3,9 @@ import { cpSync, existsSync, readFileSync, realpathSync, writeFileSync } from 'n import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); +import { bundledWorkspacePackages } from './workspaceBundleManifest.mjs'; +export { bundledWorkspacePackages }; + function findRepoRoot(startDir) { let dir = startDir; @@ -78,7 +80,8 @@ export function syncBundledWorkspaceDist(opts = {}) { const cp = opts.cpSync ?? cpSync; const readFile = opts.readFileSync ?? readFileSync; const writeFile = opts.writeFileSync ?? writeFileSync; - const packages = Array.isArray(opts.packages) && opts.packages.length > 0 ? opts.packages : ['agents', 'cli-common', 'protocol']; + const packages = + Array.isArray(opts.packages) && opts.packages.length > 0 ? opts.packages : bundledWorkspacePackages; for (const pkg of packages) { const srcDist = resolve(repoRoot, 'packages', pkg, 'dist'); @@ -102,6 +105,21 @@ export function syncBundledWorkspaceDist(opts = {}) { } } +export function resolveBundledWorkspaceBuildEntry(pkg, opts = {}) { + const repoRootArg = opts.repoRoot; + const repoRoot = typeof repoRootArg === 'string' && repoRootArg.trim() ? repoRootArg : findRepoRoot(__dirname); + const readFile = opts.readFileSync ?? readFileSync; + const packageJsonPath = resolve(repoRoot, 'packages', pkg, 'package.json'); + + try { + const packageJson = JSON.parse(readFile(packageJsonPath, 'utf8')); + const mainEntry = typeof packageJson?.main === 'string' && packageJson.main.trim() ? packageJson.main : './dist/index.js'; + return resolve(repoRoot, 'packages', pkg, mainEntry); + } catch { + return resolve(repoRoot, 'packages', pkg, 'dist', 'index.js'); + } +} + function sanitizeBundledWorkspacePackageJson(raw) { const { name, @@ -134,13 +152,15 @@ function sanitizeBundledWorkspacePackageJson(raw) { } export function main() { - runTsc(resolve(repoRoot, 'packages', 'agents', 'tsconfig.json')); - runTsc(resolve(repoRoot, 'packages', 'cli-common', 'tsconfig.json')); - runTsc(resolve(repoRoot, 'packages', 'protocol', 'tsconfig.json')); + for (const pkg of bundledWorkspacePackages) { + runTsc(resolve(repoRoot, 'packages', pkg, 'tsconfig.json')); + } - const protocolDist = resolve(repoRoot, 'packages', 'protocol', 'dist', 'index.js'); - if (!existsSync(protocolDist)) { - throw new Error(`Expected @happier-dev/protocol build output missing: ${protocolDist}`); + for (const pkg of bundledWorkspacePackages) { + const distEntry = resolveBundledWorkspaceBuildEntry(pkg, { repoRoot }); + if (!existsSync(distEntry)) { + throw new Error(`Expected @happier-dev/${pkg} build output missing: ${distEntry}`); + } } // If the CLI currently has bundled workspace deps under apps/cli/node_modules, diff --git a/apps/cli/scripts/bundleWorkspaceDeps.mjs b/apps/cli/scripts/bundleWorkspaceDeps.mjs index 2f78a673f..634aa89e8 100644 --- a/apps/cli/scripts/bundleWorkspaceDeps.mjs +++ b/apps/cli/scripts/bundleWorkspaceDeps.mjs @@ -8,34 +8,14 @@ import { vendorBundledPackageRuntimeDependencies, } from '../../../packages/cli-common/dist/workspaces/index.js'; +import { createBundledWorkspaceBundles } from './workspaceBundleManifest.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); export function bundleWorkspaceDeps(opts = {}) { const repoRoot = opts.repoRoot ?? findRepoRoot(__dirname); const happyCliDir = opts.happyCliDir ?? resolve(repoRoot, 'apps', 'cli'); - - const bundles = [ - { - packageName: '@happier-dev/agents', - srcDir: resolve(repoRoot, 'packages', 'agents'), - destDir: resolve(happyCliDir, 'node_modules', '@happier-dev', 'agents'), - }, - { - packageName: '@happier-dev/cli-common', - srcDir: resolve(repoRoot, 'packages', 'cli-common'), - destDir: resolve(happyCliDir, 'node_modules', '@happier-dev', 'cli-common'), - }, - { - packageName: '@happier-dev/protocol', - srcDir: resolve(repoRoot, 'packages', 'protocol'), - destDir: resolve(happyCliDir, 'node_modules', '@happier-dev', 'protocol'), - }, - { - packageName: '@happier-dev/release-runtime', - srcDir: resolve(repoRoot, 'packages', 'release-runtime'), - destDir: resolve(happyCliDir, 'node_modules', '@happier-dev', 'release-runtime'), - }, - ]; + const bundles = createBundledWorkspaceBundles({ repoRoot, targetRoot: happyCliDir }); bundleWorkspacePackages({ bundles }); for (const b of bundles) { diff --git a/apps/cli/scripts/workspaceBundleManifest.mjs b/apps/cli/scripts/workspaceBundleManifest.mjs new file mode 100644 index 000000000..70c5cb30b --- /dev/null +++ b/apps/cli/scripts/workspaceBundleManifest.mjs @@ -0,0 +1,11 @@ +import { resolve } from 'node:path'; + +export const bundledWorkspacePackages = ['agents', 'cli-common', 'protocol', 'release-runtime']; + +export function createBundledWorkspaceBundles({ repoRoot, targetRoot }) { + return bundledWorkspacePackages.map((pkg) => ({ + packageName: `@happier-dev/${pkg}`, + srcDir: resolve(repoRoot, 'packages', pkg), + destDir: resolve(targetRoot, 'node_modules', '@happier-dev', pkg), + })); +}