Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion internal/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,41 @@ func referenceChanges(project scanner.Project, item scanner.AssetItem, targetPat
File: ref.File,
Line: ref.Line,
OldSpecifier: ref.Specifier,
NewSpecifier: relativeSpecifier(ref.File, targetPath),
NewSpecifier: rewriteSpecifier(ref.Specifier, ref.File, targetPath),
})
}
return changes, blockers
}

func rewriteSpecifier(oldSpec, importerRepoPath, targetRepoPath string) string {
spec := strings.Split(oldSpec, "?")[0]
query := ""
if i := strings.IndexByte(oldSpec, '?'); i >= 0 {
query = oldSpec[i:]
}
for _, prefix := range []string{"@/", "~/"} {
if strings.HasPrefix(spec, prefix) {
newSpec := prefix + strings.TrimPrefix(targetRepoPath, findSrcBase(importerRepoPath)+"/")
return newSpec + query
}
}
if strings.HasPrefix(spec, "/") {
return "/" + targetRepoPath + query
}
return relativeSpecifier(importerRepoPath, targetRepoPath) + query
}

func findSrcBase(importerRepoPath string) string {
dir := filepath.ToSlash(filepath.Dir(importerRepoPath))
parts := strings.Split(dir, "/")
for i := len(parts) - 1; i >= 0; i-- {
if parts[i] == "src" {
return strings.Join(parts[:i+1], "/")
}
}
return "src"
}

