@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 +54 -1
- package/dist/cli/commands/doctor.js +7 -7
- package/dist/cli/commands/fix.js +99 -0
- package/dist/cli/generators/agent-rules.js +67 -0
- package/dist/cli/generators/git.js +8 -12
- package/dist/cli/generators/github-actions.js +6 -6
- package/dist/cli/generators/linting.js +5 -5
- package/dist/cli/generators/package-json.js +26 -7
- package/dist/cli/utils/copy-preset.js +5 -0
- package/package.json +13 -8
- package/tooling/claude/js-tooling.md +78 -0
- package/tooling/tsup/index.d.mts +8 -0
- package/tooling/tsup/index.mjs +23 -0
- package/tooling/typescript/tsconfig.base.json +4 -0
- package/tooling/tsup/index.ts +0 -50
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
|
-
##
|
|
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['
|
|
720
|
-
const hasScript = Object.values(scripts).some((s) => /\battw\b
|
|
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: '
|
|
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: '
|
|
733
|
-
hint: '
|
|
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: '
|
|
740
|
-
hint: 'Run `
|
|
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) {
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
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.
|
|
32
|
-
packageJson.module = './dist/index.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
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.
|
|
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":
|
|
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.
|
|
203
|
-
"@typescript-eslint/parser": "^8.
|
|
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": "
|
|
216
|
+
"eslint": "10.5.0",
|
|
212
217
|
"eslint-plugin-import": "^2.32.0",
|
|
213
|
-
"eslint-plugin-jest": "29.
|
|
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.
|
|
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
|
package/tooling/tsup/index.ts
DELETED
|
@@ -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
|
-
}
|