Skip to content

support absolute path aliases#7433

Merged
ardatan merged 5 commits intoardatan:masterfrom
HunterLarco:hunter/absolute-aliases
Aug 28, 2025
Merged

support absolute path aliases#7433
ardatan merged 5 commits intoardatan:masterfrom
HunterLarco:hunter/absolute-aliases

Conversation

@HunterLarco
Copy link
Contributor

@HunterLarco HunterLarco commented Aug 27, 2025

Description

This change fixes a minor bug from #7310 that affects users of path aliases - a feature that lets GraphQL users leverage tsconfig.json#paths aliasing syntax for GraphQL imports.

The issue stems from the original implementation's execution order. Path alias resolution happens inside resolveFilePath, which only runs for non-absolute paths:

  if (!isAbsolute(filePath) && !(filePath in predefinedImports)) {
    filePath = resolveFilePath(cwd, filePath, pathAliases);
  }

This means path aliases like @/*src/* work correctly, but absolute path aliases like /*src/* fail since they never reach the resolution logic.

The solution is straightforward: move path alias resolution before the absolute path check instead of after it.

Related #7311

Risk

Low impact change that only affects path alias users (a recently released feature with limited adoption). Among those users, only absolute path aliases are broken - and workarounds exist. The fix maintains full backward compatibility and doesn't impact users without path aliases.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can
reproduce. Please also list any relevant details for your test configuration

  • Added unit tests for absolute path aliases
  • Added unit tests for node.js-style subpath imports (this was not broken, but I didn't think absolute imports were broken either and given that this is a node.js builtin feature we should test for compatibility with it).
  • Added unit tests to ensure the status quo remains consistent that predefined imports take precedence over path aliases

To run the tests yourself: npm test

Test Environment:

  • OS: OSX
  • NodeJS: v22.14.0

Checklist:

  • I have followed the
    CONTRIBUTING doc and the
    style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests and linter rules pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@changeset-bot
Copy link

changeset-bot bot commented Aug 27, 2025

🦋 Changeset detected

Latest commit: a70e1f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@graphql-tools/import Patch
@graphql-tools/graphql-file-loader Patch
@graphql-tools/node-require Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 27, 2025

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Fixed path alias resolution in GraphQL imports when using tsconfig-style paths. Aliases are now applied before absolute path checks, enabling absolute and node-style subpath aliases to resolve correctly. No public API changes.
  • Tests

    • Added tests covering absolute aliases, node-style subpaths, and precedence of predefined imports over aliases.
  • Chores

    • Added a patch changeset documenting the fix.

Walkthrough

Applies path-alias rewriting early in import resolution by adding an internal applyPathAliases helper, removing pathAliases from resolveFilePath, updating visitFile flow to run alias resolution before absolute checks, and adding fixtures/tests for absolute and node-style subpath aliases. Includes a changeset documenting the fix.

Changes

Cohort / File(s) Summary
Path alias resolution refactor
packages/import/src/index.ts
Introduces applyPathAliases(filePath, pathAliases) and invokes it at the start of visitFile; removes pathAliases param from resolveFilePath; moves alias rewriting before absolute/import checks; preserves applyPathAlias behavior.
Tests: absolute path alias fixtures
packages/import/tests/schema/fixtures/path-aliases/absolute/a.graphql, packages/import/tests/schema/fixtures/path-aliases/absolute/b.graphql
Adds fixtures where a.graphql imports /b.graphql; defines TypeA, Query; b.graphql defines TypeB.
Tests: node-style subpath fixtures
packages/import/tests/schema/fixtures/path-aliases/node-subpath/a.graphql, packages/import/tests/schema/fixtures/path-aliases/node-subpath/b.graphql
Adds fixtures using a node-style alias prefix (e.g., #b.graphql) and corresponding types TypeA, TypeB.
Tests: import behavior
packages/import/tests/schema/import-schema.spec.ts
Adds tests validating absolute and node-style subpath alias resolution and precedence of predefined imports; duplicated test blocks appear in diff.
Changeset
.changeset/solid-loops-feel.md
Adds a patch changeset describing the bug fix for path alias resolution ordering and references issue #7310.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller as Importer
  participant Import as visitFile
  participant Alias as applyPathAliases
  participant Resolver as resolveFilePath
  participant FS as File System

  Caller->>Import: importSchema(entryPath, { pathAliases })
  activate Import
  Import->>Alias: applyPathAliases(entryPath, pathAliases)
  activate Alias
  Alias-->>Import: aliasedEntryPath
  deactivate Alias

  loop per import statement
    Import->>Resolver: resolveFilePath(aliasedEntryPath, importFrom)
    activate Resolver
    Resolver->>FS: join/check/resolve paths
    FS-->>Resolver: resolvedPath or not found
    Resolver-->>Import: resolvedPath
    deactivate Resolver

    Import->>FS: read resolvedPath
    FS-->>Import: SDL content
    Import-->>Import: merge definitions
  end
  Import-->>Caller: DocumentNode
  deactivate Import
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ardatan
  • enisdenjo

Poem

I hopped through paths, both sharp and wide,
From # burrows to /* I glide.
Aliases first, no steps missed now,
Carrots (schemas) found — I take a bow. 🥕🐇


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 35e694b and a70e1f8.

📒 Files selected for processing (1)
  • packages/import/tests/schema/import-schema.spec.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/import/tests/schema/import-schema.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Unit Test on Node 18 (windows-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v15
  • GitHub Check: Unit Test on Node 22 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 24 (ubuntu-latest) and GraphQL v16
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@HunterLarco HunterLarco marked this pull request as ready for review August 27, 2025 21:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
.changeset/solid-loops-feel.md (1)

1-18: Clarify behavior and precedence in the changeset.
Consider explicitly documenting:

  • Alias resolution now occurs before absolute/path checks.
  • Precedence between predefinedImports and pathAliases (ideally, predefinedImports should win to preserve virtual imports).

Proposed wording tweak:

 The solution is straightforward: move path alias resolution before the absolute path check instead of after it.
+Precedence note: virtual imports provided via `predefinedImports` should take priority over alias rewriting to avoid surprising lookups when a virtual key also matches an alias pattern.
packages/import/src/index.ts (1)

683-709: Match TS “paths” selection order and minor robustness in alias resolution.
Today, the first mapping that matches wins. TypeScript prefers:

  • Exact patterns over wildcard patterns.
  • Among wildcards, longer prefixes first.
    Also, handle absolute mapped targets without resolveFrom.

Apply this diff:

-function applyPathAliases(filePath: string, pathAliases?: PathAliases): string {
+function applyPathAliases(filePath: string, pathAliases?: PathAliases): string {
   if (pathAliases == null) {
     return filePath;
   }
 
-  for (const [prefixPattern, mapping] of Object.entries(pathAliases.mappings)) {
+  const entries = Object.entries(pathAliases.mappings).sort((a, b) => {
+    const [ak, bk] = [a[0], b[0]];
+    const aWild = ak.includes('*') ? 1 : 0;
+    const bWild = bk.includes('*') ? 1 : 0;
+    if (aWild !== bWild) return aWild - bWild; // exact before wildcard
+    const aLen = ak.includes('*') ? ak.indexOf('*') : ak.length;
+    const bLen = bk.includes('*') ? bk.indexOf('*') : bk.length;
+    return bLen - aLen; // longer prefix first
+  });
+
+  for (const [prefixPattern, mapping] of entries) {
     const matchedMapping = applyPathAlias(prefixPattern, mapping, filePath);
     if (matchedMapping == null) {
       continue;
     }
 
-    const resolvedMapping = resolveFrom(pathAliases.rootDir ?? process.cwd(), matchedMapping);
-    return realpathSync(resolvedMapping);
+    const baseDir = pathAliases.rootDir ?? process.cwd();
+    const target = isAbsolute(matchedMapping)
+      ? matchedMapping
+      : resolveFrom(baseDir, matchedMapping);
+    return realpathSync(target);
   }
 
   return filePath;
 }
packages/import/tests/schema/import-schema.spec.ts (2)

1294-1307: DRY up repeated expected SDL across the two new tests

The expected SDL is identical in both tests. Consider a small constant to reduce duplication and ease future edits.

Add this helper near the top of the describe block (or right above these tests):

const expectedAWithB = /* GraphQL */ `
  type Query {
    getA: TypeA
  }

  type TypeA {
    id: ID!
    relatedB: TypeB!
  }

  type TypeB {
    id: ID!
  }
`;

