frontpl 0.3.2 → 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/README.md CHANGED
@@ -42,12 +42,14 @@ Generated lint-related dependencies (`oxlint`, `oxlint-tsgolint`, `oxfmt`, `@kin
42
42
  ### `frontpl [name]` / `frontpl init [name]`
43
43
 
44
44
  Scaffold a new project into `./<name>` (or prompt for a name when omitted).
45
+ Project names support letters (including uppercase/camel case), numbers, `.`, `_`, `-` (cannot start with `.` or `_`).
45
46
 
46
47
  Generated output includes (based on options):
47
48
 
48
49
  - `.editorconfig`, `.gitignore`, `.gitattributes`
49
50
  - `package.json` (+ scripts like optional `lint`, `format:check`, `test`, `build`)
50
51
  - `tsconfig.json`, `src/index.ts`
52
+ - Relative TypeScript imports use explicit `.ts` extensions (e.g. generated `src/index.test.ts`)
51
53
  - Optional configs: `oxlint.config.ts`, `.oxfmtrc.json`, `tsdown.config.ts`
52
54
  - Optional GitHub Actions workflows in `.github/workflows/`
53
55
 
@@ -64,6 +66,52 @@ What it does:
64
66
  - Optionally generates `.github/workflows/release.yml` (tag/commit/both)
65
67
  - Optionally generates `.github/dependabot.yml` with grouped updates (`dependencies`, `github-actions`)
66
68
 
69
+ ### `frontpl oxlint`
70
+
71
+ Add/migrate linting in the current project to `oxlint`.
72
+
73
+ What it does:
74
+
75
+ - Asks strategy interactively:
76
+ - Migrate gradually (keep existing ESLint assets)
77
+ - Replace ESLint directly (current mode)
78
+ - Ensures `package.json` scripts use:
79
+ - `lint`: `oxlint --type-aware --type-check`
80
+ - `lint:fix`: `oxlint --type-aware --type-check --fix`
81
+ - Removes `typecheck: tsc --noEmit` when confirmed (or by default with `--yes`)
82
+ - Ensures devDependencies exist:
83
+ - `oxlint`
84
+ - `oxlint-tsgolint`
85
+ - `@kingsword/lint-config`
86
+ - Creates or updates `oxlint.config.ts` using `@kingsword/lint-config`
87
+ - In replace mode, removes ESLint deps/configs (`eslint*`, `@eslint/*`, `@typescript-eslint/*`, `eslintConfig`, `.eslintrc*`, `eslint.config.*`)
88
+ - Optionally installs dependencies with detected package manager
89
+
90
+ Use `--yes` (or `-y`) to skip confirmations and apply default choices.
91
+ With `--yes`, strategy defaults to `replace`.
92
+
93
+ ### `frontpl oxfmt`
94
+
95
+ Add/migrate formatting in the current project to `oxfmt`.
96
+
97
+ What it does:
98
+
99
+ - Asks config strategy interactively:
100
+ - Migrate from Prettier (`oxfmt --migrate=prettier`)
101
+ - Rebuild `.oxfmtrc.json` (current mode)
102
+ - Ensures `package.json` scripts use:
103
+ - `format`: `oxfmt`
104
+ - `format:check`: `oxfmt --check`
105
+ - `fmt`: `oxfmt`
106
+ - `fmt:check`: `oxfmt --check`
107
+ - Ensures `devDependencies.oxfmt` exists (defaults to `latest` when missing)
108
+ - Creates or updates `.oxfmtrc.json`
109
+ - Optionally removes `prettier` / `prettier-plugin-*` / `@prettier/plugin-*` dependencies, `package.json#prettier`, and Prettier config files (`.prettierrc*`, `prettier.config.*`)
110
+ - Optionally installs dependencies with detected package manager
111
+
112
+ Use `--yes` (or `-y`) to skip confirmations and apply default choices.
113
+ With `--yes`, config strategy defaults to rebuild `.oxfmtrc.json`.
114
+
67
115
  ## GitHub Actions (CI + Release)
68
116
 
69
117
  frontpl generates workflows that call reusable workflows from `kingsword09/workflows` (pinned to commit SHA + `# vX.Y.Z` comment by default):
@@ -100,6 +148,8 @@ pnpm run lint
100
148
  pnpm run build
101
149
  node dist/cli.mjs --help
102
150
  node dist/cli.mjs ci
151
+ node dist/cli.mjs oxlint --help
152
+ node dist/cli.mjs oxfmt --help
103
153
  ```
104
154
 
105
155
  ## Lint preset
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as runCi, t as runInit } from "./init-oJwslpqP.mjs";
2
+ import { a as runCi, n as runOxlint, r as runInit, t as runOxfmt } from "./oxfmt-Bgtl3zwv.mjs";
3
3
  import bin from "tiny-bin";
4
4
 
5
5
  //#region src/cli.ts
@@ -10,6 +10,10 @@ async function main() {
10
10
  await runInit({ nameArg: args[0] });
11
11
  }).command("ci", "Add CI/release workflows to an existing project").action(async () => {
12
12
  await runCi();
13
+ }).command("oxlint", "Add/migrate linter to oxlint in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
14
+ await runOxlint({ yes: options.yes === true });
15
+ }).command("oxfmt", "Add/migrate formatter to oxfmt in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
16
+ await runOxfmt({ yes: options.yes === true });
13
17
  }).run();
14
18
  }
15
19
  main();
package/dist/index.d.mts CHANGED
@@ -7,6 +7,23 @@ declare function runInit({
7
7
  }: {
8
8
  nameArg?: string;
9
9
  }): Promise<void>;
10
+ declare function validateProjectName(value: string | undefined): "Project name is required" | "Project name is too long" | "Project name cannot start with '.'" | "Project name cannot start with '_'" | "Use letters, numbers, '.', '_' or '-'" | undefined;
11
+ //#endregion
12
+ //#region src/commands/oxlint.d.ts
13
+ type CommandOptions$1 = {
14
+ yes?: boolean;
15
+ };
16
+ declare function runOxlint({
17
+ yes
18
+ }?: CommandOptions$1): Promise<void>;
19
+ //#endregion
20
+ //#region src/commands/oxfmt.d.ts
21
+ type CommandOptions = {
22
+ yes?: boolean;
23
+ };
24
+ declare function runOxfmt({
25
+ yes
26
+ }?: CommandOptions): Promise<void>;
10
27
  //#endregion
11
28
  //#region src/lib/templates.d.ts
12
29
  declare function oxlintConfigTemplate({
@@ -48,4 +65,4 @@ declare function githubDependabotTemplate(opts: {
48
65
  workingDirectory: string;
49
66
  }): string;
50
67
  //#endregion
51
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
68
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as oxlintConfigTemplate, i as githubDependabotTemplate, n as runCi, o as packageJsonTemplate, r as githubCliCiWorkflowTemplate, t as runInit } from "./init-oJwslpqP.mjs";
1
+ import { a as runCi, c as oxlintConfigTemplate, i as validateProjectName, l as packageJsonTemplate, n as runOxlint, o as githubCliCiWorkflowTemplate, r as runInit, s as githubDependabotTemplate, t as runOxfmt } from "./oxfmt-Bgtl3zwv.mjs";
2
2
 
3
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
3
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName };
@@ -1,5 +1,5 @@
1
1
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
2
- import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { spawn } from "node:child_process";
@@ -11,6 +11,48 @@ async function writeText(filePath, contents) {
11
11
  await writeFile(filePath, contents, "utf8");
12
12
  }
13
13
 
14
+ //#endregion
15
+ //#region src/lib/utils.ts
16
+ async function pathExists(pathname) {
17
+ try {
18
+ await access(pathname);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ //#endregion
26
+ //#region src/lib/project.ts
27
+ async function readPackageJson(filePath) {
28
+ try {
29
+ return JSON.parse(await readFile(filePath, "utf8"));
30
+ } catch {
31
+ return;
32
+ }
33
+ }
34
+ async function writePackageJson(filePath, value) {
35
+ await writeText(filePath, JSON.stringify(value, null, 2) + "\n");
36
+ }
37
+ async function detectPackageManager(rootDir) {
38
+ const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
39
+ if (pmField) {
40
+ const pm = pmField.split("@")[0] ?? "";
41
+ if (isPackageManager(pm)) return pm;
42
+ }
43
+ const candidates = [];
44
+ if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
45
+ if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
46
+ if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
47
+ if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
48
+ if (await pathExists(path.join(rootDir, "bun.lock"))) candidates.push("bun");
49
+ if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
50
+ return candidates.length === 1 ? candidates[0] : void 0;
51
+ }
52
+ function isPackageManager(value) {
53
+ return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
54
+ }
55
+
14
56
  //#endregion
15
57
  //#region src/lib/templates.ts
16
58
  function editorconfigTemplate() {
@@ -69,7 +111,7 @@ function srcIndexTemplate() {
69
111
  function srcVitestTemplate() {
70
112
  return [
71
113
  "import { describe, expect, it } from \"vitest\";",
72
- "import { hello } from \"./index.js\";",
114
+ "import { hello } from \"./index.ts\";",
73
115
  "",
74
116
  "describe(\"hello\", () => {",
75
117
  " it(\"greets\", () => {",
@@ -333,17 +375,6 @@ function yamlString(value) {
333
375
  return JSON.stringify(value);
334
376
  }
335
377
 
336
- //#endregion
337
- //#region src/lib/utils.ts
338
- async function pathExists(pathname) {
339
- try {
340
- await access(pathname);
341
- return true;
342
- } catch {
343
- return false;
344
- }
345
- }
346
-
347
378
  //#endregion
348
379
  //#region src/commands/ci.ts
349
380
  async function runCi() {
@@ -377,7 +408,7 @@ async function runCi() {
377
408
  }
378
409
  ]
379
410
  });
380
- if (isCancel(packageManager)) return abort();
411
+ if (isCancel(packageManager)) return abort$2();
381
412
  const candidates = await listPackageCandidates(rootDir, packageManager);
382
413
  if (candidates.length === 0) {
383
414
  cancel("No package found. Run this command in a project root (with package.json or deno.json).");
@@ -393,7 +424,7 @@ async function runCi() {
393
424
  label: c
394
425
  }))
395
426
  });
396
- if (isCancel(workingDirectory)) return abort();
427
+ if (isCancel(workingDirectory)) return abort$2();
397
428
  const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
398
429
  const nodeVersionText = await text({
399
430
  message: "Node.js major version (for GitHub Actions)",
@@ -403,14 +434,14 @@ async function runCi() {
403
434
  if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
404
435
  }
405
436
  });
406
- if (isCancel(nodeVersionText)) return abort();
437
+ if (isCancel(nodeVersionText)) return abort$2();
407
438
  const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
408
439
  const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
409
440
  const addRelease = await confirm({
410
441
  message: "Add release workflow too?",
411
442
  initialValue: true
412
443
  });
413
- if (isCancel(addRelease)) return abort();
444
+ if (isCancel(addRelease)) return abort$2();
414
445
  const releaseMode = addRelease ? await select({
415
446
  message: "Release workflows",
416
447
  initialValue: "tag",
@@ -429,17 +460,17 @@ async function runCi() {
429
460
  }
430
461
  ]
431
462
  }) : void 0;
432
- if (isCancel(releaseMode)) return abort();
463
+ if (isCancel(releaseMode)) return abort$2();
433
464
  const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
434
465
  message: "Release: npm trusted publishing (OIDC)?",
435
466
  initialValue: true
436
467
  }) : void 0;
437
- if (isCancel(trustedPublishing)) return abort();
468
+ if (isCancel(trustedPublishing)) return abort$2();
438
469
  const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
439
470
  message: "Add/update Dependabot config (.github/dependabot.yml)?",
440
471
  initialValue: true
441
472
  }) : false;
442
- if (isCancel(addDependabot)) return abort();
473
+ if (isCancel(addDependabot)) return abort$2();
443
474
  const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
444
475
  const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
445
476
  const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
@@ -475,19 +506,19 @@ async function runCi() {
475
506
  }
476
507
  outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
477
508
  } catch (err) {
478
- if (err instanceof CancelledError) return;
509
+ if (err instanceof CancelledError$2) return;
479
510
  throw err;
480
511
  }
481
512
  }
482
- var CancelledError = class extends Error {
513
+ var CancelledError$2 = class extends Error {
483
514
  constructor() {
484
515
  super("Cancelled");
485
516
  }
486
517
  };
487
- function abort(opts = {}) {
518
+ function abort$2(opts = {}) {
488
519
  cancel(opts.message ?? "Cancelled");
489
520
  process.exitCode = opts.exitCode ?? 0;
490
- throw new CancelledError();
521
+ throw new CancelledError$2();
491
522
  }
492
523
  async function confirmOverwriteIfExists(absPath, label) {
493
524
  if (!await pathExists(absPath)) return true;
@@ -495,26 +526,9 @@ async function confirmOverwriteIfExists(absPath, label) {
495
526
  message: `Overwrite existing ${label}?`,
496
527
  initialValue: true
497
528
  });
498
- if (isCancel(overwrite)) return abort();
529
+ if (isCancel(overwrite)) return abort$2();
499
530
  return overwrite;
500
531
  }
501
- function isPackageManager(value) {
502
- return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
503
- }
504
- async function detectPackageManager(rootDir) {
505
- const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
506
- if (pmField) {
507
- const pm = pmField.split("@")[0] ?? "";
508
- if (isPackageManager(pm)) return pm;
509
- }
510
- const candidates = [];
511
- if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
512
- if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
513
- if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
514
- if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
515
- if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
516
- return candidates.length === 1 ? candidates[0] : void 0;
517
- }
518
532
  async function listPackageCandidates(rootDir, packageManager) {
519
533
  const candidates = /* @__PURE__ */ new Set();
520
534
  if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
@@ -564,7 +578,7 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
564
578
  runTests: true
565
579
  };