func relativeSpecifier(importerRepoPath, targetRepoPath string) string {
from := filepath.Dir(filepath.FromSlash(importerRepoPath))
rel, err := filepath.Rel(from, filepath.FromSlash(targetRepoPath))
Expand Down
16 changes: 16 additions & 0 deletions internal/actions/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ func TestPathAndSpecifierHelpers(t *testing.T) {
if got := relativeSpecifier("src/components/App.tsx", "src/assets/icon.png"); got != "../assets/icon.png" {
t.Fatalf("relativeSpecifier nested = %q", got)
}
// rewriteSpecifier preserves @/ alias
if got := rewriteSpecifier("@/assets/logo.png", "apps/web/src/views/Home.vue", "apps/web/src/assets/logo.avif"); got != "@/assets/logo.avif" {
t.Fatalf("rewriteSpecifier @/ = %q", got)
}
// rewriteSpecifier preserves ~/ alias
if got := rewriteSpecifier("~/assets/icon.svg", "packages/ui/src/components/Card.tsx", "packages/ui/src/assets/icon.avif"); got != "~/assets/icon.avif" {
t.Fatalf("rewriteSpecifier ~/ = %q", got)
}
// rewriteSpecifier preserves query string
if got := rewriteSpecifier("@/assets/bg.png?raw", "src/App.tsx", "src/assets/bg.avif"); got != "@/assets/bg.avif?raw" {
t.Fatalf("rewriteSpecifier query = %q", got)
}
// rewriteSpecifier falls back to relative for non-alias paths
if got := rewriteSpecifier("./assets/logo.png", "src/components/App.tsx", "src/assets/logo.avif"); got != "../assets/logo.avif" {
t.Fatalf("rewriteSpecifier relative = %q", got)
}
for _, invalid := range []string{"", "../escape.png", "/absolute.png", "."} {
if got := cleanRepoPath(invalid); got != "" {
t.Fatalf("cleanRepoPath(%q) = %q", invalid, got)
Expand Down
26 changes: 24 additions & 2 deletions internal/references/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ func Resolve(projectRoot, importerRepoPath, specifier string) string {
return ""
}
if strings.HasPrefix(spec, "@/") {
return cleanRepoPath(filepath.ToSlash(filepath.Join("src", strings.TrimPrefix(spec, "@/"))))
srcBase := findSrcAncestor(importerRepoPath)
return cleanRepoPath(filepath.ToSlash(filepath.Join(srcBase, strings.TrimPrefix(spec, "@/"))))
}
if strings.HasPrefix(spec, "/") {
assetPath := strings.TrimPrefix(spec, "/")
Expand All @@ -323,6 +324,17 @@ func Resolve(projectRoot, importerRepoPath, specifier string) string {
return cleanRepoPath(spec)
}

func findSrcAncestor(importerRepoPath string) string {
dir := pathpkg.Dir(importerRepoPath)
for dir != "." && dir != "/" {
if pathpkg.Base(dir) == "src" {
return dir
}
dir = pathpkg.Dir(dir)
}
return "src"
}
Comment thread
hiro05097952 marked this conversation as resolved.

func resolvePublicAsset(projectRoot, importerRepoPath, assetPath string) string {
assetPath = cleanRepoPath(assetPath)
if assetPath == "" {
Expand Down Expand Up @@ -366,7 +378,17 @@ func key(projectID, repoPath string) string {
func referenceMayPointTo(repoPath, specifier string) bool {
clean := stripQuery(filepath.ToSlash(specifier))
clean = strings.TrimPrefix(clean, "./")
return clean == repoPath || strings.HasSuffix(clean, "/"+repoPath) || strings.HasSuffix(repoPath, "/"+clean)
clean = strings.TrimPrefix(clean, "/")
if clean == repoPath || strings.HasSuffix(clean, "/"+repoPath) || strings.HasSuffix(repoPath, "/"+clean) {
return true
}
if strings.HasPrefix(clean, "@/") || strings.HasPrefix(clean, "~/") {
stripped := clean[2:]
if stripped == repoPath || strings.HasSuffix(repoPath, "/"+stripped) {
return true
}
}
return false
}

func isPattern(spec string) bool {
Expand Down
54 changes: 54 additions & 0 deletions internal/references/references_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func TestResolveReferenceKinds(t *testing.T) {
{"src/components/App.tsx", "@/assets/logo.png", "src/assets/logo.png"},
{"src/components/App.tsx", "/src/assets/logo.png", "src/assets/logo.png"},
{"src/components/App.tsx", "src/assets/logo.png?raw", "src/assets/logo.png"},
// Monorepo: @/ resolves relative to nearest src/ ancestor
{"apps/mobile/src/views/Page.vue", "@/assets/logo.png", "apps/mobile/src/assets/logo.png"},
{"packages/ui/src/components/Card.tsx", "@/assets/icon.svg", "packages/ui/src/assets/icon.svg"},
// No src/ ancestor falls back to bare src/
{"lib/util.ts", "@/assets/logo.png", "src/assets/logo.png"},
}
for _, tt := range tests {
if got := Resolve(root, tt.importer, tt.spec); got != tt.want {
Expand Down Expand Up @@ -104,6 +109,49 @@ func TestBuildMapResolvesAbsolutePublicReferences(t *testing.T) {
}
}

func TestBuildMapResolvesAbsolutePathInMonorepo(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "apps", "dashboard", "src", "assets", "hero.webp"), "image")
mustWrite(t, filepath.Join(root, "apps", "dashboard", "index.html"),
`<img src="/src/assets/hero.webp" alt="hero" />`)

refs, err := BuildMap(context.Background(),
[]Project{{ID: "p", Path: root}},
[]Asset{{ProjectID: "p", RepoPath: "apps/dashboard/src/assets/hero.webp"}},
)
if err != nil {
t.Fatal(err)
}
got := refs["p\x00apps/dashboard/src/assets/hero.webp"]
if len(got) != 1 || got[0].File != "apps/dashboard/index.html" {
t.Fatalf("absolute path monorepo refs = %#v, want 1 ref from index.html", got)
}
}

func TestBuildMapResolvesMonorepoAtAliasImports(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "apps", "web", "src", "assets", "images", "banner.png"), "image")
mustWrite(t, filepath.Join(root, "apps", "web", "src", "views", "Home.vue"),
`import Banner from '@/assets/images/banner.png'`)
mustWrite(t, filepath.Join(root, "apps", "web", "src", "views", "About.vue"),
`.hero { background-image: url('@/assets/images/banner.png'); }`)

refs, err := BuildMap(context.Background(),
[]Project{{ID: "p", Path: root}},
[]Asset{{ProjectID: "p", RepoPath: "apps/web/src/assets/images/banner.png"}},
)
if err != nil {
t.Fatal(err)
}
got := refs["p\x00apps/web/src/assets/images/banner.png"]
if len(got) != 2 {
t.Fatalf("monorepo @/ refs = %#v, want 2 refs", got)
}
if got[0].File != "apps/web/src/views/About.vue" || got[1].File != "apps/web/src/views/Home.vue" {
t.Fatalf("monorepo @/ refs files = [%s, %s], want About.vue and Home.vue", got[0].File, got[1].File)
}
}

func TestBuildMapWithProgressExcludesMatchedCodeFiles(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "src", "assets", "logo.png"), "image")
Expand Down Expand Up @@ -162,6 +210,12 @@ func TestReferenceHelperFunctions(t *testing.T) {
if !referenceMayPointTo("demo/icons/a.png", "a.png") {
t.Fatal("exact filename should match via path boundary")
}
if !referenceMayPointTo("apps/web/src/assets/images/banner.png", "@/assets/images/banner.png") {
t.Fatal("@/ alias stripped suffix should match monorepo asset path")
}
if !referenceMayPointTo("packages/ui/src/assets/icon.svg", "~/assets/icon.svg") {
t.Fatal("~/ alias stripped suffix should match monorepo asset path")
}
if cleanRepoPath("../escape.png") != "" || cleanRepoPath("./src/a.png") != "src/a.png" {
t.Fatal("cleanRepoPath did not normalize safely")
}
Expand Down
Loading