Skip to content

Commit 6ddd730

Browse files
committed
fix(cli): relax happier bin preflight for workspaces
1 parent aeb2313 commit 6ddd730

2 files changed

Lines changed: 101 additions & 10 deletions

File tree

apps/cli/bin/happier.mjs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,39 @@ import { execFileSync } from 'child_process';
44
import { fileURLToPath } from 'url';
55
import { join, dirname } from 'path';
66
import { createRequire } from 'module';
7-
import { existsSync } from 'fs';
87

98
function preflightRequiredDependencies(projectRoot) {
10-
const protocolPackageJsonPath = join(projectRoot, 'node_modules', '@happier-dev', 'protocol', 'package.json');
11-
if (!existsSync(protocolPackageJsonPath)) {
12-
console.error('Missing bundled package: @happier-dev/protocol');
13-
console.error('Reinstall @happier-dev/cli to repair your installation.');
14-
process.exit(1);
9+
const cliRequire = createRequire(import.meta.url);
10+
let protocolPackageJsonPath;
11+
try {
12+
protocolPackageJsonPath = cliRequire.resolve('@happier-dev/protocol/package.json');
13+
} catch (error) {
14+
if (error && typeof error === 'object' && 'code' in error && error.code === 'MODULE_NOT_FOUND') {
15+
console.error('Missing bundled package: @happier-dev/protocol');
16+
console.error('Reinstall @happier-dev/cli to repair your installation.');
17+
process.exit(1);
18+
}
19+
throw error;
1520
}
16-
const require = createRequire(protocolPackageJsonPath);
17-
const required = ['tweetnacl', 'base64-js', '@noble/hashes/hmac', '@noble/hashes/sha512'];
21+
22+
const protocolRequire = createRequire(protocolPackageJsonPath);
23+
24+
// `tweetnacl` is a direct runtime dependency of the CLI.
25+
// `base64-js` and `@noble/hashes/*` are runtime dependencies of `@happier-dev/protocol` and may be
26+
// vendored under the bundled protocol package's node_modules when installed via `npm`.
27+
const required = [
28+
{ name: 'tweetnacl', resolveWith: cliRequire },
29+
{ name: 'base64-js', resolveWith: protocolRequire },
30+
{ name: '@noble/hashes/hmac', resolveWith: protocolRequire },
31+
{ name: '@noble/hashes/sha512', resolveWith: protocolRequire },
32+
];
1833

1934
for (const dep of required) {
2035
try {
21-
require.resolve(dep);
36+
dep.resolveWith.resolve(dep.name);
2237
} catch (error) {
2338
if (error && typeof error === 'object' && 'code' in error && error.code === 'MODULE_NOT_FOUND') {
24-
console.error(`Missing required dependency: ${dep}`);
39+
console.error(`Missing required dependency: ${dep.name}`);
2540
console.error('Reinstall @happier-dev/cli to repair your installation.');
2641
process.exit(1);
2742
}

apps/cli/scripts/__tests__/happierDependencyPreflight.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,82 @@ import { spawnSync } from 'node:child_process';
88
const __dirname = dirname(fileURLToPath(import.meta.url));
99

1010
describe('happier bin preflight', () => {
11+
it('works when protocol deps are vendored under protocol/node_modules (not hoisted to the CLI root)', () => {
12+
const repoRoot = resolve(__dirname, '..', '..', '..', '..');
13+
const tempRoot = mkdtempSync(join(tmpdir(), 'happier-bin-preflight-'));
14+
15+
mkdirSync(resolve(tempRoot, 'bin'), { recursive: true });
16+
mkdirSync(resolve(tempRoot, 'dist'), { recursive: true });
17+
18+
// Global-install / packed layout:
19+
// - protocol is present as a bundled workspace package
20+
// - protocol runtime deps are vendored under protocol/node_modules
21+
mkdirSync(resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', 'base64-js'), {
22+
recursive: true,
23+
});
24+
mkdirSync(resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', '@noble', 'hashes'), {
25+
recursive: true,
26+
});
27+
28+
// CLI direct dep is expected at the CLI root.
29+
mkdirSync(resolve(tempRoot, 'node_modules', 'tweetnacl'), { recursive: true });
30+
31+
cpSync(resolve(repoRoot, 'apps', 'cli', 'bin', 'happier.mjs'), resolve(tempRoot, 'bin', 'happier.mjs'));
32+
33+
writeFileSync(
34+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'package.json'),
35+
JSON.stringify({ name: '@happier-dev/protocol', version: '0.0.0' }),
36+
'utf8',
37+
);
38+
writeFileSync(
39+
resolve(tempRoot, 'node_modules', 'tweetnacl', 'package.json'),
40+
JSON.stringify({ name: 'tweetnacl', version: '0.0.0', main: 'index.js' }),
41+
'utf8',
42+
);
43+
writeFileSync(resolve(tempRoot, 'node_modules', 'tweetnacl', 'index.js'), 'module.exports = {};\n', 'utf8');
44+
45+
writeFileSync(
46+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', 'base64-js', 'package.json'),
47+
JSON.stringify({ name: 'base64-js', version: '0.0.0', main: 'index.js' }),
48+
'utf8',
49+
);
50+
writeFileSync(
51+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', 'base64-js', 'index.js'),
52+
'module.exports = {};\n',
53+
'utf8',
54+
);
55+
56+
writeFileSync(
57+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', '@noble', 'hashes', 'package.json'),
58+
JSON.stringify({ name: '@noble/hashes', version: '0.0.0', main: 'index.js' }),
59+
'utf8',
60+
);
61+
writeFileSync(
62+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', '@noble', 'hashes', 'hmac.js'),
63+
'export {};\n',
64+
'utf8',
65+
);
66+
writeFileSync(
67+
resolve(tempRoot, 'node_modules', '@happier-dev', 'protocol', 'node_modules', '@noble', 'hashes', 'sha512.js'),
68+
'export {};\n',
69+
'utf8',
70+
);
71+
72+
writeFileSync(resolve(tempRoot, 'dist', 'index.mjs'), "console.log('ok');\n", 'utf8');
73+
74+
const result = spawnSync(process.execPath, [resolve(tempRoot, 'bin', 'happier.mjs')], {
75+
cwd: tempRoot,
76+
encoding: 'utf8',
77+
env: {
78+
...process.env,
79+
NODE_OPTIONS: '',
80+
},
81+
});
82+
83+
expect(result.status).toBe(0);
84+
expect(result.stdout).toContain('ok');
85+
});
86+
1187
it('prints a helpful error when tweetnacl is missing', () => {
1288
const repoRoot = resolve(__dirname, '..', '..', '..', '..');
1389
const tempRoot = mkdtempSync(join(tmpdir(), 'happier-bin-preflight-'));

0 commit comments

Comments
 (0)