Skip to content

Commit 60fbb6a

Browse files
fix: ignore capacitor paths if present (#770)
1 parent 957b3c6 commit 60fbb6a

File tree

5 files changed

+266
-1
lines changed

5 files changed

+266
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ coverage
4444
*.iml
4545
.idea
4646

47+
# Zed
48+
.zed
49+
4750
# OSX
4851
.DS_Store
4952
.AppleDouble

playground/capacitor.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const config: CapacitorConfig = {
44
appId: 'io.ionic.starter',
55
appName: 'nuxt-ionic-playground',
66
webDir: 'dist',
7-
bundledWebRuntime: false,
87
}
98

109
export default config

src/module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useCSSSetup } from './parts/css'
2020
import { setupIcons } from './parts/icons'
2121
import { setupMeta } from './parts/meta'
2222
import { setupRouter } from './parts/router'
23+
import { setupCapacitor } from './parts/capacitor'
2324

2425
export interface ModuleOptions {
2526
integrations?: {
@@ -138,6 +139,17 @@ export default defineNuxtModule<ModuleOptions>({
138139
nuxt.options.typescript.hoist ||= []
139140
nuxt.options.typescript.hoist.push('@ionic/vue')
140141

142+
// add capacitor integration
143+
const { excludeNativeFolders, findCapacitorConfig, parseCapacitorConfig } = setupCapacitor()
144+
145+
// add the `android` and `ios` folders to the TypeScript config exclude list if capacitor is enabled
146+
// this is to prevent TypeScript from trying to resolve the Capacitor native code
147+
const capacitorConfigPath = await findCapacitorConfig()
148+
if (capacitorConfigPath) {
149+
const { androidPath, iosPath } = await parseCapacitorConfig(capacitorConfigPath)
150+
excludeNativeFolders(androidPath, iosPath)
151+
}
152+
141153
// Add auto-imported components
142154
IonicBuiltInComponents.map(name =>
143155
addComponent({

src/parts/capacitor.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { CapacitorConfig } from '@capacitor/cli'
2+
import { findPath, useNuxt } from '@nuxt/kit'
3+
import { join } from 'pathe'
4+
5+
export const setupCapacitor = () => {
6+
const nuxt = useNuxt()
7+
8+
/** Find the path to capacitor configuration file (if it exists) */
9+
const findCapacitorConfig = async () => {
10+
const path = await findPath(
11+
'capacitor.config',
12+
{
13+
extensions: ['ts', 'json'],
14+
virtual: false,
15+
},
16+
'file',
17+
)
18+
19+
return path
20+
}
21+
22+
const parseCapacitorConfig = async (path: string | null): Promise<{
23+
androidPath: string | null
24+
iosPath: string | null
25+
}> => {
26+
if (!path) {
27+
return {
28+
androidPath: null,
29+
iosPath: null,
30+
}
31+
}
32+
33+
const capacitorConfig = (await import(path)) as CapacitorConfig
34+
35+
return {
36+
androidPath: capacitorConfig.android?.path || null,
37+
iosPath: capacitorConfig.ios?.path || null,
38+
}
39+
}
40+
41+
/** Exclude native folder paths from type checking by excluding them in tsconfig */
42+
const excludeNativeFolders = (androidPath: string | null, iosPath: string | null) => {
43+
nuxt.hook('prepare:types', (ctx) => {
44+
const paths = [
45+
join('..', androidPath ?? 'android'),
46+
join('..', iosPath ?? 'ios'),
47+
]
48+
49+
for (const key of ['tsConfig', 'nodeTsConfig', 'sharedTsConfig'] as const) {
50+
if (ctx[key]) {
51+
ctx[key].exclude ||= []
52+
ctx[key].exclude.push(...paths)
53+
}
54+
}
55+
})
56+
57+
nuxt.options.ignore.push(
58+
join(androidPath ?? 'android'),
59+
join(iosPath ?? 'ios'),
60+
)
61+
}
62+
63+
return {
64+
excludeNativeFolders,
65+
findCapacitorConfig,
66+
parseCapacitorConfig,
67+
}
68+
}

test/unit/capacitor.spec.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { useNuxt, findPath } from '@nuxt/kit'
3+
import { setupCapacitor } from '../../src/parts/capacitor'
4+
5+
// Mock @nuxt/kit
6+
vi.mock('@nuxt/kit', () => ({
7+
findPath: vi.fn(),
8+
useNuxt: vi.fn(),
9+
}))
10+
11+
describe('useCapacitor', () => {
12+
const mockNuxt = {
13+
hook: vi.fn(),
14+
options: {
15+
ignore: [],
16+
},
17+
}
18+
19+
beforeEach(() => {
20+
vi.clearAllMocks()
21+
vi.mocked(useNuxt).mockReturnValue(mockNuxt as any)
22+
mockNuxt.options.ignore = []
23+
})
24+
25+
describe('findCapacitorConfig', () => {
26+
it('should find capacitor.config.ts', async () => {
27+
const mockPath = '/project/capacitor.config.ts'
28+
vi.mocked(findPath).mockResolvedValue(mockPath)
29+
30+
const { findCapacitorConfig } = setupCapacitor()
31+
const result = await findCapacitorConfig()
32+
33+
expect(result).toBe(mockPath)
34+
})
35+
36+
it('should return null when no config found', async () => {
37+
vi.mocked(findPath).mockResolvedValue(null)
38+
39+
const { findCapacitorConfig } = setupCapacitor()
40+
const result = await findCapacitorConfig()
41+
42+
expect(result).toBeNull()
43+
})
44+
})
45+
46+
describe('parseCapacitorConfig', () => {
47+
it('should return null paths when no config path provided', async () => {
48+
const { parseCapacitorConfig } = setupCapacitor()
49+
const result = await parseCapacitorConfig(null)
50+
51+
expect(result).toEqual({
52+
androidPath: null,
53+
iosPath: null,
54+
})
55+
})
56+
57+
it('should parse capacitor config with custom paths', async () => {
58+
const configPath = './capacitor.config.ts'
59+
const mockConfig = {
60+
android: { path: 'custom-android' },
61+
ios: { path: 'custom-ios' },
62+
}
63+
64+
vi.doMock(configPath, () => ({
65+
default: mockConfig,
66+
...mockConfig,
67+
}))
68+
69+
const { parseCapacitorConfig } = setupCapacitor()
70+
const result = await parseCapacitorConfig(configPath)
71+
72+
expect(result).toEqual({
73+
androidPath: 'custom-android',
74+
iosPath: 'custom-ios',
75+
})
76+
})
77+
78+
it('should handle config without android/ios paths', async () => {
79+
const configPath = './capacitor.config.ts'
80+
const mockConfig = {
81+
android: undefined,
82+
ios: undefined,
83+
}
84+
85+
vi.doMock(configPath, () => ({
86+
default: mockConfig,
87+
...mockConfig,
88+
}))
89+
90+
const { parseCapacitorConfig } = setupCapacitor()
91+
const result = await parseCapacitorConfig(configPath)
92+
93+
expect(result).toEqual({
94+
androidPath: null,
95+
iosPath: null,
96+
})
97+
})
98+
})
99+
100+
describe('excludeNativeFolders', () => {
101+
it('should register prepare:types hook and add native folders to ignore', () => {
102+
const { excludeNativeFolders } = setupCapacitor()
103+
excludeNativeFolders('android', 'ios')
104+
105+
expect(mockNuxt.hook).toHaveBeenCalledWith('prepare:types', expect.any(Function))
106+
expect(mockNuxt.options.ignore).toContain('android')
107+
expect(mockNuxt.options.ignore).toContain('ios')
108+
})
109+
110+
it('should handle null paths with defaults', () => {
111+
const { excludeNativeFolders } = setupCapacitor()
112+
excludeNativeFolders(null, null)
113+
114+
expect(mockNuxt.hook).toHaveBeenCalledWith('prepare:types', expect.any(Function))
115+
expect(mockNuxt.options.ignore).toContain('android')
116+
expect(mockNuxt.options.ignore).toContain('ios')
117+
})
118+
119+
it('should modify typescript configs in prepare:types hook', () => {
120+
const { excludeNativeFolders } = setupCapacitor()
121+
excludeNativeFolders('custom-android', 'custom-ios')
122+
123+
// Get the hook callback that was registered
124+
const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]
125+
expect(hookCallback).toBeDefined()
126+
127+
// Mock typescript context
128+
const mockCtx = {
129+
tsConfig: { exclude: [] },
130+
nodeTsConfig: { exclude: [] },
131+
sharedTsConfig: { exclude: [] },
132+
}
133+
134+
// Call the hook callback
135+
hookCallback(mockCtx)
136+
137+
// Verify all configs were updated
138+
expect(mockCtx.tsConfig.exclude).toContain('../custom-android')
139+
expect(mockCtx.tsConfig.exclude).toContain('../custom-ios')
140+
expect(mockCtx.nodeTsConfig.exclude).toContain('../custom-android')
141+
expect(mockCtx.nodeTsConfig.exclude).toContain('../custom-ios')
142+
expect(mockCtx.sharedTsConfig.exclude).toContain('../custom-android')
143+
expect(mockCtx.sharedTsConfig.exclude).toContain('../custom-ios')
144+
})
145+
146+
it('should initialize exclude arrays if not present in typescript configs', () => {
147+
const { excludeNativeFolders } = setupCapacitor()
148+
excludeNativeFolders('android', 'ios')
149+
150+
const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]
151+
152+
// Mock context without exclude arrays
153+
const mockCtx = {
154+
tsConfig: {} as any,
155+
nodeTsConfig: {} as any,
156+
sharedTsConfig: {} as any,
157+
}
158+
159+
hookCallback(mockCtx)
160+
161+
expect(mockCtx.tsConfig.exclude).toEqual(['../android', '../ios'])
162+
expect(mockCtx.nodeTsConfig.exclude).toEqual(['../android', '../ios'])
163+
expect(mockCtx.sharedTsConfig.exclude).toEqual(['../android', '../ios'])
164+
})
165+
166+
it('should handle missing typescript configs gracefully', () => {
167+
const { excludeNativeFolders } = setupCapacitor()
168+
excludeNativeFolders('android', 'ios')
169+
170+
const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]
171+
172+
// Mock context with only some configs present
173+
const mockCtx = {
174+
tsConfig: { exclude: [] },
175+
// nodeTsConfig and sharedTsConfig are undefined
176+
}
177+
178+
expect(() => hookCallback(mockCtx)).not.toThrow()
179+
expect(mockCtx.tsConfig.exclude).toContain('../android')
180+
expect(mockCtx.tsConfig.exclude).toContain('../ios')
181+
})
182+
})
183+
})

0 commit comments

Comments
 (0)