@chanom/cli 0.4.0 → 0.4.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 +6 -0
- package/dist/index.js +84 -25
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -72,6 +72,12 @@ installing anything. `light` sweetness works anywhere in the repo.
|
|
|
72
72
|
|
|
73
73
|
\* `.mts` instead of `.ts` when your project is CommonJS (no `"type": "module"` in `package.json`).
|
|
74
74
|
|
|
75
|
+
oxlint and oxfmt auto-discover `.js`, `.cjs`, `.mjs`, `.ts`, and the JSON rc files, but not
|
|
76
|
+
`.mts` or `.cts`. When the config in play is one of those two, the generated `oxlint`/`oxfmt`
|
|
77
|
+
scripts - and the lint-staged commands - name it with `-c`. This tracks the file that actually
|
|
78
|
+
ends up on disk, so it applies to a config `brew` detected and skipped just as much as one it
|
|
79
|
+
wrote itself.
|
|
80
|
+
|
|
75
81
|
Config files re-export presets from [`@chanom/dev-config`](../dev-config).
|
|
76
82
|
|
|
77
83
|
The `vscode` topping installs no packages and adds no scripts. It writes editor
|
package/dist/index.js
CHANGED
|
@@ -257,7 +257,7 @@ const SKILLS = [{
|
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
259
|
"path": "SKILL.md",
|
|
260
|
-
"contents": "---\nname: coding-standards\ndescription: Coding standards for this Vite + React + TypeScript app. Use when writing or changing components, hooks, utils, or tests, and before committing.\n---\n\n# Coding standards\n\nVite + React 19 + TypeScript, scaffolded with `create-chanom-app`. oxlint lints,\noxfmt formats, vitest tests. Read the reference for what you touch **before**\nwriting, so new code matches the code already here.\n\n## Which reference to load\n\n| You are writing… | Load |\n| -------------------------------------------- | -------------------------- |\n| Any `.ts` / `.tsx` file | `references/typescript.md` |\n| A component, hook, or anything rendering JSX | `references/react.md` |\n| A `*.test.ts` / `*.test.tsx` file | `references/vitest.md` |\n\nThese layer: a new component with a test reads all three. Load every row that\napplies.\n\n## Universal rules\n\n- Filenames are kebab-case: `user-card.tsx`, `use-current-user.ts`, `format-date.ts`.\n- Before adding a file, open two neighbouring files of the same kind and match\n their shape — import order, export style, test naming.\n- Source lives under `src/`. Colocate a test beside the code it covers:\n `src/utils/format-date.ts` → `src/utils/format-date.test.ts`.\n\n## Verify\n\nAfter implementing, from the project root:\n\n```sh\npnpm lint\npnpm test\npnpm build\n```\n\nDone only when all
|
|
260
|
+
"contents": "---\nname: coding-standards\ndescription: Coding standards for this Vite + React + TypeScript app. Use when writing or changing components, hooks, utils, or tests, and before committing.\n---\n\n# Coding standards\n\nVite + React 19 + TypeScript, scaffolded with `create-chanom-app`. oxlint lints,\noxfmt formats, vitest tests. Read the reference for what you touch **before**\nwriting, so new code matches the code already here.\n\n## Which reference to load\n\n| You are writing… | Load |\n| -------------------------------------------- | -------------------------- |\n| Any `.ts` / `.tsx` file | `references/typescript.md` |\n| A component, hook, or anything rendering JSX | `references/react.md` |\n| A `*.test.ts` / `*.test.tsx` file | `references/vitest.md` |\n\nThese layer: a new component with a test reads all three. Load every row that\napplies.\n\n## Universal rules\n\n- Filenames are kebab-case: `user-card.tsx`, `use-current-user.ts`, `format-date.ts`.\n- Before adding a file, open two neighbouring files of the same kind and match\n their shape — import order, export style, test naming.\n- Source lives under `src/`. Colocate a test beside the code it covers:\n `src/utils/format-date.ts` → `src/utils/format-date.test.ts`.\n\n## Verify\n\nAfter implementing, from the project root:\n\n```sh\npnpm lint\npnpm test\npnpm build\npnpm typecheck\n```\n\nDone only when all four exit clean. On failure, fix and rerun until they do.\nRun `pnpm format` before committing. Comment- or doc-only changes may skip all\nfour.\n\n## Committing\n\nAsk before committing. Use conventional commits under 100 characters:\n`<type>(<scope>): <subject>`, e.g. `feat(auth): add sign-out button`. Scope is\nthe area you touched, not the repo name.\n"
|
|
261
261
|
}
|
|
262
262
|
]
|
|
263
263
|
}];
|
|
@@ -424,6 +424,32 @@ const SETUP_FILE_CANDIDATES = {
|
|
|
424
424
|
"vite.config.mjs"
|
|
425
425
|
]
|
|
426
426
|
};
|
|
427
|
+
/** Config file name a tool gets scaffolded with, matching the project's module system. */
|
|
428
|
+
function configFileName(tool, esm) {
|
|
429
|
+
return `${tool}.config.${esm ? "ts" : "mts"}`;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Extensions oxlint/oxfmt resolve on their own. `.mts`/`.cts` are absent
|
|
433
|
+
* deliberately: the tools only auto-discover the plain `.ts` variant, so a
|
|
434
|
+
* config using one of those has to be named explicitly on the command line.
|
|
435
|
+
*/
|
|
436
|
+
const AUTO_DISCOVERED = /* @__PURE__ */ new Set([
|
|
437
|
+
"js",
|
|
438
|
+
"cjs",
|
|
439
|
+
"mjs",
|
|
440
|
+
"ts",
|
|
441
|
+
"json"
|
|
442
|
+
]);
|
|
443
|
+
/**
|
|
444
|
+
* `-c <config>` argument naming the config file, or an empty string when the
|
|
445
|
+
* tool finds the file by itself. `configFile` is the config that will actually
|
|
446
|
+
* be on disk - the one chanom is about to write, or an existing one it detected
|
|
447
|
+
* and skipped - so the generated scripts never point at a path that isn't there.
|
|
448
|
+
*/
|
|
449
|
+
function configFlag(configFile) {
|
|
450
|
+
const extension = configFile.slice(configFile.lastIndexOf(".") + 1);
|
|
451
|
+
return AUTO_DISCOVERED.has(extension) ? "" : ` -c ${configFile}`;
|
|
452
|
+
}
|
|
427
453
|
//#endregion
|
|
428
454
|
//#region src/utils/detect-setup.ts
|
|
429
455
|
/** Finds an existing config file for the given tool, if any. */
|
|
@@ -523,7 +549,7 @@ function getPackages$3(pkg, versions) {
|
|
|
523
549
|
}
|
|
524
550
|
function configFile$2(esm) {
|
|
525
551
|
return {
|
|
526
|
-
fileName:
|
|
552
|
+
fileName: configFileName("knip", esm),
|
|
527
553
|
contents: `export { default } from '@chanom/dev-config/knip/config';\n`
|
|
528
554
|
};
|
|
529
555
|
}
|
|
@@ -555,22 +581,42 @@ const apply$5 = Effect.fn("add-knip.apply")(function* (cwd, esm, pkg) {
|
|
|
555
581
|
function getPackages$2(pkg) {
|
|
556
582
|
return isPackageInstalled(pkg, "lint-staged") ? [] : ["lint-staged"];
|
|
557
583
|
}
|
|
558
|
-
function buildConfig(linters, formatters) {
|
|
584
|
+
function buildConfig(linters, formatters, configFiles) {
|
|
559
585
|
const config = {};
|
|
560
586
|
const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
|
|
561
587
|
if (linters.includes("oxlint")) {
|
|
562
588
|
config[jsGlob] ??= [];
|
|
563
|
-
config[jsGlob].push(
|
|
589
|
+
config[jsGlob].push(`oxlint --fix${configFlag(configFiles.oxlint)} --no-error-on-unmatched-pattern`);
|
|
564
590
|
}
|
|
565
591
|
if (formatters.includes("oxfmt")) {
|
|
566
592
|
config["*"] ??= [];
|
|
567
|
-
config["*"].push(
|
|
593
|
+
config["*"].push(`oxfmt${configFlag(configFiles.oxfmt)} --no-error-on-unmatched-pattern`);
|
|
568
594
|
}
|
|
569
595
|
return config;
|
|
570
596
|
}
|
|
571
597
|
//#endregion
|
|
572
598
|
//#region src/commands/add-lint-staged/index.ts
|
|
573
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Config file each tool will run against: an existing one when `add-oxlint` /
|
|
601
|
+
* `add-oxfmt` skipped writing, otherwise the name they scaffold. lint-staged
|
|
602
|
+
* invokes the same binaries as the package scripts, so it has to resolve the
|
|
603
|
+
* config the same way.
|
|
604
|
+
*/
|
|
605
|
+
const resolveConfigFiles = Effect.fn("add-lint-staged.resolveConfigFiles")(function* (cwd, esm) {
|
|
606
|
+
const path = yield* Path.Path;
|
|
607
|
+
const resolve = Effect.fn(function* (tool) {
|
|
608
|
+
const existing = yield* detectSetupFile(tool, cwd);
|
|
609
|
+
return Option.match(existing, {
|
|
610
|
+
onNone: () => configFileName(tool, esm),
|
|
611
|
+
onSome: (file) => path.basename(file)
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
oxlint: yield* resolve("oxlint"),
|
|
616
|
+
oxfmt: yield* resolve("oxfmt")
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, esm, linters = [], formatters = [], overwritePreCommit = false) {
|
|
574
620
|
if (linters.length === 0 && formatters.length === 0) {
|
|
575
621
|
yield* Effect.logDebug("no linters or formatters selected - skipping lint-staged setup");
|
|
576
622
|
return;
|
|
@@ -580,7 +626,10 @@ const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [],
|
|
|
580
626
|
const prompter = yield* Prompter;
|
|
581
627
|
const existing = yield* detectSetupFile("lint-staged", cwd);
|
|
582
628
|
if (Option.isSome(existing)) yield* prompter.warn(`\`${path.basename(existing.value)}\` already exists - skipping lint-staged config`);
|
|
583
|
-
else
|
|
629
|
+
else {
|
|
630
|
+
const configFiles = yield* resolveConfigFiles(cwd, esm);
|
|
631
|
+
yield* fs.writeFileString(path.join(cwd, ".lintstagedrc.json"), JSON.stringify(buildConfig(linters, formatters, configFiles), null, 2) + "\n");
|
|
632
|
+
}
|
|
584
633
|
const huskyDir = path.join(cwd, ".husky");
|
|
585
634
|
if (yield* fs.exists(huskyDir)) {
|
|
586
635
|
const hookPath = path.join(huskyDir, "pre-commit");
|
|
@@ -595,14 +644,15 @@ function getPackages$1(pkg, versions) {
|
|
|
595
644
|
}
|
|
596
645
|
function configFile$1(esm) {
|
|
597
646
|
return {
|
|
598
|
-
fileName:
|
|
647
|
+
fileName: configFileName("oxfmt", esm),
|
|
599
648
|
contents: `export { default } from '@chanom/dev-config/oxfmt/config';\n`
|
|
600
649
|
};
|
|
601
650
|
}
|
|
602
|
-
function getScriptPlan$1(scripts) {
|
|
651
|
+
function getScriptPlan$1(scripts, configFile) {
|
|
652
|
+
const config = configFlag(configFile);
|
|
603
653
|
return planScripts(scripts, {
|
|
604
|
-
format:
|
|
605
|
-
"format:check":
|
|
654
|
+
format: `oxfmt${config}`,
|
|
655
|
+
"format:check": `oxfmt --check${config}`
|
|
606
656
|
});
|
|
607
657
|
}
|
|
608
658
|
//#endregion
|
|
@@ -613,12 +663,16 @@ const apply$3 = Effect.fn("add-oxfmt.apply")(function* (cwd, esm, pkg) {
|
|
|
613
663
|
const path = yield* Path.Path;
|
|
614
664
|
const prompter = yield* Prompter;
|
|
615
665
|
const existing = yield* detectSetupFile("oxfmt", cwd);
|
|
616
|
-
|
|
617
|
-
|
|
666
|
+
let configFileName;
|
|
667
|
+
if (Option.isSome(existing)) {
|
|
668
|
+
configFileName = path.basename(existing.value);
|
|
669
|
+
yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxfmt config`);
|
|
670
|
+
} else {
|
|
618
671
|
const config = configFile$1(esm);
|
|
672
|
+
configFileName = config.fileName;
|
|
619
673
|
yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
|
|
620
674
|
}
|
|
621
|
-
const plan = getScriptPlan$1(pkg.scripts);
|
|
675
|
+
const plan = getScriptPlan$1(pkg.scripts, configFileName);
|
|
622
676
|
for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
|
|
623
677
|
return {
|
|
624
678
|
...pkg,
|
|
@@ -636,14 +690,15 @@ function getPackages(pkg, versions) {
|
|
|
636
690
|
}
|
|
637
691
|
function configFile(esm) {
|
|
638
692
|
return {
|
|
639
|
-
fileName:
|
|
693
|
+
fileName: configFileName("oxlint", esm),
|
|
640
694
|
contents: `export { default } from '@chanom/dev-config/oxlint/config';\n`
|
|
641
695
|
};
|
|
642
696
|
}
|
|
643
|
-
function getScriptPlan(scripts) {
|
|
697
|
+
function getScriptPlan(scripts, configFile) {
|
|
698
|
+
const config = configFlag(configFile);
|
|
644
699
|
return planScripts(scripts, {
|
|
645
|
-
lint:
|
|
646
|
-
"lint:fix":
|
|
700
|
+
lint: `oxlint${config}`,
|
|
701
|
+
"lint:fix": `oxlint --fix${config}`
|
|
647
702
|
});
|
|
648
703
|
}
|
|
649
704
|
//#endregion
|
|
@@ -654,12 +709,16 @@ const apply$2 = Effect.fn("add-oxlint.apply")(function* (cwd, esm, pkg) {
|
|
|
654
709
|
const path = yield* Path.Path;
|
|
655
710
|
const prompter = yield* Prompter;
|
|
656
711
|
const existing = yield* detectSetupFile("oxlint", cwd);
|
|
657
|
-
|
|
658
|
-
|
|
712
|
+
let configFileName;
|
|
713
|
+
if (Option.isSome(existing)) {
|
|
714
|
+
configFileName = path.basename(existing.value);
|
|
715
|
+
yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxlint config`);
|
|
716
|
+
} else {
|
|
659
717
|
const config = configFile(esm);
|
|
718
|
+
configFileName = config.fileName;
|
|
660
719
|
yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
|
|
661
720
|
}
|
|
662
|
-
const plan = getScriptPlan(pkg.scripts);
|
|
721
|
+
const plan = getScriptPlan(pkg.scripts, configFileName);
|
|
663
722
|
for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
|
|
664
723
|
return {
|
|
665
724
|
...pkg,
|
|
@@ -810,7 +869,7 @@ function planPackages(pkg, toppings, sweetness, versions) {
|
|
|
810
869
|
* value for a config the user may add later.
|
|
811
870
|
*/
|
|
812
871
|
function oxfmtConfigPath(esm) {
|
|
813
|
-
return
|
|
872
|
+
return configFileName("oxfmt", esm);
|
|
814
873
|
}
|
|
815
874
|
/** True when at least one topping gives lint-staged something to run. */
|
|
816
875
|
function wantsLintStaged(toppings) {
|
|
@@ -953,14 +1012,14 @@ const ensureRepoRootForHooks = Effect.fn("brew.ensureRepoRootForHooks")(function
|
|
|
953
1012
|
root: Option.getOrElse(root, () => cwd)
|
|
954
1013
|
});
|
|
955
1014
|
});
|
|
956
|
-
const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, toppings, sweetness) {
|
|
1015
|
+
const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, esm, toppings, sweetness) {
|
|
957
1016
|
if (sweetness !== "medium") return;
|
|
958
1017
|
const fs = yield* FileSystem.FileSystem;
|
|
959
1018
|
const path = yield* Path.Path;
|
|
960
1019
|
const prompter = yield* Prompter;
|
|
961
1020
|
const huskyExisted = yield* fs.exists(path.join(cwd, ".husky"));
|
|
962
1021
|
yield* apply$6(cwd, pm);
|
|
963
|
-
if (wantsLintStaged(toppings)) yield* apply$4(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
|
|
1022
|
+
if (wantsLintStaged(toppings)) yield* apply$4(cwd, esm, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
|
|
964
1023
|
else {
|
|
965
1024
|
yield* prompter.warn("No linters or formatters selected - skipping lint-staged");
|
|
966
1025
|
const seededHook = path.join(cwd, ".husky", "pre-commit");
|
|
@@ -1003,7 +1062,7 @@ const brew = (cwd = process.cwd()) => Effect.gen(function* () {
|
|
|
1003
1062
|
yield* installPackages(pm, cwd, packages);
|
|
1004
1063
|
const { pkg: installedPkg } = yield* readPkg(cwd);
|
|
1005
1064
|
yield* persistScripts(pkgPath, installedPkg, yield* applyToppings(cwd, isEsm(installedPkg), installedPkg, toppings));
|
|
1006
|
-
yield* setupCommitGate(cwd, pm, toppings, sweetness);
|
|
1065
|
+
yield* setupCommitGate(cwd, pm, isEsm(installedPkg), toppings, sweetness);
|
|
1007
1066
|
yield* commitChanges(cwd);
|
|
1008
1067
|
yield* prompter.outro(pc.green(`Your Chanom with ${toppings.join(", ")} toppings and ${sweetness} sweetness is ready! Enjoy! 🧋`));
|
|
1009
1068
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chanom/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dev": "tsdown --watch",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"test:coverage": "vitest run --coverage",
|
|
43
|
-
"test:watch": "vitest"
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
44
45
|
}
|
|
45
46
|
}
|