@bensandee/tooling 0.3.0 → 0.4.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/dist/bin.mjs CHANGED
@@ -48,18 +48,6 @@ const RenovateSchema = z.object({
48
48
  $schema: z.string().optional(),
49
49
  extends: z.array(z.string()).optional()
50
50
  }).loose();
51
- const KnipWorkspaceConfig = z.object({
52
- entry: z.array(z.string()).optional(),
53
- project: z.array(z.string()).optional(),
54
- ignore: z.array(z.string()).optional()
55
- });
56
- const KnipSchema = z.object({
57
- $schema: z.string().optional(),
58
- entry: z.array(z.string()).optional(),
59
- project: z.array(z.string()).optional(),
60
- ignore: z.array(z.string()).optional(),
61
- workspaces: z.record(z.string(), KnipWorkspaceConfig).optional()
62
- }).loose();
63
51
  /** Parse a JSONC string as a tsconfig.json. Returns a typed object with `{}` fallback on failure. */
64
52
  function parseTsconfig(raw) {
65
53
  const result = TsconfigSchema.safeParse(parse(raw));
@@ -70,11 +58,6 @@ function parseRenovateJson(raw) {
70
58
  const result = RenovateSchema.safeParse(parse(raw));
71
59
  return result.success ? result.data : {};
72
60
  }
73
- /** Parse a JSONC string as a knip.json. Returns a typed object with `{}` fallback on failure. */
74
- function parseKnipJson(raw) {
75
- const result = KnipSchema.safeParse(parse(raw));
76
- return result.success ? result.data : {};
77
- }
78
61
  /** Parse a JSON string as a package.json. Returns `undefined` on failure. */
79
62
  function parsePackageJson(raw) {
80
63
  try {
@@ -208,11 +191,11 @@ async function runInitPrompts(targetDir) {
208
191
  p.cancel("Cancelled.");
209
192
  process.exit(0);
210
193
  }
211
- const useLintRules = await p.confirm({
212
- message: "Include @bensandee/lint-rules?",
194
+ const useEslintPlugin = await p.confirm({
195
+ message: "Include @bensandee/eslint-plugin?",
213
196
  initialValue: true
214
197
  });
215
- if (isCancelled(useLintRules)) {
198
+ if (isCancelled(useEslintPlugin)) {
216
199
  p.cancel("Cancelled.");
217
200
  process.exit(0);
218
201
  }
@@ -361,7 +344,7 @@ async function runInitPrompts(targetDir) {
361
344
  name,
362
345
  isNew: !isExisting,
363
346
  structure,
364
- useLintRules,
347
+ useEslintPlugin,
365
348
  formatter,
366
349
  setupVitest,
367
350
  ci,
@@ -380,7 +363,7 @@ function buildDefaultConfig(targetDir, flags) {
380
363
  name: existingPkg?.name ?? path.basename(targetDir),
381
364
  isNew: !detected.hasPackageJson,
382
365
  structure: detected.hasPnpmWorkspace ? "monorepo" : "single",
383
- useLintRules: flags.lintRules ?? true,
366
+ useEslintPlugin: flags.eslintPlugin ?? true,
384
367
  formatter: detected.legacyConfigs.some((l) => l.tool === "prettier") ? "prettier" : "oxfmt",
385
368
  setupVitest: !detected.hasVitestConfig,
386
369
  ci: flags.noCi ? "none" : DEFAULT_CI,
@@ -449,7 +432,6 @@ const STANDARD_SCRIPTS_MONOREPO = {
449
432
  };
450
433
  /** DevDeps that belong in every project (single repo) or per-package (monorepo). */
451
434
  const PER_PACKAGE_DEV_DEPS = {
452
- "@tsconfig/strictest": "2.0.8",
453
435
  "@types/node": "25.3.2",
454
436
  tsdown: "0.20.3",
455
437
  typescript: "5.9.3",
@@ -527,7 +509,7 @@ async function generatePackageJson(ctx) {
527
509
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
528
510
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "latest";
529
511
  devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "latest";
530
- if (ctx.config.useLintRules) devDeps["@bensandee/lint-rules"] = isWorkspacePackage(ctx, "@bensandee/lint-rules") ? "workspace:*" : "latest";
512
+ if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "latest";
531
513
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
532
514
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
533
515
  addReleaseDeps(devDeps, ctx.config);
@@ -710,7 +692,7 @@ function generateMigratePrompt(results, config, detected) {
710
692
  sections.push("");
711
693
  sections.push("- **Lint errors**: fix the code rather than adding disable comments or rule exceptions");
712
694
  sections.push("- **Test failures**: update the test or fix the underlying bug rather than skipping or deleting the test");
713
- sections.push("- **Knip findings**: remove genuinely unused code/exports/dependencies rather than adding ignores to `knip.json`");
695
+ sections.push("- **Knip findings**: remove genuinely unused code/exports/dependencies rather than adding ignores to `knip.config.ts`");
714
696
  sections.push("- **Type errors**: add proper types rather than using `any` or `@ts-expect-error`");
715
697
  sections.push("");
716
698
  sections.push("Only suppress an issue if there is a clear, documented reason why the fix is not feasible (e.g. a third-party type mismatch). Leave a comment explaining why.");
@@ -942,7 +924,7 @@ export default defineConfig({
942
924
  `;
943
925
  async function generateOxlint(ctx) {
944
926
  const filePath = "oxlint.config.ts";
945
- const content = ctx.config.useLintRules ? CONFIG_WITH_LINT_RULES : CONFIG_PRESET_ONLY;
927
+ const content = ctx.config.useEslintPlugin ? CONFIG_WITH_LINT_RULES : CONFIG_PRESET_ONLY;
946
928
  const existing = ctx.read(filePath);
947
929
  if (existing) {
948
930
  if (existing === content) return {
@@ -1146,88 +1128,62 @@ async function generateCi(ctx) {
1146
1128
 
1147
1129
  //#endregion
1148
1130
  //#region src/generators/knip.ts
1149
- const KNIP_CONFIG_SINGLE = {
1150
- $schema: "https://unpkg.com/knip@latest/schema.json",
1151
- entry: ["src/index.ts"],
1152
- project: ["src/**/*.ts"],
1153
- ignore: ["dist/**"]
1154
- };
1155
- const KNIP_CONFIG_MONOREPO = {
1156
- $schema: "https://unpkg.com/knip@latest/schema.json",
1157
- workspaces: {
1158
- ".": {
1159
- entry: [],
1160
- project: []
1161
- },
1162
- "packages/*": {
1163
- entry: ["src/index.ts", "src/bin.ts"],
1164
- project: ["src/**/*.ts"],
1165
- ignore: ["dist/**"]
1166
- }
1167
- }
1168
- };
1131
+ const KNIP_CONFIG_SINGLE = `import type { KnipConfig } from "knip";
1132
+
1133
+ export default {
1134
+ entry: ["src/index.ts"],
1135
+ project: ["src/**/*.ts"],
1136
+ ignore: ["dist/**"],
1137
+ } satisfies KnipConfig;
1138
+ `;
1139
+ const KNIP_CONFIG_MONOREPO = `import type { KnipConfig } from "knip";
1140
+
1141
+ export default {
1142
+ workspaces: {
1143
+ ".": {
1144
+ entry: [],
1145
+ project: [],
1146
+ },
1147
+ "packages/*": {
1148
+ entry: ["src/index.ts", "src/bin.ts"],
1149
+ project: ["src/**/*.ts"],
1150
+ ignore: ["dist/**"],
1151
+ },
1152
+ },
1153
+ } satisfies KnipConfig;
1154
+ `;
1169
1155
  /** All known knip config file locations, in priority order. */
1170
1156
  const KNIP_CONFIG_PATHS = [
1157
+ "knip.config.ts",
1158
+ "knip.config.mts",
1171
1159
  "knip.json",
1172
1160
  "knip.jsonc",
1173
1161
  "knip.ts",
1174
- "knip.mts",
1175
- "knip.config.ts",
1176
- "knip.config.mts"
1162
+ "knip.mts"
1177
1163
  ];
1178
1164
  async function generateKnip(ctx) {
1179
- const filePath = "knip.json";
1165
+ const filePath = "knip.config.ts";
1180
1166
  const isMonorepo = ctx.config.structure === "monorepo";
1181
1167
  const existingPath = KNIP_CONFIG_PATHS.find((p) => ctx.exists(p));
1182
- if (existingPath === filePath) {
1183
- const existing = ctx.read(filePath);
1184
- if (existing) {
1185
- const parsed = parseKnipJson(existing);
1186
- const changes = [];
1187
- if (isMonorepo && !parsed.workspaces) {
1188
- parsed.workspaces = KNIP_CONFIG_MONOREPO.workspaces;
1189
- changes.push("added monorepo workspaces config");
1190
- }
1191
- if (!isMonorepo) {
1192
- if (!parsed.entry) {
1193
- parsed.entry = KNIP_CONFIG_SINGLE.entry;
1194
- changes.push("added entry patterns");
1195
- }
1196
- if (!parsed.project) {
1197
- parsed.project = KNIP_CONFIG_SINGLE.project;
1198
- changes.push("added project patterns");
1199
- }
1200
- }
1201
- if (changes.length === 0) return {
1202
- filePath,
1203
- action: "skipped",
1204
- description: "Already configured"
1205
- };
1206
- ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1207
- return {
1208
- filePath,
1209
- action: "updated",
1210
- description: changes.join(", ")
1211
- };
1212
- }
1213
- }
1214
1168
  if (existingPath) return {
1215
1169
  filePath: existingPath,
1216
1170
  action: "skipped",
1217
- description: `Existing config found at ${existingPath}`
1171
+ description: existingPath === filePath ? "Already configured" : `Existing config found at ${existingPath}`
1218
1172
  };
1219
1173
  const config = isMonorepo ? KNIP_CONFIG_MONOREPO : KNIP_CONFIG_SINGLE;
1220
- ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
1174
+ ctx.write(filePath, config);
1221
1175
  return {
1222
1176
  filePath,
1223
1177
  action: "created",
1224
- description: "Generated knip.json for dead code analysis"
1178
+ description: "Generated knip.config.ts for dead code analysis"
1225
1179
  };
1226
1180
  }
1227
1181
 
1228
1182
  //#endregion
1229
1183
  //#region src/generators/renovate.ts
1230
- const SHARED_PRESET = "@bensandee/config";
1184
+ const SHARED_PRESET = "local>bensandee/tooling";
1185
+ /** Deprecated npm-based preset to migrate away from. */
1186
+ const LEGACY_PRESET = "@bensandee/config";
1231
1187
  /** All known renovate config file locations, in priority order. */
1232
1188
  const RENOVATE_CONFIG_PATHS = [
1233
1189
  "renovate.json",
@@ -1250,6 +1206,17 @@ async function generateRenovate(ctx) {
1250
1206
  if (existing) {
1251
1207
  const parsed = parseRenovateJson(existing);
1252
1208
  const existingExtends = parsed.extends ?? [];
1209
+ const legacyIndex = existingExtends.indexOf(LEGACY_PRESET);
1210
+ if (legacyIndex !== -1) {
1211
+ existingExtends[legacyIndex] = SHARED_PRESET;
1212
+ parsed.extends = existingExtends;
1213
+ ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1214
+ return {
1215
+ filePath,
1216
+ action: "updated",
1217
+ description: `Migrated extends: ${LEGACY_PRESET} → ${SHARED_PRESET}`
1218
+ };
1219
+ }
1253
1220
  if (!existingExtends.includes(SHARED_PRESET)) {
1254
1221
  existingExtends.unshift(SHARED_PRESET);
1255
1222
  parsed.extends = existingExtends;
@@ -1712,7 +1679,7 @@ async function generateReleaseCi(ctx) {
1712
1679
  //#endregion
1713
1680
  //#region src/generators/lint-staged.ts
1714
1681
  function buildConfig(formatter) {
1715
- return `export default {\n "*": "${formatter === "prettier" ? "prettier --write" : "oxfmt"}",\n};\n`;
1682
+ return `export default {\n "*": "${formatter === "prettier" ? "prettier --write" : "oxfmt --no-error-on-unmatched-pattern"}",\n};\n`;
1716
1683
  }
1717
1684
  const HUSKY_PRE_COMMIT = "pnpm exec lint-staged\n";
1718
1685
  /** All known lint-staged config file locations, in priority order. */
@@ -1778,9 +1745,9 @@ const initCommand = defineCommand({
1778
1745
  alias: "y",
1779
1746
  description: "Accept all defaults (non-interactive)"
1780
1747
  },
1781
- "lint-rules": {
1748
+ "eslint-plugin": {
1782
1749
  type: "boolean",
1783
- description: "Include @bensandee/lint-rules (default: true)"
1750
+ description: "Include @bensandee/eslint-plugin (default: true)"
1784
1751
  },
1785
1752
  "no-ci": {
1786
1753
  type: "boolean",
@@ -1794,7 +1761,7 @@ const initCommand = defineCommand({
1794
1761
  async run({ args }) {
1795
1762
  const targetDir = path.resolve(args.dir ?? ".");
1796
1763
  await runInit(args.yes ? buildDefaultConfig(targetDir, {
1797
- lintRules: args["lint-rules"] === true ? true : void 0,
1764
+ eslintPlugin: args["eslint-plugin"] === true ? true : void 0,
1798
1765
  noCi: args["no-ci"] === true ? true : void 0
1799
1766
  }) : await runInitPrompts(targetDir), args["no-prompt"] === true ? { noPrompt: true } : {});
1800
1767
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
@@ -9,17 +9,13 @@
9
9
  "dist"
10
10
  ],
11
11
  "type": "module",
12
- "main": "./dist/index.mjs",
13
- "module": "./dist/index.mjs",
14
- "types": "./dist/index.d.mts",
15
12
  "imports": {
16
13
  "#src/*": "./src/*.ts"
17
14
  },
18
15
  "exports": {
19
- ".": {
20
- "types": "./dist/index.d.mts",
21
- "import": "./dist/index.mjs"
22
- }
16
+ ".": "./dist/index.mjs",
17
+ "./bin": "./dist/bin.mjs",
18
+ "./package.json": "./package.json"
23
19
  },
24
20
  "publishConfig": {
25
21
  "access": "public"
@@ -35,7 +31,7 @@
35
31
  "tsdown": "0.20.3",
36
32
  "typescript": "5.9.3",
37
33
  "vitest": "4.0.18",
38
- "@bensandee/config": "0.3.0"
34
+ "@bensandee/config": "0.4.0"
39
35
  },
40
36
  "scripts": {
41
37
  "build": "tsdown",
package/dist/bin.d.mts DELETED
@@ -1 +0,0 @@
1
- export { };
package/dist/index.d.mts DELETED
@@ -1,102 +0,0 @@
1
- import { z } from "zod";
2
-
3
- //#region src/utils/json.d.ts
4
- declare const PackageJsonSchema: z.ZodObject<{
5
- name: z.ZodOptional<z.ZodString>;
6
- version: z.ZodOptional<z.ZodString>;
7
- private: z.ZodOptional<z.ZodBoolean>;
8
- type: z.ZodOptional<z.ZodString>;
9
- scripts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
10
- dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
- devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
12
- bin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
13
- exports: z.ZodOptional<z.ZodUnknown>;
14
- main: z.ZodOptional<z.ZodString>;
15
- types: z.ZodOptional<z.ZodString>;
16
- typings: z.ZodOptional<z.ZodString>;
17
- engines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
18
- }, z.core.$loose>;
19
- type PackageJson = z.infer<typeof PackageJsonSchema>;
20
- //#endregion
21
- //#region src/types.d.ts
22
- type CiPlatform = "github" | "forgejo" | "none";
23
- type ReleaseStrategy = "release-it" | "commit-and-tag-version" | "changesets" | "none";
24
- /** User's answers from the interactive prompt or CLI flags. */
25
- interface ProjectConfig {
26
- /** Project name (from package.json name or user input) */
27
- name: string;
28
- /** Whether this is a new project or existing */
29
- isNew: boolean;
30
- /** Project structure */
31
- structure: "single" | "monorepo";
32
- /** Include @bensandee/lint-rules oxlint plugin */
33
- useLintRules: boolean;
34
- /** Formatter choice */
35
- formatter: "oxfmt" | "prettier";
36
- /** Set up vitest with a starter test */
37
- setupVitest: boolean;
38
- /** CI platform choice */
39
- ci: CiPlatform;
40
- /** Set up Renovate for automated dependency updates */
41
- setupRenovate: boolean;
42
- /** Release management strategy */
43
- releaseStrategy: ReleaseStrategy;
44
- /** Project type determines tsconfig base configuration */
45
- projectType: "default" | "node" | "react" | "library";
46
- /** Auto-detect and configure tsconfig bases for monorepo packages */
47
- detectPackageTypes: boolean;
48
- /** Target directory (default: cwd) */
49
- targetDir: string;
50
- }
51
- /** Result from a single generator: what file was written and how. */
52
- interface GeneratorResult {
53
- filePath: string;
54
- action: "created" | "updated" | "skipped";
55
- /** Human-readable description of what changed */
56
- description: string;
57
- }
58
- /** Context passed to each generator function. */
59
- interface GeneratorContext {
60
- config: ProjectConfig;
61
- /** Absolute path to target directory */
62
- targetDir: string;
63
- /** Pre-parsed package.json from the target directory, or undefined if missing/invalid */
64
- packageJson: PackageJson | undefined;
65
- /** Check whether a file exists in the target directory */
66
- exists: (relativePath: string) => boolean;
67
- /** Read an existing file from the target directory, returns undefined if not found */
68
- read: (relativePath: string) => string | undefined;
69
- /** Write a file to the target directory (creating directories as needed) */
70
- write: (relativePath: string, content: string) => void;
71
- /** Prompt user for conflict resolution on non-mergeable files */
72
- confirmOverwrite: (relativePath: string) => Promise<"overwrite" | "skip">;
73
- }
74
- /** Generator function signature. */
75
- type Generator = (ctx: GeneratorContext) => Promise<GeneratorResult>;
76
- /** State detected from an existing project directory. */
77
- interface DetectedProjectState {
78
- hasPackageJson: boolean;
79
- hasTsconfig: boolean;
80
- hasOxlintConfig: boolean;
81
- /** Legacy .oxlintrc.json found (should be migrated to oxlint.config.ts) */
82
- hasLegacyOxlintJson: boolean;
83
- hasGitignore: boolean;
84
- hasVitestConfig: boolean;
85
- hasTsdownConfig: boolean;
86
- hasPnpmWorkspace: boolean;
87
- hasKnipConfig: boolean;
88
- hasRenovateConfig: boolean;
89
- hasReleaseItConfig: boolean;
90
- hasCommitAndTagVersionConfig: boolean;
91
- hasChangesetsConfig: boolean;
92
- /** Legacy tooling configs found */
93
- legacyConfigs: LegacyConfig[];
94
- }
95
- declare const LEGACY_TOOLS: readonly ["eslint", "prettier", "jest", "webpack", "rollup"];
96
- type LegacyTool = (typeof LEGACY_TOOLS)[number];
97
- interface LegacyConfig {
98
- tool: LegacyTool;
99
- files: string[];
100
- }
101
- //#endregion
102
- export { type DetectedProjectState, type Generator, type GeneratorContext, type GeneratorResult, type LegacyConfig, type ProjectConfig };