fix: support standalone binary CLI compilation
This commit is contained in:
parent
6ed98602ef
commit
026eb28cf4
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,16 @@
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
## [0.10.1] - 2026-05-04
|
||||
|
||||
### CLI
|
||||
|
||||
- Fix Bun-compiled standalone binaries so `generate-cli --compile` can compile generated CLIs from empty directories by staging the matching published `mcporter` package dependencies when no local package tree is available.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add an opt-in standalone Bun release-binary smoke for the empty-directory generated CLI compile path.
|
||||
|
||||
## [0.10.0] - 2026-05-04
|
||||
|
||||
### CLI
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcporter",
|
||||
"version": "0.10.0",
|
||||
"version": "0.10.1",
|
||||
"description": "TypeScript runtime and CLI for connecting to configured Model Context Protocol servers.",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@ -5,6 +5,7 @@ import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { RolldownPlugin } from 'rolldown';
|
||||
import { MCPORTER_VERSION } from '../../runtime.js';
|
||||
import { markExecutable, safeCopyFile } from './fs-helpers.js';
|
||||
import { verifyBunAvailable } from './runtime.js';
|
||||
|
||||
@ -110,7 +111,7 @@ async function bundleWithBun({
|
||||
args.push('--minify');
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile(bunBin, args, { cwd: packageRoot, env: process.env }, (error) => {
|
||||
execFile(bunBin, args, { cwd: stagingDir, env: process.env }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
@ -255,6 +256,62 @@ async function ensureBundlerDeps(stagingDir: string): Promise<void> {
|
||||
await linkOrCopyDependency(sourceDir, target);
|
||||
})
|
||||
);
|
||||
const missing = await findMissingBundlerDeps(stagingDir);
|
||||
if (missing.length > 0) {
|
||||
await installPublishedBundlerDeps(stagingDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function findMissingBundlerDeps(stagingDir: string): Promise<string[]> {
|
||||
const missing: string[] = [];
|
||||
for (const specifier of BUNDLED_DEPENDENCIES) {
|
||||
const pkgPath = path.join(stagingDir, 'node_modules', specifier, 'package.json');
|
||||
try {
|
||||
await fs.access(pkgPath);
|
||||
} catch {
|
||||
missing.push(specifier);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
async function installPublishedBundlerDeps(stagingDir: string): Promise<void> {
|
||||
const installSpec = process.env.MCPORTER_BUNDLER_DEP_PACKAGE ?? MCPORTER_VERSION;
|
||||
if (installSpec === '0.0.0-dev') {
|
||||
throw new Error(
|
||||
'Unable to resolve generated-CLI bundler dependencies from this standalone mcporter binary. Install mcporter via npm/Homebrew or publish the matching mcporter package before using --compile.'
|
||||
);
|
||||
}
|
||||
await fs.writeFile(
|
||||
path.join(stagingDir, 'package.json'),
|
||||
JSON.stringify({ private: true, type: 'module', dependencies: { mcporter: installSpec } }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile(
|
||||
'npm',
|
||||
['install', '--ignore-scripts', '--no-audit', '--no-fund', '--min-release-age=0'],
|
||||
{ cwd: stagingDir, env: process.env },
|
||||
(error) => {
|
||||
if (error) {
|
||||
reject(
|
||||
new Error(
|
||||
`Unable to install ${formatMcporterInstallSpec(installSpec)} dependencies needed for Bun compilation from this standalone binary. Install mcporter via npm/Homebrew, or ensure npm can reach the registry.\n\n${error.message}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function formatMcporterInstallSpec(installSpec: string): string {
|
||||
if (installSpec === MCPORTER_VERSION) {
|
||||
return `mcporter@${MCPORTER_VERSION}`;
|
||||
}
|
||||
return `mcporter from ${installSpec}`;
|
||||
}
|
||||
|
||||
function resolveDependencyDirectory(specifier: (typeof BUNDLED_DEPENDENCIES)[number]): string | undefined {
|
||||
|
||||
@ -863,4 +863,77 @@ await new Promise((resolve) => { transport.onclose = resolve; });
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
await fs.rm(callerCwd, { recursive: true, force: true }).catch(() => {});
|
||||
}, 30_000);
|
||||
|
||||
it('compiles a generated CLI from the standalone Bun release binary in an empty directory', async () => {
|
||||
if (process.env.MCPORTER_STANDALONE_BINARY_TEST !== '1') {
|
||||
console.warn('set MCPORTER_STANDALONE_BINARY_TEST=1 to run standalone Bun release binary smoke');
|
||||
return;
|
||||
}
|
||||
if (!(await ensureBunSupport('standalone Bun release binary smoke'))) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile(PNPM_COMMAND, pnpmArgs(['build:bun']), { cwd: process.cwd(), env: process.env }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-standalone-bun-'));
|
||||
const binaryPath = path.join(tempDir, 'context7-cli');
|
||||
const mcporterBinary = path.join(process.cwd(), 'dist-bun', 'mcporter');
|
||||
const packedTarball = await new Promise<string>((resolve, reject) => {
|
||||
execFile(
|
||||
'npm',
|
||||
['pack', '--ignore-scripts', '--pack-destination', tempDir],
|
||||
{ cwd: process.cwd(), env: process.env },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
resolve(path.join(tempDir, stdout.trim().split('\n').at(-1) ?? ''));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile(
|
||||
mcporterBinary,
|
||||
['generate-cli', '--command', baseUrl.toString(), '--compile', binaryPath],
|
||||
{
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
MCPORTER_BUNDLER_DEP_PACKAGE: packedTarball,
|
||||
MCPORTER_NO_FORCE_EXIT: '1',
|
||||
},
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const helpOutput = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
execFile(binaryPath, [], { env: process.env }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
expect(helpOutput.stdout).toMatch(/Usage: .+ <command> \[options]/);
|
||||
expect(helpOutput.stdout).toContain('ping - Simple health check');
|
||||
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
}, 90_000);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user