frontpl 0.3.2 → 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 +58 -0
- package/dist/cli.mjs +5 -1
- package/dist/index.d.mts +30 -1
- package/dist/index.mjs +2 -2
- package/dist/{init-oJwslpqP.mjs → oxfmt-DlwbhnpJ.mjs} +623 -72
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ frontpl init my-frontend
|
|
|
27
27
|
Follow the prompts to choose:
|
|
28
28
|
|
|
29
29
|
- Package manager (`npm`/`pnpm`/`yarn`/`bun`/`deno`)
|
|
30
|
+
- Optional `pnpm workspace` mode (monorepo skeleton)
|
|
30
31
|
- Optional tooling: `oxlint`, `oxfmt`, `vitest`, `tsdown`
|
|
31
32
|
|
|
32
33
|
When `oxlint` is enabled, generated projects use `@kingsword/lint-config` via `oxlint.config.ts`.
|
|
@@ -42,15 +43,23 @@ Generated lint-related dependencies (`oxlint`, `oxlint-tsgolint`, `oxfmt`, `@kin
|
|
|
42
43
|
### `frontpl [name]` / `frontpl init [name]`
|
|
43
44
|
|
|
44
45
|
Scaffold a new project into `./<name>` (or prompt for a name when omitted).
|
|
46
|
+
Project names support letters (including uppercase/camel case), numbers, `.`, `_`, `-` (cannot start with `.` or `_`).
|
|
45
47
|
|
|
46
48
|
Generated output includes (based on options):
|
|
47
49
|
|
|
48
50
|
- `.editorconfig`, `.gitignore`, `.gitattributes`
|
|
49
51
|
- `package.json` (+ scripts like optional `lint`, `format:check`, `test`, `build`)
|
|
50
52
|
- `tsconfig.json`, `src/index.ts`
|
|
53
|
+
- Relative TypeScript imports use explicit `.ts` extensions (e.g. generated `src/index.test.ts`)
|
|
51
54
|
- Optional configs: `oxlint.config.ts`, `.oxfmtrc.json`, `tsdown.config.ts`
|
|
52
55
|
- Optional GitHub Actions workflows in `.github/workflows/`
|
|
53
56
|
|
|
57
|
+
When `pnpm workspace mode` is enabled:
|
|
58
|
+
|
|
59
|
+
- Root contains `pnpm-workspace.yaml` and the workspace `package.json`
|
|
60
|
+
- `oxlint`/`oxfmt` scripts, dependencies, and config files are generated at the workspace root
|
|
61
|
+
- App/library package is scaffolded under `packages/<name>/` with its own `package.json`, `src`, and `tsconfig.json`
|
|
62
|
+
|
|
54
63
|
### `frontpl ci`
|
|
55
64
|
|
|
56
65
|
Add or update CI/Release workflows for an existing project (run it in your repo root).
|
|
@@ -64,6 +73,52 @@ What it does:
|
|
|
64
73
|
- Optionally generates `.github/workflows/release.yml` (tag/commit/both)
|
|
65
74
|
- Optionally generates `.github/dependabot.yml` with grouped updates (`dependencies`, `github-actions`)
|
|
66
75
|
|
|
76
|
+
### `frontpl oxlint`
|
|
77
|
+
|
|
78
|
+
Add/migrate linting in the current project to `oxlint`.
|
|
79
|
+
|
|
80
|
+
What it does:
|
|
81
|
+
|
|
82
|
+
- Asks strategy interactively:
|
|
83
|
+
- Migrate gradually (keep existing ESLint assets)
|
|
84
|
+
- Replace ESLint directly (current mode)
|
|
85
|
+
- Ensures `package.json` scripts use:
|
|
86
|
+
- `lint`: `oxlint --type-aware --type-check`
|
|
87
|
+
- `lint:fix`: `oxlint --type-aware --type-check --fix`
|
|
88
|
+
- Removes `typecheck: tsc --noEmit` when confirmed (or by default with `--yes`)
|
|
89
|
+
- Ensures devDependencies exist:
|
|
90
|
+
- `oxlint`
|
|
91
|
+
- `oxlint-tsgolint`
|
|
92
|
+
- `@kingsword/lint-config`
|
|
93
|
+
- Creates or updates `oxlint.config.ts` using `@kingsword/lint-config`
|
|
94
|
+
- In replace mode, removes ESLint deps/configs (`eslint*`, `@eslint/*`, `@typescript-eslint/*`, `eslintConfig`, `.eslintrc*`, `eslint.config.*`)
|
|
95
|
+
- Optionally installs dependencies with detected package manager
|
|
96
|
+
|
|
97
|
+
Use `--yes` (or `-y`) to skip confirmations and apply default choices.
|
|
98
|
+
With `--yes`, strategy defaults to `replace`.
|
|
99
|
+
|
|
100
|
+
### `frontpl oxfmt`
|
|
101
|
+
|
|
102
|
+
Add/migrate formatting in the current project to `oxfmt`.
|
|
103
|
+
|
|
104
|
+
What it does:
|
|
105
|
+
|
|
106
|
+
- Asks config strategy interactively:
|
|
107
|
+
- Migrate from Prettier (`oxfmt --migrate=prettier`)
|
|
108
|
+
- Rebuild `.oxfmtrc.json` (current mode)
|
|
109
|
+
- Ensures `package.json` scripts use:
|
|
110
|
+
- `format`: `oxfmt`
|
|
111
|
+
- `format:check`: `oxfmt --check`
|
|
112
|
+
- `fmt`: `oxfmt`
|
|
113
|
+
- `fmt:check`: `oxfmt --check`
|
|
114
|
+
- Ensures `devDependencies.oxfmt` exists (defaults to `latest` when missing)
|
|
115
|
+
- Creates or updates `.oxfmtrc.json`
|
|
116
|
+
- Optionally removes `prettier` / `prettier-plugin-*` / `@prettier/plugin-*` dependencies, `package.json#prettier`, and Prettier config files (`.prettierrc*`, `prettier.config.*`)
|
|
117
|
+
- Optionally installs dependencies with detected package manager
|
|
118
|
+
|
|
119
|
+
Use `--yes` (or `-y`) to skip confirmations and apply default choices.
|
|
120
|
+
With `--yes`, config strategy defaults to rebuild `.oxfmtrc.json`.
|
|
121
|
+
|
|
67
122
|
## GitHub Actions (CI + Release)
|
|
68
123
|
|
|
69
124
|
frontpl generates workflows that call reusable workflows from `kingsword09/workflows` (pinned to commit SHA + `# vX.Y.Z` comment by default):
|
|
@@ -90,6 +145,7 @@ When CI workflows are enabled, frontpl can also generate `.github/dependabot.yml
|
|
|
90
145
|
- Keeps `github-actions` updates enabled
|
|
91
146
|
- Adds grouped dependencies updates (`groups.dependencies`)
|
|
92
147
|
- Uses the selected `workingDirectory` (`.` -> `/`, monorepo package -> `/packages/<name>`)
|
|
148
|
+
- In `frontpl init` + `pnpm workspace mode`, default `workingDirectory` is workspace root (`/`)
|
|
93
149
|
- Maps JavaScript package managers (`npm`/`pnpm`/`yarn`/`bun`) to Dependabot `package-ecosystem: "npm"`
|
|
94
150
|
|
|
95
151
|
## Development
|
|
@@ -100,6 +156,8 @@ pnpm run lint
|
|
|
100
156
|
pnpm run build
|
|
101
157
|
node dist/cli.mjs --help
|
|
102
158
|
node dist/cli.mjs ci
|
|
159
|
+
node dist/cli.mjs oxlint --help
|
|
160
|
+
node dist/cli.mjs oxfmt --help
|
|
103
161
|
```
|
|
104
162
|
|
|
105
163
|
## Lint preset
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as runCi, n as runOxlint, r as runInit, t as runOxfmt } from "./oxfmt-DlwbhnpJ.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({
|
|
@@ -29,6 +46,18 @@ declare function packageJsonTemplate(opts: {
|
|
|
29
46
|
useTsdown: boolean;
|
|
30
47
|
tsdownVersion?: string;
|
|
31
48
|
}): string;
|
|
49
|
+
declare function workspaceRootPackageJsonTemplate(opts: {
|
|
50
|
+
name: string;
|
|
51
|
+
packageManager: string;
|
|
52
|
+
useOxlint: boolean;
|
|
53
|
+
oxlintVersion?: string;
|
|
54
|
+
oxlintTsgolintVersion?: string;
|
|
55
|
+
kingswordLintConfigVersion?: string;
|
|
56
|
+
useOxfmt: boolean;
|
|
57
|
+
oxfmtVersion?: string;
|
|
58
|
+
useVitest: boolean;
|
|
59
|
+
useTsdown: boolean;
|
|
60
|
+
}): string;
|
|
32
61
|
declare function githubCliCiWorkflowTemplate(opts: {
|
|
33
62
|
packageManager: "npm" | "pnpm" | "yarn" | "bun" | "deno";
|
|
34
63
|
nodeVersion: number;
|
|
@@ -48,4 +77,4 @@ declare function githubDependabotTemplate(opts: {
|
|
|
48
77
|
workingDirectory: string;
|
|
49
78
|
}): string;
|
|
50
79
|
//#endregion
|
|
51
|
-
export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
|
|
80
|
+
export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as oxlintConfigTemplate, i as
|
|
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, u as workspaceRootPackageJsonTemplate } from "./oxfmt-DlwbhnpJ.mjs";
|
|
2
2
|
|
|
3
|
-
export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
|
|
3
|
+
export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
|
|
@@ -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.
|
|
114
|
+
"import { hello } from \"./index.ts\";",
|
|
73
115
|
"",
|
|
74
116
|
"describe(\"hello\", () => {",
|
|
75
117
|
" it(\"greets\", () => {",
|
|
@@ -110,28 +152,35 @@ function tsdownConfigTemplate() {
|
|
|
110
152
|
""
|
|
111
153
|
].join("\n");
|
|
112
154
|
}
|
|
113
|
-
function
|
|
114
|
-
const scripts = {};
|
|
155
|
+
function applyLintAndFormatScripts(scripts, opts) {
|
|
115
156
|
if (opts.useOxlint) {
|
|
116
157
|
const oxlintCmd = "oxlint --type-aware --type-check";
|
|
117
158
|
scripts.lint = oxlintCmd;
|
|
118
159
|
scripts["lint:fix"] = `${oxlintCmd} --fix`;
|
|
119
|
-
}
|
|
160
|
+
}
|
|
120
161
|
if (opts.useOxfmt) {
|
|
121
162
|
scripts.format = "oxfmt";
|
|
122
163
|
scripts["format:check"] = "oxfmt --check";
|
|
123
164
|
scripts.fmt = "oxfmt";
|
|
124
165
|
scripts["fmt:check"] = "oxfmt --check";
|
|
125
166
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const devDependencies = { typescript: opts.typescriptVersion };
|
|
167
|
+
}
|
|
168
|
+
function applyLintAndFormatDependencies(devDependencies, opts) {
|
|
129
169
|
if (opts.useOxlint) {
|
|
130
170
|
if (opts.oxlintVersion) devDependencies.oxlint = opts.oxlintVersion;
|
|
131
171
|
if (opts.oxlintTsgolintVersion) devDependencies["oxlint-tsgolint"] = opts.oxlintTsgolintVersion;
|
|
132
172
|
if (opts.kingswordLintConfigVersion) devDependencies["@kingsword/lint-config"] = opts.kingswordLintConfigVersion;
|
|
133
173
|
}
|
|
134
174
|
if (opts.useOxfmt && opts.oxfmtVersion) devDependencies.oxfmt = opts.oxfmtVersion;
|
|
175
|
+
}
|
|
176
|
+
function packageJsonTemplate(opts) {
|
|
177
|
+
const scripts = {};
|
|
178
|
+
if (!opts.useOxlint) scripts.typecheck = "tsc --noEmit";
|
|
179
|
+
applyLintAndFormatScripts(scripts, opts);
|
|
180
|
+
if (opts.useVitest) scripts.test = "vitest";
|
|
181
|
+
if (opts.useTsdown) scripts.build = "tsdown";
|
|
182
|
+
const devDependencies = { typescript: opts.typescriptVersion };
|
|
183
|
+
applyLintAndFormatDependencies(devDependencies, opts);
|
|
135
184
|
if (opts.useVitest && opts.vitestVersion) devDependencies.vitest = opts.vitestVersion;
|
|
136
185
|
if (opts.useTsdown && opts.tsdownVersion) devDependencies.tsdown = opts.tsdownVersion;
|
|
137
186
|
return JSON.stringify({
|
|
@@ -144,6 +193,22 @@ function packageJsonTemplate(opts) {
|
|
|
144
193
|
packageManager: opts.packageManager
|
|
145
194
|
}, null, 2) + "\n";
|
|
146
195
|
}
|
|
196
|
+
function workspaceRootPackageJsonTemplate(opts) {
|
|
197
|
+
const scripts = {};
|
|
198
|
+
applyLintAndFormatScripts(scripts, opts);
|
|
199
|
+
if (opts.useVitest) scripts.test = "pnpm -r --if-present run test";
|
|
200
|
+
if (opts.useTsdown) scripts.build = "pnpm -r --if-present run build";
|
|
201
|
+
const devDependencies = {};
|
|
202
|
+
applyLintAndFormatDependencies(devDependencies, opts);
|
|
203
|
+
const manifest = {
|
|
204
|
+
name: opts.name,
|
|
205
|
+
private: true,
|
|
206
|
+
packageManager: opts.packageManager
|
|
207
|
+
};
|
|
208
|
+
if (Object.keys(scripts).length > 0) manifest.scripts = scripts;
|
|
209
|
+
if (Object.keys(devDependencies).length > 0) manifest.devDependencies = devDependencies;
|
|
210
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
211
|
+
}
|
|
147
212
|
const DEFAULT_WORKFLOWS_REF = "7320d30bcd47cee17cc2d8d28250ba1ab1f742b8";
|
|
148
213
|
const DEFAULT_WORKFLOWS_VERSION = "v1.0.3";
|
|
149
214
|
function resolveWorkflowsPin(opts) {
|
|
@@ -333,17 +398,6 @@ function yamlString(value) {
|
|
|
333
398
|
return JSON.stringify(value);
|
|
334
399
|
}
|
|
335
400
|
|
|
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
401
|
//#endregion
|
|
348
402
|
//#region src/commands/ci.ts
|
|
349
403
|
async function runCi() {
|
|
@@ -377,7 +431,7 @@ async function runCi() {
|
|
|
377
431
|
}
|
|
378
432
|
]
|
|
379
433
|
});
|
|
380
|
-
if (isCancel(packageManager)) return abort();
|
|
434
|
+
if (isCancel(packageManager)) return abort$2();
|
|
381
435
|
const candidates = await listPackageCandidates(rootDir, packageManager);
|
|
382
436
|
if (candidates.length === 0) {
|
|
383
437
|
cancel("No package found. Run this command in a project root (with package.json or deno.json).");
|
|
@@ -393,7 +447,7 @@ async function runCi() {
|
|
|
393
447
|
label: c
|
|
394
448
|
}))
|
|
395
449
|
});
|
|
396
|
-
if (isCancel(workingDirectory)) return abort();
|
|
450
|
+
if (isCancel(workingDirectory)) return abort$2();
|
|
397
451
|
const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
|
|
398
452
|
const nodeVersionText = await text({
|
|
399
453
|
message: "Node.js major version (for GitHub Actions)",
|
|
@@ -403,14 +457,14 @@ async function runCi() {
|
|
|
403
457
|
if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
|
|
404
458
|
}
|
|
405
459
|
});
|
|
406
|
-
if (isCancel(nodeVersionText)) return abort();
|
|
460
|
+
if (isCancel(nodeVersionText)) return abort$2();
|
|
407
461
|
const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
|
|
408
462
|
const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
|
|
409
463
|
const addRelease = await confirm({
|
|
410
464
|
message: "Add release workflow too?",
|
|
411
465
|
initialValue: true
|
|
412
466
|
});
|
|
413
|
-
if (isCancel(addRelease)) return abort();
|
|
467
|
+
if (isCancel(addRelease)) return abort$2();
|
|
414
468
|
const releaseMode = addRelease ? await select({
|
|
415
469
|
message: "Release workflows",
|
|
416
470
|
initialValue: "tag",
|
|
@@ -429,17 +483,17 @@ async function runCi() {
|
|
|
429
483
|
}
|
|
430
484
|
]
|
|
431
485
|
}) : void 0;
|
|
432
|
-
if (isCancel(releaseMode)) return abort();
|
|
486
|
+
if (isCancel(releaseMode)) return abort$2();
|
|
433
487
|
const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
|
|
434
488
|
message: "Release: npm trusted publishing (OIDC)?",
|
|
435
489
|
initialValue: true
|
|
436
490
|
}) : void 0;
|
|
437
|
-
if (isCancel(trustedPublishing)) return abort();
|
|
491
|
+
if (isCancel(trustedPublishing)) return abort$2();
|
|
438
492
|
const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
|
|
439
493
|
message: "Add/update Dependabot config (.github/dependabot.yml)?",
|
|
440
494
|
initialValue: true
|
|
441
495
|
}) : false;
|
|
442
|
-
if (isCancel(addDependabot)) return abort();
|
|
496
|
+
if (isCancel(addDependabot)) return abort$2();
|
|
443
497
|
const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
|
|
444
498
|
const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
|
|
445
499
|
const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
|
|
@@ -475,19 +529,19 @@ async function runCi() {
|
|
|
475
529
|
}
|
|
476
530
|
outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
|
|
477
531
|
} catch (err) {
|
|
478
|
-
if (err instanceof CancelledError) return;
|
|
532
|
+
if (err instanceof CancelledError$2) return;
|
|
479
533
|
throw err;
|
|
480
534
|
}
|
|
481
535
|
}
|
|
482
|
-
var CancelledError = class extends Error {
|
|
536
|
+
var CancelledError$2 = class extends Error {
|
|
483
537
|
constructor() {
|
|
484
538
|
super("Cancelled");
|
|
485
539
|
}
|
|
486
540
|
};
|
|
487
|
-
function abort(opts = {}) {
|
|
541
|
+
function abort$2(opts = {}) {
|
|
488
542
|
cancel(opts.message ?? "Cancelled");
|
|
489
543
|
process.exitCode = opts.exitCode ?? 0;
|
|
490
|
-
throw new CancelledError();
|
|
544
|
+
throw new CancelledError$2();
|
|
491
545
|
}
|
|
492
546
|
async function confirmOverwriteIfExists(absPath, label) {
|
|
493
547
|
if (!await pathExists(absPath)) return true;
|
|
@@ -495,26 +549,9 @@ async function confirmOverwriteIfExists(absPath, label) {
|
|
|
495
549
|
message: `Overwrite existing ${label}?`,
|
|
496
550
|
initialValue: true
|
|
497
551
|
});
|
|
498
|
-
if (isCancel(overwrite)) return abort();
|
|
552
|
+
if (isCancel(overwrite)) return abort$2();
|
|
499
553
|
return overwrite;
|
|
500
554
|
}
|
|
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
555
|
async function listPackageCandidates(rootDir, packageManager) {
|
|
519
556
|
const candidates = /* @__PURE__ */ new Set();
|
|
520
557
|
if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
|
|
@@ -564,7 +601,7 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
|
|
|
564
601
|
runTests: true
|
|
565
602
|
};
|
|
566
603
|
const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
|
|
567
|
-
if (!pkg) return abort({
|
|
604
|
+
if (!pkg) return abort$2({
|
|
568
605
|
message: `Missing package.json in ${workingDirectory}`,
|
|
569
606
|
exitCode: 1
|
|
570
607
|
});
|
|
@@ -580,17 +617,17 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
|
|
|
580
617
|
message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
|
|
581
618
|
initialValue: runLintDefault
|
|
582
619
|
});
|
|
583
|
-
if (isCancel(runLint)) return abort();
|
|
620
|
+
if (isCancel(runLint)) return abort$2();
|
|
584
621
|
const runFormatCheck = await confirm({
|
|
585
622
|
message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
|
|
586
623
|
initialValue: runFormatCheckDefault
|
|
587
624
|
});
|
|
588
|
-
if (isCancel(runFormatCheck)) return abort();
|
|
625
|
+
if (isCancel(runFormatCheck)) return abort$2();
|
|
589
626
|
const runTests = await confirm({
|
|
590
627
|
message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
|
|
591
628
|
initialValue: runTestsDefault
|
|
592
629
|
});
|
|
593
|
-
if (isCancel(runTests)) return abort();
|
|
630
|
+
if (isCancel(runTests)) return abort$2();
|
|
594
631
|
return {
|
|
595
632
|
runLint,
|
|
596
633
|
runFormatCheck,
|
|
@@ -606,7 +643,7 @@ async function promptCommand(message, initialValue) {
|
|
|
606
643
|
initialValue,
|
|
607
644
|
validate: (v = "") => !v.trim() ? "Command is required" : void 0
|
|
608
645
|
});
|
|
609
|
-
if (isCancel(value)) return abort();
|
|
646
|
+
if (isCancel(value)) return abort$2();
|
|
610
647
|
return String(value).trim();
|
|
611
648
|
}
|
|
612
649
|
function pmRun$1(pm, script) {
|
|
@@ -618,13 +655,6 @@ function pmRun$1(pm, script) {
|
|
|
618
655
|
case "deno": return script;
|
|
619
656
|
}
|
|
620
657
|
}
|
|
621
|
-
async function readPackageJson(filePath) {
|
|
622
|
-
try {
|
|
623
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
624
|
-
} catch {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
658
|
|
|
629
659
|
//#endregion
|
|
630
660
|
//#region src/lib/exec.ts
|
|
@@ -826,6 +856,9 @@ async function runInit({ nameArg }) {
|
|
|
826
856
|
return;
|
|
827
857
|
}
|
|
828
858
|
const pkgDir = pnpmWorkspace ? path.join(rootDir, "packages", projectName) : rootDir;
|
|
859
|
+
const toolingDir = pnpmWorkspace ? rootDir : pkgDir;
|
|
860
|
+
const packageUseOxlint = pnpmWorkspace ? false : useOxlint;
|
|
861
|
+
const packageUseOxfmt = pnpmWorkspace ? false : useOxfmt;
|
|
829
862
|
const pmVersion = await detectPackageManagerVersion(packageManager);
|
|
830
863
|
const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
|
|
831
864
|
await mkdir(path.join(pkgDir, "src"), { recursive: true });
|
|
@@ -840,11 +873,18 @@ async function runInit({ nameArg }) {
|
|
|
840
873
|
" - \"packages/*\"",
|
|
841
874
|
""
|
|
842
875
|
].join("\n"));
|
|
843
|
-
await writeText(path.join(rootDir, "package.json"),
|
|
876
|
+
await writeText(path.join(rootDir, "package.json"), workspaceRootPackageJsonTemplate({
|
|
844
877
|
name: projectName,
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
878
|
+
packageManager: packageManagerField,
|
|
879
|
+
useOxlint,
|
|
880
|
+
oxlintVersion: "latest",
|
|
881
|
+
oxlintTsgolintVersion: "latest",
|
|
882
|
+
kingswordLintConfigVersion: "latest",
|
|
883
|
+
useOxfmt,
|
|
884
|
+
oxfmtVersion: "latest",
|
|
885
|
+
useVitest,
|
|
886
|
+
useTsdown
|
|
887
|
+
}));
|
|
848
888
|
}
|
|
849
889
|
await Promise.all([
|
|
850
890
|
writeText(path.join(pkgDir, "README.md"), readmeTemplate(projectName)),
|
|
@@ -854,11 +894,11 @@ async function runInit({ nameArg }) {
|
|
|
854
894
|
name: projectName,
|
|
855
895
|
packageManager: packageManagerField,
|
|
856
896
|
typescriptVersion: "latest",
|
|
857
|
-
useOxlint,
|
|
897
|
+
useOxlint: packageUseOxlint,
|
|
858
898
|
oxlintVersion: "latest",
|
|
859
899
|
oxlintTsgolintVersion: "latest",
|
|
860
900
|
kingswordLintConfigVersion: "latest",
|
|
861
|
-
useOxfmt,
|
|
901
|
+
useOxfmt: packageUseOxfmt,
|
|
862
902
|
oxfmtVersion: "latest",
|
|
863
903
|
useVitest,
|
|
864
904
|
vitestVersion: "latest",
|
|
@@ -866,13 +906,13 @@ async function runInit({ nameArg }) {
|
|
|
866
906
|
tsdownVersion: "latest"
|
|
867
907
|
}))
|
|
868
908
|
]);
|
|
869
|
-
if (useOxlint) await writeText(path.join(
|
|
870
|
-
if (useOxfmt) await writeText(path.join(
|
|
909
|
+
if (useOxlint) await writeText(path.join(toolingDir, "oxlint.config.ts"), oxlintConfigTemplate({ useVitest }));
|
|
910
|
+
if (useOxfmt) await writeText(path.join(toolingDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
|
|
871
911
|
if (useVitest) await writeText(path.join(pkgDir, "src/index.test.ts"), srcVitestTemplate());
|
|
872
912
|
if (useTsdown) await writeText(path.join(pkgDir, "tsdown.config.ts"), tsdownConfigTemplate());
|
|
873
913
|
if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
|
|
874
914
|
if (githubActions !== "none") {
|
|
875
|
-
const workingDirectory =
|
|
915
|
+
const workingDirectory = ".";
|
|
876
916
|
const lintCommand = useOxlint && packageManager !== "deno" ? pmRun(packageManager, "lint") : void 0;
|
|
877
917
|
const formatCheckCommand = useOxfmt && packageManager !== "deno" ? pmRun(packageManager, "format:check") : void 0;
|
|
878
918
|
const testCommand = useVitest && packageManager !== "deno" ? pmRun(packageManager, "test") : void 0;
|
|
@@ -918,8 +958,7 @@ function validateProjectName(value) {
|
|
|
918
958
|
if (name.length > 214) return "Project name is too long";
|
|
919
959
|
if (name.startsWith(".")) return "Project name cannot start with '.'";
|
|
920
960
|
if (name.startsWith("_")) return "Project name cannot start with '_'";
|
|
921
|
-
if (
|
|
922
|
-
if (!/^[a-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
|
|
961
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
|
|
923
962
|
}
|
|
924
963
|
function onCancel() {
|
|
925
964
|
cancel("Cancelled");
|
|
@@ -936,4 +975,516 @@ function nextStepHint(pm) {
|
|
|
936
975
|
}
|
|
937
976
|
|
|
938
977
|
//#endregion
|
|
939
|
-
|
|
978
|
+
//#region src/commands/oxlint.ts
|
|
979
|
+
const OXLINT_COMMAND = "oxlint --type-aware --type-check";
|
|
980
|
+
const OXLINT_FIX_COMMAND = `${OXLINT_COMMAND} --fix`;
|
|
981
|
+
const OXLINT_SCRIPTS = {
|
|
982
|
+
lint: OXLINT_COMMAND,
|
|
983
|
+
"lint:fix": OXLINT_FIX_COMMAND
|
|
984
|
+
};
|
|
985
|
+
const ESLINT_CONFIG_FILES = [
|
|
986
|
+
".eslintrc",
|
|
987
|
+
".eslintrc.js",
|
|
988
|
+
".eslintrc.cjs",
|
|
989
|
+
".eslintrc.mjs",
|
|
990
|
+
".eslintrc.json",
|
|
991
|
+
".eslintrc.yaml",
|
|
992
|
+
".eslintrc.yml",
|
|
993
|
+
".eslintrc.ts",
|
|
994
|
+
".eslintrc.cts",
|
|
995
|
+
".eslintrc.mts",
|
|
996
|
+
"eslint.config.js",
|
|
997
|
+
"eslint.config.cjs",
|
|
998
|
+
"eslint.config.mjs",
|
|
999
|
+
"eslint.config.ts",
|
|
1000
|
+
"eslint.config.cts",
|
|
1001
|
+
"eslint.config.mts"
|
|
1002
|
+
];
|
|
1003
|
+
const OXLINT_DEPENDENCIES = [
|
|
1004
|
+
"oxlint",
|
|
1005
|
+
"oxlint-tsgolint",
|
|
1006
|
+
"@kingsword/lint-config"
|
|
1007
|
+
];
|
|
1008
|
+
async function runOxlint({ yes = false } = {}) {
|
|
1009
|
+
try {
|
|
1010
|
+
intro("frontpl (oxlint)");
|
|
1011
|
+
const rootDir = process.cwd();
|
|
1012
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
1013
|
+
const oxlintConfigPath = path.join(rootDir, "oxlint.config.ts");
|
|
1014
|
+
const pkg = await readPackageJson(packageJsonPath);
|
|
1015
|
+
if (!pkg) {
|
|
1016
|
+
cancel("Missing package.json. Run this command in a Node project root.");
|
|
1017
|
+
process.exitCode = 1;
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
|
|
1021
|
+
const stats = await migrateToOxlint({
|
|
1022
|
+
pkg,
|
|
1023
|
+
rootDir,
|
|
1024
|
+
oxlintConfigPath,
|
|
1025
|
+
strategy: yes ? "replace" : await askMigrationStrategy({
|
|
1026
|
+
rootDir,
|
|
1027
|
+
pkg
|
|
1028
|
+
}),
|
|
1029
|
+
yes
|
|
1030
|
+
});
|
|
1031
|
+
await writePackageJson(packageJsonPath, pkg);
|
|
1032
|
+
const installOk = await maybeInstallDependencies$1({
|
|
1033
|
+
yes,
|
|
1034
|
+
packageManager,
|
|
1035
|
+
rootDir
|
|
1036
|
+
});
|
|
1037
|
+
const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
|
|
1038
|
+
const dependencySummary = stats.addedDevDependencies.length > 0 ? `added devDependencies: ${stats.addedDevDependencies.join(", ")}` : "required oxlint devDependencies already present";
|
|
1039
|
+
const typecheckSummary = stats.removedTypecheckScript ? "removed redundant typecheck script (tsc --noEmit)" : "kept typecheck script";
|
|
1040
|
+
const eslintDependencySummary = stats.removedDependencies.length > 0 ? `removed eslint deps: ${stats.removedDependencies.join(", ")}` : "no eslint deps removed";
|
|
1041
|
+
const eslintConfigSummary = stats.removedPackageJsonEslintConfig ? "removed package.json#eslintConfig" : "no package.json#eslintConfig removed";
|
|
1042
|
+
const eslintFileSummary = stats.removedConfigFiles.length > 0 ? `removed eslint config files: ${stats.removedConfigFiles.join(", ")}` : "no eslint config files removed";
|
|
1043
|
+
const oxlintConfigSummary = stats.oxlintConfigAction === "written" ? "wrote oxlint.config.ts" : "kept existing oxlint.config.ts";
|
|
1044
|
+
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";
|
|
1045
|
+
outro([
|
|
1046
|
+
"Done. Applied oxlint migration.",
|
|
1047
|
+
`- strategy: ${stats.strategy === "migrate" ? "migrate (keep ESLint assets)" : "replace ESLint assets"}`,
|
|
1048
|
+
`- ${scriptSummary}`,
|
|
1049
|
+
`- ${typecheckSummary}`,
|
|
1050
|
+
`- ${dependencySummary}`,
|
|
1051
|
+
`- ${eslintDependencySummary}`,
|
|
1052
|
+
`- ${eslintConfigSummary}`,
|
|
1053
|
+
`- ${eslintFileSummary}`,
|
|
1054
|
+
`- ${oxlintConfigSummary}`,
|
|
1055
|
+
`- ${installSummary}`
|
|
1056
|
+
].join("\n"));
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (error instanceof CancelledError$1) return;
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async function migrateToOxlint(opts) {
|
|
1063
|
+
const { pkg, rootDir, oxlintConfigPath, strategy, yes } = opts;
|
|
1064
|
+
const scripts = { ...pkg.scripts };
|
|
1065
|
+
const conflictingScripts = Object.entries(OXLINT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
|
|
1066
|
+
const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm$1({
|
|
1067
|
+
message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxlint?`,
|
|
1068
|
+
initialValue: true
|
|
1069
|
+
});
|
|
1070
|
+
const scriptsUpdated = [];
|
|
1071
|
+
const scriptsKept = [];
|
|
1072
|
+
for (const [name, command] of Object.entries(OXLINT_SCRIPTS)) {
|
|
1073
|
+
const current = scripts[name];
|
|
1074
|
+
if (current === command) continue;
|
|
1075
|
+
if (current && !shouldOverwriteConflicts) {
|
|
1076
|
+
scriptsKept.push(name);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
scripts[name] = command;
|
|
1080
|
+
scriptsUpdated.push(name);
|
|
1081
|
+
}
|
|
1082
|
+
let removedTypecheckScript = false;
|
|
1083
|
+
if (scripts.typecheck === "tsc --noEmit") {
|
|
1084
|
+
if (yes || await askConfirm$1({
|
|
1085
|
+
message: "Remove redundant typecheck script (tsc --noEmit)?",
|
|
1086
|
+
initialValue: true
|
|
1087
|
+
})) {
|
|
1088
|
+
delete scripts.typecheck;
|
|
1089
|
+
removedTypecheckScript = true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
pkg.scripts = scripts;
|
|
1093
|
+
const devDependencies = { ...pkg.devDependencies };
|
|
1094
|
+
const addedDevDependencies = [];
|
|
1095
|
+
for (const dependency of OXLINT_DEPENDENCIES) {
|
|
1096
|
+
if (devDependencies[dependency]) continue;
|
|
1097
|
+
devDependencies[dependency] = "latest";
|
|
1098
|
+
addedDevDependencies.push(dependency);
|
|
1099
|
+
}
|
|
1100
|
+
pkg.devDependencies = devDependencies;
|
|
1101
|
+
const oxlintConfigAction = await applyOxlintConfig({
|
|
1102
|
+
pkg,
|
|
1103
|
+
oxlintConfigPath,
|
|
1104
|
+
yes
|
|
1105
|
+
});
|
|
1106
|
+
let removedDependencies = [];
|
|
1107
|
+
let removedPackageJsonEslintConfig = false;
|
|
1108
|
+
const removedConfigFiles = [];
|
|
1109
|
+
if (strategy === "replace") {
|
|
1110
|
+
removedDependencies = [...removeEslintDependencies(pkg, "dependencies"), ...removeEslintDependencies(pkg, "devDependencies")];
|
|
1111
|
+
removedPackageJsonEslintConfig = removeEslintConfigFromPackageJson(pkg);
|
|
1112
|
+
cleanupEmptyDependencyBuckets$1(pkg);
|
|
1113
|
+
for (const file of ESLINT_CONFIG_FILES) {
|
|
1114
|
+
const filePath = path.join(rootDir, file);
|
|
1115
|
+
if (!await pathExists(filePath)) continue;
|
|
1116
|
+
await unlink(filePath);
|
|
1117
|
+
removedConfigFiles.push(file);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
strategy,
|
|
1122
|
+
scriptsUpdated,
|
|
1123
|
+
scriptsKept,
|
|
1124
|
+
removedTypecheckScript,
|
|
1125
|
+
addedDevDependencies,
|
|
1126
|
+
removedDependencies,
|
|
1127
|
+
removedPackageJsonEslintConfig,
|
|
1128
|
+
removedConfigFiles,
|
|
1129
|
+
oxlintConfigAction
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
async function maybeInstallDependencies$1(opts) {
|
|
1133
|
+
const { yes, packageManager, rootDir } = opts;
|
|
1134
|
+
if (packageManager === "deno") return void 0;
|
|
1135
|
+
if (!(yes || await askConfirm$1({
|
|
1136
|
+
message: `Install dependencies now with ${packageManager}?`,
|
|
1137
|
+
initialValue: true
|
|
1138
|
+
}))) return void 0;
|
|
1139
|
+
const installSpinner = spinner();
|
|
1140
|
+
installSpinner.start(`Installing dependencies with ${packageManager}`);
|
|
1141
|
+
const result = await exec(packageManager, ["install"], { cwd: rootDir });
|
|
1142
|
+
installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
|
|
1143
|
+
return result.ok;
|
|
1144
|
+
}
|
|
1145
|
+
async function askConfirm$1(opts) {
|
|
1146
|
+
const answer = await confirm({
|
|
1147
|
+
message: opts.message,
|
|
1148
|
+
initialValue: opts.initialValue
|
|
1149
|
+
});
|
|
1150
|
+
if (isCancel(answer)) return abort$1();
|
|
1151
|
+
return answer;
|
|
1152
|
+
}
|
|
1153
|
+
async function askMigrationStrategy(opts) {
|
|
1154
|
+
const strategy = await select({
|
|
1155
|
+
message: "ESLint strategy",
|
|
1156
|
+
initialValue: await detectEslintAssets(opts.rootDir, opts.pkg) ? "migrate" : "replace",
|
|
1157
|
+
options: [{
|
|
1158
|
+
value: "migrate",
|
|
1159
|
+
label: "Migrate gradually (keep ESLint assets)"
|
|
1160
|
+
}, {
|
|
1161
|
+
value: "replace",
|
|
1162
|
+
label: "Replace ESLint directly (current mode)"
|
|
1163
|
+
}]
|
|
1164
|
+
});
|
|
1165
|
+
if (isCancel(strategy)) return abort$1();
|
|
1166
|
+
return strategy;
|
|
1167
|
+
}
|
|
1168
|
+
async function detectEslintAssets(rootDir, pkg) {
|
|
1169
|
+
if (Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return true;
|
|
1170
|
+
const dependencies = pkg.dependencies ?? {};
|
|
1171
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
1172
|
+
if (Object.keys(dependencies).some(isEslintDependency) || Object.keys(devDependencies).some(isEslintDependency)) return true;
|
|
1173
|
+
for (const file of ESLINT_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
async function applyOxlintConfig(opts) {
|
|
1177
|
+
const { pkg, oxlintConfigPath, yes } = opts;
|
|
1178
|
+
if (!(!await pathExists(oxlintConfigPath) || yes || await askConfirm$1({
|
|
1179
|
+
message: "Overwrite existing oxlint.config.ts?",
|
|
1180
|
+
initialValue: true
|
|
1181
|
+
}))) return "kept-existing";
|
|
1182
|
+
await writeText(oxlintConfigPath, oxlintConfigTemplate({ useVitest: detectUseVitest(pkg.scripts) }));
|
|
1183
|
+
return "written";
|
|
1184
|
+
}
|
|
1185
|
+
function detectUseVitest(scripts) {
|
|
1186
|
+
return typeof scripts?.test === "string" && scripts.test.includes("vitest");
|
|
1187
|
+
}
|
|
1188
|
+
function removeEslintDependencies(pkg, key) {
|
|
1189
|
+
const bucket = pkg[key];
|
|
1190
|
+
if (!bucket) return [];
|
|
1191
|
+
const removed = [];
|
|
1192
|
+
for (const name of Object.keys(bucket)) {
|
|
1193
|
+
if (!isEslintDependency(name)) continue;
|
|
1194
|
+
delete bucket[name];
|
|
1195
|
+
removed.push(name);
|
|
1196
|
+
}
|
|
1197
|
+
return removed;
|
|
1198
|
+
}
|
|
1199
|
+
function isEslintDependency(name) {
|
|
1200
|
+
return name === "eslint" || name === "typescript-eslint" || name.startsWith("@eslint/") || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-") || /(^|\/)eslint-(plugin|config|import-resolver)-/.test(name);
|
|
1201
|
+
}
|
|
1202
|
+
function removeEslintConfigFromPackageJson(pkg) {
|
|
1203
|
+
if (!Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return false;
|
|
1204
|
+
delete pkg.eslintConfig;
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
function cleanupEmptyDependencyBuckets$1(pkg) {
|
|
1208
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
|
|
1209
|
+
if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
|
|
1210
|
+
}
|
|
1211
|
+
var CancelledError$1 = class extends Error {
|
|
1212
|
+
constructor() {
|
|
1213
|
+
super("Cancelled");
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
function abort$1(opts = {}) {
|
|
1217
|
+
cancel(opts.message ?? "Cancelled");
|
|
1218
|
+
process.exitCode = opts.exitCode ?? 0;
|
|
1219
|
+
throw new CancelledError$1();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/commands/oxfmt.ts
|
|
1224
|
+
const OXFMT_SCRIPTS = {
|
|
1225
|
+
format: "oxfmt",
|
|
1226
|
+
"format:check": "oxfmt --check",
|
|
1227
|
+
fmt: "oxfmt",
|
|
1228
|
+
"fmt:check": "oxfmt --check"
|
|
1229
|
+
};
|
|
1230
|
+
const PRETTIER_CONFIG_FILES = [
|
|
1231
|
+
".prettierrc",
|
|
1232
|
+
".prettierrc.json",
|
|
1233
|
+
".prettierrc.json5",
|
|
1234
|
+
".prettierrc.yaml",
|
|
1235
|
+
".prettierrc.yml",
|
|
1236
|
+
".prettierrc.toml",
|
|
1237
|
+
".prettierrc.js",
|
|
1238
|
+
".prettierrc.cjs",
|
|
1239
|
+
".prettierrc.mjs",
|
|
1240
|
+
".prettierrc.ts",
|
|
1241
|
+
".prettierrc.cts",
|
|
1242
|
+
".prettierrc.mts",
|
|
1243
|
+
"prettier.config.js",
|
|
1244
|
+
"prettier.config.cjs",
|
|
1245
|
+
"prettier.config.mjs",
|
|
1246
|
+
"prettier.config.ts",
|
|
1247
|
+
"prettier.config.cts",
|
|
1248
|
+
"prettier.config.mts"
|
|
1249
|
+
];
|
|
1250
|
+
async function runOxfmt({ yes = false } = {}) {
|
|
1251
|
+
try {
|
|
1252
|
+
intro("frontpl (oxfmt)");
|
|
1253
|
+
const rootDir = process.cwd();
|
|
1254
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
1255
|
+
const oxfmtConfigPath = path.join(rootDir, ".oxfmtrc.json");
|
|
1256
|
+
const pkg = await readPackageJson(packageJsonPath);
|
|
1257
|
+
if (!pkg) {
|
|
1258
|
+
cancel("Missing package.json. Run this command in a Node project root.");
|
|
1259
|
+
process.exitCode = 1;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
|
|
1263
|
+
const stats = await migrateToOxfmt({
|
|
1264
|
+
pkg,
|
|
1265
|
+
rootDir,
|
|
1266
|
+
oxfmtConfigPath,
|
|
1267
|
+
packageManager,
|
|
1268
|
+
configMode: yes ? "rebuild" : await askConfigMode({
|
|
1269
|
+
rootDir,
|
|
1270
|
+
pkg
|
|
1271
|
+
}),
|
|
1272
|
+
yes
|
|
1273
|
+
});
|
|
1274
|
+
await writePackageJson(packageJsonPath, pkg);
|
|
1275
|
+
const installOk = await maybeInstallDependencies({
|
|
1276
|
+
yes,
|
|
1277
|
+
packageManager,
|
|
1278
|
+
rootDir
|
|
1279
|
+
});
|
|
1280
|
+
const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
|
|
1281
|
+
const depSummary = stats.addedOxfmtDependency ? "added devDependency: oxfmt" : "devDependency oxfmt already present";
|
|
1282
|
+
const removedDepsSummary = stats.removedDependencies.length > 0 ? `removed prettier deps: ${stats.removedDependencies.join(", ")}` : "no prettier deps removed";
|
|
1283
|
+
const removedPackageJsonPrettierSummary = stats.removedPackageJsonPrettierConfig ? "removed package.json#prettier" : "no package.json#prettier removed";
|
|
1284
|
+
const removedFilesSummary = stats.removedConfigFiles.length > 0 ? `removed prettier config files: ${stats.removedConfigFiles.join(", ")}` : "no prettier config files removed";
|
|
1285
|
+
const configSummary = stats.oxfmtConfigAction === "migrated" ? "migrated .oxfmtrc.json from prettier" : stats.oxfmtConfigAction === "rebuilt" ? "rebuilt .oxfmtrc.json" : "kept existing .oxfmtrc.json";
|
|
1286
|
+
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";
|
|
1287
|
+
outro([
|
|
1288
|
+
"Done. Applied oxfmt migration.",
|
|
1289
|
+
`- ${scriptSummary}`,
|
|
1290
|
+
`- ${depSummary}`,
|
|
1291
|
+
`- ${removedDepsSummary}`,
|
|
1292
|
+
`- ${removedPackageJsonPrettierSummary}`,
|
|
1293
|
+
`- ${removedFilesSummary}`,
|
|
1294
|
+
`- ${configSummary}`,
|
|
1295
|
+
`- ${installSummary}`
|
|
1296
|
+
].join("\n"));
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
if (error instanceof CancelledError) return;
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function migrateToOxfmt(opts) {
|
|
1303
|
+
const { pkg, rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
|
|
1304
|
+
const scripts = { ...pkg.scripts };
|
|
1305
|
+
const conflictingScripts = Object.entries(OXFMT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
|
|
1306
|
+
const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm({
|
|
1307
|
+
message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxfmt?`,
|
|
1308
|
+
initialValue: true
|
|
1309
|
+
});
|
|
1310
|
+
const scriptsUpdated = [];
|
|
1311
|
+
const scriptsKept = [];
|
|
1312
|
+
for (const [name, command] of Object.entries(OXFMT_SCRIPTS)) {
|
|
1313
|
+
const current = scripts[name];
|
|
1314
|
+
if (current === command) continue;
|
|
1315
|
+
if (current && !shouldOverwriteConflicts) {
|
|
1316
|
+
scriptsKept.push(name);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
scripts[name] = command;
|
|
1320
|
+
scriptsUpdated.push(name);
|
|
1321
|
+
}
|
|
1322
|
+
pkg.scripts = scripts;
|
|
1323
|
+
const devDependencies = { ...pkg.devDependencies };
|
|
1324
|
+
let addedOxfmtDependency = false;
|
|
1325
|
+
if (!devDependencies.oxfmt) {
|
|
1326
|
+
devDependencies.oxfmt = "latest";
|
|
1327
|
+
addedOxfmtDependency = true;
|
|
1328
|
+
}
|
|
1329
|
+
pkg.devDependencies = devDependencies;
|
|
1330
|
+
const removePrettier = yes ? true : await askConfirm({
|
|
1331
|
+
message: "Remove prettier dependencies and config files?",
|
|
1332
|
+
initialValue: true
|
|
1333
|
+
});
|
|
1334
|
+
const removedDependencies = [];
|
|
1335
|
+
let removedPackageJsonPrettierConfig = false;
|
|
1336
|
+
if (removePrettier) {
|
|
1337
|
+
removedDependencies.push(...removePrettierDependencies(pkg, "dependencies"));
|
|
1338
|
+
removedDependencies.push(...removePrettierDependencies(pkg, "devDependencies"));
|
|
1339
|
+
removedPackageJsonPrettierConfig = removePrettierConfigFromPackageJson(pkg);
|
|
1340
|
+
cleanupEmptyDependencyBuckets(pkg);
|
|
1341
|
+
}
|
|
1342
|
+
const oxfmtConfigAction = await applyOxfmtConfig({
|
|
1343
|
+
rootDir,
|
|
1344
|
+
oxfmtConfigPath,
|
|
1345
|
+
packageManager,
|
|
1346
|
+
configMode,
|
|
1347
|
+
yes
|
|
1348
|
+
});
|
|
1349
|
+
const removedConfigFiles = [];
|
|
1350
|
+
if (removePrettier) for (const file of PRETTIER_CONFIG_FILES) {
|
|
1351
|
+
const filePath = path.join(rootDir, file);
|
|
1352
|
+
if (!await pathExists(filePath)) continue;
|
|
1353
|
+
await unlink(filePath);
|
|
1354
|
+
removedConfigFiles.push(file);
|
|
1355
|
+
}
|
|
1356
|
+
return {
|
|
1357
|
+
scriptsUpdated,
|
|
1358
|
+
scriptsKept,
|
|
1359
|
+
addedOxfmtDependency,
|
|
1360
|
+
removedPackageJsonPrettierConfig,
|
|
1361
|
+
removedDependencies,
|
|
1362
|
+
removedConfigFiles,
|
|
1363
|
+
oxfmtConfigAction
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
async function maybeInstallDependencies(opts) {
|
|
1367
|
+
const { yes, packageManager, rootDir } = opts;
|
|
1368
|
+
if (packageManager === "deno") return void 0;
|
|
1369
|
+
if (!(yes || await askConfirm({
|
|
1370
|
+
message: `Install dependencies now with ${packageManager}?`,
|
|
1371
|
+
initialValue: true
|
|
1372
|
+
}))) return void 0;
|
|
1373
|
+
const installSpinner = spinner();
|
|
1374
|
+
installSpinner.start(`Installing dependencies with ${packageManager}`);
|
|
1375
|
+
const result = await exec(packageManager, ["install"], { cwd: rootDir });
|
|
1376
|
+
installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
|
|
1377
|
+
return result.ok;
|
|
1378
|
+
}
|
|
1379
|
+
async function askConfirm(opts) {
|
|
1380
|
+
const answer = await confirm({
|
|
1381
|
+
message: opts.message,
|
|
1382
|
+
initialValue: opts.initialValue
|
|
1383
|
+
});
|
|
1384
|
+
if (isCancel(answer)) return abort();
|
|
1385
|
+
return answer;
|
|
1386
|
+
}
|
|
1387
|
+
async function askConfigMode(opts) {
|
|
1388
|
+
const mode = await select({
|
|
1389
|
+
message: "Prettier config strategy",
|
|
1390
|
+
initialValue: await detectPrettierConfig(opts.rootDir, opts.pkg) ? "migrate" : "rebuild",
|
|
1391
|
+
options: [{
|
|
1392
|
+
value: "migrate",
|
|
1393
|
+
label: "Migrate from Prettier (oxfmt --migrate=prettier)"
|
|
1394
|
+
}, {
|
|
1395
|
+
value: "rebuild",
|
|
1396
|
+
label: "Rebuild .oxfmtrc.json (current mode)"
|
|
1397
|
+
}]
|
|
1398
|
+
});
|
|
1399
|
+
if (isCancel(mode)) return abort();
|
|
1400
|
+
return mode;
|
|
1401
|
+
}
|
|
1402
|
+
async function detectPrettierConfig(rootDir, pkg) {
|
|
1403
|
+
if (Object.prototype.hasOwnProperty.call(pkg, "prettier")) return true;
|
|
1404
|
+
for (const file of PRETTIER_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
async function applyOxfmtConfig(opts) {
|
|
1408
|
+
const { rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
|
|
1409
|
+
if (!(!await pathExists(oxfmtConfigPath) || yes || await askConfirm({
|
|
1410
|
+
message: configMode === "migrate" ? "Overwrite existing .oxfmtrc.json via prettier migration?" : "Overwrite existing .oxfmtrc.json?",
|
|
1411
|
+
initialValue: true
|
|
1412
|
+
}))) return "kept-existing";
|
|
1413
|
+
if (configMode === "migrate") {
|
|
1414
|
+
if (await runOxfmtPrettierMigration({
|
|
1415
|
+
rootDir,
|
|
1416
|
+
packageManager
|
|
1417
|
+
})) return "migrated";
|
|
1418
|
+
if (!(yes || await askConfirm({
|
|
1419
|
+
message: "Migration failed. Rebuild .oxfmtrc.json with defaults instead?",
|
|
1420
|
+
initialValue: true
|
|
1421
|
+
}))) return "kept-existing";
|
|
1422
|
+
}
|
|
1423
|
+
await writeText(oxfmtConfigPath, oxfmtConfigTemplate());
|
|
1424
|
+
return "rebuilt";
|
|
1425
|
+
}
|
|
1426
|
+
async function runOxfmtPrettierMigration(opts) {
|
|
1427
|
+
const { rootDir, packageManager } = opts;
|
|
1428
|
+
const migrateSpinner = spinner();
|
|
1429
|
+
migrateSpinner.start("Migrating prettier config to .oxfmtrc.json");
|
|
1430
|
+
if ((await exec("oxfmt", ["--migrate=prettier"], { cwd: rootDir })).ok) {
|
|
1431
|
+
migrateSpinner.stop("Migrated config with oxfmt");
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
const fallbackRun = packageManager === "pnpm" ? await exec("pnpm", [
|
|
1435
|
+
"exec",
|
|
1436
|
+
"oxfmt",
|
|
1437
|
+
"--migrate=prettier"
|
|
1438
|
+
], { cwd: rootDir }) : packageManager === "npm" ? await exec("npm", [
|
|
1439
|
+
"exec",
|
|
1440
|
+
"oxfmt",
|
|
1441
|
+
"--",
|
|
1442
|
+
"--migrate=prettier"
|
|
1443
|
+
], { cwd: rootDir }) : packageManager === "yarn" ? await exec("yarn", [
|
|
1444
|
+
"dlx",
|
|
1445
|
+
"oxfmt",
|
|
1446
|
+
"--migrate=prettier"
|
|
1447
|
+
], { cwd: rootDir }) : packageManager === "bun" ? await exec("bun", [
|
|
1448
|
+
"x",
|
|
1449
|
+
"oxfmt",
|
|
1450
|
+
"--migrate=prettier"
|
|
1451
|
+
], { cwd: rootDir }) : { ok: false };
|
|
1452
|
+
migrateSpinner.stop(fallbackRun.ok ? "Migrated config with oxfmt" : "Prettier migration failed");
|
|
1453
|
+
return fallbackRun.ok;
|
|
1454
|
+
}
|
|
1455
|
+
function removePrettierDependencies(pkg, key) {
|
|
1456
|
+
const bucket = pkg[key];
|
|
1457
|
+
if (!bucket) return [];
|
|
1458
|
+
const removed = [];
|
|
1459
|
+
for (const name of Object.keys(bucket)) {
|
|
1460
|
+
if (!isPrettierDependency(name)) continue;
|
|
1461
|
+
delete bucket[name];
|
|
1462
|
+
removed.push(name);
|
|
1463
|
+
}
|
|
1464
|
+
return removed;
|
|
1465
|
+
}
|
|
1466
|
+
function cleanupEmptyDependencyBuckets(pkg) {
|
|
1467
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
|
|
1468
|
+
if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
|
|
1469
|
+
}
|
|
1470
|
+
function removePrettierConfigFromPackageJson(pkg) {
|
|
1471
|
+
if (!Object.prototype.hasOwnProperty.call(pkg, "prettier")) return false;
|
|
1472
|
+
delete pkg.prettier;
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
function isPrettierDependency(name) {
|
|
1476
|
+
return name === "prettier" || /(^|\/)prettier-plugin-/.test(name) || name.startsWith("@prettier/plugin-");
|
|
1477
|
+
}
|
|
1478
|
+
var CancelledError = class extends Error {
|
|
1479
|
+
constructor() {
|
|
1480
|
+
super("Cancelled");
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
function abort(opts = {}) {
|
|
1484
|
+
cancel(opts.message ?? "Cancelled");
|
|
1485
|
+
process.exitCode = opts.exitCode ?? 0;
|
|
1486
|
+
throw new CancelledError();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
//#endregion
|
|
1490
|
+
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, workspaceRootPackageJsonTemplate as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontpl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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.
|
|
56
|
+
"@kingsword/lint-config": "^0.2.1",
|
|
57
57
|
"@types/node": "^25.0.10",
|
|
58
|
-
"oxfmt": "^0.
|
|
58
|
+
"oxfmt": "^0.32.0",
|
|
59
59
|
"oxlint": "^1.46.0",
|
|
60
|
-
"oxlint-tsgolint": "^0.
|
|
60
|
+
"oxlint-tsgolint": "^0.13.0",
|
|
61
61
|
"tsdown": "^0.20.1",
|
|
62
62
|
"typescript": "^5.9.3"
|
|
63
63
|
},
|