Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions apps/cli/scripts/buildSharedDeps.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { execFileSync } from 'node:child_process';
import { cpSync, existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { cpSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { bundledWorkspaceDirNames } from './bundledWorkspacePackages.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));

function findRepoRoot(startDir) {
Expand Down Expand Up @@ -51,6 +53,7 @@ export function resolveTscBin({ exists } = {}) {
}

const tscBin = resolveTscBin();
export const bundledWorkspaceDirs = bundledWorkspaceDirNames;

export function runTsc(tsconfigPath, opts) {
const exec = opts?.execFileSync ?? execFileSync;
Expand Down Expand Up @@ -78,7 +81,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 : bundledWorkspaceDirs;

for (const pkg of packages) {
const srcDist = resolve(repoRoot, 'packages', pkg, 'dist');
Expand Down Expand Up @@ -134,13 +138,12 @@ 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'));

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 bundledWorkspaceDirs) {
runTsc(resolve(repoRoot, 'packages', pkg, 'tsconfig.json'));
const distDir = resolve(repoRoot, 'packages', pkg, 'dist');
if (!existsSync(distDir)) {
throw new Error(`Expected @happier-dev/${pkg} build output missing: ${distDir}`);
}
}

// If the CLI currently has bundled workspace deps under apps/cli/node_modules,
Expand Down
24 changes: 2 additions & 22 deletions apps/cli/scripts/bundleWorkspaceDeps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,15 @@ import {
findRepoRoot,
vendorBundledPackageRuntimeDependencies,
} from '../../../packages/cli-common/dist/workspaces/index.js';
import { createBundledWorkspaceBundles } from './bundledWorkspacePackages.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, happyCliDir });
bundleWorkspacePackages({ bundles });

for (const b of bundles) {
Expand Down
59 changes: 59 additions & 0 deletions apps/cli/scripts/bundledWorkspacePackages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Canonical list of workspace packages that are bundled into the CLI npm package.
*
* This module is the single source of truth consumed by both the build step
* (`buildSharedDeps.mjs`) and the bundle step (`bundleWorkspaceDeps.mjs`).
* Adding or removing a package here automatically keeps both steps in sync.
*/

import { resolve } from 'node:path';

/**
* Frozen array of all workspace packages that must be compiled and bundled
* during `yarn prepack`. Each entry is itself frozen to prevent accidental
* mutation that could desync the derived exports.
*
* @type {ReadonlyArray<{dirName: string, packageName: string}>}
*/
export const bundledWorkspacePackages = Object.freeze([
Object.freeze({ dirName: 'agents', packageName: '@happier-dev/agents' }),
Object.freeze({ dirName: 'cli-common', packageName: '@happier-dev/cli-common' }),
Object.freeze({ dirName: 'protocol', packageName: '@happier-dev/protocol' }),
Object.freeze({ dirName: 'release-runtime', packageName: '@happier-dev/release-runtime' }),
]);

/**
* Frozen array of directory names derived from {@link bundledWorkspacePackages}.
* Matches the subdirectory names under `packages/` in the monorepo root.
*
* @type {ReadonlyArray<string>}
*/
export const bundledWorkspaceDirNames = Object.freeze(
bundledWorkspacePackages.map(({ dirName }) => dirName),
);

/**
* Frozen array of npm package names derived from {@link bundledWorkspacePackages}.
* Must stay in sync with the `bundledDependencies` field in `apps/cli/package.json`.
*
* @type {ReadonlyArray<string>}
*/
export const bundledWorkspacePackageNames = Object.freeze(
bundledWorkspacePackages.map(({ packageName }) => packageName),
);

/**
* Creates the bundle descriptor objects used by `bundleWorkspaceDeps.mjs`.
* Each descriptor maps a workspace package to its source directory (in the
* monorepo) and the destination directory under `apps/cli/node_modules`.
*
* @param {{ repoRoot: string, happyCliDir: string }} opts
* @returns {Array<{ packageName: string, srcDir: string, destDir: string }>}
*/
export function createBundledWorkspaceBundles({ repoRoot, happyCliDir }) {
return bundledWorkspacePackages.map(({ dirName, packageName }) => ({
packageName,
srcDir: resolve(repoRoot, 'packages', dirName),
destDir: resolve(happyCliDir, 'node_modules', '@happier-dev', dirName),
}));
}
42 changes: 42 additions & 0 deletions apps/cli/scripts/prepack-script.test.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { bundledWorkspaceDirs } from './buildSharedDeps.mjs';
import {
bundledWorkspaceDirNames,
bundledWorkspacePackageNames,
createBundledWorkspaceBundles,
} from './bundledWorkspacePackages.mjs';

test('apps/cli prepack builds dist for npm pack', () => {
const pkgPath = new URL('../package.json', import.meta.url);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const prepack = String(pkg?.scripts?.prepack ?? '');
assert.ok(prepack.includes('build'), `expected scripts.prepack to include a build step, got: ${prepack || '(missing)'}`);
assert.ok(
prepack.includes('bundleWorkspaceDeps.mjs'),
`expected scripts.prepack to bundle workspace deps, got: ${prepack || '(missing)'}`,
);
});

test('apps/cli npm files list ships archives (not unpacked tools)', () => {
Expand All @@ -23,3 +35,33 @@ test('apps/cli npm files list ships archives (not unpacked tools)', () => {
assert.ok(!files.includes('tools'), 'expected not to ship entire tools/ tree (would include unpacked binaries)');
assert.ok(!files.includes('tools/unpacked'), 'expected tools/unpacked to be excluded');
});

test('apps/cli bundled workspace package definitions stay in sync', () => {
const pkgPath = new URL('../package.json', import.meta.url);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const bundledDependencies = Array.isArray(pkg?.bundledDependencies)
? pkg.bundledDependencies.map((value) => String(value)).sort()
: [];

assert.deepEqual(bundledDependencies, [...bundledWorkspacePackageNames].sort());
assert.deepEqual([...bundledWorkspaceDirs], [...bundledWorkspaceDirNames]);

const repoRoot = resolve('/tmp', 'happier-repo');
const happyCliDir = resolve(repoRoot, 'apps', 'cli');
const bundles = createBundledWorkspaceBundles({ repoRoot, happyCliDir });

assert.deepEqual(
bundles.map(({ packageName }) => packageName).sort(),
[...bundledWorkspacePackageNames].sort(),
);

const releaseRuntimeBundle = bundles.find(
({ packageName }) => packageName === '@happier-dev/release-runtime',
);
assert.ok(releaseRuntimeBundle, 'expected release-runtime to be bundled for npm publish');
assert.equal(releaseRuntimeBundle.srcDir, resolve(repoRoot, 'packages', 'release-runtime'));
assert.equal(
releaseRuntimeBundle.destDir,
resolve(happyCliDir, 'node_modules', '@happier-dev', 'release-runtime'),
);
});