Skip to content

fix: avoid ambiguous migration class rewrites - #11

Merged
Naloam merged 1 commit into
masterfrom
fix/ambiguous-migration-class-rewrites
Apr 2, 2026
Merged

fix: avoid ambiguous migration class rewrites#11
Naloam merged 1 commit into
masterfrom
fix/ambiguous-migration-class-rewrites

Conversation

@Naloam

@Naloam Naloam commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • stop migrate apply from guessing when the same class name comes from multiple imported style modules
  • leave ambiguous class tokens untouched while still rewriting uniquely attributable classes
  • document the new conservative behavior and add React/Vue regression coverage

Testing

  • pnpm test
  • pnpm lint
  • pnpm build

Copilot AI review requested due to automatic review settings April 2, 2026 11:32
@Naloam
Naloam merged commit c3099b8 into master Apr 2, 2026
3 checks passed
@Naloam
Naloam deleted the fix/ambiguous-migration-class-rewrites branch April 2, 2026 11:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes the CSS Modules migration more conservative by avoiding “guesses” when the same class name can come from multiple imported style modules, leaving ambiguous class tokens unchanged while still rewriting uniquely attributable classes. It also documents the new behavior and adds regression tests for React and Vue.

Changes:

  • Track and suppress rewrites for class names that are ambiguous across multiple imported style modules.
  • Add React/Vue regression tests ensuring ambiguous class tokens remain untouched while unique tokens still rewrite.
  • Update documentation to describe the new conservative ambiguity behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Documents that ambiguous class names are left untouched.
packages/cli/README.md Mirrors the same ambiguity limitation in CLI docs.
packages/core/README.md Updates migration limitations to include ambiguity behavior.
packages/core/src/migrate/index.ts Adds ambiguity tracking when building class-to-expression mappings during apply.
packages/core/tests/migrate/suggestions.test.ts Adds React/Vue regression coverage for ambiguous class tokens.
Comments suppressed due to low confidence (1)

packages/core/src/migrate/index.ts:1824

  • applyMigrationSuggestions processes suggestions in the caller-provided order. Because ensureModuleImportAlias() picks the next available styles* name based on the current file content, the suggestion order can change the generated import aliases and therefore the final rewritten output (even when functionally equivalent). To keep migrations deterministic, consider sorting suggestions (e.g., by suggestion.file or suggestedModuleFile) before this loop, or making buildMigrationSuggestions() return a stable ordering.
    for (const suggestion of suggestions) {
      const sourceDir = dirname(sourceFile);
      const oldImportPath = toImportPath(relative(sourceDir, suggestion.file));
      const newImportPath = toImportPath(
        relative(sourceDir, suggestion.suggestedModuleFile),
      );

      content = replaceQuotedPath(content, oldImportPath, newImportPath);


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +342 to +346
expect(rewritten).toContain('import styles from "./base.module.scss";');
expect(rewritten).toContain('import styles2 from "./theme.module.scss";');
expect(rewritten).toContain('className={["card", styles2.accent].join(" ")}');
expect(rewritten).not.toContain("styles.card");
expect(rewritten).not.toContain("styles2.card");

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new React ambiguity test hard-codes the generated import aliases (styles/styles2) and expects styles2.accent. Alias assignment depends on the order suggestions are processed (which can vary with filesystem readdir ordering), so this assertion can be flaky. Consider making the test order-insensitive by extracting the alias used for ./theme.module.scss from the rewritten imports and asserting that .accent uses that alias, without assuming a specific alias name.

Suggested change
expect(rewritten).toContain('import styles from "./base.module.scss";');
expect(rewritten).toContain('import styles2 from "./theme.module.scss";');
expect(rewritten).toContain('className={["card", styles2.accent].join(" ")}');
expect(rewritten).not.toContain("styles.card");
expect(rewritten).not.toContain("styles2.card");
// Extract aliases for the generated CSS module imports to make the test
// independent of alias naming and filesystem ordering.
const importRegex =
/import\s+(\w+)\s+from\s+"(.+?\.module\.scss)";/g;
const aliasesByPath: Record<string, string> = {};
let match: RegExpExecArray | null;
// eslint-disable-next-line no-cond-assign
while ((match = importRegex.exec(rewritten)) !== null) {
const [, alias, path] = match;
aliasesByPath[path] = alias;
}
const baseAlias = aliasesByPath["./base.module.scss"];
const themeAlias = aliasesByPath["./theme.module.scss"];
expect(baseAlias).toBeTruthy();
expect(themeAlias).toBeTruthy();
// Ensure the accent class comes from the theme module alias, and the
// ambiguous "card" class remains a plain string.
expect(rewritten).toContain(
`className={["card", ${themeAlias}.accent].join(" ")}`,
);
// No alias (from either module) should be used for ".card".
for (const alias of Object.values(aliasesByPath)) {
expect(rewritten).not.toContain(`${alias}.card`);
}

Copilot uses AI. Check for mistakes.
Comment on lines 1825 to 1837
if (REACT_SOURCE_EXTENSIONS.has(extension)) {
const ensured = ensureModuleImportAlias(content, newImportPath);
content = ensured.content;
if (ensured.alias) {
for (const className of suggestion.classNames) {
if (!classToExpr.has(className)) {
classToExpr.set(
className,
toStyleAccess(ensured.alias, className),
);
}
registerClassExpression(
classToExpr,
ambiguousReactClasses,
className,
toStyleAccess(ensured.alias, className),
);
}
}

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new ambiguity handling, a style import can be converted from side-effect to a default import (e.g. import styles from "./base.module.scss";) even when none of that module’s classes are ultimately rewritten (because they became ambiguous). That can leave an unused imported identifier and trigger common TS/ESLint no-unused-vars/unused-imports rules. Consider keeping such imports as side-effect-only (import "./base.module.scss";) when a module ends up contributing no unambiguous class mappings for the current source file, or adding a post-pass that reverts/removes unused module aliases.

Copilot uses AI. Check for mistakes.
Naloam added a commit that referenced this pull request Apr 3, 2026
…ites

fix: avoid ambiguous migration class rewrites
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants