cistack 5.5.0 → 6.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cistack",
3
- "version": "5.5.0",
3
+ "version": "6.0.0",
4
4
  "description": "Automatically generate GitHub Actions CI/CD pipelines by analysing your codebase",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -127,25 +127,78 @@ class CodebaseAnalyzer {
127
127
  'composer.json',
128
128
  // Build tools
129
129
  'vite.config.js',
130
+ 'vite.config.cjs',
131
+ 'vite.config.mjs',
130
132
  'vite.config.ts',
133
+ 'vite.config.cts',
134
+ 'vite.config.mts',
131
135
  'webpack.config.js',
136
+ 'webpack.config.cjs',
137
+ 'webpack.config.mjs',
132
138
  'webpack.config.ts',
139
+ 'webpack.config.cts',
140
+ 'webpack.config.mts',
133
141
  'rollup.config.js',
142
+ 'rollup.config.cjs',
143
+ 'rollup.config.mjs',
144
+ 'rollup.config.ts',
145
+ 'rollup.config.cts',
146
+ 'rollup.config.mts',
147
+ 'next.config.js',
148
+ 'next.config.cjs',
149
+ 'next.config.mjs',
150
+ 'next.config.ts',
151
+ 'next.config.cts',
152
+ 'next.config.mts',
153
+ 'nuxt.config.js',
154
+ 'nuxt.config.cjs',
155
+ 'nuxt.config.mjs',
156
+ 'nuxt.config.ts',
157
+ 'nuxt.config.cts',
158
+ 'nuxt.config.mts',
159
+ 'svelte.config.js',
160
+ 'svelte.config.cjs',
161
+ 'svelte.config.mjs',
162
+ 'svelte.config.ts',
163
+ 'svelte.config.cts',
164
+ 'svelte.config.mts',
165
+ 'astro.config.js',
166
+ 'astro.config.cjs',
134
167
  'turbo.json',
135
168
  'nx.json',
136
169
  'lerna.json',
137
170
  'rush.json',
138
171
  // Test
139
172
  'jest.config.js',
173
+ 'jest.config.cjs',
174
+ 'jest.config.mjs',
140
175
  'jest.config.ts',
176
+ 'jest.config.cts',
177
+ 'jest.config.mts',
141
178
  'vitest.config.js',
179
+ 'vitest.config.cjs',
180
+ 'vitest.config.mjs',
142
181
  'vitest.config.ts',
182
+ 'vitest.config.cts',
183
+ 'vitest.config.mts',
143
184
  'cypress.config.js',
185
+ 'cypress.config.cjs',
186
+ 'cypress.config.mjs',
144
187
  'cypress.config.ts',
188
+ 'cypress.config.cts',
189
+ 'cypress.config.mts',
145
190
  'playwright.config.js',
191
+ 'playwright.config.cjs',
192
+ 'playwright.config.mjs',
146
193
  'playwright.config.ts',
194
+ 'playwright.config.cts',
195
+ 'playwright.config.mts',
147
196
  '.mocharc.js',
197
+ '.mocharc.cjs',
198
+ '.mocharc.mjs',
148
199
  '.mocharc.yml',
200
+ '.mocharc.yaml',
201
+ '.mocharc.json',
149
202
  'phpunit.xml',
150
203
  'pytest.ini',
151
204
  'conftest.py',
@@ -19,16 +19,16 @@ class FrameworkDetector {
19
19
  async detect() {
20
20
  const results = [
21
21
  // JS / TS frontend
22
- this._check('Next.js', ['next'], ['next.config.js', 'next.config.ts', 'next.config.mjs'], { buildDir: '.next', priority: 10 }),
23
- this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.ts'], { buildDir: '.nuxt', priority: 10 }),
24
- this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js'], { buildDir: '.svelte-kit', priority: 10 }),
22
+ this._check('Next.js', ['next'], ['next.config.js', 'next.config.cjs', 'next.config.mjs', 'next.config.ts', 'next.config.cts', 'next.config.mts'], { buildDir: '.next', priority: 10 }),
23
+ this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.cjs', 'nuxt.config.mjs', 'nuxt.config.ts', 'nuxt.config.cts', 'nuxt.config.mts'], { buildDir: '.nuxt', priority: 10 }),
24
+ this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js', 'svelte.config.cjs', 'svelte.config.mjs', 'svelte.config.ts', 'svelte.config.cts', 'svelte.config.mts'], { buildDir: '.svelte-kit', priority: 10 }),
25
25
  this._check('Remix', ['@remix-run/react', '@remix-run/node'], [], { priority: 10 }),
26
- this._check('Astro', ['astro'], ['astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist', priority: 10 }),
27
- this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.ts'], { buildDir: 'dist', priority: 5 }),
26
+ this._check('Astro', ['astro'], ['astro.config.js', 'astro.config.cjs', 'astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist', priority: 10 }),
27
+ this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.cjs', 'vite.config.mjs', 'vite.config.ts', 'vite.config.cts', 'vite.config.mts'], { buildDir: 'dist', priority: 5 }),
28
28
  this._check('React', ['react', 'react-dom'], [], { buildDir: 'build', priority: 1 }),
29
29
  this._check('Vue', ['vue'], [], { buildDir: 'dist', priority: 1 }),
30
30
  this._check('Angular', ['@angular/core'], [], { buildDir: 'dist', priority: 1 }),
31
- this._check('Svelte', ['svelte'], ['svelte.config.js'], { buildDir: 'public', priority: 1 }),
31
+ this._check('Svelte', ['svelte'], ['svelte.config.js', 'svelte.config.cjs', 'svelte.config.mjs', 'svelte.config.ts', 'svelte.config.cts', 'svelte.config.mts'], { buildDir: 'public', priority: 1 }),
32
32
  this._check('Gatsby', ['gatsby'], [], { buildDir: 'public', priority: 1 }),
33
33
  this._check('Ember', ['ember-cli'], [], { priority: 1 }),
34
34
  // Node / backend
@@ -46,7 +46,14 @@ class TestingDetector {
46
46
  _checkJest() {
47
47
  let conf = 0;
48
48
  if (this.deps['jest'] || this.deps['@jest/core']) conf += 0.5;
49
- if (this.configs.has('jest.config.js') || this.configs.has('jest.config.ts')) conf += 0.4;
49
+ if (this._hasConfig([
50
+ 'jest.config.js',
51
+ 'jest.config.cjs',
52
+ 'jest.config.mjs',
53
+ 'jest.config.ts',
54
+ 'jest.config.cts',
55
+ 'jest.config.mts',
56
+ ])) conf += 0.4;
50
57
  if (this.scripts.test && this.scripts.test.includes('jest')) conf += 0.2;
51
58
  return { name: 'Jest', confidence: Math.min(conf, 1), command: this._testScript('jest') || 'npx jest --coverage', type: 'unit' };
52
59
  }
@@ -54,21 +61,35 @@ class TestingDetector {
54
61
  _checkVitest() {
55
62
  let conf = 0;
56
63
  if (this.deps['vitest']) conf += 0.6;
57
- if (this.configs.has('vitest.config.js') || this.configs.has('vitest.config.ts')) conf += 0.4;
64
+ if (this._hasConfig([
65
+ 'vitest.config.js',
66
+ 'vitest.config.cjs',
67
+ 'vitest.config.mjs',
68
+ 'vitest.config.ts',
69
+ 'vitest.config.cts',
70
+ 'vitest.config.mts',
71
+ ])) conf += 0.4;
58
72
  return { name: 'Vitest', confidence: Math.min(conf, 1), command: this._testScript('vitest') || 'npx vitest run --coverage', type: 'unit' };
59
73
  }
60
74
 
61
75
  _checkMocha() {
62
76
  let conf = 0;
63
77
  if (this.deps['mocha']) conf += 0.5;
64
- if (this.configs.has('.mocharc.js') || this.configs.has('.mocharc.yml')) conf += 0.4;
78
+ if (this._hasConfig(['.mocharc.js', '.mocharc.cjs', '.mocharc.mjs', '.mocharc.yml', '.mocharc.yaml', '.mocharc.json'])) conf += 0.4;
65
79
  return { name: 'Mocha', confidence: Math.min(conf, 1), command: 'npx mocha', type: 'unit' };
66
80
  }
67
81
 
68
82
  _checkCypress() {
69
83
  let conf = 0;
70
84
  if (this.deps['cypress']) conf += 0.6;
71
- if (this.configs.has('cypress.config.js') || this.configs.has('cypress.config.ts')) conf += 0.4;
85
+ if (this._hasConfig([
86
+ 'cypress.config.js',
87
+ 'cypress.config.cjs',
88
+ 'cypress.config.mjs',
89
+ 'cypress.config.ts',
90
+ 'cypress.config.cts',
91
+ 'cypress.config.mts',
92
+ ])) conf += 0.4;
72
93
  if (this.files.has('cypress/e2e') || [...this.files].some((f) => f.startsWith('cypress/'))) conf += 0.2;
73
94
  return { name: 'Cypress', confidence: Math.min(conf, 1), command: 'npx cypress run', type: 'e2e' };
74
95
  }
@@ -76,7 +97,14 @@ class TestingDetector {
76
97
  _checkPlaywright() {
77
98
  let conf = 0;
78
99
  if (this.deps['@playwright/test']) conf += 0.6;
79
- if (this.configs.has('playwright.config.js') || this.configs.has('playwright.config.ts')) conf += 0.4;
100
+ if (this._hasConfig([
101
+ 'playwright.config.js',
102
+ 'playwright.config.cjs',
103
+ 'playwright.config.mjs',
104
+ 'playwright.config.ts',
105
+ 'playwright.config.cts',
106
+ 'playwright.config.mts',
107
+ ])) conf += 0.4;
80
108
  return { name: 'Playwright', confidence: Math.min(conf, 1), command: 'npx playwright test', type: 'e2e' };
81
109
  }
82
110
 
@@ -148,6 +176,10 @@ class TestingDetector {
148
176
  if (this.packageManager === 'bun') return `bun run ${name}`;
149
177
  return `npm run ${name}`;
150
178
  }
179
+
180
+ _hasConfig(candidates) {
181
+ return candidates.some((candidate) => this.configs.has(candidate));
182
+ }
151
183
  }
152
184
 
153
185
  module.exports = TestingDetector;
@@ -188,29 +188,6 @@ class WorkflowGenerator {
188
188
  };
189
189
  }
190
190
 
191
- // ── lighthouse job ──────────────────────────────────────────────────
192
- if (jobs.build && this.frameworks.some(f => ['Next.js', 'React', 'Vue', 'Svelte', 'Nuxt'].includes(f.name))) {
193
- jobs.lighthouse = {
194
- name: '⚡ Lighthouse Audit',
195
- 'runs-on': 'ubuntu-latest',
196
- needs: ['build'],
197
- if: "github.event_name == 'pull_request'",
198
- steps: [
199
- this._stepCheckout(),
200
- {
201
- name: 'Run Lighthouse on build output',
202
- uses: 'treosh/lighthouse-ci-action@v11',
203
- with: {
204
- uploadArtifacts: true,
205
- temporaryPublicStorage: true,
206
- ...this._lighthouseActionConfig(),
207
- },
208
- },
209
- ],
210
- 'continue-on-error': true,
211
- };
212
- }
213
-
214
191
  // ── e2e job ───────────────────────────────────────────────────────────
215
192
  if (this.e2eTests.length > 0) {
216
193
  const e2eTest = this.e2eTests[0];
@@ -339,38 +316,6 @@ class WorkflowGenerator {
339
316
  },
340
317
  };
341
318
 
342
- if (buildablePackages.length > 0 && this.frameworks.some(f => ['Next.js', 'React', 'Vue', 'Svelte', 'Nuxt'].includes(f.name))) {
343
- workflow.jobs.lighthouse = {
344
- name: '⚡ Lighthouse (Root)',
345
- 'runs-on': 'ubuntu-latest',
346
- strategy: {
347
- matrix: {
348
- include: buildablePackages,
349
- },
350
- },
351
- steps: [
352
- this._stepCheckout(),
353
- ...this._setupSteps(lang),
354
- {
355
- ...this._stepInstallDeps(lang),
356
- },
357
- {
358
- name: 'Build workspace',
359
- run: this._workspaceRunCommand(lang, '${{ matrix.buildScript }}'),
360
- },
361
- {
362
- name: 'Lighthouse',
363
- uses: 'treosh/lighthouse-ci-action@v11',
364
- with: {
365
- uploadArtifacts: true,
366
- temporaryPublicStorage: true,
367
- },
368
- },
369
- ],
370
- 'continue-on-error': true,
371
- };
372
- }
373
-
374
319
  return this._toYaml(
375
320
  workflow,
376
321
  `# Generated by cistack v${version} — https://github.com/cistack\n# Monorepo CI — matrix over all workspaces\n\n`
@@ -698,11 +643,6 @@ class WorkflowGenerator {
698
643
  return steps;
699
644
  }
700
645
 
701
- _lighthouseActionConfig() {
702
- const configPath = path.join(this.projectPath, '.lighthouserc.json');
703
- return fs.existsSync(configPath) ? { configPath: './.lighthouserc.json' } : {};
704
- }
705
-
706
646
  _stepInstallDeps(lang) {
707
647
  const pm = lang.packageManager;
708
648
  if (pm === 'npm') return { name: 'Install dependencies', run: this.lockFiles.has('package-lock.json') ? 'npm ci' : 'npm install' };
@@ -890,9 +830,9 @@ class WorkflowGenerator {
890
830
  {
891
831
  name: 'Validate Vercel credentials',
892
832
  run: [
893
- 'test -n "$VERCEL_TOKEN" || (echo "Missing VERCEL_TOKEN secret. Add it in GitHub Actions secrets." && exit 1)',
894
- 'test -n "$VERCEL_ORG_ID" || (echo "Missing VERCEL_ORG_ID secret. Add it in GitHub Actions secrets." && exit 1)',
895
- 'test -n "$VERCEL_PROJECT_ID" || (echo "Missing VERCEL_PROJECT_ID secret. Add it in GitHub Actions secrets." && exit 1)',
833
+ 'test -n "$VERCEL_TOKEN" || (echo "Missing VERCEL_TOKEN secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs." && exit 1)',
834
+ 'test -n "$VERCEL_ORG_ID" || (echo "Missing VERCEL_ORG_ID secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs." && exit 1)',
835
+ 'test -n "$VERCEL_PROJECT_ID" || (echo "Missing VERCEL_PROJECT_ID secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs." && exit 1)',
896
836
  ].join('\n'),
897
837
  env: vercelEnv,
898
838
  },
package/tests/run.js CHANGED
@@ -151,6 +151,22 @@ test('FrameworkDetector detects Spring Boot from build.gradle.kts', async () =>
151
151
  assert(frameworks.some((framework) => framework.name === 'Spring Boot'));
152
152
  });
153
153
 
154
+ test('FrameworkDetector recognizes modern framework config files like next.config.mjs', async () => {
155
+ const projectDir = makeTempDir();
156
+ writeFiles(projectDir, {
157
+ 'package.json': json({
158
+ name: 'next-config-mjs-app',
159
+ version: '1.0.0',
160
+ }),
161
+ 'next.config.mjs': 'export default {};\n',
162
+ });
163
+
164
+ const info = await new CodebaseAnalyzer(projectDir).analyse();
165
+ const frameworks = await new FrameworkDetector(projectDir, info).detect();
166
+
167
+ assert(frameworks.some((framework) => framework.name === 'Next.js'));
168
+ });
169
+
154
170
  test('HostingDetector recognizes Azure pipelines in azure/pipelines.yml', async () => {
155
171
  const projectDir = makeTempDir();
156
172
  writeFiles(projectDir, {
@@ -274,6 +290,24 @@ test('ReleaseDetector reads release-it CJS config and honors npm.publish false',
274
290
  assert.equal(release.requiresNpmToken, false);
275
291
  });
276
292
 
293
+ test('TestingDetector recognizes modern config file extensions like vitest.config.mjs', async () => {
294
+ const projectDir = makeTempDir();
295
+ writeFiles(projectDir, {
296
+ 'package.json': json({
297
+ name: 'modern-vitest-app',
298
+ version: '1.0.0',
299
+ }),
300
+ 'vitest.config.mjs': 'export default {};\n',
301
+ });
302
+
303
+ const info = await new CodebaseAnalyzer(projectDir).analyse();
304
+ const testing = await new TestingDetector(projectDir, info).detect();
305
+ const vitest = testing.find((entry) => entry.name === 'Vitest');
306
+
307
+ assert(vitest);
308
+ assert(vitest.confidence > 0);
309
+ });
310
+
277
311
  test('bun.lock is recognized as Bun across codebase, testing, and release detection', async () => {
278
312
  const projectDir = makeTempDir();
279
313
  writeFiles(projectDir, {
@@ -440,9 +474,9 @@ test('Vercel deploy workflows validate secrets before running vercel pull', () =
440
474
  const pullStep = deploySteps.find((step) => step.name === 'Pull Vercel environment');
441
475
 
442
476
  assert(validateStep);
443
- assert(validateStep.run.includes('Missing VERCEL_TOKEN secret'));
444
- assert(validateStep.run.includes('Missing VERCEL_ORG_ID secret'));
445
- assert(validateStep.run.includes('Missing VERCEL_PROJECT_ID secret'));
477
+ assert(validateStep.run.includes('Missing VERCEL_TOKEN secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs.'));
478
+ assert(validateStep.run.includes('Missing VERCEL_ORG_ID secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs.'));
479
+ assert(validateStep.run.includes('Missing VERCEL_PROJECT_ID secret. Add it in GitHub Actions secrets, or Dependabot secrets for Dependabot PRs.'));
446
480
  assert.equal(pullStep.run, 'vercel pull --yes --environment=production --token="$VERCEL_TOKEN"');
447
481
  });
448
482
 
@@ -494,64 +528,6 @@ test('Frontend Lighthouse is omitted when no build job exists', () => {
494
528
  assert(!parsed.jobs.lighthouse);
495
529
  });
496
530
 
497
- test('Lighthouse job does not require .lighthouserc.json when the file is absent', () => {
498
- const projectDir = makeTempDir();
499
- writeFiles(projectDir, {
500
- 'package.json': json({
501
- name: 'lighthouse-default-app',
502
- version: '1.0.0',
503
- scripts: {
504
- build: 'echo build',
505
- },
506
- }),
507
- });
508
-
509
- const generator = new WorkflowGenerator(
510
- makeJsProject({
511
- frameworks: [{ name: 'React', confidence: 1, buildDir: 'dist' }],
512
- }),
513
- projectDir
514
- );
515
-
516
- const parsed = parseWorkflow(generator._buildCIWorkflow());
517
- const lighthouseStep = parsed.jobs.lighthouse.steps.find((step) => step.name === 'Run Lighthouse on build output');
518
-
519
- assert(lighthouseStep);
520
- assert(!('configPath' in lighthouseStep.with));
521
- });
522
-
523
- test('Lighthouse job uses .lighthouserc.json when the file exists', () => {
524
- const projectDir = makeTempDir();
525
- writeFiles(projectDir, {
526
- 'package.json': json({
527
- name: 'lighthouse-config-app',
528
- version: '1.0.0',
529
- scripts: {
530
- build: 'echo build',
531
- },
532
- }),
533
- '.lighthouserc.json': json({
534
- ci: {
535
- collect: {
536
- numberOfRuns: 1,
537
- },
538
- },
539
- }),
540
- });
541
-
542
- const generator = new WorkflowGenerator(
543
- makeJsProject({
544
- frameworks: [{ name: 'React', confidence: 1, buildDir: 'dist' }],
545
- }),
546
- projectDir
547
- );
548
-
549
- const parsed = parseWorkflow(generator._buildCIWorkflow());
550
- const lighthouseStep = parsed.jobs.lighthouse.steps.find((step) => step.name === 'Run Lighthouse on build output');
551
-
552
- assert.equal(lighthouseStep.with.configPath, './.lighthouserc.json');
553
- });
554
-
555
531
  test('E2E jobs fall back to existing jobs instead of depending on a missing build job', () => {
556
532
  const projectDir = makeTempDir();
557
533
  const generator = new WorkflowGenerator(
@@ -929,7 +905,6 @@ test('Monorepo root CI installs dependencies at the repo root and does not hide
929
905
  const lintStep = ciSteps.find((step) => step.name === 'Lint');
930
906
  const testStep = ciSteps.find((step) => step.name === 'Test');
931
907
  const buildStep = ciSteps.find((step) => step.name === 'Build');
932
- const lighthouseBuildStep = rootWorkflow.jobs.lighthouse.steps.find((step) => step.name === 'Build workspace');
933
908
 
934
909
  assert.equal(installStep.run, 'yarn install --frozen-lockfile');
935
910
  assert.equal(lintStep.if, '${{ matrix.lintScript != \'\' }}');
@@ -938,7 +913,7 @@ test('Monorepo root CI installs dependencies at the repo root and does not hide
938
913
  assert(!lintStep.run.includes('|| true'));
939
914
  assert(!testStep.run.includes('|| true'));
940
915
  assert(!buildStep.run.includes('|| true'));
941
- assert.equal(lighthouseBuildStep.run, 'yarn workspace ${{ matrix.name }} run ${{ matrix.buildScript }}');
916
+ assert(!rootWorkflow.jobs.lighthouse);
942
917
  });
943
918
 
944
919
  test('Bun monorepo matrix commands are scoped to the workspace path', () => {