nuxt-spec 0.4.0-alpha.1 → 0.4.0-alpha.2

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/cli.js CHANGED
@@ -5,12 +5,15 @@ import { getCmd, resolvePackageManager } from './helpers/commands.js'
5
5
  /**
6
6
  * CLI tool to scaffold necessary adjustments in project folder.
7
7
  *
8
- * Allows `setup` or `update` to be passed as parameter.
8
+ * Allows `setup`, `update` or `ci` to be passed as parameter.
9
9
  *
10
- * Second (optional) parameter might be a boolean to indicate auto mode
10
+ * `ci` requires the target Git host (`github` or `gitlab`) as the next parameter,
11
+ * the remaining optional parameters are shifted by one position (package manager is not used yet).
12
+ *
13
+ * Next (optional) parameter might be a boolean to indicate auto mode
11
14
  * (no prompts, force = true) or manual mode (with prompts, force = false).
12
15
  *
13
- * Third (optional) parameter allows explicitly passing the package manager
16
+ * Next (optional) parameter allows explicitly passing the package manager
14
17
  * (`npm`, `pnpm`, `yarn`, `bun` or `deno`), bypassing auto-detection.
15
18
  */
16
19
 
@@ -22,18 +25,24 @@ const args = process.argv.slice(2);
22
25
  (async () => {
23
26
  let status = 0
24
27
  try {
25
- const packageManager = resolvePackageManager(args[2])
28
+ const command = args[0]
29
+ const host = command === 'ci' ? args[1] : undefined
30
+ const [autoRunArg, packageManagerArg] = args.slice(command === 'ci' ? 2 : 1)
31
+ const packageManager = resolvePackageManager(packageManagerArg)
26
32
  // CLI args are always strings, so 'false' must be parsed explicitly rather than treated as truthy
27
- const autoRun = args[1] === 'true'
28
- switch (args[0]) {
33
+ const autoRun = autoRunArg === 'true'
34
+ switch (command) {
29
35
  case 'setup':
30
- await (await import('./setup.js')).specSetup(autoRun, packageManager)
36
+ await (await import('./commands/setup.js')).specSetup(autoRun, packageManager)
31
37
  break
32
38
  case 'update':
33
- await (await import('./update.js')).specUpdate(autoRun, packageManager)
39
+ await (await import('./commands/update.js')).specUpdate(autoRun, packageManager)
40
+ break
41
+ case 'ci':
42
+ await (await import('./commands/ci.js')).specCi(host, autoRun)
34
43
  break
35
44
  default:
36
- console.log(`[Nuxt Spec] Usage: \`${getCmd(packageManager)} setup [true|false] [npm|pnpm|yarn|bun|deno]\` or \`${getCmd(packageManager)} update [true|false] [npm|pnpm|yarn|bun|deno]\``)
45
+ console.log(`[Nuxt Spec] Usage: \`${getCmd(packageManager)} setup [true|false] [npm|pnpm|yarn|bun|deno]\`, \`${getCmd(packageManager)} update [true|false] [npm|pnpm|yarn|bun|deno]\` or \`${getCmd(packageManager)} ci <github|gitlab> [true|false]\``)
37
46
  status = 1
38
47
  }
39
48
  } catch (error) {
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync } from 'node:fs'
4
+ import {
5
+ createFileFromWebTemplate, pathExists, promptUser, showMessage, updateYamlFile,
6
+ } from 'elrh-cosca'
7
+ import { isGitRepo, resolveDefaultBranch } from '../helpers/git.js'
8
+ import { COSCA_URL, PLAYWRIGHT_VERSION } from '../helpers/constants.js'
9
+
10
+ const SUPPORTED_CI_HOSTS = ['github', 'gitlab']
11
+
12
+ const GITLAB_ROOT_FILE = '.gitlab-ci.yml'
13
+
14
+ export const CI_TEMPLATES = {
15
+ github: [
16
+ { template: '.github/workflows/ci-test-verify.yml.template', targetFile: '.github/workflows/ci-test-verify.yml' },
17
+ { template: '.github/workflows/ci-test-update.yml.template', targetFile: '.github/workflows/ci-test-update.yml' },
18
+ ],
19
+ gitlab: [
20
+ { template: '.gitlab/ci/ci-test-verify.yml.template', targetFile: '.gitlab/ci/ci-test-verify.yml' },
21
+ { template: '.gitlab/ci/ci-test-update.yml.template', targetFile: '.gitlab/ci/ci-test-update.yml' },
22
+ ],
23
+ }
24
+
25
+ function getDefaultBranch() {
26
+ if (!isGitRepo()) {
27
+ console.warn('[Nuxt Spec] Current folder is not a Git repository.')
28
+ }
29
+ const { branch, source } = resolveDefaultBranch()
30
+ // value is inserted into YAML unquoted
31
+ if (!/^[\w./+-]+$/.test(branch)) {
32
+ throw new Error(`Default branch name '${branch}' (resolved from ${source}) contains unsupported characters.`)
33
+ }
34
+ showMessage({ message: `[Nuxt Spec] Using '${branch}' as the default branch (resolved from ${source}).` })
35
+ return branch
36
+ }
37
+
38
+ /**
39
+ * CLI tool to scaffold CI test workflows for selected Git host.
40
+ *
41
+ * It first asks whether to run in "auto" mode (no prompts, force = true) or "manual" mode (with prompts, force = false).
42
+ *
43
+ * Then it:
44
+ * 1) creates CI workflow files for the selected host
45
+ * 2) fills in placeholders - Playwright version and default branch of the Git repository in the current folder (resolved only if used)
46
+ * 3) (GitLab only) adds CI files into `include` section of `.gitlab-ci.yml` (scaffolded from template if missing)
47
+ *
48
+ * @param {string} host - Target Git host (`github` or `gitlab`).
49
+ * @param {boolean} autoRun - Whether to run the scaffolding automatically without any prompts (defaults to false).
50
+ */
51
+ export async function specCi(host, autoRun = false) {
52
+ showMessage({ message: 'NUXT SPEC CI' })
53
+ showMessage({ message: `This CLI tool will help you scaffold CI test workflows for '${host}'.` })
54
+ showMessage({ message: 'Refer to the documentation for more information.', linesAfter: 2 })
55
+
56
+ if (!SUPPORTED_CI_HOSTS.includes(host)) {
57
+ throw new Error(`${host ? `Unsupported CI host '${host}'` : 'Missing CI host'}. Use one of: ${SUPPORTED_CI_HOSTS.join(', ')}`)
58
+ }
59
+
60
+ const templates = CI_TEMPLATES[host]
61
+
62
+ const isAutoRun = autoRun || await promptUser({ question: '[Nuxt Spec] Do you want to scaffold everything automatically (no more prompts)?' })
63
+ showMessage({ message: '' })
64
+
65
+ let branch
66
+ const created = []
67
+ for (const { template, targetFile } of templates) {
68
+ // 1) create workflow files
69
+ const exists = pathExists({ targetPath: targetFile })
70
+ const proceed = isAutoRun || await promptUser({
71
+ question: exists
72
+ ? `[Nuxt Spec] File '${targetFile}' already exists. Overwrite?`
73
+ : `[Nuxt Spec] This will create '${targetFile}' file. Continue?`,
74
+ })
75
+ if (!proceed) {
76
+ if (!exists) {
77
+ throw new Error(`Creation of '${targetFile}' declined - CI setup cannot be completed without it.`)
78
+ }
79
+ console.log(`[Nuxt Spec] Existing '${targetFile}' kept.`)
80
+ continue
81
+ }
82
+ await createFileFromWebTemplate({ url: `${COSCA_URL}/config/templates/${template}`, targetFile, force: true })
83
+
84
+ // 2) fill in placeholders
85
+ let content = readFileSync(targetFile, 'utf8').replaceAll('__PLAYWRIGHT_VERSION__', PLAYWRIGHT_VERSION)
86
+ if (content.includes('__DEFAULT_BRANCH__')) {
87
+ branch ??= getDefaultBranch()
88
+ content = content.replaceAll('__DEFAULT_BRANCH__', branch)
89
+ }
90
+ writeFileSync(targetFile, content)
91
+ created.push(targetFile)
92
+ }
93
+
94
+ // 3) (GitLab only) register CI files in root pipeline
95
+ if (host === 'gitlab') {
96
+ if (pathExists({ targetPath: GITLAB_ROOT_FILE })) {
97
+ await updateYamlFile({
98
+ targetFile: GITLAB_ROOT_FILE,
99
+ yamlKey: 'include',
100
+ patch: templates.map(({ targetFile }) => ({ local: targetFile })),
101
+ force: isAutoRun,
102
+ prompt: `[Nuxt Spec] This will add CI files into 'include' section of your '${GITLAB_ROOT_FILE}'. Continue?`,
103
+ })
104
+ } else {
105
+ await createFileFromWebTemplate({
106
+ url: `${COSCA_URL}/config/templates/${GITLAB_ROOT_FILE}.template`,
107
+ targetFile: GITLAB_ROOT_FILE,
108
+ force: isAutoRun,
109
+ prompt: `[Nuxt Spec] This will create '${GITLAB_ROOT_FILE}' file including the CI files. Continue?`,
110
+ })
111
+ }
112
+ }
113
+
114
+ // 4) inform user
115
+ showMessage({ message: '' })
116
+ showMessage({ message: 'NUXT SPEC CI COMPLETE', linesAfter: 2 })
117
+ if (created.length > 0) {
118
+ showMessage({ message: 'Review the created files and complete the prerequisites described in their headers.' })
119
+ }
120
+ if (branch) {
121
+ showMessage({ message: `Verify '${branch}' is the correct default branch.` })
122
+ showMessage({ message: `If not, run \`git remote set-head origin --auto\` and re-run this command, or adjust the files manually.` })
123
+ }
124
+ }
@@ -9,9 +9,8 @@ import {
9
9
  } from 'elrh-cosca'
10
10
  import {
11
11
  getPlaywrightInstallCmd, getPrepareCmd,
12
- } from './helpers/commands.js'
13
-
14
- const TARGET_VERSION = '0.4.0-alpha.1'
12
+ } from '../helpers/commands.js'
13
+ import { COSCA_VERSION, COSCA_URL } from '../helpers/constants.js'
15
14
 
16
15
  /**
17
16
  * CLI tool to scaffold necessary adjustments in project folder.
@@ -51,7 +50,7 @@ export async function specSetup(autoRun = false, packageManager) {
51
50
  targetFile: 'package.json',
52
51
  jsonKey: 'dependencies',
53
52
  patch: {
54
- ['nuxt-spec']: TARGET_VERSION,
53
+ ['nuxt-spec']: COSCA_VERSION,
55
54
  },
56
55
  force: isAutoRun,
57
56
  prompt: `[Nuxt Spec] This will add 'nuxt-spec' dependency to your 'package.json'. Continue?`,
@@ -123,7 +122,7 @@ export async function specSetup(autoRun = false, packageManager) {
123
122
  })
124
123
  } else {
125
124
  await createFileFromWebTemplate({
126
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/config/templates/pnpm-workspace.yaml.template`,
125
+ url: `${COSCA_URL}/config/templates/pnpm-workspace.yaml.template`,
127
126
  targetFile: 'pnpm-workspace.yaml',
128
127
  force: isAutoRun,
129
128
  prompt: '[Nuxt Spec] This will add \'pnpm-workspace.yaml\' file for your project. Continue?',
@@ -137,7 +136,7 @@ export async function specSetup(autoRun = false, packageManager) {
137
136
  // 4) create vitest.config.ts
138
137
  try {
139
138
  await createFileFromWebTemplate({
140
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/config/templates/vitest.config.ts.template`,
139
+ url: `${COSCA_URL}/config/templates/vitest.config.ts.template`,
141
140
  targetFile: 'vitest.config.ts',
142
141
  force: isAutoRun,
143
142
  prompt: '[Nuxt Spec] This will create a new \'vitest.config.ts\' file for your project. Continue?',
@@ -150,7 +149,7 @@ export async function specSetup(autoRun = false, packageManager) {
150
149
  if (!pathExists({ targetPath: '.nuxtrc' })) {
151
150
  try {
152
151
  await createFileFromWebTemplate({
153
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/.nuxtrc`,
152
+ url: `${COSCA_URL}/.nuxtrc`,
154
153
  targetFile: '.nuxtrc',
155
154
  force: isAutoRun,
156
155
  prompt: '[Nuxt Spec] This will create a \'.nuxtrc\' file to prevent @nuxt/test-utils setup from running automatically when dev server starts. Continue?',
@@ -168,6 +167,7 @@ export async function specSetup(autoRun = false, packageManager) {
168
167
  patch: {
169
168
  'test': 'vitest run',
170
169
  'test-u': 'vitest run -u',
170
+ 'test-c': 'vitest run --coverage',
171
171
  'test-i': 'vitest',
172
172
  },
173
173
  force: isAutoRun,
@@ -182,7 +182,7 @@ export async function specSetup(autoRun = false, packageManager) {
182
182
  if (createSampleTests) {
183
183
  try {
184
184
  await createFileFromWebTemplate({
185
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/browser/vitest-browser.test.ts`,
185
+ url: `${COSCA_URL}/test/browser/vitest-browser.test.ts`,
186
186
  targetFile: 'test/browser/vitest-browser.test.ts',
187
187
  force: true,
188
188
  })
@@ -191,7 +191,7 @@ export async function specSetup(autoRun = false, packageManager) {
191
191
  }
192
192
  try {
193
193
  await createFileFromWebTemplate({
194
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/e2e/nuxt-e2e.test.ts`,
194
+ url: `${COSCA_URL}/test/e2e/nuxt-e2e.test.ts`,
195
195
  targetFile: 'test/e2e/nuxt-e2e.test.ts',
196
196
  force: true,
197
197
  })
@@ -200,7 +200,7 @@ export async function specSetup(autoRun = false, packageManager) {
200
200
  }
201
201
  try {
202
202
  await createFileFromWebTemplate({
203
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/e2e/nuxt-visual.test.ts`,
203
+ url: `${COSCA_URL}/test/e2e/nuxt-visual.test.ts`,
204
204
  targetFile: 'test/e2e/nuxt-visual.test.ts',
205
205
  force: true,
206
206
  })
@@ -210,7 +210,7 @@ export async function specSetup(autoRun = false, packageManager) {
210
210
  // pre-existing baseline ensures the sample "wrong" visual test fails as intended
211
211
  try {
212
212
  await createFileFromWebTemplate({
213
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/e2e/__baseline__/wrong.png`,
213
+ url: `${COSCA_URL}/test/e2e/__baseline__/wrong.png`,
214
214
  targetFile: 'test/e2e/__baseline__/wrong.png',
215
215
  force: true,
216
216
  })
@@ -219,7 +219,7 @@ export async function specSetup(autoRun = false, packageManager) {
219
219
  }
220
220
  try {
221
221
  await createFileFromWebTemplate({
222
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/nuxt/nuxt-unit.test.ts`,
222
+ url: `${COSCA_URL}/test/nuxt/nuxt-unit.test.ts`,
223
223
  targetFile: 'test/nuxt/nuxt-unit.test.ts',
224
224
  force: true,
225
225
  })
@@ -228,7 +228,7 @@ export async function specSetup(autoRun = false, packageManager) {
228
228
  }
229
229
  try {
230
230
  await createFileFromWebTemplate({
231
- url: `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/unit/vitest-unit.test.ts`,
231
+ url: `${COSCA_URL}/test/unit/vitest-unit.test.ts`,
232
232
  targetFile: 'test/unit/vitest-unit.test.ts',
233
233
  force: true,
234
234
  })
@@ -241,6 +241,7 @@ export async function specSetup(autoRun = false, packageManager) {
241
241
  try {
242
242
  const gitignoreEntries = [
243
243
  ['# vitest output folder', '.vitest'],
244
+ ['# vitest coverage output folder', 'coverage'],
244
245
  ['# nuxt-spec screenshots folder', '__current__'],
245
246
  ['# nuxt-spec HTML test reports folder', '__reports__'],
246
247
  ]
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'node:child_process'
4
+ import { readFileSync, writeFileSync } from 'node:fs'
4
5
  import {
5
- hasJsonKey, promptUser, showMessage,
6
+ hasJsonKey, pathExists, promptUser, showMessage,
6
7
  } from 'elrh-cosca'
8
+ import { CI_TEMPLATES } from './ci.js'
7
9
  import {
8
10
  getPlaywrightInstallCmd, getPrepareCmd, getUpdateCmd,
9
- } from './helpers/commands.js'
11
+ } from '../helpers/commands.js'
12
+ import { COSCA_VERSION, PLAYWRIGHT_VERSION } from '../helpers/constants.js'
10
13
 
11
- const TARGET_VERSION = '0.4.0-alpha.1'
14
+ // for automated version patching in CI workflow files
15
+ const PLAYWRIGHT_IMAGE_REGEX = /(mcr\.microsoft\.com\/playwright:v)\d+\.\d+\.\d+/g
12
16
 
13
17
  /**
14
18
  * CLI tool to update existing `nuxt-spec` installation.
@@ -20,6 +24,7 @@ const TARGET_VERSION = '0.4.0-alpha.1'
20
24
  * 2) runs the package manager's update command to bump `nuxt-spec` to the latest version
21
25
  * 3) run the `nuxt prepare` command to regenerate types and auto-imports
22
26
  * 4) run the `playwright install` command to ensure the Playwright browser runtimes are up to date
27
+ * 5) bump the Playwright container image version in CI workflow files created by the `ci` command
23
28
  *
24
29
  * @param {boolean} autoRun - Whether to run the update automatically without any prompts (defaults to false).
25
30
  * @param {string} [packageManager] - Package manager to be used (`npm`, `pnpm`, `yarn`, `bun` or `deno`).
@@ -41,8 +46,8 @@ export async function specUpdate(autoRun = false, packageManager) {
41
46
  showMessage({ message: '' })
42
47
 
43
48
  // 1) run 'update nuxt-spec'
44
- const updateCmd = getUpdateCmd(packageManager, `nuxt-spec@${TARGET_VERSION}`)
45
- const runUpdate = isAutoRun || await promptUser({ question: `[Nuxt Spec] This will bump 'nuxt-spec' to version '${TARGET_VERSION}' by running \`${updateCmd}\`. Continue?` })
49
+ const updateCmd = getUpdateCmd(packageManager, `nuxt-spec@${COSCA_VERSION}`)
50
+ const runUpdate = isAutoRun || await promptUser({ question: `[Nuxt Spec] This will bump 'nuxt-spec' to version '${COSCA_VERSION}' by running \`${updateCmd}\`. Continue?` })
46
51
  if (runUpdate) {
47
52
  try {
48
53
  showMessage({ message: `Running \`${updateCmd}\`...` })
@@ -76,7 +81,31 @@ export async function specUpdate(autoRun = false, packageManager) {
76
81
  }
77
82
  }
78
83
 
79
- // 4) inform user
84
+ // 4) bump Playwright container image in CI workflow files
85
+ const outdatedCiFiles = []
86
+ const ciFiles = Object.values(CI_TEMPLATES).flat()
87
+ .map(({ targetFile }) => targetFile)
88
+ .filter(targetFile => pathExists({ targetPath: targetFile }))
89
+ for (const targetFile of ciFiles) {
90
+ try {
91
+ const content = readFileSync(targetFile, 'utf8')
92
+ const bumped = content.replace(PLAYWRIGHT_IMAGE_REGEX, `$1${PLAYWRIGHT_VERSION}`)
93
+ if (bumped === content) {
94
+ continue
95
+ }
96
+ const runBump = isAutoRun || await promptUser({ question: `[Nuxt Spec] This will bump the Playwright container image in '${targetFile}' to version '${PLAYWRIGHT_VERSION}'. Continue?` })
97
+ if (runBump) {
98
+ writeFileSync(targetFile, bumped)
99
+ showMessage({ message: `Playwright container image in '${targetFile}' bumped to version '${PLAYWRIGHT_VERSION}'.` })
100
+ } else {
101
+ outdatedCiFiles.push(targetFile)
102
+ }
103
+ } catch (error) {
104
+ console.error(`[Nuxt Spec] Error bumping Playwright container image in '${targetFile}':\n`, error.message)
105
+ }
106
+ }
107
+
108
+ // 5) inform user
80
109
  showMessage({ message: '' })
81
110
  showMessage({ message: 'NUXT SPEC UPDATE COMPLETE', linesAfter: 2 })
82
111
  if (!runUpdate) {
@@ -88,6 +117,9 @@ export async function specUpdate(autoRun = false, packageManager) {
88
117
  if (!runPlaywrightUpdate) {
89
118
  showMessage({ message: `Run \`${playwrightUpdateCmd}\` to update the Playwright browser runtimes for e2e tests.` })
90
119
  }
120
+ for (const targetFile of outdatedCiFiles) {
121
+ showMessage({ message: `Bump the Playwright container image in '${targetFile}' to version '${PLAYWRIGHT_VERSION}' to match the \`playwright\` dependency.` })
122
+ }
91
123
 
92
124
  // force exit to prevent #20
93
125
  process.exit(0)
@@ -0,0 +1,8 @@
1
+ // current elrh-cosca version for usage in CLI scripts
2
+ export const COSCA_VERSION = '0.4.0-alpha.2'
3
+
4
+ // URL to current tag in GitHub repository to fetch template files in CLI scripts
5
+ export const COSCA_URL = `https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${COSCA_VERSION}`
6
+
7
+ // current Playwright version for usage in CLI scripts
8
+ export const PLAYWRIGHT_VERSION = '1.64.0'
@@ -0,0 +1,73 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ export const FALLBACK_DEFAULT_BRANCH = 'main'
4
+
5
+ // run a read-only git command, returning trimmed stdout or `undefined` on failure
6
+ function git(...args) {
7
+ try {
8
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined
9
+ } catch {
10
+ return undefined
11
+ }
12
+ }
13
+
14
+ function hasRef(ref) {
15
+ try {
16
+ execFileSync('git', ['show-ref', '--verify', '--quiet', ref], { stdio: 'ignore' })
17
+ return true
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ export function isGitRepo() {
24
+ return git('rev-parse', '--is-inside-work-tree') === 'true'
25
+ }
26
+
27
+ /**
28
+ * Resolve the default branch of the Git repository in the current working directory.
29
+ *
30
+ * Tries (in order):
31
+ * 1) HEAD of `origin` remote (or the first available remote) - set when cloned or via `git remote set-head <remote> --auto`
32
+ * 2) existing local or remote-tracking `main` or `master` branch
33
+ * 3) the only local branch, or current branch if there are no commits yet
34
+ * 4) `init.defaultBranch` from Git config
35
+ *
36
+ * @returns {{ branch: string, source: string }} resolved branch name and description of where it came from
37
+ */
38
+ export function resolveDefaultBranch() {
39
+ if (isGitRepo()) {
40
+ const remotes = git('remote')?.split(/\r?\n/) ?? []
41
+ const remote = remotes.includes('origin') ? 'origin' : remotes[0]
42
+ if (remote) {
43
+ const remoteHead = git('symbolic-ref', '--quiet', '--short', `refs/remotes/${remote}/HEAD`)
44
+ if (remoteHead?.startsWith(`${remote}/`)) {
45
+ return { branch: remoteHead.slice(remote.length + 1), source: `HEAD of '${remote}' remote` }
46
+ }
47
+ }
48
+
49
+ for (const candidate of ['main', 'master']) {
50
+ if (hasRef(`refs/heads/${candidate}`) || (remote && hasRef(`refs/remotes/${remote}/${candidate}`))) {
51
+ return { branch: candidate, source: 'existing branch' }
52
+ }
53
+ }
54
+
55
+ const localBranches = git('for-each-ref', '--format=%(refname:short)', 'refs/heads')?.split(/\r?\n/) ?? []
56
+ if (localBranches.length === 1) {
57
+ return { branch: localBranches[0], source: 'the only local branch' }
58
+ }
59
+ if (localBranches.length === 0) {
60
+ const unbornHead = git('symbolic-ref', '--quiet', '--short', 'HEAD')
61
+ if (unbornHead) {
62
+ return { branch: unbornHead, source: 'current branch of repository without commits' }
63
+ }
64
+ }
65
+ }
66
+
67
+ const configured = git('config', '--get', 'init.defaultBranch')
68
+ if (configured) {
69
+ return { branch: configured, source: 'Git config \'init.defaultBranch\'' }
70
+ }
71
+
72
+ return { branch: FALLBACK_DEFAULT_BRANCH, source: 'fallback value' }
73
+ }
package/config/index.mjs CHANGED
@@ -32,6 +32,13 @@ export async function loadVitestConfig(userVitestConfig, projects = true) {
32
32
  // Vitest defaults to 5 which might be unnecessarily restrictive
33
33
  // if the host machine can support more
34
34
  maxConcurrency: availableParallelism() / 2,
35
+ // coverage is NOT enabled by default
36
+ // (use `test-c` command or `--coverage` flag)
37
+ // Bun runtime lacks support for the V8 provider
38
+ // (`bun run` without `--bun` still runs Vitest in Node, where V8 works)
39
+ coverage: {
40
+ provider: process.versions.bun ? 'istanbul' : 'v8',
41
+ },
35
42
  },
36
43
  }
37
44
 
@@ -0,0 +1,105 @@
1
+ # GitHub Actions workflow (scaffolded by Nuxt Spec) to update test baselines (manually triggered).
2
+ # Runs in the same pinned Playwright container as "verify" workflow.
3
+ # Updated baselines (`__baseline__`, `__screenshots__`) and snapshots (`__snapshots__`) are committed back to the dispatched branch.
4
+ # If the push is rejected (e.g. protected branch), a new branch with a pull request is created instead.
5
+ # PREREQUISITE: for the pull request fallback, enable
6
+ # "Settings > Actions > General > Allow GitHub Actions to create and approve pull requests".
7
+ # NOTE: commits and pull requests created via GITHUB_TOKEN don't trigger other workflows - run "verify" manually afterwards.
8
+
9
+ name: Nuxt Spec CI Tests (update baselines)
10
+
11
+ on:
12
+ workflow_dispatch:
13
+
14
+ concurrency:
15
+ group: ci-test-update-${{ github.ref }}
16
+ cancel-in-progress: false
17
+
18
+ permissions:
19
+ contents: write
20
+ pull-requests: write
21
+
22
+ jobs:
23
+ update:
24
+ runs-on: ubuntu-latest
25
+ container:
26
+ # must match `playwright` dependency version
27
+ image: mcr.microsoft.com/playwright:v__PLAYWRIGHT_VERSION__-jammy
28
+ steps:
29
+ - name: Checkout
30
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
+ with:
32
+ ref: ${{ github.ref_name }}
33
+
34
+ - name: Enable pnpm via corepack
35
+ run: corepack enable
36
+
37
+ - name: Resolve pnpm store path
38
+ id: pnpm-store
39
+ run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
40
+
41
+ - name: Cache pnpm store
42
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
43
+ with:
44
+ path: ${{ steps.pnpm-store.outputs.path }}
45
+ key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
46
+ restore-keys: pnpm-${{ runner.os }}-
47
+
48
+ - name: Install dependencies
49
+ run: pnpm install --frozen-lockfile
50
+
51
+ - name: Prune pnpm store
52
+ run: pnpm store prune
53
+
54
+ - name: Regenerate test baselines
55
+ run: pnpm test-u
56
+
57
+ - name: Commit updated baselines
58
+ env:
59
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60
+ REPO: ${{ github.repository }}
61
+ REF_NAME: ${{ github.ref_name }}
62
+ RUN_NUMBER: ${{ github.run_number }}
63
+ run: |
64
+ set -eu
65
+
66
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
67
+ git config user.name "github-actions[bot]"
68
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
69
+
70
+ # `git add` fails on a glob without matches, `git ls-files` doesn't
71
+ git ls-files -z -m -d -o --exclude-standard -- \
72
+ ':(glob)**/__baseline__/**' \
73
+ ':(glob)**/__screenshots__/**' \
74
+ ':(glob)**/__snapshots__/**' \
75
+ | xargs -0 -r git add -A --
76
+ if git diff --cached --quiet; then
77
+ echo "No baseline changes to commit."
78
+ exit 0
79
+ fi
80
+
81
+ git commit -m "test(ci): update test baselines"
82
+
83
+ if git push origin "HEAD:$REF_NAME"; then
84
+ echo "Updated baselines pushed to '$REF_NAME'."
85
+ exit 0
86
+ fi
87
+
88
+ echo "Push to '$REF_NAME' was rejected (e.g. protected branch) - creating pull request..."
89
+ export NEW_BRANCH="ci-test/update-$(date +%Y%m%d%H%M%S)"
90
+ git push origin "HEAD:refs/heads/$NEW_BRANCH"
91
+ BODY=$(node -e '
92
+ const { NEW_BRANCH, REF_NAME, RUN_NUMBER } = process.env
93
+ console.log(JSON.stringify({
94
+ title: "test(ci): update test baselines",
95
+ head: NEW_BRANCH,
96
+ base: REF_NAME,
97
+ body: `Test baselines update triggered on run #${RUN_NUMBER} against branch \`${REF_NAME}\`.`,
98
+ }))')
99
+ curl -sS -X POST \
100
+ -H "Authorization: Bearer $GH_TOKEN" \
101
+ -H "Accept: application/vnd.github+json" \
102
+ -H "Content-Type: application/json" \
103
+ -d "$BODY" \
104
+ --fail-with-body \
105
+ "https://api.github.com/repos/$REPO/pulls"
@@ -0,0 +1,68 @@
1
+ # GitHub Actions workflow (scaffolded by Nuxt Spec) to run test suite in a pinned Playwright container for reproducible results.
2
+ # Triggered automatically on push/pull requests to default branch with possible manual dispatch.
3
+ # Uses pnpm - the pnpm store is cached so subsequent `pnpm install` runs reuse downloaded deps.
4
+ # If tests fail, `ci-test-report` artifact (HTML report, current screenshots, Vitest attachments) is uploaded for inspection.
5
+ # NOTE: commits pushed by "update baselines" workflow don't trigger this workflow - dispatch it manually afterwards.
6
+
7
+ name: Nuxt Spec CI Tests (verify)
8
+
9
+ on:
10
+ push:
11
+ branches: [__DEFAULT_BRANCH__]
12
+ pull_request:
13
+ branches: [__DEFAULT_BRANCH__]
14
+ workflow_dispatch:
15
+
16
+ concurrency:
17
+ group: ci-test-verify-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ verify:
25
+ runs-on: ubuntu-latest
26
+ container:
27
+ # must match `playwright` dependency version
28
+ image: mcr.microsoft.com/playwright:v__PLAYWRIGHT_VERSION__-jammy
29
+ steps:
30
+ - name: Checkout
31
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+
33
+ - name: Enable pnpm via corepack
34
+ run: corepack enable
35
+
36
+ - name: Resolve pnpm store path
37
+ id: pnpm-store
38
+ run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
39
+
40
+ - name: Cache pnpm store
41
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
42
+ with:
43
+ path: ${{ steps.pnpm-store.outputs.path }}
44
+ key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
45
+ restore-keys: pnpm-${{ runner.os }}-
46
+
47
+ - name: Install dependencies
48
+ run: pnpm install --frozen-lockfile
49
+
50
+ - name: Prune pnpm store
51
+ run: pnpm store prune
52
+
53
+ - name: Run tests
54
+ run: pnpm test
55
+
56
+ - name: Upload test report
57
+ if: failure()
58
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
59
+ with:
60
+ name: ci-test-report-${{ github.run_id }}-${{ github.run_attempt }}
61
+ path: |
62
+ test/__reports__
63
+ **/__current__/**
64
+ .vitest/attachments
65
+ !**/node_modules/**
66
+ include-hidden-files: true
67
+ if-no-files-found: ignore
68
+ retention-days: 14
@@ -0,0 +1,69 @@
1
+ # GitLab CI/CD job (scaffolded by Nuxt Spec) to update test baselines (manually triggered).
2
+ # Start it via "Build > Pipelines > Run pipeline" on selected branch with `NUXT_SPEC_UPDATE_BASELINES` set to `true`.
3
+ # Runs in the same pinned Playwright container as "verify" job.
4
+ # Updated baselines (`__baseline__`, `__screenshots__`) and snapshots (`__snapshots__`) are committed back to the selected branch.
5
+ # If the push is rejected (e.g. protected branch), a new branch with a merge request is created instead.
6
+ # PREREQUISITE: project access token with `write_repository` scope stored as masked CI/CD variable `NUXT_SPEC_CI_TOKEN`
7
+ # (CI_JOB_TOKEN cannot be used for pushing; the token role decides whether it may push into protected branches).
8
+
9
+ variables:
10
+ NUXT_SPEC_UPDATE_BASELINES:
11
+ value: "false"
12
+ options: ["false", "true"]
13
+ description: Set to "true" to regenerate and commit test baselines (Nuxt Spec).
14
+
15
+ ci-test-update:
16
+ stage: test
17
+ # must match `playwright` dependency version
18
+ image: mcr.microsoft.com/playwright:v__PLAYWRIGHT_VERSION__-jammy
19
+ resource_group: ci-test-update-$CI_COMMIT_REF_SLUG
20
+ rules:
21
+ - if: $CI_PIPELINE_SOURCE == "web" && $NUXT_SPEC_UPDATE_BASELINES == "true" && $CI_COMMIT_BRANCH
22
+ variables:
23
+ PNPM_STORE_DIR: $CI_PROJECT_DIR/.pnpm-store
24
+ cache:
25
+ key:
26
+ prefix: pnpm
27
+ files:
28
+ - pnpm-lock.yaml
29
+ paths:
30
+ - .pnpm-store/
31
+ before_script:
32
+ - ': "${NUXT_SPEC_CI_TOKEN:?CI/CD variable NUXT_SPEC_CI_TOKEN (project access token with write_repository scope) is required}"'
33
+ - corepack enable
34
+ - pnpm install --frozen-lockfile --store-dir "$PNPM_STORE_DIR"
35
+ - pnpm store prune --store-dir "$PNPM_STORE_DIR"
36
+ script:
37
+ - pnpm test-u
38
+ - |
39
+ set -eu
40
+
41
+ git config user.name "$GITLAB_USER_NAME"
42
+ git config user.email "$GITLAB_USER_EMAIL"
43
+
44
+ # `git add` fails on a glob without matches, `git ls-files` doesn't
45
+ git ls-files -z -m -d -o --exclude-standard -- \
46
+ ':(glob)**/__baseline__/**' \
47
+ ':(glob)**/__screenshots__/**' \
48
+ ':(glob)**/__snapshots__/**' \
49
+ | xargs -0 -r git add -A --
50
+ if git diff --cached --quiet; then
51
+ echo "No baseline changes to commit."
52
+ exit 0
53
+ fi
54
+
55
+ git commit -m "test(ci): update test baselines"
56
+
57
+ PUSH_URL="${CI_SERVER_PROTOCOL}://oauth2:${NUXT_SPEC_CI_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_PATH}.git"
58
+ if git push "$PUSH_URL" "HEAD:refs/heads/$CI_COMMIT_BRANCH"; then
59
+ echo "Updated baselines pushed to '$CI_COMMIT_BRANCH'."
60
+ exit 0
61
+ fi
62
+
63
+ echo "Push to '$CI_COMMIT_BRANCH' was rejected (e.g. protected branch) - creating merge request..."
64
+ NEW_BRANCH="ci-test/update-$(date +%Y%m%d%H%M%S)"
65
+ git push "$PUSH_URL" "HEAD:refs/heads/$NEW_BRANCH" \
66
+ -o merge_request.create \
67
+ -o merge_request.target="$CI_COMMIT_BRANCH" \
68
+ -o merge_request.title="test(ci): update test baselines" \
69
+ -o merge_request.description="Test baselines update triggered by pipeline #$CI_PIPELINE_IID against branch \`$CI_COMMIT_BRANCH\`."
@@ -0,0 +1,41 @@
1
+ # GitLab CI/CD job (scaffolded by Nuxt Spec) to run test suite in a pinned Playwright container for reproducible results.
2
+ # Triggered automatically on push to default branch and on merge requests targeting it.
3
+ # Can be started manually via "Build > Pipelines > Run pipeline" (with `NUXT_SPEC_UPDATE_BASELINES` left as `false`).
4
+ # Uses pnpm - the pnpm store is cached so subsequent `pnpm install` runs reuse downloaded deps.
5
+ # If tests fail, artifacts (HTML report, current screenshots, Vitest attachments) are kept for inspection.
6
+
7
+ ci-test-verify:
8
+ stage: test
9
+ # must match `playwright` dependency version
10
+ image: mcr.microsoft.com/playwright:v__PLAYWRIGHT_VERSION__-jammy
11
+ interruptible: true
12
+ rules:
13
+ - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
14
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH
15
+ - if: $CI_PIPELINE_SOURCE == "web" && $NUXT_SPEC_UPDATE_BASELINES != "true"
16
+ variables:
17
+ PNPM_STORE_DIR: $CI_PROJECT_DIR/.pnpm-store
18
+ cache:
19
+ key:
20
+ prefix: pnpm
21
+ files:
22
+ - pnpm-lock.yaml
23
+ paths:
24
+ - .pnpm-store/
25
+ before_script:
26
+ - corepack enable
27
+ - pnpm install --frozen-lockfile --store-dir "$PNPM_STORE_DIR"
28
+ - pnpm store prune --store-dir "$PNPM_STORE_DIR"
29
+ script:
30
+ - pnpm test
31
+ artifacts:
32
+ name: ci-test-report-$CI_PIPELINE_ID
33
+ when: on_failure
34
+ expire_in: 14 days
35
+ paths:
36
+ - test/__reports__/
37
+ - "**/__current__/"
38
+ - .vitest/attachments/
39
+ exclude:
40
+ - node_modules/**/*
41
+ - .pnpm-store/**/*
@@ -0,0 +1,6 @@
1
+ # GitLab CI/CD configuration (scaffolded by Nuxt Spec).
2
+ # Nuxt Spec test jobs are defined in the included files.
3
+
4
+ include:
5
+ - local: .gitlab/ci/ci-test-verify.yml
6
+ - local: .gitlab/ci/ci-test-update.yml
package/nuxt.config.ts CHANGED
@@ -10,7 +10,7 @@ export default defineNuxtConfig({
10
10
  dirs: [{ path: '~/components', ignore: ['index.ts'] }],
11
11
  },
12
12
 
13
- compatibilityDate: '2026-10-07',
13
+ compatibilityDate: '2026-10-13',
14
14
 
15
15
  eslint: {
16
16
  config: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-spec",
3
- "version": "0.4.0-alpha.1",
3
+ "version": "0.4.0-alpha.2",
4
4
  "description": "Test-pack layer for Nuxt Applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,7 @@
21
21
  "start": "nuxt start",
22
22
  "test": "vitest run",
23
23
  "test-u": "vitest run -u",
24
+ "test-c": "vitest run --coverage",
24
25
  "test-i": "vitest"
25
26
  },
26
27
  "bin": {
@@ -53,21 +54,24 @@
53
54
  "nuxt.config.ts"
54
55
  ],
55
56
  "dependencies": {
57
+ "@nuxt/devtools": "4.0.0-beta.4",
56
58
  "@nuxt/eslint": "~1.17.0",
57
59
  "@nuxt/hints": "1.1.4",
58
60
  "@nuxt/test-utils": "4.3.3",
59
61
  "@vitejs/plugin-vue": "~6.0.9",
60
62
  "@vitest/browser": "5.0.3",
61
63
  "@vitest/browser-playwright": "5.0.3",
64
+ "@vitest/coverage-istanbul": "5.0.3",
65
+ "@vitest/coverage-v8": "5.0.3",
62
66
  "@vitest/ui": "5.0.3",
63
67
  "@vue/test-utils": "2.5.1",
64
- "elrh-cosca": "0.4.1",
68
+ "elrh-cosca": "0.4.3",
65
69
  "fast-png": "~8.0.0",
66
70
  "happy-dom": "~20.14.5",
67
- "nuxt": "4.6.0",
71
+ "nuxt": "4.6.1",
68
72
  "pixelmatch": "~7.2.0",
69
- "playwright": "~1.63.0",
70
- "playwright-core": "~1.63.0",
73
+ "playwright": "~1.64.0",
74
+ "playwright-core": "~1.64.0",
71
75
  "typescript": "~6.0.3",
72
76
  "vitest": "5.0.3",
73
77
  "vitest-browser-vue": "3.1.0",
@@ -75,5 +79,5 @@
75
79
  "vue-router": "~5.3.1",
76
80
  "vue-tsc": "~3.3.12"
77
81
  },
78
- "packageManager": "pnpm@12.9.1"
82
+ "packageManager": "pnpm@12.11.2"
79
83
  }