@bensandee/tooling 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +118 -57
  2. package/package.json +2 -2
package/dist/bin.mjs CHANGED
@@ -449,6 +449,7 @@ const STANDARD_SCRIPTS_MONOREPO = {
449
449
  };
450
450
  /** DevDeps that belong in every project (single repo) or per-package (monorepo). */
451
451
  const PER_PACKAGE_DEV_DEPS = {
452
+ "@tsconfig/strictest": "2.0.8",
452
453
  "@types/node": "25.3.2",
453
454
  tsdown: "0.20.3",
454
455
  typescript: "5.9.3",
@@ -1051,12 +1052,19 @@ const STANDARD_ENTRIES = [
1051
1052
  "!.env.example",
1052
1053
  ".tooling-migrate.md"
1053
1054
  ];
1055
+ /** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
1056
+ function normalizeEntry(entry) {
1057
+ let s = entry.trim();
1058
+ if (s.startsWith("/")) s = s.slice(1);
1059
+ if (s.endsWith("/")) s = s.slice(0, -1);
1060
+ return s;
1061
+ }
1054
1062
  async function generateGitignore(ctx) {
1055
1063
  const filePath = ".gitignore";
1056
1064
  const existing = ctx.read(filePath);
1057
1065
  if (existing) {
1058
- const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter((line) => line.length > 0));
1059
- const missing = STANDARD_ENTRIES.filter((entry) => !existingLines.has(entry));
1066
+ const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
1067
+ const missing = STANDARD_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
1060
1068
  if (missing.length === 0) return {
1061
1069
  filePath,
1062
1070
  action: "skipped",
@@ -1158,39 +1166,56 @@ const KNIP_CONFIG_MONOREPO = {
1158
1166
  }
1159
1167
  }
1160
1168
  };
1169
+ /** All known knip config file locations, in priority order. */
1170
+ const KNIP_CONFIG_PATHS = [
1171
+ "knip.json",
1172
+ "knip.jsonc",
1173
+ "knip.ts",
1174
+ "knip.mts",
1175
+ "knip.config.ts",
1176
+ "knip.config.mts"
1177
+ ];
1161
1178
  async function generateKnip(ctx) {
1162
1179
  const filePath = "knip.json";
1163
- const existing = ctx.read(filePath);
1164
1180
  const isMonorepo = ctx.config.structure === "monorepo";
1165
- if (existing) {
1166
- const parsed = parseKnipJson(existing);
1167
- const changes = [];
1168
- if (isMonorepo && !parsed.workspaces) {
1169
- parsed.workspaces = KNIP_CONFIG_MONOREPO.workspaces;
1170
- changes.push("added monorepo workspaces config");
1171
- }
1172
- if (!isMonorepo) {
1173
- if (!parsed.entry) {
1174
- parsed.entry = KNIP_CONFIG_SINGLE.entry;
1175
- changes.push("added entry patterns");
1181
+ 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");
1176
1190
  }
1177
- if (!parsed.project) {
1178
- parsed.project = KNIP_CONFIG_SINGLE.project;
1179
- changes.push("added project patterns");
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
+ }
1180
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
+ };
1181
1212
  }
1182
- if (changes.length === 0) return {
1183
- filePath,
1184
- action: "skipped",
1185
- description: "Already configured"
1186
- };
1187
- ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1188
- return {
1189
- filePath,
1190
- action: "updated",
1191
- description: changes.join(", ")
1192
- };
1193
1213
  }
1214
+ if (existingPath) return {
1215
+ filePath: existingPath,
1216
+ action: "skipped",
1217
+ description: `Existing config found at ${existingPath}`
1218
+ };
1194
1219
  const config = isMonorepo ? KNIP_CONFIG_MONOREPO : KNIP_CONFIG_SINGLE;
1195
1220
  ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
1196
1221
  return {
@@ -1203,6 +1228,15 @@ async function generateKnip(ctx) {
1203
1228
  //#endregion
1204
1229
  //#region src/generators/renovate.ts
1205
1230
  const SHARED_PRESET = "@bensandee/config";
1231
+ /** All known renovate config file locations, in priority order. */
1232
+ const RENOVATE_CONFIG_PATHS = [
1233
+ "renovate.json",
1234
+ "renovate.json5",
1235
+ ".renovaterc",
1236
+ ".renovaterc.json",
1237
+ ".github/renovate.json",
1238
+ ".github/renovate.json5"
1239
+ ];
1206
1240
  async function generateRenovate(ctx) {
1207
1241
  const filePath = "renovate.json";
1208
1242
  if (!ctx.config.setupRenovate) return {
@@ -1210,26 +1244,34 @@ async function generateRenovate(ctx) {
1210
1244
  action: "skipped",
1211
1245
  description: "Renovate not requested"
1212
1246
  };
1213
- const existing = ctx.read(filePath);
1214
- if (existing) {
1215
- const parsed = parseRenovateJson(existing);
1216
- const existingExtends = parsed.extends ?? [];
1217
- if (!existingExtends.includes(SHARED_PRESET)) {
1218
- existingExtends.unshift(SHARED_PRESET);
1219
- parsed.extends = existingExtends;
1220
- ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1247
+ const existingPath = RENOVATE_CONFIG_PATHS.find((p) => ctx.exists(p));
1248
+ if (existingPath === filePath) {
1249
+ const existing = ctx.read(filePath);
1250
+ if (existing) {
1251
+ const parsed = parseRenovateJson(existing);
1252
+ const existingExtends = parsed.extends ?? [];
1253
+ if (!existingExtends.includes(SHARED_PRESET)) {
1254
+ existingExtends.unshift(SHARED_PRESET);
1255
+ parsed.extends = existingExtends;
1256
+ ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1257
+ return {
1258
+ filePath,
1259
+ action: "updated",
1260
+ description: `Added extends: ${SHARED_PRESET}`
1261
+ };
1262
+ }
1221
1263
  return {
1222
1264
  filePath,
1223
- action: "updated",
1224
- description: `Added extends: ${SHARED_PRESET}`
1265
+ action: "skipped",
1266
+ description: "Already extends shared config"
1225
1267
  };
1226
1268
  }
1227
- return {
1228
- filePath,
1229
- action: "skipped",
1230
- description: "Already extends shared config"
1231
- };
1232
1269
  }
1270
+ if (existingPath) return {
1271
+ filePath: existingPath,
1272
+ action: "skipped",
1273
+ description: `Existing config found at ${existingPath}`
1274
+ };
1233
1275
  const config = {
1234
1276
  $schema: "https://docs.renovatebot.com/renovate-schema.json",
1235
1277
  extends: [SHARED_PRESET]
@@ -1673,28 +1715,47 @@ function buildConfig(formatter) {
1673
1715
  return `export default {\n "*": "${formatter === "prettier" ? "prettier --write" : "oxfmt"}",\n};\n`;
1674
1716
  }
1675
1717
  const HUSKY_PRE_COMMIT = "pnpm exec lint-staged\n";
1718
+ /** All known lint-staged config file locations, in priority order. */
1719
+ const LINT_STAGED_CONFIG_PATHS = [
1720
+ "lint-staged.config.mjs",
1721
+ "lint-staged.config.js",
1722
+ "lint-staged.config.cjs",
1723
+ ".lintstagedrc",
1724
+ ".lintstagedrc.json",
1725
+ ".lintstagedrc.yaml",
1726
+ ".lintstagedrc.yml",
1727
+ ".lintstagedrc.mjs",
1728
+ ".lintstagedrc.cjs"
1729
+ ];
1676
1730
  async function generateLintStaged(ctx) {
1677
1731
  const filePath = "lint-staged.config.mjs";
1678
1732
  const huskyPath = ".husky/pre-commit";
1679
1733
  const content = buildConfig(ctx.config.formatter);
1680
- const existing = ctx.read(filePath);
1681
1734
  if (ctx.read(huskyPath) !== HUSKY_PRE_COMMIT) ctx.write(huskyPath, HUSKY_PRE_COMMIT);
1682
- if (existing) {
1683
- if (existing === content) return {
1684
- filePath,
1685
- action: "skipped",
1686
- description: "Already configured"
1687
- };
1688
- if (await ctx.confirmOverwrite(filePath) === "skip") return {
1689
- filePath,
1690
- action: "skipped",
1691
- description: "Existing lint-staged config preserved"
1692
- };
1693
- }
1735
+ const existingPath = LINT_STAGED_CONFIG_PATHS.find((p) => ctx.exists(p));
1736
+ if (existingPath === filePath) {
1737
+ const existing = ctx.read(filePath);
1738
+ if (existing) {
1739
+ if (existing === content) return {
1740
+ filePath,
1741
+ action: "skipped",
1742
+ description: "Already configured"
1743
+ };
1744
+ if (await ctx.confirmOverwrite(filePath) === "skip") return {
1745
+ filePath,
1746
+ action: "skipped",
1747
+ description: "Existing lint-staged config preserved"
1748
+ };
1749
+ }
1750
+ } else if (existingPath) return {
1751
+ filePath: existingPath,
1752
+ action: "skipped",
1753
+ description: `Existing config found at ${existingPath}`
1754
+ };
1694
1755
  ctx.write(filePath, content);
1695
1756
  return {
1696
1757
  filePath,
1697
- action: existing ? "updated" : "created",
1758
+ action: existingPath === filePath ? "updated" : "created",
1698
1759
  description: "Generated lint-staged config and husky pre-commit hook"
1699
1760
  };
1700
1761
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
@@ -35,7 +35,7 @@
35
35
  "tsdown": "0.20.3",
36
36
  "typescript": "5.9.3",
37
37
  "vitest": "4.0.18",
38
- "@bensandee/config": "0.2.0"
38
+ "@bensandee/config": "0.3.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsdown",