Skip to content

Commit 0c0a235

Browse files
committed
improvement: enhance findComponent filename matching
1 parent 5a54d7c commit 0c0a235

File tree

2 files changed

+155
-5
lines changed

2 files changed

+155
-5
lines changed

assets/js/live_vue/utils.test.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ describe("findComponent", () => {
1111
template: "<div>Create Workspace Component</div>",
1212
}
1313

14+
const MockListComponent = {
15+
template: "<div>List Component</div>",
16+
}
17+
18+
const MockAccountListComponent = {
19+
template: "<div>Account List Component</div>",
20+
}
21+
1422
it("should find exact component match for 'workspace'", () => {
1523
const components: ComponentMap = {
1624
"../../lib/live_vue/web/pages/workspace.vue": MockComponent,
@@ -69,7 +77,7 @@ describe("findComponent", () => {
6977
}
7078

7179
const result1 = findComponent(components, "workspace")
72-
const result2 = findComponent(components, "dashboard")
80+
const result2 = findComponent(components, "dashboard/index.vue")
7381

7482
expect(result1).toBe(MockComponent)
7583
expect(result2).toBe(MockCreateWorkspaceComponent)
@@ -85,4 +93,118 @@ describe("findComponent", () => {
8593

8694
expect(result).toBe(MockComponent)
8795
})
96+
97+
it("should find component by path suffix when filename is ambiguous", () => {
98+
const components: ComponentMap = {
99+
"../../lib/live_vue/web/components/workspaces/List.vue": MockListComponent,
100+
"../../lib/live_vue/web/components/accounts/List.vue": MockAccountListComponent,
101+
}
102+
103+
const result1 = findComponent(components, "workspaces/List")
104+
const result2 = findComponent(components, "accounts/List")
105+
106+
expect(result1).toBe(MockListComponent)
107+
expect(result2).toBe(MockAccountListComponent)
108+
})
109+
110+
it("should throw ambiguous error when filename matches multiple components", () => {
111+
const components: ComponentMap = {
112+
"../../lib/live_vue/web/components/workspaces/List.vue": MockListComponent,
113+
"../../lib/live_vue/web/components/accounts/List.vue": MockAccountListComponent,
114+
}
115+
116+
expect(() => findComponent(components, "List")).toThrow("Component 'List' is ambiguous")
117+
})
118+
119+
it("should match by shorter path suffix", () => {
120+
const components: ComponentMap = {
121+
"../../lib/live_vue/web/pages/admin/workspaces/List.vue": MockListComponent,
122+
}
123+
124+
const result = findComponent(components, "workspaces/List")
125+
126+
expect(result).toBe(MockListComponent)
127+
})
128+
129+
it("should handle components with .vue extension in name", () => {
130+
const components: ComponentMap = {
131+
"../../lib/live_vue/web/components/workspaces/List.vue": MockListComponent,
132+
}
133+
134+
const result = findComponent(components, "workspaces/List.vue")
135+
136+
expect(result).toBe(MockListComponent)
137+
})
138+
139+
it("should match longest unambiguous path suffix", () => {
140+
const components: ComponentMap = {
141+
"../../lib/live_vue/web/admin/accounts/settings/Form.vue": MockComponent,
142+
"../../lib/live_vue/web/public/accounts/settings/Form.vue": MockListComponent,
143+
}
144+
145+
const result = findComponent(components, "admin/accounts/settings/Form")
146+
147+
expect(result).toBe(MockComponent)
148+
})
149+
150+
it("should handle deep nested index.vue files", () => {
151+
const components: ComponentMap = {
152+
"../../lib/live_vue/web/pages/admin/settings/profile/index.vue": MockComponent,
153+
}
154+
155+
const result1 = findComponent(components, "profile")
156+
const result2 = findComponent(components, "settings/profile")
157+
const result3 = findComponent(components, "admin/settings/profile")
158+
159+
expect(result1).toBe(MockComponent)
160+
expect(result2).toBe(MockComponent)
161+
expect(result3).toBe(MockComponent)
162+
})
163+
164+
it("should throw ambiguous error for multiple index.vue matches", () => {
165+
const components: ComponentMap = {
166+
"../../lib/live_vue/web/pages/admin/settings/index.vue": MockComponent,
167+
"../../lib/live_vue/web/pages/user/settings/index.vue": MockListComponent,
168+
}
169+
170+
expect(() => findComponent(components, "settings")).toThrow("Component 'settings' is ambiguous")
171+
})
172+
173+
it("should handle mix of index.vue and regular .vue files", () => {
174+
const components: ComponentMap = {
175+
"../../lib/live_vue/web/pages/workspace/index.vue": MockComponent,
176+
"../../lib/live_vue/web/pages/workspace.vue": MockListComponent,
177+
}
178+
179+
expect(() => findComponent(components, "workspace")).toThrow("Component 'workspace' is ambiguous")
180+
})
181+
182+
it("should handle empty component name gracefully", () => {
183+
const components: ComponentMap = {
184+
"../../lib/live_vue/web/pages/workspace.vue": MockComponent,
185+
}
186+
187+
expect(() => findComponent(components, "")).toThrow("Component '' not found!")
188+
})
189+
190+
it("should handle names with multiple slashes", () => {
191+
const components: ComponentMap = {
192+
"../../lib/live_vue/web/pages/admin/users/settings/Form.vue": MockComponent,
193+
}
194+
195+
const result = findComponent(components, "admin/users/settings/Form")
196+
197+
expect(result).toBe(MockComponent)
198+
})
199+
200+
it("should not match partial directory names", () => {
201+
const components: ComponentMap = {
202+
"../../lib/live_vue/web/components/workspace-list/Item.vue": MockComponent,
203+
"../../lib/live_vue/web/components/workspace/Item.vue": MockListComponent,
204+
}
205+
206+
const result = findComponent(components, "workspace/Item")
207+
208+
expect(result).toBe(MockListComponent)
209+
})
88210
})

assets/js/live_vue/utils.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,43 @@ export const flatMapKeys = <T>(
3333
* @returns The component if found, otherwise throws an error with a list of available components.
3434
*/
3535
export const findComponent = (components: ComponentMap, name: string): ComponentOrComponentPromise => {
36-
// we're looking for a component by exact filename match
36+
const nameParts = name.replace(/\.vue$/, '').split('/').filter(part => part !== 'index')
37+
const matches: [string, ComponentOrComponentPromise][] = []
38+
3739
for (const [key, value] of Object.entries(components)) {
38-
const fileName = key.split('/').pop() // Get the actual filename
39-
if (fileName === `${name}.vue` || (fileName === 'index.vue' && key.endsWith(`/${name}/index.vue`))) {
40-
return value
40+
let keyParts = key.split('/')
41+
42+
if (keyParts[keyParts.length - 1] === 'index.vue') {
43+
keyParts = keyParts.slice(0, -1)
44+
} else {
45+
keyParts[keyParts.length - 1] = keyParts[keyParts.length - 1].replace(/\.vue$/, '')
46+
}
47+
48+
if (nameParts.length <= keyParts.length) {
49+
let isMatch = true
50+
for (let i = 0; i < nameParts.length; i++) {
51+
const keyPart = keyParts[keyParts.length - nameParts.length + i]
52+
if (nameParts[i] !== keyPart) {
53+
isMatch = false
54+
break
55+
}
56+
}
57+
58+
if (isMatch) {
59+
matches.push([key, value])
60+
}
4161
}
4262
}
4363

64+
if (matches.length === 1) return matches[0][1]
65+
66+
if (matches.length > 1) {
67+
const matchList = matches.map(([key]) => key).join('\n')
68+
throw new Error(`Component '${name}' is ambiguous. Found multiple matches:\n\n${matchList}\n\n`)
69+
}
70+
4471
// a helpful message for the user
72+
4573
const availableComponents = Object.keys(components)
4674
.map(key => key.replace("../../lib/", "").replace("/index.vue", "").replace(".vue", "").replace("./", ""))
4775
.filter(key => !key.startsWith("_build"))

0 commit comments

Comments
 (0)