Skip to content

codemod v1-to-v2 hoists rewritten imports above the file's license header #2575

Closed

Description

@igorsaevets

v1-to-v2 moves a rewritten import above the file's leading comment block. On a file whose leading comment is a license or copyright header, the header stops being the first thing in the file, and any lint rule that enforces header position (eslint-plugin-header, eslint-plugin-notice, SPDX scanners) starts failing on a file it previously passed.

The guide's remedy for layout damage is "run your formatter". Prettier does not move the import back, so this survives the documented cleanup step.

Repro

mkdir -p repro/src && cd repro
cat > package.json <<'EOF'
{ "name": "repro", "version": "1.0.0", "type": "module",
  "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0" } }
EOF
cat > src/a.ts <<'EOF'
// Copyright (c) 2026 Example Corp.
// SPDX-License-Identifier: Apache-2.0

import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

export const ok = (): CallToolResult => ({ content: [] });
EOF
npx @modelcontextprotocol/codemod@2.0.0 v1-to-v2 .
cat src/a.ts

Actual:

import type { CallToolResult } from "@modelcontextprotocol/server";

// Copyright (c) 2026 Example Corp.
// SPDX-License-Identifier: Apache-2.0
export const ok = (): CallToolResult => ({ content: [] });

Expected: the import stays where it was, below the header.

// Copyright (c) 2026 Example Corp.
// SPDX-License-Identifier: Apache-2.0

import type { CallToolResult } from "@modelcontextprotocol/server";

export const ok = (): CallToolResult => ({ content: [] });

Two things go wrong in those six lines. The import is hoisted above the header, and the blank line that separated the header from export const ok is consumed, so the header now reads as that declaration's doc comment.

Running npx prettier@latest --write src/a.ts afterwards produces byte-identical output, so step 5 of the TL;DR does not cover this.

Second shape, same cause

When the leading comment is a multi-line // run and the rewritten import is not the first import in the file, the import is inserted inside the comment run rather than above it. From a real migration:

// page_to_markdown tool: fetches a URL and returns clean Markdown for LLM context.
import type { CallToolResult } from "@modelcontextprotocol/server";

// Uses @page2ai/core under the hood — inherits SSRF protection, 10MB size cap,
// 15s AbortController timeout, `.md`-first attempt (Mintlify convention), and
// static tab discovery (emits `### Tab: {label}` sections for docs sites).

import { fetchAndConvert } from '@page2ai/core';

Line 1 of a four-line file comment now sits alone above an unrelated import. Prettier leaves this one alone too.

Both look like the same root cause: the new import node is attached at a position derived from the old node without carrying the leading comments with it.

Environment

  • @modelcontextprotocol/codemod 2.0.0
  • Node 24.14.1, npm 11.13.0, Windows 11
  • Migrating a published stdio server (@modelcontextprotocol/sdk@^1.29.0 to @modelcontextprotocol/server@^2.0.0)

Everything else in the run was correct, including the zod >= 4.2.0 floor warning, which was accurate and saved a debugging session: the ZodError.errors to .issues rename only surfaces at type-check.

Activity

  1. igorsaevets commented on Jul 29, 2026

    @igorsaevets
    Author

    Adding the specific location, since the original report only showed the symptom.

    The insertion happens in packages/codemod/src/utils/importUtils.ts, in addOrMergeImport:

    const clampedIndex = Math.min(insertIndex, sourceFile.getImportDeclarations().length);
    sourceFile.insertImportDeclaration(clampedIndex, { ... });

    insertIndex reaches it from packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts, where it is computed against the pre-removal import list:

    const insertIndex = sourceFile.getImportDeclarations().indexOf(sdkImports[0]!);

    and then used further down, after the original declaration has already been dropped by imp.remove(), at the addOrMergeImport(sourceFile, target, [...valueSpecs.values()], false, insertIndex) call.

    That combination is what produces the reordering. A leading comment block is trivia attached to the first statement, not a statement of its own, so when the old SDK import is the file's first statement the license header belongs to it. Remove that statement and insert a new declaration at index 0, and the new import lands above the header rather than below it.

    Two things follow that might be worth deciding separately:

    1. The index is captured before the mutation and consumed after it, so it describes a list that no longer exists by the time it is used. Even where the header is not involved, that is a fragile contract.
    2. Whether insertIndex === 0 should mean "before everything" or "at the position the old first import occupied, keeping that import's leading trivia in place" is a design call, which is why this is filed as an issue rather than a patch. If the second reading is the intended one, the fix is to carry the removed declaration's leading trivia onto the replacement rather than letting it float.

    Repro, formatter and version details are unchanged from the original report, including that prettier --write leaves the output byte-identical, so the documented remedy does not cover this case.

    Happy to send a PR if you tell me which of the two readings above you want.

  2. axits-lab commented on Jul 30, 2026

    @axits-lab

    Both shapes reproduce on main. Root cause is the same for each: an index-inserted import is positioned against the full start of the declaration it replaces, i.e. ahead of that declaration's leading trivia — so the emitted import lands above the header, or between two lines of a multi-line // run.

    Picking this up — PR incoming.

  3. added
    bugSomething isn't working
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Aug 17, 2026
  4. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingready for workEnough information for someone to start working onv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions