fix(sync): an import of a folder further up links its index file when it appears (#2669)

An import that names a folder only through where it is written (`.`, `..`,
`../..`) is parked under no key, so sync tries each where it is written when
files are added (#2528). That lookup listed `.`, `./`, `..` and `../` only, so
`import { Excalidraw } from "../.."` in excalidraw's tests, or any import of a
folder two or more levels up, stayed unlinked after its folder's `index` file
was added or restored, though a fresh index links it. It now lists the folder
up to twelve levels above, with or without the trailing slash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 17:37:06 +00:00
committed by GitHub
parent 654a853f5a
commit 3695818fa2
4 changed files with 34 additions and 6 deletions
+19 -4
View File
@@ -45,6 +45,20 @@ function isLowValueFile(filePath: string, generated?: ReadonlySet<string>): bool
const SQLITE_PARAM_CHUNK_SIZE = 500;
/**
* How an import names a folder only through where it is written: `.`, or a
* folder up to twelve levels above (`..`, `../..`, with or without the
* trailing `/`).
*/
const FOLDER_SPECIFIERS: readonly string[] = (() => {
const names = ['.', './'];
for (let levels = 1; levels <= 12; levels++) {
const up = Array.from({ length: levels }, () => '..').join('/');
names.push(up, `${up}/`);
}
return names;
})();
/** A name a member reference's receiver can be: not a file's name, nor a `module:` key. */
const RECEIVER_NAME = /^[A-Za-z_$][\w$]*$/;
@@ -4222,16 +4236,17 @@ export class QueryBuilder {
/**
* Failed imports of the folder the importing file is in or under (`.`,
* `..`). Their tail is empty, since they name a folder only through where
* they are written, so no added file's keys find them.
* `..`, `../..`). Their tail is empty, since they name a folder only
* through where they are written, so no added file's keys find them.
*/
getFailedFolderImports(): UnresolvedReference[] {
const placeholders = FOLDER_SPECIFIERS.map(() => '?').join(',');
return (this.db
.prepare(
`SELECT * FROM unresolved_refs
WHERE status = 'failed' AND reference_kind = 'imports' AND reference_name IN ('.', './', '..', '../')`
WHERE status = 'failed' AND reference_kind = 'imports' AND reference_name IN (${placeholders})`
)
.all() as UnresolvedRefRow[])
.all(...FOLDER_SPECIFIERS) as UnresolvedRefRow[])
.map((row) => ({
fromNodeId: row.from_node_id,
referenceName: row.reference_name,
+3 -2
View File
@@ -1064,8 +1064,9 @@ export class CodeGraph {
// to an added file (`@excalidraw/common` to `packages/common/src/
// index.ts`) waits under its last segment, which that file's keys
// never give: look its failed imports up by the specifier itself.
// An import of the importer's own folder or the one above (`.`,
// `..`) waits under no key at all, so each is tried where written.
// An import of the importer's own folder or one above it (`.`,
// `..`, `../..`) waits under no key at all, so each is tried
// where written.
const addedFiles = new Set(result.addedFilePaths ?? []);
const resolvesToAdded = (specifier: string, fromFile: string, language: Language): boolean => {
const file = this.resolver.resolveModuleFile(specifier, fromFile, language);