@rtorcato/js-tooling 2.5.0 → 2.6.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.
@@ -124,9 +124,10 @@ const FIXERS = [
124
124
  },
125
125
  {
126
126
  target: 'husky',
127
- description: 'Set up Husky + lint-staged (deep-merges existing lint-staged field)',
127
+ description: 'Set up Husky + lint-staged',
128
128
  appliesTo: ['Husky', 'lint-staged'],
129
129
  outputs: ['.husky/pre-commit', 'package.json (lint-staged field)'],
130
+ riskLevel: 'safe-merge',
130
131
  canFixDrift: true,
131
132
  async run({ targetDir, pkg }) {
132
133
  const pkgPath = path.join(targetDir, 'package.json');
@@ -209,9 +210,10 @@ const FIXERS = [
209
210
  },
210
211
  {
211
212
  target: 'engines',
212
- description: 'Add engines.node to package.json (never overwrites)',
213
+ description: 'Add engines.node to package.json',
213
214
  appliesTo: ['engines.node'],
214
215
  outputs: ['package.json (engines.node field)'],
216
+ riskLevel: 'safe-merge',
215
217
  canFixDrift: true,
216
218
  async run({ targetDir }) {
217
219
  const result = await ensureEnginesNode(targetDir);
@@ -234,6 +236,7 @@ const FIXERS = [
234
236
  description: 'Add @rtorcato/js-tooling to devDependencies',
235
237
  appliesTo: ['package.json'],
236
238
  outputs: ['package.json (devDependencies)'],
239
+ riskLevel: 'safe-merge',
237
240
  canFixDrift: true,
238
241
  async run({ targetDir, pkg }) {
239
242
  const pkgPath = path.join(targetDir, 'package.json');
@@ -282,15 +285,29 @@ async function applyFixer(fixer, result, targetDir, pkg, dryRun, silent) {
282
285
  }
283
286
  return { filesWritten, dryRun: false };
284
287
  }
288
+ function promptMessageFor(fixer, result) {
289
+ const risk = fixer.riskLevel ?? 'destructive';
290
+ if (risk === 'safe-merge') {
291
+ return { message: `${fixer.description} (existing fields preserved)?`, default: true };
292
+ }
293
+ if (risk === 'safe-add') {
294
+ return { message: `${fixer.description}?`, default: true };
295
+ }
296
+ // destructive
297
+ if (result.status === 'drift') {
298
+ return {
299
+ message: `⚠️ ${fixer.description} — overwrite existing file? user customizations will be lost`,
300
+ default: false,
301
+ };
302
+ }
303
+ return { message: `Apply ${fixer.description}?`, default: true };
304
+ }
285
305
  async function confirmApply(fixer, result, assumeYes) {
286
306
  if (assumeYes)
287
307
  return true;
288
- const isDrift = result.status === 'drift';
289
- const message = isDrift
290
- ? `⚠️ ${fixer.description} — overwrite existing file? user customizations will be lost`
291
- : `Apply ${fixer.description}?`;
308
+ const { message, default: defaultValue } = promptMessageFor(fixer, result);
292
309
  const { confirm } = await inquirer.prompt([
293
- { type: 'confirm', name: 'confirm', message, default: !isDrift },
310
+ { type: 'confirm', name: 'confirm', message, default: defaultValue },
294
311
  ]);
295
312
  return confirm === true;
296
313
  }
@@ -0,0 +1,193 @@
1
+ export const PRESET_NAMES = [
2
+ 'library',
3
+ 'web-app',
4
+ 'node-api',
5
+ 'nextjs-app',
6
+ 'react-app',
7
+ ];
8
+ const BASE = {
9
+ linting: { tool: 'biome' },
10
+ formatting: { tool: 'biome' },
11
+ testing: { framework: 'vitest', environment: 'node' },
12
+ gitHooks: true,
13
+ commitLint: true,
14
+ semanticRelease: false,
15
+ securityAutomation: true,
16
+ };
17
+ export function buildPresetConfig(name, projectName) {
18
+ switch (name) {
19
+ case 'library':
20
+ return {
21
+ ...BASE,
22
+ projectName,
23
+ projectType: 'library',
24
+ typescript: { enabled: true, config: 'base' },
25
+ semanticRelease: true,
26
+ bundler: 'tsup',
27
+ };
28
+ case 'web-app':
29
+ return {
30
+ ...BASE,
31
+ projectName,
32
+ projectType: 'web-app',
33
+ typescript: { enabled: true, config: 'base' },
34
+ testing: { framework: 'vitest', environment: 'browser' },
35
+ bundler: 'vite',
36
+ };
37
+ case 'node-api':
38
+ return {
39
+ ...BASE,
40
+ projectName,
41
+ projectType: 'node-api',
42
+ typescript: { enabled: true, config: 'node' },
43
+ bundler: 'esbuild',
44
+ };
45
+ case 'nextjs-app':
46
+ return {
47
+ ...BASE,
48
+ projectName,
49
+ projectType: 'nextjs-app',
50
+ typescript: { enabled: true, config: 'next' },
51
+ linting: { tool: 'eslint', eslintConfig: 'nextjs' },
52
+ formatting: { tool: 'prettier' },
53
+ bundler: 'none',
54
+ };
55
+ case 'react-app':
56
+ return {
57
+ ...BASE,
58
+ projectName,
59
+ projectType: 'react-app',
60
+ typescript: { enabled: true, config: 'react' },
61
+ testing: { framework: 'vitest', environment: 'browser' },
62
+ bundler: 'vite',
63
+ };
64
+ }
65
+ }
66
+ export const CONFIG_SCHEMA = {
67
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
68
+ $id: 'https://rtorcato.github.io/js-tooling/schemas/project-config.json',
69
+ title: 'ProjectConfig',
70
+ description: '@rtorcato/js-tooling setup configuration',
71
+ type: 'object',
72
+ additionalProperties: false,
73
+ required: [
74
+ 'projectName',
75
+ 'projectType',
76
+ 'typescript',
77
+ 'linting',
78
+ 'formatting',
79
+ 'testing',
80
+ 'gitHooks',
81
+ 'commitLint',
82
+ 'semanticRelease',
83
+ 'securityAutomation',
84
+ 'bundler',
85
+ ],
86
+ properties: {
87
+ projectName: { type: 'string', minLength: 1 },
88
+ projectType: {
89
+ type: 'string',
90
+ enum: ['library', 'web-app', 'node-api', 'nextjs-app', 'react-app'],
91
+ },
92
+ typescript: {
93
+ type: 'object',
94
+ additionalProperties: false,
95
+ required: ['enabled', 'config'],
96
+ properties: {
97
+ enabled: { type: 'boolean' },
98
+ config: { type: 'string', enum: ['base', 'react', 'next', 'node', 'express'] },
99
+ },
100
+ },
101
+ linting: {
102
+ type: 'object',
103
+ additionalProperties: false,
104
+ required: ['tool'],
105
+ properties: {
106
+ tool: { type: 'string', enum: ['biome', 'eslint', 'both', 'none'] },
107
+ eslintConfig: { type: 'string', enum: ['base', 'nextjs'] },
108
+ },
109
+ },
110
+ formatting: {
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ required: ['tool'],
114
+ properties: { tool: { type: 'string', enum: ['biome', 'prettier', 'none'] } },
115
+ },
116
+ testing: {
117
+ type: 'object',
118
+ additionalProperties: false,
119
+ required: ['framework'],
120
+ properties: {
121
+ framework: { type: 'string', enum: ['vitest', 'jest', 'playwright', 'none'] },
122
+ environment: { type: 'string', enum: ['node', 'browser', 'both'] },
123
+ },
124
+ },
125
+ gitHooks: { type: 'boolean' },
126
+ commitLint: { type: 'boolean' },
127
+ semanticRelease: { type: 'boolean' },
128
+ securityAutomation: { type: 'boolean' },
129
+ bundler: { type: 'string', enum: ['tsup', 'esbuild', 'vite', 'none'] },
130
+ },
131
+ };
132
+ const ALLOWED_KEYS = new Set(CONFIG_SCHEMA.required);
133
+ export function validateProjectConfig(input) {
134
+ const errors = [];
135
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
136
+ return { valid: false, errors: ['Config must be a JSON object'] };
137
+ }
138
+ const obj = input;
139
+ for (const key of Object.keys(obj)) {
140
+ if (!ALLOWED_KEYS.has(key))
141
+ errors.push(`Unknown field: ${key}`);
142
+ }
143
+ for (const required of CONFIG_SCHEMA.required) {
144
+ if (!(required in obj))
145
+ errors.push(`Missing required field: ${required}`);
146
+ }
147
+ return { valid: errors.length === 0, errors };
148
+ }
149
+ export function computeFileList(config) {
150
+ const files = ['package.json'];
151
+ files.push('.editorconfig', '.nvmrc', 'knip.json');
152
+ if (config.typescript.enabled) {
153
+ files.push('tsconfig.json', 'reset.d.ts');
154
+ }
155
+ if (config.linting.tool === 'biome' || config.linting.tool === 'both') {
156
+ files.push('biome.jsonc');
157
+ }
158
+ if (config.linting.tool === 'eslint' || config.linting.tool === 'both') {
159
+ files.push('eslint.config.mjs');
160
+ }
161
+ if (config.linting.tool === 'eslint') {
162
+ files.push('prettier.config.mjs');
163
+ }
164
+ if (config.testing.framework === 'vitest') {
165
+ files.push('vitest.config.ts', 'vitest.setup.ts');
166
+ }
167
+ if (config.testing.framework === 'jest') {
168
+ files.push('jest.config.mjs');
169
+ }
170
+ if (config.testing.framework === 'playwright') {
171
+ files.push('playwright.config.ts');
172
+ }
173
+ if (config.gitHooks) {
174
+ files.push('.husky/pre-commit', '.gitignore');
175
+ }
176
+ if (config.commitLint) {
177
+ files.push('.husky/commit-msg', 'commitlint.config.mjs');
178
+ }
179
+ files.push('.github/workflows/ci.yml');
180
+ if (config.securityAutomation) {
181
+ files.push('.github/dependabot.yml', '.github/workflows/codeql.yml');
182
+ }
183
+ if (config.bundler === 'tsup')
184
+ files.push('tsup.config.ts');
185
+ else if (config.bundler === 'esbuild')
186
+ files.push('build.mjs');
187
+ else if (config.bundler === 'vite')
188
+ files.push('vite.config.ts');
189
+ if (config.semanticRelease)
190
+ files.push('release.config.mjs');
191
+ files.push('README.md');
192
+ return files;
193
+ }
@@ -1,17 +1,58 @@
1
+ import path from 'node:path';
1
2
  import chalk from 'chalk';
2
3
  import fs from 'fs-extra';
3
4
  import inquirer from 'inquirer';
4
- import path from 'node:path';
5
5
  import { generateConfigs } from '../generators/index.js';
6
6
  import { installDependencies } from '../utils/install.js';
7
+ import { buildPresetConfig, computeFileList, CONFIG_SCHEMA, PRESET_NAMES, validateProjectConfig, } from './setup-presets.js';
8
+ async function resolveConfig(options) {
9
+ if (options.config && options.preset) {
10
+ console.warn(chalk.yellow('⚠️ Both --config and --preset given; --config wins.\n'));
11
+ }
12
+ if (options.config) {
13
+ const configPath = path.resolve(options.config);
14
+ if (!(await fs.pathExists(configPath))) {
15
+ throw new Error(`Config file not found: ${configPath}`);
16
+ }
17
+ const raw = await fs.readJson(configPath);
18
+ const { valid, errors } = validateProjectConfig(raw);
19
+ if (!valid) {
20
+ throw new Error(`Invalid config:\n - ${errors.join('\n - ')}`);
21
+ }
22
+ return raw;
23
+ }
24
+ if (options.preset) {
25
+ if (!PRESET_NAMES.includes(options.preset)) {
26
+ throw new Error(`Unknown preset: ${options.preset}. Available: ${PRESET_NAMES.join(', ')}`);
27
+ }
28
+ const projectName = path.basename(path.resolve(options.directory));
29
+ return buildPresetConfig(options.preset, projectName);
30
+ }
31
+ return promptForConfig();
32
+ }
7
33
  export async function setupProject(options) {
34
+ if (options.configSchema) {
35
+ console.log(JSON.stringify(CONFIG_SCHEMA, null, 2));
36
+ return;
37
+ }
8
38
  const targetDir = path.resolve(options.directory);
9
- console.log(chalk.cyan('\n🛠️ Welcome to JS Tooling Setup!\n'));
10
- console.log(chalk.gray(`Setting up tooling in: ${targetDir}\n`));
39
+ const interactive = !options.config && !options.preset;
40
+ const dryRun = options.dryRun === true;
41
+ if (interactive && !dryRun) {
42
+ console.log(chalk.cyan('\n🛠️ Welcome to JS Tooling Setup!\n'));
43
+ console.log(chalk.gray(`Setting up tooling in: ${targetDir}\n`));
44
+ }
11
45
  try {
12
- // Check if directory exists and is writable
13
46
  await fs.ensureDir(targetDir);
14
- const config = await promptForConfig();
47
+ const config = await resolveConfig(options);
48
+ if (dryRun) {
49
+ const files = computeFileList(config);
50
+ console.log(JSON.stringify({ directory: targetDir, config, files }, null, 2));
51
+ return;
52
+ }
53
+ if (!interactive) {
54
+ console.log(chalk.cyan(`\n🛠️ Scaffolding ${config.projectType} in ${targetDir}\n`));
55
+ }
15
56
  console.log(chalk.cyan('\n📝 Generating configuration files...\n'));
16
57
  await generateConfigs(config, targetDir);
17
58
  if (!options.skipInstall) {
@@ -19,7 +60,6 @@ export async function setupProject(options) {
19
60
  await installDependencies(config, targetDir);
20
61
  }
21
62
  console.log(chalk.green('\n✅ Setup completed successfully!\n'));
22
- // Show next steps
23
63
  showNextSteps(config, targetDir);
24
64
  }
25
65
  catch (error) {
@@ -53,32 +53,17 @@ console.log('Build completed!')
53
53
  `;
54
54
  await fs.writeFile(esbuildConfigPath, esbuildConfig);
55
55
  }
56
- async function generateViteConfig(config, targetDir) {
56
+ export async function generateViteConfig(config, targetDir) {
57
57
  const viteConfigPath = path.join(targetDir, 'vite.config.ts');
58
- let viteConfig = `import { defineConfig } from 'vite'
59
- `;
60
- if (config.projectType === 'react-app') {
61
- viteConfig += `import react from '@vitejs/plugin-react'
62
- `;
63
- }
64
- viteConfig += `
65
- export default defineConfig({
66
- plugins: [${config.projectType === 'react-app' ? 'react()' : ''}],
67
- resolve: {
68
- alias: {
69
- '@': '/src',
70
- '~': '/src'
71
- }
72
- },
73
- build: {
74
- outDir: 'dist',
75
- sourcemap: true,
76
- },
77
- server: {
78
- port: 3000,
79
- open: true
80
- }
81
- })
58
+ // React apps need the plugin; we layer it on top of the shipped preset.
59
+ const viteConfig = config.projectType === 'react-app'
60
+ ? `import preset from '@rtorcato/js-tooling/vite'
61
+ import react from '@vitejs/plugin-react'
62
+ import { defineConfig, mergeConfig } from 'vite'
63
+
64
+ export default mergeConfig(preset, defineConfig({ plugins: [react()] }))
65
+ `
66
+ : `export { default } from '@rtorcato/js-tooling/vite'
82
67
  `;
83
68
  await fs.writeFile(viteConfigPath, viteConfig);
84
69
  }
@@ -38,41 +38,9 @@ async function generateJestConfig(config, targetDir) {
38
38
  `;
39
39
  await fs.writeFile(jestConfigPath, jestConfig);
40
40
  }
41
- async function generatePlaywrightConfig(targetDir) {
41
+ export async function generatePlaywrightConfig(targetDir) {
42
42
  const playwrightConfigPath = path.join(targetDir, 'playwright.config.ts');
43
- const playwrightConfig = `import { defineConfig } from '@playwright/test'
44
-
45
- export default defineConfig({
46
- testDir: './tests/e2e',
47
- fullyParallel: true,
48
- forbidOnly: !!process.env.CI,
49
- retries: process.env.CI ? 2 : 0,
50
- workers: process.env.CI ? 1 : undefined,
51
- reporter: 'html',
52
- use: {
53
- baseURL: 'http://localhost:3000',
54
- trace: 'on-first-retry',
55
- },
56
- projects: [
57
- {
58
- name: 'chromium',
59
- use: { ...devices['Desktop Chrome'] },
60
- },
61
- {
62
- name: 'firefox',
63
- use: { ...devices['Desktop Firefox'] },
64
- },
65
- {
66
- name: 'webkit',
67
- use: { ...devices['Desktop Safari'] },
68
- },
69
- ],
70
- webServer: {
71
- command: 'npm run dev',
72
- url: 'http://localhost:3000',
73
- reuseExistingServer: !process.env.CI,
74
- },
75
- })
43
+ const playwrightConfig = `export { default } from '@rtorcato/js-tooling/playwright'
76
44
  `;
77
45
  await fs.writeFile(playwrightConfigPath, playwrightConfig);
78
46
  }
package/dist/cli/index.js CHANGED
@@ -28,6 +28,10 @@ program
28
28
  .description('🚀 Setup tooling for a new or existing project')
29
29
  .option('-d, --directory <path>', 'Target directory for setup', process.cwd())
30
30
  .option('--skip-install', 'Skip installing dependencies')
31
+ .option('--preset <name>', 'Skip prompts; use defaults for a project type')
32
+ .option('--config <path>', 'Skip prompts; read a JSON ProjectConfig from <path>')
33
+ .option('--dry-run', 'Print the resolved config and file list, write nothing')
34
+ .option('--config-schema', 'Print the JSON Schema for ProjectConfig and exit')
31
35
  .action(setupProject);
32
36
  program
33
37
  .command('copy <config>')
@@ -52,41 +56,155 @@ program
52
56
  console.error(chalk.red(`\n❌ Error copying configuration: ${error}\n`));
53
57
  }
54
58
  });
59
+ const TOOL_CATALOG = [
60
+ {
61
+ name: 'TypeScript',
62
+ description: 'Base, React, Next.js, Node.js, Express tsconfig presets',
63
+ exports: [
64
+ '@rtorcato/js-tooling/typescript/base',
65
+ '@rtorcato/js-tooling/typescript/react',
66
+ '@rtorcato/js-tooling/typescript/next',
67
+ '@rtorcato/js-tooling/typescript/node',
68
+ '@rtorcato/js-tooling/typescript/express',
69
+ '@rtorcato/js-tooling/typescript/test',
70
+ '@rtorcato/js-tooling/typescript/reset',
71
+ ],
72
+ fixTarget: 'tsconfig',
73
+ },
74
+ {
75
+ name: 'ESLint',
76
+ description: 'Base and Next.js ESLint configurations',
77
+ exports: ['@rtorcato/js-tooling/eslint/base', '@rtorcato/js-tooling/eslint/nextjs'],
78
+ fixTarget: 'eslint',
79
+ },
80
+ {
81
+ name: 'Biome',
82
+ description: 'Fast formatter and linter configuration',
83
+ exports: ['@rtorcato/js-tooling/biome'],
84
+ fixTarget: 'biome',
85
+ },
86
+ {
87
+ name: 'Prettier',
88
+ description: 'Code formatter configuration',
89
+ exports: ['@rtorcato/js-tooling/prettier'],
90
+ fixTarget: 'prettier',
91
+ },
92
+ {
93
+ name: 'Vitest',
94
+ description: 'Testing framework configuration',
95
+ exports: [
96
+ '@rtorcato/js-tooling/vitest/config',
97
+ '@rtorcato/js-tooling/vitest/react',
98
+ '@rtorcato/js-tooling/vitest/setup',
99
+ ],
100
+ fixTarget: 'vitest',
101
+ },
102
+ {
103
+ name: 'Jest',
104
+ description: 'Testing framework presets for browser and Node.js',
105
+ exports: [
106
+ '@rtorcato/js-tooling/jest-presets/browser/jest-preset',
107
+ '@rtorcato/js-tooling/jest-presets/node/jest-preset',
108
+ ],
109
+ fixTarget: null,
110
+ },
111
+ {
112
+ name: 'Playwright',
113
+ description: 'End-to-end testing configuration',
114
+ exports: ['@rtorcato/js-tooling/playwright'],
115
+ fixTarget: null,
116
+ },
117
+ {
118
+ name: 'Commitlint',
119
+ description: 'Conventional commit linting',
120
+ exports: ['@rtorcato/js-tooling/commitlint/config'],
121
+ fixTarget: 'commitlint',
122
+ },
123
+ {
124
+ name: 'Husky',
125
+ description: 'Git hooks for pre-commit validation',
126
+ exports: [],
127
+ fixTarget: 'husky',
128
+ },
129
+ {
130
+ name: 'lint-staged',
131
+ description: 'Run linters on staged files (pairs with Husky)',
132
+ exports: [],
133
+ fixTarget: 'husky',
134
+ },
135
+ {
136
+ name: 'Semantic Release',
137
+ description: 'Automated versioning and publishing',
138
+ exports: [
139
+ '@rtorcato/js-tooling/semantic-release',
140
+ '@rtorcato/js-tooling/semantic-release/github',
141
+ '@rtorcato/js-tooling/semantic-release/docker',
142
+ ],
143
+ fixTarget: 'semantic-release',
144
+ },
145
+ {
146
+ name: 'tsup',
147
+ description: 'TypeScript bundler configuration',
148
+ exports: ['@rtorcato/js-tooling/tsup'],
149
+ fixTarget: null,
150
+ },
151
+ {
152
+ name: 'esbuild',
153
+ description: 'Fast JavaScript bundler configuration',
154
+ exports: ['@rtorcato/js-tooling/esbuild'],
155
+ fixTarget: null,
156
+ },
157
+ {
158
+ name: 'Vite',
159
+ description: 'Modern web app build tool configuration',
160
+ exports: ['@rtorcato/js-tooling/vite'],
161
+ fixTarget: null,
162
+ },
163
+ {
164
+ name: 'EditorConfig',
165
+ description: 'Cross-editor formatting consistency (.editorconfig)',
166
+ exports: [],
167
+ fixTarget: 'editorconfig',
168
+ },
169
+ {
170
+ name: '.nvmrc',
171
+ description: 'Pin Node version per repository',
172
+ exports: [],
173
+ fixTarget: 'nvmrc',
174
+ },
175
+ {
176
+ name: 'knip',
177
+ description: 'Find unused files, exports, and dependencies',
178
+ exports: [],
179
+ fixTarget: 'knip',
180
+ },
181
+ {
182
+ name: 'Dependabot',
183
+ description: 'Weekly automated dependency updates',
184
+ exports: [],
185
+ fixTarget: 'dependabot',
186
+ },
187
+ {
188
+ name: 'CodeQL',
189
+ description: 'GitHub security scanning workflow',
190
+ exports: [],
191
+ fixTarget: 'codeql',
192
+ },
193
+ ];
55
194
  program
56
195
  .command('list')
57
196
  .alias('ls')
58
197
  .description('📋 List all available tooling configurations')
59
- .action(() => {
198
+ .option('--json', 'Emit machine-readable JSON output')
199
+ .action((options) => {
200
+ if (options.json) {
201
+ console.log(JSON.stringify({ tools: TOOL_CATALOG }, null, 2));
202
+ return;
203
+ }
60
204
  console.log(chalk.cyan('\n🛠️ Available tooling configurations:\n'));
61
- const configs = [
62
- {
63
- name: 'TypeScript',
64
- desc: 'Base, React, Next.js, Node.js, Express configurations',
65
- },
66
- { name: 'ESLint', desc: 'Base and Next.js ESLint configurations' },
67
- { name: 'Biome', desc: 'Fast formatter and linter configuration' },
68
- { name: 'Prettier', desc: 'Code formatter configuration' },
69
- { name: 'Vitest', desc: 'Testing framework configuration' },
70
- {
71
- name: 'Jest',
72
- desc: 'Testing framework presets for browser and Node.js',
73
- },
74
- { name: 'Playwright', desc: 'End-to-end testing configuration' },
75
- { name: 'Commitlint', desc: 'Conventional commit linting' },
76
- { name: 'Husky', desc: 'Git hooks for pre-commit validation' },
77
- { name: 'lint-staged', desc: 'Run linters on staged files (pairs with Husky)' },
78
- { name: 'Semantic Release', desc: 'Automated versioning and publishing' },
79
- { name: 'tsup', desc: 'TypeScript bundler configuration' },
80
- { name: 'esbuild', desc: 'Fast JavaScript bundler configuration' },
81
- { name: 'EditorConfig', desc: 'Cross-editor formatting consistency (.editorconfig)' },
82
- { name: '.nvmrc', desc: 'Pin Node version per repository' },
83
- { name: 'knip', desc: 'Find unused files, exports, and dependencies' },
84
- { name: 'Dependabot', desc: 'Weekly automated dependency updates' },
85
- { name: 'CodeQL', desc: 'GitHub security scanning workflow' },
86
- ];
87
- configs.forEach(({ name, desc }) => {
88
- console.log(` ${chalk.green('●')} ${chalk.bold(name)}: ${chalk.gray(desc)}`);
89
- });
205
+ for (const { name, description } of TOOL_CATALOG) {
206
+ console.log(` ${chalk.green('●')} ${chalk.bold(name)}: ${chalk.gray(description)}`);
207
+ }
90
208
  console.log(chalk.dim('\n💡 Run `js-tooling setup` for a new project'));
91
209
  console.log(chalk.dim(' or `js-tooling fix` to apply missing pieces to an existing one\n'));
92
210
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -47,10 +47,14 @@
47
47
  "tooling/eslint/*.d.mts",
48
48
  "tooling/eslint/types.d.ts",
49
49
  "tooling/jest-presets/**",
50
+ "tooling/playwright/playwright.config.mjs",
51
+ "tooling/playwright/playwright.config.d.mts",
50
52
  "tooling/prettier/index.mjs",
51
53
  "tooling/prettier/index.d.mts",
52
54
  "tooling/typescript/*.json",
53
55
  "tooling/typescript/reset.d.ts",
56
+ "tooling/vite/vite.config.mjs",
57
+ "tooling/vite/vite.config.d.mts",
54
58
  "tooling/vitest/vitest.config.mjs",
55
59
  "tooling/vitest/vitest.config.d.mts",
56
60
  "tooling/vitest/vitest.config.react.mjs",
@@ -88,6 +92,10 @@
88
92
  "types": "./tooling/jest-presets/node/jest-preset.d.mts",
89
93
  "import": "./tooling/jest-presets/node/jest-preset.mjs"
90
94
  },
95
+ "./playwright": {
96
+ "types": "./tooling/playwright/playwright.config.d.mts",
97
+ "import": "./tooling/playwright/playwright.config.mjs"
98
+ },
91
99
  "./prettier": {
92
100
  "types": "./tooling/prettier/index.d.mts",
93
101
  "import": "./tooling/prettier/index.mjs"
@@ -99,6 +107,10 @@
99
107
  "./typescript/react": "./tooling/typescript/tsconfig.react.json",
100
108
  "./typescript/test": "./tooling/typescript/tsconfig.test.json",
101
109
  "./typescript/reset": "./tooling/typescript/reset.d.ts",
110
+ "./vite": {
111
+ "types": "./tooling/vite/vite.config.d.mts",
112
+ "import": "./tooling/vite/vite.config.mjs"
113
+ },
102
114
  "./vitest/config": {
103
115
  "types": "./tooling/vitest/vitest.config.d.mts",
104
116
  "import": "./tooling/vitest/vitest.config.mjs"
@@ -20,8 +20,9 @@ export default {
20
20
  "test",
21
21
  ],
22
22
  ],
23
- // Enforce length limits (strict)
24
- "header-max-length": [2, "always", 50],
23
+ // Enforce length limits: 72 chars matches the conventional-commits
24
+ // recommendation and git's default email format
25
+ "header-max-length": [2, "always", 72],
25
26
  "body-max-line-length": [2, "always", 72],
26
27
  "footer-max-line-length": [2, "always", 72],
27
28
  // Enforce case rules (allow common patterns)
@@ -0,0 +1,4 @@
1
+ import type { PlaywrightTestConfig } from '@playwright/test'
2
+
3
+ declare const config: PlaywrightTestConfig
4
+ export default config
@@ -0,0 +1,19 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+
3
+ export default defineConfig({
4
+ testDir: './tests/e2e',
5
+ fullyParallel: true,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 2 : 0,
8
+ workers: process.env.CI ? 1 : undefined,
9
+ reporter: 'html',
10
+ use: {
11
+ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000',
12
+ trace: 'on-first-retry',
13
+ },
14
+ projects: [
15
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
16
+ { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
17
+ { name: 'webkit', use: { ...devices['Desktop Safari'] } },
18
+ ],
19
+ })
@@ -0,0 +1,4 @@
1
+ import type { UserConfig } from 'vite'
2
+
3
+ declare const config: UserConfig
4
+ export default config
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite'
2
+
3
+ export default defineConfig({
4
+ resolve: {
5
+ alias: {
6
+ '@': '/src',
7
+ '~': '/src',
8
+ },
9
+ },
10
+ build: {
11
+ outDir: 'dist',
12
+ sourcemap: true,
13
+ },
14
+ server: {
15
+ port: 3000,
16
+ open: true,
17
+ },
18
+ })