566
580
  const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
567
- if (!pkg) return abort({
581
+ if (!pkg) return abort$2({
568
582
  message: `Missing package.json in ${workingDirectory}`,
569
583
  exitCode: 1
570
584
  });
@@ -580,17 +594,17 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
580
594
  message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
581
595
  initialValue: runLintDefault
582
596
  });
583
- if (isCancel(runLint)) return abort();
597
+ if (isCancel(runLint)) return abort$2();
584
598
  const runFormatCheck = await confirm({
585
599
  message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
586
600
  initialValue: runFormatCheckDefault
587
601
  });
588
- if (isCancel(runFormatCheck)) return abort();
602
+ if (isCancel(runFormatCheck)) return abort$2();
589
603
  const runTests = await confirm({
590
604
  message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
591
605
  initialValue: runTestsDefault
592
606
  });
593
- if (isCancel(runTests)) return abort();
607
+ if (isCancel(runTests)) return abort$2();
594
608
  return {
595
609
  runLint,
596
610
  runFormatCheck,
@@ -606,7 +620,7 @@ async function promptCommand(message, initialValue) {
606
620
  initialValue,
607
621
  validate: (v = "") => !v.trim() ? "Command is required" : void 0
608
622
  });
609
- if (isCancel(value)) return abort();
623
+ if (isCancel(value)) return abort$2();
610
624
  return String(value).trim();
