fix(codemod): only count real module specifiers in project-type inference (#2765)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot] 2026-09-09 23:50:57 +03:00 • committed by GitHub
commit 5ecc791d81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -180,6 +180,74 @@ describe('analyzeProject', () => {
expect(analyzeProject(dir).projectType).toBe('client');
});
it('ignores an SDK subpath that appears only in a string literal (not a module specifier)', () => {
// A real client import plus a server subpath stored as data in an ordinary string
// literal. Counting quoted paths anywhere would flip this to "both" (rewriting shared
// imports to the server package and adding a server dependency to a client-only
// project); only genuine module specifiers may contribute to inference (#2760).
const dir = v1Project({
'a.ts': [
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
`const example = '@modelcontextprotocol/sdk/server/mcp.js';`,
''
].join('\n')
});
expect(analyzeProject(dir).projectType).toBe('client');
});
it('still infers from dynamic import() and require() specifiers', () => {
const dir = v1Project({
'a.ts': `const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');`,
'b.cjs': `const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');`
});
expect(analyzeProject(dir).projectType).toBe('both');
});
it('still infers from a side-effect import and an export-from re-export', () => {
const dir = v1Project({
'a.ts': `import '@modelcontextprotocol/sdk/client/index.js';`,
'b.ts': `export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`
});
expect(analyzeProject(dir).projectType).toBe('both');
});
it('still infers from vi./jest. mock-method specifiers (the forms the mock-paths transform rewrites)', () => {
const dir = v1Project({
'a.test.ts': `vi.mock('@modelcontextprotocol/sdk/client/index.js');`,
'b.test.ts': `const actual = jest.requireActual('@modelcontextprotocol/sdk/server/mcp.js');`
});
expect(analyzeProject(dir).projectType).toBe('both');
});
it('still infers from a dynamic import() carrying a webpack magic comment', () => {
const dir = v1Project({
'a.ts': `const mod = await import(/* webpackChunkName: "mcp-client" */ '@modelcontextprotocol/sdk/client/index.js');`
});
expect(analyzeProject(dir).projectType).toBe('client');
});
it('still infers from require.resolve()', () => {
const dir = v1Project({
'a.cjs': `const p = require.resolve('@modelcontextprotocol/sdk/server/mcp.js');`
});
expect(analyzeProject(dir).projectType).toBe('server');
});
it('documents a known limitation: a string whose text embeds a full import statement still counts', () => {
// The scan is lexical: the inner `from '` puts the quoted path in a specifier position
// even though it sits inside string data. Distinguishing that from a real import needs
// parsing, which the budget-bounded scan deliberately avoids. If the analyzer ever gets
// smart enough to make this fail, flip the expectation to 'client'.
const dir = v1Project({
'a.ts': [
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
`const help = "import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'";`,
''
].join('\n')
});
expect(analyzeProject(dir).projectType).toBe('both');
});
it('infers from source even without a package.json', () => {
const dir = createTempDir();
mkdirSync(path.join(dir, 'src'), { recursive: true });