Skip to content
Open
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
6 changes: 6 additions & 0 deletions playground/nuxt-ionic.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default defineNuxtIonicConfig(() => {
const backButtonText = isPlatform('ios') ? 'Go Back' : undefined
return {
backButtonText,
}
})
62 changes: 11 additions & 51 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import {
defineNuxtModule,
addComponent,
addPlugin,
addTemplate,
addImportsSources,
} from '@nuxt/kit'
import { join, resolve } from 'pathe'
import { readPackageJSON } from 'pkg-types'
import { defineUnimportPreset } from 'unimport'

import type { AnimationBuilder, SpinnerTypes, PlatformConfig } from '@ionic/vue'
import type { IonicConfig } from './runtime/config'
import { runtimeDir } from './utils'
import { IonicBuiltInComponents, IonicHooks } from './imports'

Expand All @@ -21,6 +20,7 @@ import { setupIcons } from './parts/icons'
import { setupMeta } from './parts/meta'
import { setupRouter } from './parts/router'
import { setupCapacitor } from './parts/capacitor'
import { setupVueConfigTemplate } from './parts/config'

export interface ModuleOptions {
integrations?: {
Expand All @@ -33,52 +33,11 @@ export interface ModuleOptions {
basic?: boolean
utilities?: boolean
}
config?: {
actionSheetEnter?: AnimationBuilder
actionSheetLeave?: AnimationBuilder
alertEnter?: AnimationBuilder
alertLeave?: AnimationBuilder
animated?: boolean
backButtonDefaultHref?: string
backButtonIcon?: string
backButtonText?: string
innerHTMLTemplatesEnabled?: boolean
hardwareBackButton?: boolean
infiniteLoadingSpinner?: SpinnerTypes
loadingEnter?: AnimationBuilder
loadingLeave?: AnimationBuilder
loadingSpinner?: SpinnerTypes
menuIcon?: string
menuType?: string
modalEnter?: AnimationBuilder
modalLeave?: AnimationBuilder
mode?: 'ios' | 'md'
navAnimation?: AnimationBuilder
pickerEnter?: AnimationBuilder
pickerLeave?: AnimationBuilder
platform?: PlatformConfig
popoverEnter?: AnimationBuilder
popoverLeave?: AnimationBuilder
refreshingIcon?: string
refreshingSpinner?: SpinnerTypes
sanitizerEnabled?: boolean
spinner?: SpinnerTypes
statusTap?: boolean
swipeBackEnabled?: boolean
tabButtonLayout?:
| 'icon-top'
| 'icon-start'
| 'icon-end'
| 'icon-bottom'
| 'icon-hide'
| 'label-hide'
toastDuration?: number
toastEnter?: AnimationBuilder
toastLeave?: AnimationBuilder
toggleOnOffLabels?: boolean
}
config?: IonicConfig
}

export type { IonicConfig } from './runtime/config'

export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@nuxtjs/ionic',
Expand All @@ -104,11 +63,8 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push(/@ionic/, /@stencil/)

// Inject options for the Ionic Vue plugin as a virtual module
addTemplate({
filename: 'ionic/vue-config.mjs',
getContents: () => `export default ${JSON.stringify(options.config)}`,
})
// Inject Ionic Vue plugin config
await setupVueConfigTemplate(options.config)

// Create an Ionic config file if it doesn't exist yet
const ionicConfigPath = join(nuxt.options.rootDir, 'ionic.config.json')
Expand Down Expand Up @@ -170,6 +126,10 @@ export default defineNuxtModule<ModuleOptions>({
imports: ['useHead'],
priority: 2,
}),
defineUnimportPreset({
from: resolve(runtimeDir, 'config'),
imports: ['defineNuxtIonicConfig'],
}),
])

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
51 changes: 51 additions & 0 deletions src/parts/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { addTemplate, findPath, useNuxt } from '@nuxt/kit'
import { resolve } from 'pathe'
import type { IonicConfig } from '../runtime/config'

export const findIonicConfigFile = async (rootDir: string) => {
return await findPath(
resolve(rootDir, 'nuxt-ionic.config'),
{
extensions: ['ts', 'js', 'mjs'],
virtual: false,
},
'file',
)
}

export const hasInlineConfig = (config: IonicConfig | undefined) => {
return !!config && Object.keys(config).length > 0
}

