@rtorcato/js-tooling 2.18.0 → 2.19.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/README.md CHANGED
@@ -17,12 +17,65 @@ Most tooling libraries give you one piece — just TypeScript configs, or just a
17
17
 
18
18
  **[Full documentation →](https://rtorcato.github.io/js-tooling/)**
19
19
 
20
- ## Quick start
20
+ ## Start a new project
21
+
22
+ Interactive wizard — answers every prompt, scaffolds the whole project:
21
23
 
22
24
  ```bash
23
25
  npx @rtorcato/js-tooling setup
24
26
  ```
25
27
 
28
+ Non-interactive — scaffold from a named preset in one shot (CI-friendly):
29
+
30
+ ```bash
31
+ npx @rtorcato/js-tooling setup --preset library -d ./my-lib --skip-install
32
+ # presets: library | web-app | node-api | nextjs-app | react-app
33
+ ```
34
+
35
+ Just one config file? Use `copy`:
36
+
37
+ ```bash
38
+ npx @rtorcato/js-tooling copy biome # → biome.json
39
+ npx @rtorcato/js-tooling copy tsconfig # → tsconfig.json
40
+ npx @rtorcato/js-tooling copy changesets # → .changeset/config.json
41
+ npx @rtorcato/js-tooling copy oxlint # → .oxlintrc.json
42
+ npx @rtorcato/js-tooling copy claude-skill # → .claude/skills/js-tooling.md
43
+ ```
44
+
45
+ **Already have a project?** Don't rerun `setup` — use `doctor` + `fix`:
46
+
47
+ ```bash
48
+ npx @rtorcato/js-tooling doctor # find what's missing or drifted
49
+ npx @rtorcato/js-tooling fix # apply scaffolders, prompting per item
50
+ ```
51
+
52
+ See the [Getting Started guide](https://rtorcato.github.io/js-tooling/guides/getting-started/) for the full walkthrough.
53
+
54
+ ## AI agent rules
55
+
56
+ The package ships rules that teach AI coding agents to drive the CLI
57
+ (`doctor` / `fix` / `setup`) non-interactively. Install for your agent — all
58
+ generated from one source, so guidance never drifts between them:
59
+
60
+ ```bash
61
+ npx @rtorcato/js-tooling fix claude-skill --yes # → .claude/skills/js-tooling.md
62
+ npx @rtorcato/js-tooling fix cursor-rules --yes # → .cursor/rules/js-tooling.mdc
63
+ npx @rtorcato/js-tooling fix copilot-instructions --yes # → .github/copilot-instructions.md
64
+ npx @rtorcato/js-tooling fix agents-md --yes # → AGENTS.md
65
+ ```
66
+
67
+ `copilot-instructions` and `agents-md` upsert a delimited block, so your own
68
+ content in those shared files is never clobbered. Re-running updates the block
69
+ in place on upgrade.
70
+
71
+ Prefer a symlink that auto-syncs the Claude skill on every upgrade?
72
+
73
+ ```bash
74
+ mkdir -p .claude/skills
75
+ ln -sf ../../node_modules/@rtorcato/js-tooling/tooling/claude/js-tooling.md \
76
+ .claude/skills/js-tooling.md
77
+ ```
78
+
26
79
  ## What's new
27
80
 
28
81
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
@@ -716,28 +716,28 @@ async function checkAreTheTypesWrong(_dir, pkg) {
716
716
  ...(pkg?.devDependencies ?? {}),
717
717
  };
718
718
  const scripts = pkg?.scripts ?? {};
719
- const hasDep = !!deps['are-the-types-wrong'];
720
- const hasScript = Object.values(scripts).some((s) => /\battw\b|are-the-types-wrong/.test(s));
719
+ const hasDep = !!deps['@arethetypeswrong/cli'];
720
+ const hasScript = Object.values(scripts).some((s) => /\battw\b/.test(s));
721
721
  if (hasDep && hasScript) {
722
722
  return {
723
723
  check: 'are-the-types-wrong',
724
724
  status: 'ok',
725
- detail: 'are-the-types-wrong installed and wired into a script',
725
+ detail: '@arethetypeswrong/cli installed and wired into a script',
726
726
  };
727
727
  }
728
728
  if (hasDep) {
729
729
  return {
730
730
  check: 'are-the-types-wrong',
731
731
  status: 'drift',
732
- detail: 'are-the-types-wrong installed but no script runs it',
733
- hint: 'Add `"attw": "attw --pack"` to package.json scripts and call it from your verify/CI chain',
732
+ detail: '@arethetypeswrong/cli installed but no script runs it',
733
+ hint: 'Run `npx @rtorcato/js-tooling fix attw` to add an `attw` script and wire it into verify',
734
734
  };
735
735
  }
736
736
  return {
737
737
  check: 'are-the-types-wrong',
738
738
  status: 'optional-missing',
739
- detail: 'are-the-types-wrong not configured',
740
- hint: 'Run `pnpm add -D are-the-types-wrong && attw --pack` to validate TypeScript exports before publishing',
739
+ detail: '@arethetypeswrong/cli not configured',
740
+ hint: 'Run `npx @rtorcato/js-tooling fix attw` to validate TypeScript exports before publishing',
741
741
  };
742
742
  }
743
743
  async function checkTreeshakeSetup(dir, pkg) {
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { createPatch } from 'diff';
5
5
  import fs from 'fs-extra';
6
6
  import inquirer from 'inquirer';
7
+ import { installAgentRules } from '../generators/agent-rules.js';
7
8
  import { generateSemanticReleaseConfig } from '../generators/build.js';
8
9
  import { generateCommitlintConfig, generateHuskyConfig, generatePrePushHook, } from '../generators/git.js';
9
10
  import { generateGitHubActions } from '../generators/github-actions.js';
@@ -59,6 +60,24 @@ async function readPackageJson(dir) {
59
60
  return null;
60
61
  }
61
62
  }
63
+ /** True when any (possibly nested) exports condition declares a `require`/CJS entry. */
64
+ function hasRequireCondition(exports) {
65
+ if (!exports || typeof exports !== 'object')
66
+ return false;
67
+ for (const value of Object.values(exports)) {
68
+ if (value && typeof value === 'object') {
69
+ if ('require' in value)
70
+ return true;
71
+ if (hasRequireCondition(value))
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ /** ESM-only = `"type": "module"` and no CJS/`require` resolution in exports. */
78
+ function isEsmOnly(pkg) {
79
+ return pkg.type === 'module' && !hasRequireCondition(pkg.exports);
80
+ }
62
81
  const FIXERS = [
63
82
  {
64
83
  target: 'biome',
@@ -408,6 +427,86 @@ const FIXERS = [
408
427
  return { filesWritten };
409
428
  },
410
429
  },
430
+ {
431
+ target: 'attw',
432
+ description: 'Install @arethetypeswrong/cli + add an `attw` script (esm-only profile when applicable) and wire it into verify',
433
+ appliesTo: ['are-the-types-wrong'],
434
+ outputs: ['package.json (devDependencies + scripts.attw)'],
435
+ riskLevel: 'safe-merge',
436
+ canFixDrift: true,
437
+ async run({ targetDir, pkg }) {
438
+ const pkgPath = path.join(targetDir, 'package.json');
439
+ if (!pkg) {
440
+ console.log(chalk.yellow(' no package.json found — skipping'));
441
+ return { filesWritten: [] };
442
+ }
443
+ const updated = { ...pkg };
444
+ const devDeps = {
445
+ ...(updated.devDependencies ?? {}),
446
+ };
447
+ if (!devDeps['@arethetypeswrong/cli'])
448
+ devDeps['@arethetypeswrong/cli'] = '^0.18.2';
449
+ updated.devDependencies = devDeps;
450
+ const scripts = { ...(updated.scripts ?? {}) };
451
+ scripts.attw = isEsmOnly(pkg) ? 'attw --pack --profile esm-only' : 'attw --pack';
452
+ if (scripts.verify && !/\battw\b/.test(scripts.verify)) {
453
+ scripts.verify = `${scripts.verify} && pnpm attw`;
454
+ }
455
+ updated.scripts = scripts;
456
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
457
+ return { filesWritten: ['package.json'] };
458
+ },
459
+ },
460
+ {
461
+ target: 'claude-skill',
462
+ description: 'Install the js-tooling Claude Code skill into .claude/skills/',
463
+ appliesTo: ['Claude skill'],
464
+ outputs: ['.claude/skills/js-tooling.md'],
465
+ riskLevel: 'safe-add',
466
+ canFixDrift: true,
467
+ async run({ targetDir }) {
468
+ const result = await copyPreset('claude-skill', targetDir);
469
+ return { filesWritten: [result.target] };
470
+ },
471
+ },
472
+ {
473
+ target: 'cursor-rules',
474
+ description: 'Install the js-tooling rules for Cursor (.cursor/rules/js-tooling.mdc)',
475
+ appliesTo: ['Cursor rules'],
476
+ outputs: ['.cursor/rules/js-tooling.mdc'],
477
+ riskLevel: 'safe-add',
478
+ canFixDrift: true,
479
+ async run({ targetDir }) {
480
+ const written = await installAgentRules(targetDir, 'cursor');
481
+ return { filesWritten: [written] };
482
+ },
483
+ },
484
+ {
485
+ target: 'copilot-instructions',
486
+ description: 'Install the js-tooling rules for GitHub Copilot (.github/copilot-instructions.md)',
487
+ appliesTo: ['Copilot instructions'],
488
+ outputs: ['.github/copilot-instructions.md'],
489
+ // Upserts a delimited block — never clobbers the consumer's own instructions.
490
+ riskLevel: 'safe-merge',
491
+ canFixDrift: true,
492
+ async run({ targetDir }) {
493
+ const written = await installAgentRules(targetDir, 'copilot');
494
+ return { filesWritten: [written] };
495
+ },
496
+ },
497
+ {
498
+ target: 'agents-md',
499
+ description: 'Install the js-tooling rules into AGENTS.md (universal agent instructions)',
500
+ appliesTo: ['AGENTS.md rules'],
501
+ outputs: ['AGENTS.md'],
502
+ // Upserts a delimited block — never clobbers existing AGENTS.md content.
503
+ riskLevel: 'safe-merge',
504
+ canFixDrift: true,
505
+ async run({ targetDir }) {
506
+ const written = await installAgentRules(targetDir, 'agents-md');
507
+ return { filesWritten: [written] };
508
+ },
509
+ },
411
510
  {
412
511
  target: 'package-json',
413
512
  description: 'Add @rtorcato/js-tooling to devDependencies',
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { getPackageRoot } from '../utils/copy-preset.js';
4
+ /**
5
+ * All agent rule files are generated from one source of truth — the shipped
6
+ * Claude skill — so the guidance never drifts between agents. Only the
7
+ * location and the frontmatter differ per agent.
8
+ */
9
+ const SOURCE = 'tooling/claude/js-tooling.md';
10
+ const BLOCK_START = '<!-- js-tooling:start -->';
11
+ const BLOCK_END = '<!-- js-tooling:end -->';
12
+ /** Read the shipped skill and split its frontmatter from the markdown body. */
13
+ async function readSkill() {
14
+ const raw = await fs.readFile(path.join(getPackageRoot(), SOURCE), 'utf8');
15
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
16
+ if (!match)
17
+ return { description: '', body: raw.trim() };
18
+ const description = (match[1].match(/^description:\s*(.+)$/m)?.[1] ?? '').trim();
19
+ return { description, body: match[2].trim() };
20
+ }
21
+ /**
22
+ * Upsert a delimited js-tooling block into a file that may already hold the
23
+ * consumer's own content (AGENTS.md, copilot-instructions). Replaces an
24
+ * existing block, appends if the file exists without one, creates otherwise.
25
+ * Never clobbers surrounding content.
26
+ */
27
+ async function upsertBlock(filePath, body) {
28
+ const block = `${BLOCK_START}\n${body}\n${BLOCK_END}`;
29
+ if (await fs.pathExists(filePath)) {
30
+ const existing = await fs.readFile(filePath, 'utf8');
31
+ const start = existing.indexOf(BLOCK_START);
32
+ const end = existing.indexOf(BLOCK_END);
33
+ if (start !== -1 && end !== -1) {
34
+ const next = existing.slice(0, start) + block + existing.slice(end + BLOCK_END.length);
35
+ await fs.writeFile(filePath, next);
36
+ return;
37
+ }
38
+ await fs.writeFile(filePath, `${existing.trimEnd()}\n\n${block}\n`);
39
+ return;
40
+ }
41
+ await fs.ensureDir(path.dirname(filePath));
42
+ await fs.writeFile(filePath, `${block}\n`);
43
+ }
44
+ /** Install the js-tooling rules for one agent. Returns the written path (relative). */
45
+ export async function installAgentRules(targetDir, agent) {
46
+ const { description, body } = await readSkill();
47
+ switch (agent) {
48
+ case 'cursor': {
49
+ const rel = path.join('.cursor', 'rules', 'js-tooling.mdc');
50
+ const file = path.join(targetDir, rel);
51
+ const frontmatter = `---\ndescription: ${description}\nglobs:\nalwaysApply: false\n---\n\n`;
52
+ await fs.ensureDir(path.dirname(file));
53
+ await fs.writeFile(file, `${frontmatter}${body}\n`);
54
+ return rel;
55
+ }
56
+ case 'copilot': {
57
+ const rel = path.join('.github', 'copilot-instructions.md');
58
+ await upsertBlock(path.join(targetDir, rel), body);
59
+ return rel;
60
+ }
61
+ case 'agents-md': {
62
+ const rel = 'AGENTS.md';
63
+ await upsertBlock(path.join(targetDir, rel), body);
64
+ return rel;
65
+ }
66
+ }
67
+ }
@@ -57,19 +57,15 @@ npx --no -- commitlint --edit $1
57
57
  // lint-staged configuration in package.json
58
58
  const packageJsonPath = path.join(targetDir, 'package.json');
59
59
  const packageJson = await fs.readJson(packageJsonPath);
60
+ // No explicit `git add` — lint-staged stages tool output itself, and the
61
+ // extra add races its index lock. `--no-errors-on-unmatched` keeps biome
62
+ // from failing a commit when every matched file is biome-ignored.
63
+ const useBiome = config.linting.tool === 'biome' || config.linting.tool === 'both';
60
64
  packageJson['lint-staged'] = {
61
- '*.{js,ts,jsx,tsx}': [
62
- config.linting.tool === 'biome' || config.linting.tool === 'both'
63
- ? 'biome check --fix'
64
- : 'eslint --fix',
65
- 'git add',
66
- ],
67
- '*.{json,md,yml,yaml}': [
68
- config.linting.tool === 'biome' || config.linting.tool === 'both'
69
- ? 'biome format --write'
70
- : 'prettier --write',
71
- 'git add',
72
- ],
65
+ '*.{js,ts,jsx,tsx}': useBiome ? 'biome check --fix --no-errors-on-unmatched' : 'eslint --fix',
66
+ '*.{json,md,yml,yaml}': useBiome
67
+ ? 'biome format --write --no-errors-on-unmatched'
68
+ : 'prettier --write',
73
69
  };
74
70
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
75
71
  }
@@ -53,7 +53,7 @@ jobs:
53
53
  - name: 📦 Setup Node.js
54
54
  uses: actions/setup-node@v4
55
55
  with:
56
- node-version: '20'
56
+ node-version-file: .nvmrc
57
57
 
58
58
  - name: 📦 Setup pnpm
59
59
  uses: pnpm/action-setup@v4
@@ -88,7 +88,7 @@ jobs:
88
88
  - name: 📦 Setup Node.js
89
89
  uses: actions/setup-node@v4
90
90
  with:
91
- node-version: '20'
91
+ node-version-file: .nvmrc
92
92
 
93
93
  - name: 📦 Setup pnpm
94
94
  uses: pnpm/action-setup@v4
@@ -118,7 +118,7 @@ ${hasTypeScript
118
118
  - name: 📦 Setup Node.js
119
119
  uses: actions/setup-node@v4
120
120
  with:
121
- node-version: '20'
121
+ node-version-file: .nvmrc
122
122
 
123
123
  - name: 📦 Setup pnpm
124
124
  uses: pnpm/action-setup@v4
@@ -149,7 +149,7 @@ ${hasTests
149
149
  - name: 📦 Setup Node.js
150
150
  uses: actions/setup-node@v4
151
151
  with:
152
- node-version: '20'
152
+ node-version-file: .nvmrc
153
153
 
154
154
  - name: 📦 Setup pnpm
155
155
  uses: pnpm/action-setup@v4
@@ -180,7 +180,7 @@ ${hasBuild
180
180
  - name: 📦 Setup Node.js
181
181
  uses: actions/setup-node@v4
182
182
  with:
183
- node-version: '20'
183
+ node-version-file: .nvmrc
184
184
 
185
185
  - name: 📦 Setup pnpm
186
186
  uses: pnpm/action-setup@v4
@@ -229,7 +229,7 @@ ${isLibrary && config.semanticRelease
229
229
  - name: 📦 Setup Node.js
230
230
  uses: actions/setup-node@v4
231
231
  with:
232
- node-version: '20'
232
+ node-version-file: .nvmrc
233
233
  registry-url: 'https://registry.npmjs.org'
234
234
 
235
235
  - name: 📦 Setup pnpm
@@ -27,13 +27,13 @@ export async function generateOxlintConfig(targetDir) {
27
27
  }
28
28
  export async function generateBiomeConfig(targetDir) {
29
29
  const biomeConfigPath = path.join(targetDir, 'biome.jsonc');
30
+ // Biome 2.x schema + shape. The base preset (extends) already defines the
31
+ // file globs via `files.includes`; emitting the old 1.x `include`/`ignore`
32
+ // keys here forced consumers to run `biome migrate` before `biome check`
33
+ // would run at all.
30
34
  const biomeConfig = {
31
- $schema: 'https://biomejs.dev/schemas/1.9.4/schema.json',
35
+ $schema: 'https://biomejs.dev/schemas/2.3.0/schema.json',
32
36
  extends: ['@rtorcato/js-tooling/biome'],
33
- files: {
34
- include: ['src/**/*', '*.ts', '*.js', '*.tsx', '*.jsx'],
35
- ignore: ['node_modules', 'dist', 'build', '.next'],
36
- },
37
37
  };
38
38
  await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
39
39
  }
@@ -26,16 +26,22 @@ export async function generatePackageJson(config, targetDir) {
26
26
  ...existingPackageJson?.devDependencies,
27
27
  },
28
28
  };
29
- // Add additional package.json fields based on project type
29
+ // Add additional package.json fields based on project type.
30
+ // Exports must match tsup's output for a "type": "module" package with
31
+ // format: ['cjs','esm']: ESM → index.js, CJS → index.cjs, types →
32
+ // index.d.ts (ESM) / index.d.cts (CJS).
30
33
  if (config.projectType === 'library') {
31
- packageJson.main = './dist/index.js';
32
- packageJson.module = './dist/index.mjs';
34
+ packageJson.main = './dist/index.cjs';
35
+ packageJson.module = './dist/index.js';
33
36
  packageJson.types = './dist/index.d.ts';
34
37
  packageJson.exports = {
35
38
  '.': {
36
- import: './dist/index.mjs',
37
- require: './dist/index.js',
38
- types: './dist/index.d.ts',
39
+ types: {
40
+ import: './dist/index.d.ts',
41
+ require: './dist/index.d.cts',
42
+ },
43
+ import: './dist/index.js',
44
+ require: './dist/index.cjs',
39
45
  },
40
46
  };
41
47
  packageJson.files = ['dist'];
@@ -43,6 +49,15 @@ export async function generatePackageJson(config, targetDir) {
43
49
  access: 'public',
44
50
  };
45
51
  }
52
+ // pnpm 11 refuses to run a dependency's build script unless it's approved.
53
+ // esbuild (pulled in by tsup/esbuild/vite) has one, so `pnpm install` exits
54
+ // 1 with ERR_PNPM_IGNORED_BUILDS until it's whitelisted here.
55
+ if (config.bundler === 'tsup' || config.bundler === 'esbuild' || config.bundler === 'vite') {
56
+ packageJson.pnpm = {
57
+ ...packageJson.pnpm,
58
+ onlyBuiltDependencies: ['esbuild'],
59
+ };
60
+ }
46
61
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
47
62
  }
48
63
  function getScripts(config, opts = {}) {
@@ -213,10 +228,14 @@ function getDependencies(config) {
213
228
  deps['@commitlint/cli'] = '^20.0.0';
214
229
  deps['@commitlint/config-conventional'] = '^20.0.0';
215
230
  }
216
- // Semantic release
231
+ // Semantic release. The shipped github preset activates the changelog and
232
+ // git plugins (and @semantic-release/github), so they must be installed too
233
+ // or `semantic-release` crashes with "Cannot find module".
217
234
  if (config.semanticRelease) {
218
235
  deps['semantic-release'] = '^25.0.0';
219
236
  deps['@semantic-release/github'] = '^12.0.0';
237
+ deps['@semantic-release/changelog'] = '^6.0.0';
238
+ deps['@semantic-release/git'] = '^10.0.0';
220
239
  }
221
240
  return deps;
222
241
  }
@@ -21,6 +21,11 @@ export const PRESETS = {
21
21
  target: 'tsconfig.json',
22
22
  desc: 'TypeScript base configuration',
23
23
  },
24
+ 'claude-skill': {
25
+ source: 'tooling/claude/js-tooling.md',
26
+ target: '.claude/skills/js-tooling.md',
27
+ desc: 'Claude Code skill for driving the js-tooling CLI',
28
+ },
24
29
  };
25
30
  export function getPackageRoot() {
26
31
  const cliFile = new URL(import.meta.url).pathname;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.18.0",
3
+ "version": "2.19.1",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -71,13 +71,15 @@
71
71
  "tooling/tests/exports-resolution.d.mts",
72
72
  "tooling/tests/ssr-safety.mjs",
73
73
  "tooling/tests/ssr-safety.d.mts",
74
- "tooling/tsup/index.ts",
74
+ "tooling/tsup/index.mjs",
75
+ "tooling/tsup/index.d.mts",
75
76
  "tooling/biome/biome.json",
76
77
  "tooling/changesets/config.json",
77
78
  "tooling/oxlint/oxlintrc.json",
78
79
  "tooling/typedoc/typedoc.json",
79
80
  "tooling/semantic-release/*.mjs",
80
81
  "tooling/semantic-release/*.d.mts",
82
+ "tooling/claude/*.md",
81
83
  "README.md"
82
84
  ],
83
85
  "exports": {
@@ -146,7 +148,10 @@
146
148
  "types": "./tooling/vitest/jsdom-shims.d.mts",
147
149
  "import": "./tooling/vitest/jsdom-shims.mjs"
148
150
  },
149
- "./tsup": "./tooling/tsup/index.ts",
151
+ "./tsup": {
152
+ "types": "./tooling/tsup/index.d.mts",
153
+ "import": "./tooling/tsup/index.mjs"
154
+ },
150
155
  "./biome": "./tooling/biome/biome.json",
151
156
  "./changesets": "./tooling/changesets/config.json",
152
157
  "./oxlint": "./tooling/oxlint/oxlintrc.json",
@@ -199,8 +204,8 @@
199
204
  "@total-typescript/ts-reset": "0.6.1",
200
205
  "@types/fs-extra": "^11.0.4",
201
206
  "@types/node": "^25.9.2",
202
- "@typescript-eslint/eslint-plugin": "^8.46.2",
203
- "@typescript-eslint/parser": "^8.46.2",
207
+ "@typescript-eslint/eslint-plugin": "^8.61.1",
208
+ "@typescript-eslint/parser": "^8.61.1",
204
209
  "@vitejs/plugin-react": "^5.1.0",
205
210
  "@vitest/coverage-v8": "^4.0.3",
206
211
  "commitizen": "^4.3.1",
@@ -208,9 +213,9 @@
208
213
  "cz-conventional-changelog": "^3.3.0",
209
214
  "esbuild": "^0.28.0",
210
215
  "esbuild-node-externals": "^1.22.0",
211
- "eslint": "9.38.0",
216
+ "eslint": "10.5.0",
212
217
  "eslint-plugin-import": "^2.32.0",
213
- "eslint-plugin-jest": "29.0.1",
218
+ "eslint-plugin-jest": "29.15.2",
214
219
  "husky": "^9.1.7",
215
220
  "is-ci": "^4.1.0",
216
221
  "jest": "^29.7.0",
@@ -223,7 +228,7 @@
223
228
  "ts-jest": "^29.4.11",
224
229
  "tsup": "8.5.1",
225
230
  "typescript": "^5.9.3",
226
- "typescript-eslint": "^8.60.0",
231
+ "typescript-eslint": "^8.61.1",
227
232
  "vitest": "^4.1.8"
228
233
  },
229
234
  "peerDependencies": {
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: js-tooling
3
+ description: Use when auditing or fixing TypeScript/JavaScript project tooling in a repo that depends on @rtorcato/js-tooling, or scaffolding a new project with it. Triggers on "audit my tooling", "fix tooling drift", "is my tsconfig/biome/vitest config right", "set up CI/semantic-release/dependabot", "scaffold a TS library/web-app/node-api", "run doctor", "run fix", or "/js-tooling". Drives the `@rtorcato/js-tooling` CLI non-interactively (--json --yes). NOT for hand-editing configs the CLI owns — let the CLI scaffold them so they stay in sync with the presets.
4
+ ---
5
+
6
+ # js-tooling
7
+
8
+ `@rtorcato/js-tooling` is a single-package TS/JS tooling distribution: every preset
9
+ (TypeScript, Biome, ESLint, Prettier, Vitest/Jest, Commitlint, semantic-release,
10
+ tsup/esbuild/Vite/Playwright) plus a CLI to scaffold and audit. Prefer the CLI over
11
+ hand-editing the configs it owns — a manual edit drifts from the preset and `doctor`
12
+ will flag it.
13
+
14
+ Every command takes `--json` and a non-interactive mode; pair with `--yes` for
15
+ autonomous use. `--json` implies `--yes` (a prompt would corrupt the JSON).
16
+
17
+ Run via `npx @rtorcato/js-tooling <cmd>` (or the local `js-tooling` bin if installed).
18
+ `-d <dir>` targets a directory other than cwd.
19
+
20
+ ## The two workflows you'll use most
21
+
22
+ ### Audit → fix → confirm (existing repo)
23
+
24
+ ```bash
25
+ npx @rtorcato/js-tooling doctor --json # findings
26
+ npx @rtorcato/js-tooling fix --yes --json # apply every fixable finding
27
+ npx @rtorcato/js-tooling doctor --json # confirm clean
28
+ ```
29
+
30
+ `doctor` returns `{ directory, results: [{ check, status, detail, hint? }] }`.
31
+ Status is one of:
32
+ - `ok` — configured correctly, nothing to do.
33
+ - `drift` — file exists but doesn't extend our preset. `fix` defaults the overwrite
34
+ prompt to **No**; `--yes` is required to overwrite.
35
+ - `missing` — required and absent → fix it.
36
+ - `optional-missing` — opt-in tool not configured. Only fix if the user wants that tool.
37
+
38
+ `fix` returns `FixActionRecord[]` with `status: applied | dry-run | skipped | already-ok | unsupported`.
39
+
40
+ ### Targeted fix (one concern)
41
+
42
+ ```bash
43
+ npx @rtorcato/js-tooling list --json # enumerate targets
44
+ npx @rtorcato/js-tooling fix <target> --yes --json # e.g. biome, vitest, dependabot, attw
45
+ npx @rtorcato/js-tooling fix <target> --dry-run --json # preview writes
46
+ npx @rtorcato/js-tooling fix <target> --diff # unified diff before confirming
47
+ ```
48
+
49
+ `list --json` is the source of truth for valid targets — read it, don't guess.
50
+
51
+ ## Scaffolding a new project
52
+
53
+ ```bash
54
+ # Quick: from a named preset (library | web-app | node-api | nextjs-app | react-app)
55
+ npx @rtorcato/js-tooling setup --preset library -d ./my-lib --skip-install
56
+
57
+ # Full control: validate a config against the schema, preview, then write
58
+ npx @rtorcato/js-tooling setup --config-schema > project-config.schema.json
59
+ npx @rtorcato/js-tooling setup --config project.json --dry-run # preview file list
60
+ npx @rtorcato/js-tooling setup --config project.json -d ./my-lib --skip-install
61
+ ```
62
+
63
+ ## Drift policy (don't surprise the user)
64
+
65
+ - Safe-merge fixers (`engines`, `husky`, `package-json`) never overwrite — they add/merge.
66
+ - Drift on a config file (`biome`, `tsconfig`, …) is only overwritten with `--yes`.
67
+ Before overwriting drift the user wrote by hand, show `fix <target> --diff` first.
68
+ - `optional-missing` ≠ broken. Don't install opt-in tools (typedoc, size-limit,
69
+ treeshake-check, attw, codeql) unless the user asked for that capability.
70
+
71
+ ## Rules
72
+
73
+ - Let the CLI own its configs. If `doctor` says `drift`, fix via the CLI, don't hand-patch.
74
+ - Use `--json` whenever you'll parse the result; use `--dry-run`/`--diff` before any
75
+ destructive overwrite.
76
+ - After a `fix`, re-run `doctor` to confirm the finding cleared.
77
+
78
+ Full docs: https://rtorcato.github.io/js-tooling/guides/cli/
@@ -0,0 +1,8 @@
1
+ import type { Options } from 'tsup'
2
+ import type { defineConfig } from 'tsup'
3
+
4
+ export type DefineConfig = ReturnType<typeof defineConfig>
5
+
6
+ export declare const getConfig: (customOptions: Options, env: string) => DefineConfig
7
+
8
+ export declare const baseOptions: (options: Options, env: string) => Options
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export const getConfig = (customOptions, env) => {
4
+ return defineConfig((options = customOptions) => {
5
+ return baseOptions(options, env)
6
+ })
7
+ }
8
+
9
+ export const baseOptions = (options, env) => {
10
+ const opts = {
11
+ treeshake: true,
12
+ splitting: true,
13
+ format: ['cjs', 'esm'], // generate cjs and esm files
14
+ entry: ['src/**/*.ts'],
15
+ skipNodeModulesBundle: true, // Skips building dependencies for node modules
16
+ minify: !options.watch && env === 'production',
17
+ bundle: false,
18
+ clean: true, // clean up the dist folder
19
+ dts: true, // generate dts file for main module
20
+ ...options,
21
+ }
22
+ return opts
23
+ }
@@ -27,6 +27,10 @@
27
27
  // 📈 Performance
28
28
  "skipLibCheck": true, // Skip type checking of declaration files
29
29
  "incremental": true, // Enable incremental compilation
30
+ // Pin the build-info location so downstream emit tools (e.g. tsup's `dts`
31
+ // build) don't hit TS5074 from inheriting `incremental` without a file.
32
+ // ${configDir} resolves to the consuming project's dir (TS 5.5+).
33
+ "tsBuildInfoFile": "${configDir}/node_modules/.cache/tsconfig.tsbuildinfo",
30
34
  "disableSourceOfProjectReferenceRedirect": true, // Disable source of project reference redirect
31
35
 
32
36
  // 🚨 Strict Type Checking
@@ -1,50 +0,0 @@
1
- import type { Options } from 'tsup'
2
- import { defineConfig } from 'tsup'
3
-
4
- export type DefineConfig = ReturnType<typeof defineConfig>
5
-
6
- export const getConfig: (customOptions: Options, env: string) => DefineConfig = (
7
- customOptions: Options,
8
- env: string
9
- ): DefineConfig => {
10
- return defineConfig((options: Options = customOptions) => {
11
- return baseOptions(options, env)
12
- })
13
- }
14
-
15
- export const baseOptions = (options: Options, env: string): Options => {
16
- const opts: Options = {
17
- treeshake: true,
18
- splitting: true,
19
- // target: 'es2020',
20
- // target: 'nodeNext',
21
- format: ['cjs', 'esm'], // generate cjs and esm files
22
- entry: [
23
- // './src/index.ts',
24
- 'src/**/*.ts',
25
- // './src/**/*!(index).ts?(x)',
26
- // '!./src/**/*.spec.*',
27
- // '!./src/**/*.stories.*',
28
- ],
29
- skipNodeModulesBundle: true, // Skips building dependencies for node modules
30
- minify: !options.watch && env === 'production',
31
- bundle: false, //env === 'production',
32
- clean: true, // clean up the dist folder
33
- dts: true, // generate dts file for main module
34
- // sourcemap: env === 'production', // source map is only available in prod
35
- // sourcemap: true,
36
- // outDir: env === 'production' ? 'dist' : 'lib',
37
- // outDir: 'dist',
38
- // tsconfig: path.resolve(__dirname, './tsconfig.build.json'),
39
- // esbuildOptions(options, context) {
40
- // options.outbase = './'
41
- // },
42
- // external: ['react'],
43
- ...options,
44
- // banner: {js: '"use client";'},
45
- // dts: {
46
- // footer: "declare module 'knex/types/tables';"
47
- // },
48
- }
49
- return opts
50
- }