611
625
  }
612
626
  function pmRun$1(pm, script) {
@@ -618,13 +632,6 @@ function pmRun$1(pm, script) {
618
632
  case "deno": return script;
619
633
  }
620
634
  }
621
- async function readPackageJson(filePath) {
622
- try {
623
- return JSON.parse(await readFile(filePath, "utf8"));
624
- } catch {
625
- return;
626
- }
627
- }
628
635
 
629
636
  //#endregion
630
637
  //#region src/lib/exec.ts
@@ -918,8 +925,7 @@ function validateProjectName(value) {
918
925
  if (name.length > 214) return "Project name is too long";
919
926
  if (name.startsWith(".")) return "Project name cannot start with '.'";
920
927
  if (name.startsWith("_")) return "Project name cannot start with '_'";
921
- if (/[A-Z]/.test(name)) return "Use lowercase letters only";
922
- if (!/^[a-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
928
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
923
929
  }
924
930
  function onCancel() {
925
931
  cancel("Cancelled");
@@ -936,4 +942,516 @@ function nextStepHint(pm) {
936
942
  }
937
943
 
938
944
  //#endregion
939
- export { oxlintConfigTemplate as a, githubDependabotTemplate as i, runCi as n, packageJsonTemplate as o, githubCliCiWorkflowTemplate as r, runInit as t };
945
+ //#region src/commands/oxlint.ts
946
+ const OXLINT_COMMAND = "oxlint --type-aware --type-check";
947
+ const OXLINT_FIX_COMMAND = `${OXLINT_COMMAND} --fix`;
948
+ const OXLINT_SCRIPTS = {
949
+ lint: OXLINT_COMMAND,
950
+ "lint:fix": OXLINT_FIX_COMMAND
951
+ };
952
+ const ESLINT_CONFIG_FILES = [
953
+ ".eslintrc",
954
+ ".eslintrc.js",
955
+ ".eslintrc.cjs",
956
+ ".eslintrc.mjs",
957
+ ".eslintrc.json",
958
+ ".eslintrc.yaml",
959
+ ".eslintrc.yml",
960
+ ".eslintrc.ts",
961
+ ".eslintrc.cts",
962
+ ".eslintrc.mts",
963
+ "eslint.config.js",
964
+ "eslint.config.cjs",
965
+ "eslint.config.mjs",
966
+ "eslint.config.ts",
967
+ "eslint.config.cts",
968
+ "eslint.config.mts"
969
+ ];
970
+ const OXLINT_DEPENDENCIES = [
971
+ "oxlint",
972
+ "oxlint-tsgolint",
973
+ "@kingsword/lint-config"
974
+ ];
975
+ async function runOxlint({ yes = false } = {}) {
976
+ try {
977
+ intro("frontpl (oxlint)");
978
+ const rootDir = process.cwd();
979
+ const packageJsonPath = path.join(rootDir, "package.json");
980
+ const oxlintConfigPath = path.join(rootDir, "oxlint.config.ts");
981
+ const pkg = await readPackageJson(packageJsonPath);
982
+ if (!pkg) {
983
+ cancel("Missing package.json. Run this command in a Node project root.");
984
+ process.exitCode = 1;
985
+ return;
986
+ }
987
+ const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
988
+ const stats = await migrateToOxlint({
989
+ pkg,
990
+ rootDir,
991
+ oxlintConfigPath,
992
+ strategy: yes ? "replace" : await askMigrationStrategy({
993
+ rootDir,
994
+ pkg
995
+ }),
996
+ yes
997
+ });
998
+ await writePackageJson(packageJsonPath, pkg);
999
+ const installOk = await maybeInstallDependencies$1({
1000
+ yes,
1001
+ packageManager,
1002
+ rootDir
1003
+ });
1004
+ const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
1005
+ const dependencySummary = stats.addedDevDependencies.length > 0 ? `added devDependencies: ${stats.addedDevDependencies.join(", ")}` : "required oxlint devDependencies already present";
1006
+ const typecheckSummary = stats.removedTypecheckScript ? "removed redundant typecheck script (tsc --noEmit)" : "kept typecheck script";
1007
+ const eslintDependencySummary = stats.removedDependencies.length > 0 ? `removed eslint deps: ${stats.removedDependencies.join(", ")}` : "no eslint deps removed";
1008
+ const eslintConfigSummary = stats.removedPackageJsonEslintConfig ? "removed package.json#eslintConfig" : "no package.json#eslintConfig removed";
1009
+ const eslintFileSummary = stats.removedConfigFiles.length > 0 ? `removed eslint config files: ${stats.removedConfigFiles.join(", ")}` : "no eslint config files removed";
1010
+ const oxlintConfigSummary = stats.oxlintConfigAction === "written" ? "wrote oxlint.config.ts" : "kept existing oxlint.config.ts";
1011
+ const installSummary = packageManager === "deno" ? "skipped dependency install (deno project)" : installOk === true ? `installed dependencies with ${packageManager}` : installOk === false ? `dependency install failed with ${packageManager}` : "skipped dependency install";
1012
+ outro([
1013
+ "Done. Applied oxlint migration.",
1014
+ `- strategy: ${stats.strategy === "migrate" ? "migrate (keep ESLint assets)" : "replace ESLint assets"}`,
1015
+ `- ${scriptSummary}`,
1016
+ `- ${typecheckSummary}`,
1017
+ `- ${dependencySummary}`,
1018
+ `- ${eslintDependencySummary}`,
1019
+ `- ${eslintConfigSummary}`,
1020
+ `- ${eslintFileSummary}`,
1021
+ `- ${oxlintConfigSummary}`,
1022
+ `- ${installSummary}`
1023
+ ].join("\n"));
1024
+ } catch (error) {
1025
+ if (error instanceof CancelledError$1) return;
1026
+ throw error;
1027
+ }
1028
+ }
1029
+ async function migrateToOxlint(opts) {
1030
+ const { pkg, rootDir, oxlintConfigPath, strategy, yes } = opts;
1031
+ const scripts = { ...pkg.scripts };
1032
+ const conflictingScripts = Object.entries(OXLINT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
1033
+ const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm$1({
1034
+ message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxlint?`,
1035
+ initialValue: true
1036
+ });
1037
+ const scriptsUpdated = [];
1038
+ const scriptsKept = [];
1039
+ for (const [name, command] of Object.entries(OXLINT_SCRIPTS)) {
1040
+ const current = scripts[name];
1041
+ if (current === command) continue;
1042
+ if (current && !shouldOverwriteConflicts) {
1043
+ scriptsKept.push(name);
1044
+ continue;
1045
+ }
1046
+ scripts[name] = command;
1047
+ scriptsUpdated.push(name);
1048
+ }
1049
+ let removedTypecheckScript = false;
1050
+ if (scripts.typecheck === "tsc --noEmit") {
1051
+ if (yes || await askConfirm$1({
1052
+ message: "Remove redundant typecheck script (tsc --noEmit)?",
1053
+ initialValue: true
1054
+ })) {
1055
+ delete scripts.typecheck;
1056
+ removedTypecheckScript = true;
1057
+ }
1058
+ }
1059
+ pkg.scripts = scripts;
1060
+ const devDependencies = { ...pkg.devDependencies };
1061
+ const addedDevDependencies = [];
1062
+ for (const dependency of OXLINT_DEPENDENCIES) {
1063
+ if (devDependencies[dependency]) continue;
1064
+ devDependencies[dependency] = "latest";
1065
+ addedDevDependencies.push(dependency);
1066
+ }
1067
+ pkg.devDependencies = devDependencies;
1068
+ const oxlintConfigAction = await applyOxlintConfig({
1069
+ pkg,
1070
+ oxlintConfigPath,
1071
+ yes
1072
+ });
1073
+ let removedDependencies = [];
1074
+ let removedPackageJsonEslintConfig = false;
1075
+ const removedConfigFiles = [];
1076
+ if (strategy === "replace") {
1077
+ removedDependencies = [...removeEslintDependencies(pkg, "dependencies"), ...removeEslintDependencies(pkg, "devDependencies")];
1078
+ removedPackageJsonEslintConfig = removeEslintConfigFromPackageJson(pkg);
1079
+ cleanupEmptyDependencyBuckets$1(pkg);
1080
+ for (const file of ESLINT_CONFIG_FILES) {
1081
+ const filePath = path.join(rootDir, file);
1082
+ if (!await pathExists(filePath)) continue;
1083
+ await unlink(filePath);
1084
+ removedConfigFiles.push(file);
1085
+ }
1086
+ }
1087
+ return {
1088
+ strategy,
1089
+ scriptsUpdated,
1090
+ scriptsKept,
1091
+ removedTypecheckScript,
1092
+ addedDevDependencies,
1093
+ removedDependencies,
1094
+ removedPackageJsonEslintConfig,
1095
+ removedConfigFiles,
1096
+ oxlintConfigAction
1097
+ };
1098
+ }
1099
+ async function maybeInstallDependencies$1(opts) {
1100
+ const { yes, packageManager, rootDir } = opts;
1101
+ if (packageManager === "deno") return void 0;
1102
+ if (!(yes || await askConfirm$1({
1103
+ message: `Install dependencies now with ${packageManager}?`,
1104
+ initialValue: true
1105
+ }))) return void 0;
1106
+ const installSpinner = spinner();
1107
+ installSpinner.start(`Installing dependencies with ${packageManager}`);
1108
+ const result = await exec(packageManager, ["install"], { cwd: rootDir });
1109
+ installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
1110
+ return result.ok;
1111
+ }
1112
+ async function askConfirm$1(opts) {
1113
+ const answer = await confirm({
1114
+ message: opts.message,
1115
+ initialValue: opts.initialValue
1116
+ });
1117
+ if (isCancel(answer)) return abort$1();
1118
+ return answer;
1119
+ }
1120
+ async function askMigrationStrategy(opts) {
1121
+ const strategy = await select({
1122
+ message: "ESLint strategy",
1123
+ initialValue: await detectEslintAssets(opts.rootDir, opts.pkg) ? "migrate" : "replace",
1124
+ options: [{
1125
+ value: "migrate",
1126
+ label: "Migrate gradually (keep ESLint assets)"
1127
+ }, {
1128
+ value: "replace",
1129
+ label: "Replace ESLint directly (current mode)"
1130
+ }]
1131
+ });
1132
+ if (isCancel(strategy)) return abort$1();
1133
+ return strategy;
1134
+ }
1135
+ async function detectEslintAssets(rootDir, pkg) {
1136
+ if (Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return true;
1137
+ const dependencies = pkg.dependencies ?? {};
1138
+ const devDependencies = pkg.devDependencies ?? {};
1139
+ if (Object.keys(dependencies).some(isEslintDependency) || Object.keys(devDependencies).some(isEslintDependency)) return true;
1140
+ for (const file of ESLINT_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
1141
+ return false;
1142
+ }
1143
+ async function applyOxlintConfig(opts) {
1144
+ const { pkg, oxlintConfigPath, yes } = opts;
1145
+ if (!(!await pathExists(oxlintConfigPath) || yes || await askConfirm$1({
1146
+ message: "Overwrite existing oxlint.config.ts?",
1147
+ initialValue: true
1148
+ }))) return "kept-existing";
1149
+ await writeText(oxlintConfigPath, oxlintConfigTemplate({ useVitest: detectUseVitest(pkg.scripts) }));
1150
+ return "written";
1151
+ }
1152
+ function detectUseVitest(scripts) {
1153
+ return typeof scripts?.test === "string" && scripts.test.includes("vitest");
1154
+ }
1155
+ function removeEslintDependencies(pkg, key) {
1156
+ const bucket = pkg[key];
1157
+ if (!bucket) return [];
1158
+ const removed = [];
1159
+ for (const name of Object.keys(bucket)) {
1160
+ if (!isEslintDependency(name)) continue;
1161
+ delete bucket[name];
1162
+ removed.push(name);
1163
+ }
1164
+ return removed;
1165
+ }
1166
+ function isEslintDependency(name) {
1167
+ return name === "eslint" || name === "typescript-eslint" || name.startsWith("@eslint/") || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-") || /(^|\/)eslint-(plugin|config|import-resolver)-/.test(name);
1168
+ }
1169
+ function removeEslintConfigFromPackageJson(pkg) {
1170
+ if (!Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return false;
1171
+ delete pkg.eslintConfig;
1172
+ return true;
1173
+ }
1174
+ function cleanupEmptyDependencyBuckets$1(pkg) {
1175
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
1176
+ if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
1177
+ }
1178
+ var CancelledError$1 = class extends Error {
1179
+ constructor() {
1180
+ super("Cancelled");
1181
+ }
1182
+ };
1183
+ function abort$1(opts = {}) {
1184
+ cancel(opts.message ?? "Cancelled");
1185
+ process.exitCode = opts.exitCode ?? 0;
1186
+ throw new CancelledError$1();
1187
+ }
1188
+
1189
+ //#endregion
1190
+ //#region src/commands/oxfmt.ts
1191
+ const OXFMT_SCRIPTS = {
1192
+ format: "oxfmt",
1193
+ "format:check": "oxfmt --check",
1194
+ fmt: "oxfmt",
1195
+ "fmt:check": "oxfmt --check"
1196
+ };
1197
+ const PRETTIER_CONFIG_FILES = [
1198
+ ".prettierrc",
1199
+ ".prettierrc.json",
1200
+ ".prettierrc.json5",
1201
+ ".prettierrc.yaml",
1202
+ ".prettierrc.yml",
1203
+ ".prettierrc.toml",
1204
+ ".prettierrc.js",
1205
+ ".prettierrc.cjs",
1206
+ ".prettierrc.mjs",
1207
+ ".prettierrc.ts",
1208
+ ".prettierrc.cts",
1209
+ ".prettierrc.mts",
1210
+ "prettier.config.js",
1211
+ "prettier.config.cjs",
1212
+ "prettier.config.mjs",
1213
+ "prettier.config.ts",
1214
+ "prettier.config.cts",
1215
+ "prettier.config.mts"
1216
+ ];
1217
+ async function runOxfmt({ yes = false } = {}) {
1218
+ try {
1219
+ intro("frontpl (oxfmt)");
1220
+ const rootDir = process.cwd();
1221
+ const packageJsonPath = path.join(rootDir, "package.json");
1222
+ const oxfmtConfigPath = path.join(rootDir, ".oxfmtrc.json");
1223
+ const pkg = await readPackageJson(packageJsonPath);
1224
+ if (!pkg) {
1225
+ cancel("Missing package.json. Run this command in a Node project root.");
1226
+ process.exitCode = 1;
1227
+ return;
1228
+ }
1229
+ const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
1230
+ const stats = await migrateToOxfmt({
1231
+ pkg,
1232
+ rootDir,
1233
+ oxfmtConfigPath,
1234
+ packageManager,
1235
+ configMode: yes ? "rebuild" : await askConfigMode({
1236
+ rootDir,
1237
+ pkg
1238
+ }),
1239
+ yes
1240
+ });
1241
+ await writePackageJson(packageJsonPath, pkg);
1242
+ const installOk = await maybeInstallDependencies({
1243
+ yes,
1244
+ packageManager,
1245
+ rootDir
1246
+ });
1247
+ const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
1248
+ const depSummary = stats.addedOxfmtDependency ? "added devDependency: oxfmt" : "devDependency oxfmt already present";
1249
+ const removedDepsSummary = stats.removedDependencies.length > 0 ? `removed prettier deps: ${stats.removedDependencies.join(", ")}` : "no prettier deps removed";
1250
+ const removedPackageJsonPrettierSummary = stats.removedPackageJsonPrettierConfig ? "removed package.json#prettier" : "no package.json#prettier removed";
1251
+ const removedFilesSummary = stats.removedConfigFiles.length > 0 ? `removed prettier config files: ${stats.removedConfigFiles.join(", ")}` : "no prettier config files removed";
1252
+ const configSummary = stats.oxfmtConfigAction === "migrated" ? "migrated .oxfmtrc.json from prettier" : stats.oxfmtConfigAction === "rebuilt" ? "rebuilt .oxfmtrc.json" : "kept existing .oxfmtrc.json";
1253
+ const installSummary = packageManager === "deno" ? "skipped dependency install (deno project)" : installOk === true ? `installed dependencies with ${packageManager}` : installOk === false ? `dependency install failed with ${packageManager}` : "skipped dependency install";
1254
+ outro([
1255
+ "Done. Applied oxfmt migration.",
1256
+ `- ${scriptSummary}`,
1257
+ `- ${depSummary}`,
1258
+ `- ${removedDepsSummary}`,
1259
+ `- ${removedPackageJsonPrettierSummary}`,
1260
+ `- ${removedFilesSummary}`,
1261
+ `- ${configSummary}`,
1262
+ `- ${installSummary}`
1263
+ ].join("\n"));
1264
+ } catch (error) {
1265
+ if (error instanceof CancelledError) return;
1266
+ throw error;
1267
+ }
1268
+ }
1269
+ async function migrateToOxfmt(opts) {
1270
+ const { pkg, rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
1271
+ const scripts = { ...pkg.scripts };
1272
+ const conflictingScripts = Object.entries(OXFMT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
1273
+ const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm({
1274
+ message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxfmt?`,
1275
+ initialValue: true
1276
+ });
1277
+ const scriptsUpdated = [];
1278
+ const scriptsKept = [];
1279
+ for (const [name, command] of Object.entries(OXFMT_SCRIPTS)) {
1280
+ const current = scripts[name];
1281
+ if (current === command) continue;
1282
+ if (current && !shouldOverwriteConflicts) {
1283
+ scriptsKept.push(name);
1284
+ continue;
1285
+ }
1286
+ scripts[name] = command;
1287
+ scriptsUpdated.push(name);
1288
+ }
1289
+ pkg.scripts = scripts;
1290
+ const devDependencies = { ...pkg.devDependencies };
1291
+ let addedOxfmtDependency = false;
1292
+ if (!devDependencies.oxfmt) {
1293
+ devDependencies.oxfmt = "latest";
1294
+ addedOxfmtDependency = true;
1295
+ }
1296
+ pkg.devDependencies = devDependencies;
1297
+ const removePrettier = yes ? true : await askConfirm({
1298
+ message: "Remove prettier dependencies and config files?",
1299
+ initialValue: true
1300
+ });
1301
+ const removedDependencies = [];
1302
+ let removedPackageJsonPrettierConfig = false;
1303
+ if (removePrettier) {
1304
+ removedDependencies.push(...removePrettierDependencies(pkg, "dependencies"));
1305
+ removedDependencies.push(...removePrettierDependencies(pkg, "devDependencies"));
1306
+ removedPackageJsonPrettierConfig = removePrettierConfigFromPackageJson(pkg);
1307
+ cleanupEmptyDependencyBuckets(pkg);
1308
+ }
1309
+ const oxfmtConfigAction = await applyOxfmtConfig({
1310
+ rootDir,
1311
+ oxfmtConfigPath,
1312
+ packageManager,
1313
+ configMode,
1314
+ yes
1315
+ });
1316
+ const removedConfigFiles = [];
1317
+ if (removePrettier) for (const file of PRETTIER_CONFIG_FILES) {
1318
+ const filePath = path.join(rootDir, file);
1319
+ if (!await pathExists(filePath)) continue;
1320
+ await unlink(filePath);
1321
+ removedConfigFiles.push(file);
1322
+ }
1323
+ return {
1324
+ scriptsUpdated,
1325
+ scriptsKept,
1326
+ addedOxfmtDependency,
1327
+ removedPackageJsonPrettierConfig,
1328
+ removedDependencies,
1329
+ removedConfigFiles,
1330
+ oxfmtConfigAction
1331
+ };
1332
+ }
1333
+ async function maybeInstallDependencies(opts) {
1334
+ const { yes, packageManager, rootDir } = opts;
1335
+ if (packageManager === "deno") return void 0;
1336
+ if (!(yes || await askConfirm({
1337
+ message: `Install dependencies now with ${packageManager}?`,
1338
+ initialValue: true
1339
+ }))) return void 0;
1340
+ const installSpinner = spinner();
1341
+ installSpinner.start(`Installing dependencies with ${packageManager}`);
1342
+ const result = await exec(packageManager, ["install"], { cwd: rootDir });
1343
+ installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
1344
+ return result.ok;
1345
+ }
1346
+ async function askConfirm(opts) {
1347
+ const answer = await confirm({
1348
+ message: opts.message,
1349
+ initialValue: opts.initialValue
1350
+ });
1351
+ if (isCancel(answer)) return abort();
1352
+ return answer;
1353
+ }
1354
+ async function askConfigMode(opts) {
1355
+ const mode = await select({
1356
+ message: "Prettier config strategy",
1357
+ initialValue: await detectPrettierConfig(opts.rootDir, opts.pkg) ? "migrate" : "rebuild",
1358
+ options: [{
1359
+ value: "migrate",
1360
+ label: "Migrate from Prettier (oxfmt --migrate=prettier)"
1361
+ }, {
1362
+ value: "rebuild",
1363
+ label: "Rebuild .oxfmtrc.json (current mode)"
1364
+ }]
1365
+ });
1366
+ if (isCancel(mode)) return abort();
1367
+ return mode;
1368
+ }
1369
+ async function detectPrettierConfig(rootDir, pkg) {
1370
+ if (Object.prototype.hasOwnProperty.call(pkg, "prettier")) return true;
1371
+ for (const file of PRETTIER_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
1372
+ return false;
1373
+ }
1374
+ async function applyOxfmtConfig(opts) {
1375
+ const { rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
1376
+ if (!(!await pathExists(oxfmtConfigPath) || yes || await askConfirm({
1377
+ message: configMode === "migrate" ? "Overwrite existing .oxfmtrc.json via prettier migration?" : "Overwrite existing .oxfmtrc.json?",
1378
+ initialValue: true
1379
+ }))) return "kept-existing";
1380
+ if (configMode === "migrate") {
1381
+ if (await runOxfmtPrettierMigration({
1382
+ rootDir,
1383
+ packageManager
1384
+ })) return "migrated";
1385
+ if (!(yes || await askConfirm({
1386
+ message: "Migration failed. Rebuild .oxfmtrc.json with defaults instead?",
1387
+ initialValue: true
1388
+ }))) return "kept-existing";
1389
+ }
1390
+ await writeText(oxfmtConfigPath, oxfmtConfigTemplate());
1391
+ return "rebuilt";
1392
+ }
1393
+ async function runOxfmtPrettierMigration(opts) {
1394
+ const { rootDir, packageManager } = opts;
1395
+ const migrateSpinner = spinner();
1396
+ migrateSpinner.start("Migrating prettier config to .oxfmtrc.json");
1397
+ if ((await exec("oxfmt", ["--migrate=prettier"], { cwd: rootDir })).ok) {
1398
+ migrateSpinner.stop("Migrated config with oxfmt");
1399
+ return true;
1400
+ }
1401
+ const fallbackRun = packageManager === "pnpm" ? await exec("pnpm", [
1402
+ "exec",
1403
+ "oxfmt",
1404
+ "--migrate=prettier"
1405
+ ], { cwd: rootDir }) : packageManager === "npm" ? await exec("npm", [
1406
+ "exec",
1407
+ "oxfmt",
1408
+ "--",
1409
+ "--migrate=prettier"
1410
+ ], { cwd: rootDir }) : packageManager === "yarn" ? await exec("yarn", [
1411
+ "dlx",
1412
+ "oxfmt",
1413
+ "--migrate=prettier"
1414
+ ], { cwd: rootDir }) : packageManager === "bun" ? await exec("bun", [
1415
+ "x",
1416
+ "oxfmt",
1417
+ "--migrate=prettier"
1418
+ ], { cwd: rootDir }) : { ok: false };
1419
+ migrateSpinner.stop(fallbackRun.ok ? "Migrated config with oxfmt" : "Prettier migration failed");
1420
+ return fallbackRun.ok;
1421
+ }
1422
+ function removePrettierDependencies(pkg, key) {
1423
+ const bucket = pkg[key];
1424
+ if (!bucket) return [];
1425
+ const removed = [];
1426
+ for (const name of Object.keys(bucket)) {
1427
+ if (!isPrettierDependency(name)) continue;
1428
+ delete bucket[name];
1429
+ removed.push(name);
1430
+ }
1431
+ return removed;
1432
+ }
1433
+ function cleanupEmptyDependencyBuckets(pkg) {
1434
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
1435
+ if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
1436
+ }
1437
+ function removePrettierConfigFromPackageJson(pkg) {
1438
+ if (!Object.prototype.hasOwnProperty.call(pkg, "prettier")) return false;
1439
+ delete pkg.prettier;
1440
+ return true;
1441
+ }
1442
+ function isPrettierDependency(name) {
1443
+ return name === "prettier" || /(^|\/)prettier-plugin-/.test(name) || name.startsWith("@prettier/plugin-");
1444
+ }
1445
+ var CancelledError = class extends Error {
1446
+ constructor() {
1447
+ super("Cancelled");
1448
+ }
1449
+ };
1450
+ function abort(opts = {}) {
1451
+ cancel(opts.message ?? "Cancelled");
1452
+ process.exitCode = opts.exitCode ?? 0;
1453
+ throw new CancelledError();
1454
+ }
1455
+
1456
+ //#endregion
1457
+ export { runCi as a, oxlintConfigTemplate as c, validateProjectName as i, packageJsonTemplate as l, runOxlint as n, githubCliCiWorkflowTemplate as o, runInit as r, githubDependabotTemplate as s, runOxfmt as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpl",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Interactive CLI to scaffold standardized frontend project templates.",
5
5
  "keywords": [
6
6
  "cli",
@@ -53,11 +53,11 @@
53
53
  "tiny-bin": "^2.0.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@kingsword/lint-config": "^0.1.1",
56
+ "@kingsword/lint-config": "^0.2.1",
57
57
  "@types/node": "^25.0.10",
58
- "oxfmt": "^0.31.0",
58
+ "oxfmt": "^0.32.0",
59
59
  "oxlint": "^1.46.0",
60
- "oxlint-tsgolint": "^0.11.5",
60
+ "oxlint-tsgolint": "^0.13.0",
61
61
  "tsdown": "^0.20.1",
62
62
  "typescript": "^5.9.3"
63
63
  },