export const setupVueConfigTemplate = async (config?: IonicConfig) => {
const nuxt = useNuxt()

// Resolve an external `nuxt-ionic.config.{ts,js,mjs}` file
const ionicConfigFile = await findIonicConfigFile(nuxt.options.rootDir)
if (!ionicConfigFile) {
addTemplate({
filename: 'ionic/vue-config.mjs',
getContents: () => `export default ${JSON.stringify(config ?? {})}`,
})
return
}

nuxt.options.watch ||= []
nuxt.options.watch.push(ionicConfigFile)

if (hasInlineConfig(config)) {
console.warn(
`[@nuxtjs/ionic] A \`nuxt-ionic.config\` file was found, so the inline \`ionic.config\` \`config\` option is ignored. Move it into ${ionicConfigFile}.`,
)
}

const contents = [
`import config from ${JSON.stringify(ionicConfigFile)}`,
'export default typeof config === "function" ? config() : config',
].join('\n')

addTemplate({
filename: 'ionic/vue-config.mjs',
getContents: () => contents,
})
}
11 changes: 11 additions & 0 deletions src/runtime/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IonicConfig } from '@ionic/core'

export type { IonicConfig }

export type NuxtIonicConfigInput = IonicConfig | (() => IonicConfig)

export function defineNuxtIonicConfig(
config: NuxtIonicConfigInput,
): NuxtIonicConfigInput {
return config
}
144 changes: 144 additions & 0 deletions test/unit/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { addTemplate, findPath, useNuxt } from '@nuxt/kit'
import {
findIonicConfigFile,
setupVueConfigTemplate,
} from '../../src/parts/config'
import { defineNuxtIonicConfig } from '../../src/runtime/config'

// Mock @nuxt/kit
vi.mock('@nuxt/kit', () => ({
findPath: vi.fn(),
addTemplate: vi.fn(),
useNuxt: vi.fn(),
}))

describe('config:findIonicConfigFile', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('looks up `nuxt-ionic.config` in the root dir', async () => {
const mockPath = '/project/nuxt-ionic.config.ts'
vi.mocked(findPath).mockResolvedValue(mockPath)

const result = await findIonicConfigFile('/project')

expect(result).toBe(mockPath)
})

it('returns null when no config file is found', async () => {
vi.mocked(findPath).mockResolvedValue(null)

expect(await findIonicConfigFile('/project')).toBeNull()
})
})

describe('config:setupVueConfigTemplate', () => {
const mockNuxt = {
options: {
rootDir: '/project',
watch: [] as string[],
},
}

// Read back the contents the module registered for the virtual config module
const addedTemplateContents = () => {
const call = vi.mocked(addTemplate).mock.calls.at(-1)?.[0]
expect(call).toMatchObject({ filename: 'ionic/vue-config.mjs' })
return (call as { getContents: () => string }).getContents()
}

beforeEach(() => {
vi.clearAllMocks()
mockNuxt.options.watch = []
vi.mocked(useNuxt).mockReturnValue(mockNuxt as never)
})

describe('without an external config file', () => {
beforeEach(() => {
vi.mocked(findPath).mockResolvedValue(null)
})

it('serialises the inline config', async () => {
await setupVueConfigTemplate({ mode: 'ios' })

expect(addedTemplateContents()).toBe('export default {"mode":"ios"}')
})

it('falls back to an empty object for an undefined inline config', async () => {
await setupVueConfigTemplate(undefined)

expect(addedTemplateContents()).toBe('export default {}')
})

it('does not register a watcher', async () => {
await setupVueConfigTemplate({ mode: 'ios' })

expect(mockNuxt.options.watch).toEqual([])
})
})

describe('with an external config file', () => {
const mockPath = '/project/nuxt-ionic.config.ts'

beforeEach(() => {
vi.mocked(findPath).mockResolvedValue(mockPath)
})

it('re-exports the file and resolves its factory form', async () => {
await setupVueConfigTemplate(undefined)

const contents = addedTemplateContents()
expect(contents).toContain(`import config from "${mockPath}"`)
expect(contents).toContain(
'export default typeof config === "function" ? config() : config',
)
})

it('ignores the inline config', async () => {
await setupVueConfigTemplate({ mode: 'ios' })

const contents = addedTemplateContents()
expect(contents).not.toContain('mode')
expect(contents).not.toContain('ios')
})

it('registers a watcher on the config file', async () => {
await setupVueConfigTemplate(undefined)

expect(mockNuxt.options.watch).toContain(mockPath)
})

it('warns when an inline config is also set', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})

await setupVueConfigTemplate({ mode: 'ios' })

expect(warn).toHaveBeenCalledOnce()
expect(warn.mock.calls[0]?.[0]).toContain(mockPath)
warn.mockRestore()
})

it('does not warn when there is no inline config', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})

await setupVueConfigTemplate({})

expect(warn).not.toHaveBeenCalled()
warn.mockRestore()
})
})
})

describe('config:defineNuxtIonicConfig', () => {
it('returns an object config unchanged', () => {
const config = { backButtonText: 'Go back' }
expect(defineNuxtIonicConfig(config)).toBe(config)
})

it('returns a factory config unchanged', () => {
const factory = () => ({ mode: 'ios' as const })
expect(defineNuxtIonicConfig(factory)).toBe(factory)
})
})