nuxt-spec 0.3.3 → 0.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/update.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'node:child_process'
4
- import { getPackageManager, hasJsonKey, promptUser, showMessage } from 'elrh-cosca'
5
- import { getPlaywrightInstallCmd, getUpdateCmd } from './helpers/commands.js'
4
+ import {
5
+ hasJsonKey, promptUser, showMessage,
6
+ } from 'elrh-cosca'
7
+ import {
8
+ getPlaywrightInstallCmd, getPrepareCmd, getUpdateCmd,
9
+ } from './helpers/commands.js'
6
10
 
7
- const TARGET_VERSION = '0.3.3'
11
+ const TARGET_VERSION = '0.4.0-alpha.1'
8
12
 
9
13
  /**
10
14
  * CLI tool to update existing `nuxt-spec` installation.
@@ -14,60 +18,75 @@ const TARGET_VERSION = '0.3.3'
14
18
  * Then it:
15
19
  * 1) verifies `nuxt-spec` is present in `package.json` (fails with a hint to run `setup` otherwise)
16
20
  * 2) runs the package manager's update command to bump `nuxt-spec` to the latest version
17
- * 3) run the `playwright-core install` command to ensure the Playwright browser runtimes are up to date
21
+ * 3) run the `nuxt prepare` command to regenerate types and auto-imports
22
+ * 4) run the `playwright install` command to ensure the Playwright browser runtimes are up to date
18
23
  *
19
24
  * @param {boolean} autoRun - Whether to run the update automatically without any prompts (defaults to false).
25
+ * @param {string} [packageManager] - Package manager to be used (`npm`, `pnpm`, `yarn`, `bun` or `deno`).
20
26
  */
