From 43ec3a969514e9e9c72451a9acfafcc1caf50f78 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Sat, 13 Jun 2026 11:00:35 -0500 Subject: [PATCH 1/2] eslint-plugin-boxel: treat URL-form and RRI-form base imports as equivalent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `missing-card-api-import` matched a configured target module against existing imports by exact string equality, so a missing import configured against the RRI prefix form (`@cardstack/base/card-api`) would not merge into an existing import written in the equivalent virtual-alias URL form (`https://cardstack.com/base/card-api`) — the fixer emitted a second, duplicate import line instead. Add a small `canonicalizeModuleSpecifier` helper (backed by a `REALM_PREFIX_ALIASES` table) and use it when locating the existing import, so the two forms are recognized as the same module and the new specifier merges into the existing line. This stands alone: it makes the rule tolerant of either form so it keeps working regardless of which form source files use. Co-Authored-By: Claude Opus 4.7 --- .../lib/utils/import-utils.js | 33 ++++++++++++- .../lib/rules/missing-card-api-import-test.js | 49 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-boxel/lib/utils/import-utils.js b/packages/eslint-plugin-boxel/lib/utils/import-utils.js index 2e21ce6f858..b1a4bdc78b1 100644 --- a/packages/eslint-plugin-boxel/lib/utils/import-utils.js +++ b/packages/eslint-plugin-boxel/lib/utils/import-utils.js @@ -1,5 +1,30 @@ /** @type {import('eslint').Rule.RuleModule} */ +// URL-form alias for each registered RRI prefix. Lets the rule treat +// `https://cardstack.com/base/X` and `@cardstack/base/X` as the same +// module so a missing import configured in one form merges into an +// existing import that uses the other form. Add new realms here as the +// runtime registers their aliases. +const REALM_PREFIX_ALIASES = { + '@cardstack/base/': 'https://cardstack.com/base/', +}; + +function canonicalizeModuleSpecifier(specifier) { + if (typeof specifier !== 'string') { + return specifier; + } + for (const [rriPrefix, urlPrefix] of Object.entries(REALM_PREFIX_ALIASES)) { + if (specifier.startsWith(rriPrefix)) { + return urlPrefix + specifier.slice(rriPrefix.length); + } + } + return specifier; +} + +function modulesAreEquivalent(a, b) { + return canonicalizeModuleSpecifier(a) === canonicalizeModuleSpecifier(b); +} + /** * Adds an import statement for a missing import, or augments an existing import statement * @param {import('eslint').Rule.RuleFixer} fixer The fixer instance @@ -16,11 +41,13 @@ function fixMissingImport( exportedName, module, ) { - // Check if an import from this module already exists + // Check if an import from this module already exists. + // URL-form and RRI-form imports of the same registered realm module + // are treated as equivalent — see `REALM_PREFIX_ALIASES`. const importDeclarations = sourceCode.ast.body.filter( (node) => node.type === 'ImportDeclaration' && - node.source.value === module && + modulesAreEquivalent(node.source.value, module) && // Skip type-only imports node.importKind !== 'type', ); @@ -174,4 +201,6 @@ module.exports = { fixMissingImport, isBound, buildImportStatement, + modulesAreEquivalent, + canonicalizeModuleSpecifier, }; diff --git a/packages/eslint-plugin-boxel/tests/lib/rules/missing-card-api-import-test.js b/packages/eslint-plugin-boxel/tests/lib/rules/missing-card-api-import-test.js index acd010353a3..f4f07e78ca2 100644 --- a/packages/eslint-plugin-boxel/tests/lib/rules/missing-card-api-import-test.js +++ b/packages/eslint-plugin-boxel/tests/lib/rules/missing-card-api-import-test.js @@ -94,6 +94,55 @@ ruleTester.run('missing-card-api-import', rule, { }, ], }, + { + // The configured target module is the RRI prefix form, but the + // file imports the equivalent virtual-alias URL form. The fix + // must merge into the existing import rather than emitting a + // second, duplicate import line — the two specifiers refer to the + // same module (see REALM_PREFIX_ALIASES in import-utils). + code: `import { + contains, + field, + linksTo, + } from 'https://cardstack.com/base/card-api'; + import StringField from 'https://cardstack.com/base/string'; + + import { Chain } from './chain'; + + export class Payment extends FieldDef { + @field chain = linksTo(Chain); + @field address = contains(StringField); + } + `, + output: `import { + contains, + field, + linksTo, FieldDef, + } from 'https://cardstack.com/base/card-api'; + import StringField from 'https://cardstack.com/base/string'; + + import { Chain } from './chain'; + + export class Payment extends FieldDef { + @field chain = linksTo(Chain); + @field address = contains(StringField); + } + `, + options: [ + { + importMappings: { + FieldDef: ['FieldDef', '@cardstack/base/card-api'], + }, + }, + ], + + errors: [ + { + type: 'Identifier', + message: rule.meta.messages['missing-card-api-import'], + }, + ], + }, { code: `import { field, From 7375debdce3d2960ed6804b164baea44357fb572 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 15 Jun 2026 11:37:12 -0500 Subject: [PATCH 2/2] Note REALM_PREFIX_ALIASES as a transition shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark the URL↔RRI alias map as removable once the virtual-alias URL form is fully retired and all source uses the RRI prefix form, at which point the equivalence helpers collapse to identity. Co-Authored-By: Claude Opus 4.7 --- packages/eslint-plugin-boxel/lib/utils/import-utils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin-boxel/lib/utils/import-utils.js b/packages/eslint-plugin-boxel/lib/utils/import-utils.js index b1a4bdc78b1..e06366e3087 100644 --- a/packages/eslint-plugin-boxel/lib/utils/import-utils.js +++ b/packages/eslint-plugin-boxel/lib/utils/import-utils.js @@ -5,6 +5,12 @@ // module so a missing import configured in one form merges into an // existing import that uses the other form. Add new realms here as the // runtime registers their aliases. +// +// Transition shim: this map only exists because base-realm modules can +// still be imported in either form. Once the virtual-alias URL form +// (`https://cardstack.com/base/...`) is fully retired and all source +// uses the RRI prefix form, this map and the two helpers below collapse +// to identity and can be removed — callers compare specifiers directly. const REALM_PREFIX_ALIASES = { '@cardstack/base/': 'https://cardstack.com/base/', };