Then update both assertions:

-    expect(document).toBeSimilarGqlDoc(/* GraphQL */ `
-      type Query {
-        getA: TypeA
-      }
-
-      type TypeA {
-        id: ID!
-        relatedB: TypeB!
-      }
-
-      type TypeB {
-        id: ID!
-      }
-    `);
+    expect(document).toBeSimilarGqlDoc(expectedAWithB);

Also applies to: 1321-1334


1310-1311: Rename for clarity

Consider tightening the test name to: "should handle node-style subpath import aliases".

-it('should handle node-style subpath imports path aliases', () => {
+it('should handle node-style subpath import aliases', () => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2bda066 and d98e9cc.

📒 Files selected for processing (7)
  • .changeset/solid-loops-feel.md (1 hunks)
  • packages/import/src/index.ts (3 hunks)
  • packages/import/tests/schema/fixtures/path-aliases/absolute/a.graphql (1 hunks)
  • packages/import/tests/schema/fixtures/path-aliases/absolute/b.graphql (1 hunks)
  • packages/import/tests/schema/fixtures/path-aliases/node-subpath/a.graphql (1 hunks)
  • packages/import/tests/schema/fixtures/path-aliases/node-subpath/b.graphql (1 hunks)
  • packages/import/tests/schema/import-schema.spec.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v15
  • GitHub Check: Unit Test on Node 18 (windows-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v16
🔇 Additional comments (6)
packages/import/tests/schema/fixtures/path-aliases/absolute/b.graphql (1)

1-3: LGTM — minimal, correct fixture.
Defines TypeB as expected for the absolute-alias tests.

packages/import/tests/schema/fixtures/path-aliases/node-subpath/b.graphql (1)

1-3: LGTM — minimal, correct fixture.
Matches the node-style subpath alias scenario.

packages/import/tests/schema/fixtures/path-aliases/node-subpath/a.graphql (1)

1-10: LGTM — covers node-style subpath alias.
Imports via "#b.graphql" are correctly exercised; types are consistent.

packages/import/src/index.ts (1)

670-681: All resolveFilePath calls use the new 2-arg signature

I ran the suggested grep across the packages/import/src/index.ts file:

  • No instances of resolveFilePath with three arguments were found.
  • The only call (line 177) uses exactly two parameters (cwd, filePath).

No stale invocations remain—this change can be considered complete.

packages/import/tests/schema/fixtures/path-aliases/absolute/a.graphql (1)

1-10: LGTM: Absolute alias import in fixture verified

The companion b.graphql fixture is present at packages/import/tests/schema/fixtures/path-aliases/absolute/b.graphql, and a.graphql correctly imports it via

#import TypeB from "/b.graphql"

Approving these changes.

packages/import/tests/schema/import-schema.spec.ts (1)

1283-1292: LGTM: Adds a focused test for absolute path aliases

This directly guards the regression by mapping '/*' and importing "/b.graphql". Nice coverage.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/import/src/index.ts (1)

175-180: Predefined-import precedence preserved; absolute aliases now resolved early — LGTM.

Moving aliasing ahead of the absolute-path check while skipping predefinedImports fixes the bug without breaking virtual imports. Good call.

🧹 Nitpick comments (2)
packages/import/src/index.ts (2)

672-683: Consider extension fallbacks for local schema files.

Optional: before falling back to resolveFrom, try .graphql/.gql variants when the direct joined path isn’t found. This helps common extensionless specifiers in repos that keep GraphQL next to code.

-function resolveFilePath(filePath: string, importFrom: string): string {
+function resolveFilePath(filePath: string, importFrom: string): string {
   const dirName = dirname(filePath);
   try {
-    const fullPath = join(dirName, importFrom);
-    return realpathSync(fullPath);
+    const base = join(dirName, importFrom);
+    const candidates = [base, base + '.graphql', base + '.gql'];
+    for (const p of candidates) {
+      try {
+        return realpathSync(p);
+      } catch (e: any) {
+        if (e?.code !== 'ENOENT') throw e;
+      }
+    }
   } catch (e: any) {
     if (e.code === 'ENOENT') {
       return resolveFrom(dirName, importFrom);
     }
     throw e;
   }
 }

685-711: Match tsconfig paths semantics by preferring more specific alias patterns.

Iterating Object.entries() order may mis-pick when multiple patterns match. Sort to prefer exact matches over wildcards and longer prefixes first.

-function applyPathAliases(filePath: string, pathAliases?: PathAliases): string {
+function applyPathAliases(filePath: string, pathAliases?: PathAliases): string {
   if (pathAliases == null) {
     return filePath;
   }
 
-  for (const [prefixPattern, mapping] of Object.entries(pathAliases.mappings)) {
+  const entries = Object.entries(pathAliases.mappings).sort((a, b) => {
+    const [ap] = a;
+    const [bp] = b;
+    const aIsWildcard = ap.endsWith('*') ? 1 : 0;
+    const bIsWildcard = bp.endsWith('*') ? 1 : 0;
+    if (aIsWildcard !== bIsWildcard) return aIsWildcard - bIsWildcard; // exact before wildcard
+    const aLen = ap.replace('*', '').length;
+    const bLen = bp.replace('*', '').length;
+    return bLen - aLen; // longer (more specific) first
+  });
+
+  for (const [prefixPattern, mapping] of entries) {
     const matchedMapping = applyPathAlias(prefixPattern, mapping, filePath);
     if (matchedMapping == null) {
       continue;
     }
 
-    const resolvedMapping = resolveFrom(pathAliases.rootDir ?? process.cwd(), matchedMapping);
-    return realpathSync(resolvedMapping);
+    const baseDir = pathAliases.rootDir ?? process.cwd();
+    const resolvedMapping = resolveFrom(baseDir, matchedMapping);
+    return realpathSync(resolvedMapping);
   }
 
   return filePath;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d98e9cc and 35e694b.

📒 Files selected for processing (2)
  • .changeset/solid-loops-feel.md (1 hunks)
  • packages/import/src/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/solid-loops-feel.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Unit Test on Node 24 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 22 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (windows-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v15
  • GitHub Check: Full Check on GraphQL v16

@Urigo Urigo requested a review from ardatan August 28, 2025 19:18
@ardatan ardatan merged commit 07124d5 into ardatan:master Aug 28, 2025
15 checks passed
@ardatan
Copy link
Owner

ardatan commented Aug 28, 2025

Thanks 🙏

@HunterLarco HunterLarco deleted the hunter/absolute-aliases branch August 28, 2025 20:48
ardatan pushed a commit that referenced this pull request Sep 22, 2025
* support absolute aliases

* test node-style subpath import syntax

* add changeset

* address comments

* more tests
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