21
- export async function specUpdate(autoRun = false) {
22
- showMessage('NUXT SPEC UPDATE')
23
- showMessage('This CLI tool will help you update Nuxt Spec in your project.')
24
- showMessage('Refer to the documentation for more information.', 2)
27
+ export async function specUpdate(autoRun = false, packageManager) {
28
+ showMessage({ message: 'NUXT SPEC UPDATE' })
29
+ showMessage({ message: 'This CLI tool will help you update Nuxt Spec in your project.' })
30
+ showMessage({ message: 'Refer to the documentation for more information.', linesAfter: 2 })
25
31
 
26
32
  // fail fast if there is nothing to update
27
- const isInstalled = hasJsonKey('package.json', `dependencies.nuxt-spec`)
28
- || hasJsonKey('package.json', `devDependencies.nuxt-spec`)
33
+ const isInstalled = hasJsonKey({ targetFile: 'package.json', jsonKey: `dependencies.nuxt-spec` })
34
+ || hasJsonKey({ targetFile: 'package.json', jsonKey: `devDependencies.nuxt-spec` })
29
35
  if (!isInstalled) {
30
- console.error(`'nuxt-spec' was not found in your 'package.json'. Run the 'setup' command first.`)
36
+ console.error(`[Nuxt Spec] 'nuxt-spec' was not found in your 'package.json'. Run the 'setup' command first.`)
31
37
  process.exit(1)
32
38
  }
33
39
 
34
- const isAutoRun = autoRun || await promptUser('Do you want to update everything automatically (no more prompts)?')
35
- showMessage('')
36
-
37
- const packageManager = getPackageManager()
40
+ const isAutoRun = autoRun || await promptUser({ question: '[Nuxt Spec] Do you want to update everything automatically (no more prompts)?' })
41
+ showMessage({ message: '' })
38
42
 
39
43
  // 1) run 'update nuxt-spec'
40
44
  const updateCmd = getUpdateCmd(packageManager, `nuxt-spec@${TARGET_VERSION}`)
41
- const runUpdate = isAutoRun || await promptUser(`This will bump 'nuxt-spec' to version '${TARGET_VERSION}' by running \`${updateCmd}\`. Continue?`)
45
+ const runUpdate = isAutoRun || await promptUser({ question: `[Nuxt Spec] This will bump 'nuxt-spec' to version '${TARGET_VERSION}' by running \`${updateCmd}\`. Continue?` })
42
46
  if (runUpdate) {
43
47
  try {
44
- showMessage(`Running \`${updateCmd}\`...`)
48
+ showMessage({ message: `Running \`${updateCmd}\`...` })
45
49
  execSync(updateCmd, { stdio: 'inherit' })
46
50
  } catch (error) {
47
- console.error(`Error running \`${updateCmd}\`:\n`, error.message)
51
+ console.error(`[Nuxt Spec] Error running \`${updateCmd}\`:\n`, error.message)
48
52
  }
49
53
  }
50
54
 
51
- // 2) run 'playwright-core install'
55
+ // 2) run 'nuxt prepare'
56
+ const prepareCmd = getPrepareCmd(packageManager)
57
+ const runPrepare = isAutoRun || await promptUser({ question: `[Nuxt Spec] Nuxt types and auto-imports should be regenerated after the update. Do you want to run \`${prepareCmd}\` now?` })
58
+ if (runPrepare) {
59
+ try {
60
+ showMessage({ message: `Running \`${prepareCmd}\`...` })
61
+ execSync(prepareCmd, { stdio: 'inherit' })
62
+ } catch (error) {
63
+ console.error(`[Nuxt Spec] Error running \`${prepareCmd}\`:\n`, error.message)
64
+ }
65
+ }
66
+
67
+ // 3) run 'playwright install'
52
68
  const playwrightUpdateCmd = getPlaywrightInstallCmd(packageManager)
53
- const runPlaywrightUpdate = isAutoRun || await promptUser(`Playwright browser runtimes might need to be updated for e2e tests. Do you want to run \`${playwrightUpdateCmd}\` now?`)
69
+ const runPlaywrightUpdate = isAutoRun || await promptUser({ question: `[Nuxt Spec] Playwright browser runtimes might need to be updated for e2e tests. Do you want to run \`${playwrightUpdateCmd}\` now?` })
54
70
  if (runPlaywrightUpdate) {
55
71
  try {
56
- showMessage(`Running \`${playwrightUpdateCmd}\`...`)
72
+ showMessage({ message: `Running \`${playwrightUpdateCmd}\`...` })
57
73
  execSync(playwrightUpdateCmd, { stdio: 'inherit' })
58
74
  } catch (error) {
59
- console.error(`Error running \`${playwrightUpdateCmd}\`:\n`, error.message)
75
+ console.error(`[Nuxt Spec] Error running \`${playwrightUpdateCmd}\`:\n`, error.message)
60
76
  }
61
77
  }
62
78
 
63
- // 3) inform user
64
- showMessage('')
65
- showMessage('NUXT SPEC UPDATE COMPLETE', 2)
79
+ // 4) inform user
80
+ showMessage({ message: '' })
81
+ showMessage({ message: 'NUXT SPEC UPDATE COMPLETE', linesAfter: 2 })
66
82
  if (!runUpdate) {
67
- showMessage(`Run \`${updateCmd}\` to update 'nuxt-spec'.`)
83
+ showMessage({ message: `Run \`${updateCmd}\` to update 'nuxt-spec'.` })
84
+ }
85
+ if (!runPrepare) {
86
+ showMessage({ message: `Run \`${prepareCmd}\` to regenerate the Nuxt types and auto-imports.` })
68
87
  }
69
88
  if (!runPlaywrightUpdate) {
70
- showMessage(`Run \`${playwrightUpdateCmd}\` to update the Playwright browser runtimes for e2e tests.`)
89
+ showMessage({ message: `Run \`${playwrightUpdateCmd}\` to update the Playwright browser runtimes for e2e tests.` })
71
90
  }
72
91
 
73
92
  // force exit to prevent #20
package/config/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { UserConfig } from 'vite'
2
2
  import type { TestUserConfig } from 'vitest/config'
3
3
 
4
+ export type { HtmlReportOpenMode, NuxtSpecOptions } from './utils/options.mjs'
5
+
4
6
  /** Typed Vite configuration object for tests */
5
7
  type ExtendedUserConfig = UserConfig & {
6
8
  test?: TestUserConfig
@@ -15,7 +17,7 @@ type ExtendedUserConfig = UserConfig & {
15
17
  */
16
18
  type ProjectsConfig = boolean | {
17
19
  default?: boolean
18
- node?: boolean
20
+ unit?: boolean
19
21
  nuxt?: boolean
20
22
  e2e?: boolean
21
23
  browser?: boolean
@@ -23,6 +25,7 @@ type ProjectsConfig = boolean | {
23
25
 
24
26
  /**
25
27
  * Prepare Vitest configuration object - user config merged with nuxt-spec defaults
28
+ * (defaults are adjusted by `spec` key from nuxt.config.ts and NUXT_SPEC_* env variables)
26
29
  * @param userVitestConfig - custom Vitest config passed from the user
27
30
  * @param projects - allows fine-grained control over default `test.projects` - see `ProjectsConfig` for details
28
31
  * @returns Promise resolving to defu-merged Vitest configuration
package/config/index.mjs CHANGED
@@ -4,21 +4,25 @@
4
4
 
5
5
  import { availableParallelism } from 'node:os'
6
6
  import { fileURLToPath } from 'node:url'
7
- import { onConsoleLog } from './utils/warnings.mjs' // filter out unnecessary logs
7
+ import { addMessageFilters, onConsoleLog } from './utils/warnings.mjs' // filter out unnecessary logs
8
8
  import { mergeConfig } from './utils/merge.mjs' // defu-based merge function
9
+ import { loadSpecOptions } from './utils/options.mjs' // `spec` key from nuxt.config.ts + NUXT_SPEC_* env variables
10
+ import { nuxtSpecReportPlugin } from './utils/reporter.mjs' // HTML test report
9
11
  import { defineConfig } from 'vitest/config'
10
12
  import { defineVitestProject } from '@nuxt/test-utils/config'
11
13
  import { playwright } from '@vitest/browser-playwright'
12
14
  import vue from '@vitejs/plugin-vue'
13
15
 
14
16
  // absolute paths so it works from within the nuxt-ignis package
15
- const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot.ts', import.meta.url))
16
17
  const externalPlaywrightSetup = fileURLToPath(new URL('../utils/playwright.ts', import.meta.url))
17
18
 
18
- // external Playwright instance (if configured)
19
- const externalPlaywright = process.env.NUXT_SPEC_EXTERNAL_PLAYWRIGHT
20
-
21
19
  export async function loadVitestConfig(userVitestConfig, projects = true) {
20
+ const { externalPlaywright, htmlReport, messageFilters } = await loadSpecOptions()
21
+
22
+ // extend array of messages that will be filtered out from the output
23
+ // with values passed by the user (if any)
24
+ addMessageFilters(messageFilters)
25
+
22
26
  const baseConfig = {
23
27
  test: {
24
28
  // filter-out unnecessary console logs coming from Vitest
@@ -31,9 +35,15 @@ export async function loadVitestConfig(userVitestConfig, projects = true) {
31
35
  },
32
36
  }
33
37
 
38
+ // HTML test report for all projects
39
+ // report is generated unless explicitly disabled
40
+ if (htmlReport.enabled) {
41
+ baseConfig.plugins = [nuxtSpecReportPlugin({ open: htmlReport.open })]
42
+ }
43
+
34
44
  // add proposed projects settings
35
45
  // user can control the inclusion via `projects: ProjectsConfig` config object
36
- // projects are included by default unless not explicitly disabled
46
+ // projects are included by default unless explicitly disabled
37
47
  if (projects !== false) {
38
48
  baseConfig.test.projects = []
39
49
 
@@ -50,11 +60,11 @@ export async function loadVitestConfig(userVitestConfig, projects = true) {
50
60
  }
51
61
 
52
62
  // proposed setup for Unit tests
53
- if (projects.node !== false) {
63
+ if (projects.unit !== false) {
54
64
  baseConfig.test.projects.push({
55
65
  extends: true,
56
66
  test: {
57
- name: 'node',
67
+ name: 'unit',
58
68
  include: ['test/unit/**/*.{test,spec}.ts'],
59
69
  environment: 'node',
60
70
  },
@@ -83,22 +93,20 @@ export async function loadVitestConfig(userVitestConfig, projects = true) {
83
93
  name: 'e2e',
84
94
  include: ['test/e2e/**/*.{test,spec}.ts'],
85
95
  environment: 'node',
86
- // create report file for visual regression testing
87
- globalSetup: [screenshotReportSetup],
88
- // allows connecting to an external Playwright instance,
89
- // if NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
96
+ // allows connecting to an external Playwright instance (if configured)
90
97
  setupFiles: externalPlaywright ? [externalPlaywrightSetup] : [],
98
+ // setup file will just read the value via `inject()`
99
+ provide: externalPlaywright ? { nuxtSpecExternalPlaywright: externalPlaywright } : {},
91
100
  },
92
101
  })
93
102
  }
94
103
 
95
104
  // proposed setup for browser component tests (with Playwright runner)
96
105
  if (projects.browser !== false) {
97
- // allows connecting to an external Playwright instance,
98
- // if NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
106
+ // allows connecting to an external Playwright instance (if configured)
99
107
  const playwrightConfig = {}
100
108
  if (externalPlaywright) {
101
- console.log(`[Nuxt Spec - browser] Using external Playwright instance at: ${externalPlaywright}`)
109
+ console.log(`[Nuxt Spec] Using external Playwright instance for browser tests at: ${externalPlaywright}`)
102
110
  playwrightConfig.connectOptions = {
103
111
  wsEndpoint: externalPlaywright,
104
112
  // this allows reaching caller's localhost from within the external Playwright
@@ -0,0 +1,6 @@
1
+ <div class="failure">
2
+ <h2>{{TITLE}}</h2>
3
+ <p class="location">{{LOCATION}}</p>
4
+ {{ERRORS}}
5
+ {{SCREENSHOTS}}
6
+ </div>
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Nuxt Spec Test Report - {{TIMESTAMP}}</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; margin: 2rem; background: #f5f5f5; color: #222; }
8
+ h1 { font-size: 1.4rem; }
9
+ .summary { font-weight: 700; font-size: 18px; }
10
+ .summary.passed { color: #0b0; }
11
+ .summary.failed { color: #b00; }
12
+ .failure { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; box-shadow: 0 1px 3px rgba(0, 0, 0, .1); }
13
+ .failure h2 { font-size: 1rem; margin: 0 0 .25rem; }
14
+ .failure .location { color: #666; font-size: .8rem; margin: 0 0 .75rem; }
15
+ .failure pre { white-space: pre-wrap; word-break: break-word; font-size: .8rem; margin: 0 0 .75rem; padding: .5rem; background: #fafafa; border: 1px solid #eee; border-radius: 4px; }
16
+ .failure pre.message { color: #b00; }
17
+ .failure details summary { cursor: pointer; font-size: .8rem; margin-bottom: .25rem; }
18
+ .screenshot { margin-top: .75rem; }
19
+ .screenshot h3 { font-size: .9rem; margin: 0 0 .25rem; }
20
+ .screenshot .message { color: #b00; font-size: .85rem; margin: 0 0 .75rem; }
21
+ .pair { display: flex; gap: 1rem; flex-wrap: wrap; }
22
+ .pair figure { margin: 0; flex: 1 1 0; min-width: 240px; }
23
+ .pair figcaption { font-size: .8rem; margin-bottom: .25rem; font-weight: bold; }
24
+ .pair figure:first-child figcaption { color: #0b0; }
25
+ .pair figure:last-child figcaption { color: #b00; }
26
+ .pair img { max-width: 100%; background: #fff; }
27
+ .pair figure:first-child img { border: 2px solid #0b0; }
28
+ .pair figure:last-child img { border: 2px solid #b00; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <h1>Nuxt Spec Test Report - {{TIMESTAMP}}</h1>
33
+ <p class="summary {{STATUS}}">{{SUMMARY}}</p>
34
+ <!-- BEGIN failure entries -->
@@ -1,5 +1,5 @@
1
- <div class="failure">
2
- <h2>{{FILE_NAME}}</h2>
1
+ <div class="screenshot">
2
+ <h3>{{FILE_NAME}}</h3>
3
3
  <p class="message">{{MESSAGE}}</p>
4
4
  <div class="pair">
5
5
  <figure><figcaption>Baseline image</figcaption><img src="{{BASELINE_URI}}" alt="Baseline image for reference"></figure>
@@ -0,0 +1,4 @@
1
+ <!-- END failure entries -->
2
+ <div>Generated by <a href="https://github.com/AloisSeckar/nuxt-spec">Nuxt Spec</a> @ {{TIMESTAMP}}</div>
3
+ </body>
4
+ </html>
@@ -1,4 +1,19 @@
1
1
  # settings for Nuxt & pnpm
2
2
 
3
- # https://pnpm.io/settings#shamefullyhoist
3
+ # https://pnpm.io/settings/node-modules#shamefullyhoist
4
+ # required to handle transitive dependencies
5
+ # without having to explicitly declare them
4
6
  shamefullyHoist: true
7
+
8
+ # https://pnpm.io/settings/build#allowbuilds
9
+ # pnpm naturally blocks all post-install scripts
10
+ # some are required for development, others are not relevant
11
+ allowBuilds:
12
+ # provides fallback repair of platform-specific Rust native engine
13
+ 'unrs-resolver': true
14
+ # allows node-gyp build of associated native binaries from local sources
15
+ # disabled because it would require an explicit opt-in
16
+ '@parcel/watcher': false
17
+ # optimization for CLI usage of 'esbuild'
18
+ # disabled because its runtime resolves native binaries itself
19
+ 'esbuild': false
@@ -0,0 +1,59 @@
1
+ export type HtmlReportOpenMode = 'always' | 'failed' | 'never'
2
+
3
+ /**
4
+ * Nuxt Spec options set via `spec` key in nuxt.config.ts.
5
+ * Each option can also be set via respective NUXT_SPEC_* env variable,
6
+ * which takes precedence over the value from nuxt.config.ts.
7
+ */
8
+ export interface NuxtSpecOptions {
9
+ /**
10
+ * Include `@nuxt/hints` module.
11
+ * Env: `NUXT_SPEC_HINTS_ENABLED`
12
+ * @default true
13
+ */
14
+ hints?: boolean
15
+ /**
16
+ * WebSocket endpoint of an external Playwright server used by `e2e` and `browser` projects.
17
+ * Env: `NUXT_SPEC_EXTERNAL_PLAYWRIGHT`
18
+ */
19
+ externalPlaywright?: string
20
+ /** HTML test report settings */
21
+ htmlReport?: {
22
+ /**
23
+ * Generate HTML report after each test run.
24
+ * Env: `NUXT_SPEC_HTML_REPORT`
25
+ * @default true
26
+ */
27
+ enabled?: boolean
28
+ /**
29
+ * When to open the report in the default browser.
30
+ * Env: `NUXT_SPEC_HTML_REPORT_OPEN`
31
+ * @default 'failed'
32
+ */
33
+ open?: HtmlReportOpenMode
34
+ }
35
+ /**
36
+ * Plain text patterns of log messages that should be omitted.
37
+ * Env: `NUXT_SPEC_MESSAGE_FILTERS` (comma-separated, added to values from nuxt.config.ts)
38
+ */
39
+ messageFilters?: string[]
40
+ }
41
+
42
+ export interface ResolvedNuxtSpecOptions {
43
+ hints: boolean
44
+ externalPlaywright: string | undefined
45
+ htmlReport: {
46
+ enabled: boolean
47
+ open: HtmlReportOpenMode
48
+ }
49
+ messageFilters: string[]
50
+ }
51
+
52
+ export declare const NUXT_SPEC_DEFAULTS: ResolvedNuxtSpecOptions
53
+
54
+ export declare function resolveSpecOptions(
55
+ fromConfig?: NuxtSpecOptions | false | null,
56
+ env?: Record<string, string | undefined>,
57
+ ): ResolvedNuxtSpecOptions
58
+
59
+ export declare function loadSpecOptions(cwd?: string): Promise<ResolvedNuxtSpecOptions>
@@ -0,0 +1,116 @@
1
+ // Nuxt Spec options resolution
2
+ // - values can be set via `spec` key in nuxt.config.ts or via NUXT_SPEC_* env variables
3
+ // - precedence: env variables > nuxt.config.ts > defaults
4
+
5
+ import { loadNuxtConfig } from 'nuxt/kit'
6
+
7
+ export const NUXT_SPEC_DEFAULTS = {
8
+ hints: true,
9
+ externalPlaywright: undefined,
10
+ htmlReport: {
11
+ enabled: true,
12
+ open: 'failed',
13
+ },
14
+ messageFilters: [],
15
+ }
16
+
17
+ const HTML_REPORT_OPEN_MODES = ['always', 'failed', 'never']
18
+
19
+ // raw `spec` values per cwd - nuxt.config.ts is only loaded once
20
+ const specCache = new Map()
21
+
22
+ // options are resolved by both the Nuxt module and the Vitest config in the same process
23
+ const reportedWarnings = new Set()
24
+
25
+ /**
26
+ * Resolve final Nuxt Spec options.
27
+ * @param {import('./options.d.mts').NuxtSpecOptions | false | null} [fromConfig] - `spec` key from nuxt.config.ts
28
+ * @param {Record<string, string | undefined>} [env] - env variables
29
+ * @returns {import('./options.d.mts').ResolvedNuxtSpecOptions}
30
+ */
31
+ export function resolveSpecOptions(fromConfig, env = process.env) {
32
+ const config = isObject(fromConfig) ? fromConfig : {}
33
+ const htmlReport = checkType('htmlReport', config.htmlReport, 'object') ?? {}
34
+ return {
35
+ hints: toBoolean(env.NUXT_SPEC_HINTS_ENABLED)
36
+ ?? checkType('hints', config.hints, 'boolean')
37
+ ?? NUXT_SPEC_DEFAULTS.hints,
38
+ externalPlaywright: env.NUXT_SPEC_EXTERNAL_PLAYWRIGHT
39
+ || checkType('externalPlaywright', config.externalPlaywright, 'string')
40
+ || NUXT_SPEC_DEFAULTS.externalPlaywright,
41
+ htmlReport: {
42
+ enabled: toBoolean(env.NUXT_SPEC_HTML_REPORT)
43
+ ?? checkType('htmlReport.enabled', htmlReport.enabled, 'boolean')
44
+ ?? NUXT_SPEC_DEFAULTS.htmlReport.enabled,
45
+ open: env.NUXT_SPEC_HTML_REPORT_OPEN
46
+ ? toOpenMode('NUXT_SPEC_HTML_REPORT_OPEN', env.NUXT_SPEC_HTML_REPORT_OPEN)
47
+ : toOpenMode('htmlReport.open', htmlReport.open),
48
+ },
49
+ messageFilters: [
50
+ ...(checkType('messageFilters', config.messageFilters, 'array') ?? []),
51
+ ...(env.NUXT_SPEC_MESSAGE_FILTERS?.split(',') ?? []),
52
+ ]
53
+ .filter((f) => {
54
+ if (typeof f === 'string') return true
55
+ warnInvalid('messageFilters', f, 'string', 'Value is ignored.')
56
+ return false
57
+ })
58
+ .map(f => f.trim())
59
+ // empty pattern would match (and swallow) every message
60
+ .filter(Boolean),
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Load `spec` key from nuxt.config.ts (including extended layers)
66
+ * and resolve final Nuxt Spec options.
67
+ * @param {string} [cwd] - directory with nuxt.config.ts
68
+ * @returns {Promise<import('./options.d.mts').ResolvedNuxtSpecOptions>}
69
+ */
70
+ export async function loadSpecOptions(cwd = process.cwd()) {
71
+ if (!specCache.has(cwd)) {
72
+ specCache.set(cwd, loadNuxtConfig({ cwd })
73
+ .then(config => config.spec)
74
+ .catch((error) => {
75
+ console.warn(`[Nuxt Spec] Failed to read \`spec\` options from nuxt.config: ${error?.message ?? error}`)
76
+ return undefined
77
+ }))
78
+ }
79
+ return resolveSpecOptions(await specCache.get(cwd))
80
+ }
81
+
82
+ function isObject(value) {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
84
+ }
85
+
86
+ // returns the value if it matches the expected type, otherwise warns and returns undefined
87
+ function checkType(option, value, type) {
88
+ if (value === undefined) return undefined
89
+ const matches = type === 'object'
90
+ ? isObject(value)
91
+ : type === 'array' ? Array.isArray(value) : typeof value === type
92
+ if (matches) return value
93
+ warnInvalid(option, value, type)
94
+ return undefined
95
+ }
96
+
97
+ function toOpenMode(option, value) {
98
+ if (value === undefined) return NUXT_SPEC_DEFAULTS.htmlReport.open
99
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : value
100
+ if (HTML_REPORT_OPEN_MODES.includes(normalized)) return normalized
101
+ warnInvalid(option, value, HTML_REPORT_OPEN_MODES.map(m => `'${m}'`).join(' | '))
102
+ return NUXT_SPEC_DEFAULTS.htmlReport.open
103
+ }
104
+
105
+ function warnInvalid(option, value, expected, outcome = 'Falling back to default.') {
106
+ const message = `[Nuxt Spec] Invalid value ${JSON.stringify(value) ?? String(value)} for \`${option}\` (expected ${expected}). ${outcome}`
107
+ if (reportedWarnings.has(message)) return
108
+ reportedWarnings.add(message)
109
+ console.warn(message)
110
+ }
111
+
112
+ // unset or empty = not defined, only explicit 'false' disables
113
+ function toBoolean(value) {
114
+ if (value === undefined || value === '') return undefined
115
+ return value !== 'false'
116
+ }