diff --git a/.gitignore b/.gitignore index ec1e1531..873126ff 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ coverage *.iml .idea +# Zed +.zed + # OSX .DS_Store .AppleDouble diff --git a/lint-staged.config.cjs b/lint-staged.config.cjs index e74b7787..218dd6e7 100644 --- a/lint-staged.config.cjs +++ b/lint-staged.config.cjs @@ -1,3 +1,3 @@ module.exports = { - '*.{js,ts,mjs,cjs,json}': ['pnpm lint:eslint'], + '*.{js,ts,mjs,cjs,json}': ['pnpm lint'], } diff --git a/playground/capacitor.config.ts b/playground/capacitor.config.ts index a95c5588..669063ae 100644 --- a/playground/capacitor.config.ts +++ b/playground/capacitor.config.ts @@ -4,7 +4,6 @@ const config: CapacitorConfig = { appId: 'io.ionic.starter', appName: 'nuxt-ionic-playground', webDir: 'dist', - bundledWebRuntime: false, } export default config diff --git a/src/module.ts b/src/module.ts index f644efe5..bf4c3def 100644 --- a/src/module.ts +++ b/src/module.ts @@ -20,6 +20,7 @@ import { useCSSSetup } from './parts/css' import { setupIcons } from './parts/icons' import { setupMeta } from './parts/meta' import { setupRouter } from './parts/router' +import { useCapacitor } from './parts/capacitor' export interface ModuleOptions { integrations?: { @@ -138,6 +139,18 @@ export default defineNuxtModule({ nuxt.options.typescript.hoist ||= [] nuxt.options.typescript.hoist.push('@ionic/vue') + // add capacitor integration + const { excludeNativeFolders, findCapacitorConfig, parseCapacitorConfig } + = useCapacitor() + + // add the `android` and `ios` folders to the TypeScript config exclude list if capacitor is enabled + // this is to prevent TypeScript from trying to resolve the Capacitor native code + const capacitorConfigPath = await findCapacitorConfig() + if (capacitorConfigPath) { + const { androidPath, iosPath } = await parseCapacitorConfig(capacitorConfigPath) + excludeNativeFolders(androidPath, iosPath) + } + // Add auto-imported components IonicBuiltInComponents.map(name => addComponent({ diff --git a/src/parts/capacitor.ts b/src/parts/capacitor.ts new file mode 100644 index 00000000..df6b280e --- /dev/null +++ b/src/parts/capacitor.ts @@ -0,0 +1,56 @@ +import type { CapacitorConfig } from '@capacitor/cli' +import { findPath, useNuxt } from '@nuxt/kit' +import { join } from 'pathe' + +export const useCapacitor = () => { + const nuxt = useNuxt() + + /** Find the path to capacitor configuration file (if it exists) */ + const findCapacitorConfig = async () => { + const path = await findPath( + ['capacitor.config.ts', 'capacitor.config.json'], + { + extensions: ['ts', 'json'], + virtual: false, + }, + 'file', + ) + + return path + } + + const parseCapacitorConfig = async (path: string | null): Promise<{ + androidPath: string | null + iosPath: string | null + }> => { + if (!path) { + return { + androidPath: null, + iosPath: null, + } + } + + const capacitorConfig = (await import(path)) as CapacitorConfig + + return { + androidPath: capacitorConfig.android?.path || null, + iosPath: capacitorConfig.ios?.path || null, + } + } + + /** Exclude native folder paths from type checking by excluding them in tsconfig */ + const excludeNativeFolders = (androidPath: string | null, iosPath: string | null) => { + nuxt.options.typescript.tsConfig ||= {} + nuxt.options.typescript.tsConfig.exclude ||= [] + nuxt.options.typescript.tsConfig.exclude.push( + join('../', androidPath ?? '/android'), + join('../', iosPath ?? '/ios'), + ) + } + + return { + excludeNativeFolders, + findCapacitorConfig, + parseCapacitorConfig, + } +} diff --git a/test/unit/capacitor.spec.ts b/test/unit/capacitor.spec.ts new file mode 100644 index 00000000..5d1da42f --- /dev/null +++ b/test/unit/capacitor.spec.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useNuxt, findPath } from '@nuxt/kit' +import { useCapacitor } from '../../src/parts/capacitor' + +// Mock @nuxt/kit +vi.mock('@nuxt/kit', () => ({ + findPath: vi.fn(), + useNuxt: vi.fn(), +})) + +describe('useCapacitor', () => { + const mockNuxt = { + options: { + typescript: { + tsConfig: { + exclude: [], + }, + }, + }, + } + + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(useNuxt).mockReturnValue(mockNuxt as any) + mockNuxt.options.typescript.tsConfig.exclude = [] + }) + + describe('findCapacitorConfig', () => { + it('should find capacitor.config.ts', async () => { + const mockPath = '/project/capacitor.config.ts' + vi.mocked(findPath).mockResolvedValue(mockPath) + + const { findCapacitorConfig } = useCapacitor() + const result = await findCapacitorConfig() + + expect(result).toBe(mockPath) + }) + + it('should return null when no config found', async () => { + vi.mocked(findPath).mockResolvedValue(null) + + const { findCapacitorConfig } = useCapacitor() + const result = await findCapacitorConfig() + + expect(result).toBeNull() + }) + }) + + describe('parseCapacitorConfig', () => { + it('should return null paths when no config path provided', async () => { + const { parseCapacitorConfig } = useCapacitor() + const result = await parseCapacitorConfig(null) + + expect(result).toEqual({ + androidPath: null, + iosPath: null, + }) + }) + + it('should parse capacitor config with custom paths', async () => { + const configPath = './capacitor.config.ts' + const mockConfig = { + android: { path: 'custom-android' }, + ios: { path: 'custom-ios' }, + } + + vi.doMock(configPath, () => ({ + default: mockConfig, + ...mockConfig, + })) + + const { parseCapacitorConfig } = useCapacitor() + const result = await parseCapacitorConfig(configPath) + + expect(result).toEqual({ + androidPath: 'custom-android', + iosPath: 'custom-ios', + }) + }) + + it('should handle config without android/ios paths', async () => { + const configPath = './capacitor.config.ts' + const mockConfig = { + android: undefined, + ios: undefined, + } + + vi.doMock(configPath, () => ({ + default: mockConfig, + ...mockConfig, + })) + + const { parseCapacitorConfig } = useCapacitor() + const result = await parseCapacitorConfig(configPath) + + expect(result).toEqual({ + androidPath: null, + iosPath: null, + }) + }) + }) + + describe('excludeNativeFolders', () => { + it('should add native folders to typescript exclude', () => { + const { excludeNativeFolders } = useCapacitor() + excludeNativeFolders('android', 'ios') + + expect(mockNuxt.options.typescript.tsConfig.exclude).toContain('../android') + expect(mockNuxt.options.typescript.tsConfig.exclude).toContain('../ios') + }) + + it('should handle null paths with defaults', () => { + const { excludeNativeFolders } = useCapacitor() + excludeNativeFolders(null, null) + + expect(mockNuxt.options.typescript.tsConfig.exclude).toContain('../android') + expect(mockNuxt.options.typescript.tsConfig.exclude).toContain('../ios') + }) + + it('should initialize tsConfig if not present', () => { + // @ts-expect-error should not be undefined + mockNuxt.options.typescript.tsConfig = undefined + + const { excludeNativeFolders } = useCapacitor() + excludeNativeFolders('android', 'ios') + + expect(mockNuxt.options.typescript.tsConfig).toBeDefined() + expect(mockNuxt.options.typescript.tsConfig.exclude).toContain('../android') + }) + }) +})