frontpl 0.1.1 → 0.2.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 +79 -1
- package/dist/cli.mjs +3 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +2 -2
- package/dist/init-DXlH6jJs.mjs +846 -0
- package/package.json +3 -3
- package/dist/init-Cva-s-yN.mjs +0 -380
package/README.md
CHANGED
|
@@ -1,11 +1,89 @@
|
|
|
1
1
|
# frontpl
|
|
2
2
|
|
|
3
|
-
Interactive CLI to scaffold standardized frontend project templates.
|
|
3
|
+
Interactive CLI to scaffold standardized frontend project templates (with optional CI/Release workflows).
|
|
4
|
+
|
|
5
|
+
> Node.js >= 22
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# If published on npm:
|
|
11
|
+
npm i -g frontpl
|
|
12
|
+
# or
|
|
13
|
+
pnpm add -g frontpl
|
|
14
|
+
|
|
15
|
+
# Or run once via npx:
|
|
16
|
+
npx frontpl --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
frontpl my-frontend
|
|
23
|
+
# or
|
|
24
|
+
frontpl init my-frontend
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Follow the prompts to choose:
|
|
28
|
+
|
|
29
|
+
- Package manager (`npm`/`pnpm`/`yarn`/`bun`/`deno`)
|
|
30
|
+
- Optional tooling: `oxlint`, `oxfmt`, `vitest`, `tsdown`
|
|
31
|
+
- Git init
|
|
32
|
+
- GitHub Actions workflows:
|
|
33
|
+
- CI only
|
|
34
|
+
- CI + release (release supports tag/commit/both)
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
### `frontpl [name]` / `frontpl init [name]`
|
|
39
|
+
|
|
40
|
+
Scaffold a new project into `./<name>` (or prompt for a name when omitted).
|
|
41
|
+
|
|
42
|
+
Generated output includes (based on options):
|
|
43
|
+
|
|
44
|
+
- `.editorconfig`, `.gitignore`, `.gitattributes`
|
|
45
|
+
- `package.json` (+ scripts like `typecheck`, optional `lint`, `format:check`, `test`, `build`)
|
|
46
|
+
- `tsconfig.json`, `src/index.ts`
|
|
47
|
+
- Optional configs: `.oxlintrc.json`, `.oxfmtrc.json`, `tsdown.config.ts`
|
|
48
|
+
- Optional GitHub Actions workflows in `.github/workflows/`
|
|
49
|
+
|
|
50
|
+
### `frontpl ci`
|
|
51
|
+
|
|
52
|
+
Add or update CI/Release workflows for an existing project (run it in your repo root).
|
|
53
|
+
|
|
54
|
+
What it does:
|
|
55
|
+
|
|
56
|
+
- Detects the package manager via `package.json#packageManager` or lockfiles
|
|
57
|
+
- Suggests a `workingDirectory` (supports monorepo layouts like `packages/*` / `apps/*`)
|
|
58
|
+
- Detects Node.js major version from `.nvmrc`, `.node-version`, or `package.json#engines.node` (defaults to `22`)
|
|
59
|
+
- Generates `.github/workflows/ci.yml`
|
|
60
|
+
- Optionally generates `.github/workflows/release.yml` (tag/commit/both)
|
|
61
|
+
|
|
62
|
+
## GitHub Actions (CI + Release)
|
|
63
|
+
|
|
64
|
+
frontpl generates workflows that call reusable workflows from `kingsword09/workflows` (pinned to `@v1` by default):
|
|
65
|
+
|
|
66
|
+
- CI: `cli-ci.yml`
|
|
67
|
+
- Release (tag, recommended): `cli-release-tag.yml`
|
|
68
|
+
- Release (commit, legacy): `cli-release.yml`
|
|
69
|
+
|
|
70
|
+
### Release modes
|
|
71
|
+
|
|
72
|
+
- **Tag (recommended)**: trigger on tag push (`vX.Y.Z`), validate `package.json#version` matches the tag.
|
|
73
|
+
- **Commit (legacy)**: trigger on `main` push, publish only when the commit message matches `chore(release): vX.Y.Z` (also supports `chore: release vX.Y.Z`), and the workflow will create/push the tag.
|
|
74
|
+
- **Both**: a single `release.yml` listens to both `main` and `tags`, and routes to the corresponding reusable workflow.
|
|
75
|
+
|
|
76
|
+
### Publishing auth
|
|
77
|
+
|
|
78
|
+
- **Trusted publishing (OIDC)**: enable `trustedPublishing: true` (no `NPM_TOKEN` required). Your repo must be configured on npm as a trusted publisher for the calling workflow.
|
|
79
|
+
- **NPM token**: set `trustedPublishing: false` and provide `NPM_TOKEN` in GitHub secrets.
|
|
4
80
|
|
|
5
81
|
## Development
|
|
6
82
|
|
|
7
83
|
```sh
|
|
8
84
|
pnpm install
|
|
85
|
+
pnpm run typecheck
|
|
9
86
|
pnpm run build
|
|
10
87
|
node dist/cli.mjs --help
|
|
88
|
+
node dist/cli.mjs ci
|
|
11
89
|
```
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as runInit } from "./init-
|
|
2
|
+
import { n as runCi, t as runInit } from "./init-DXlH6jJs.mjs";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
|
|
5
5
|
//#region src/cli.ts
|
|
@@ -8,6 +8,8 @@ async function main() {
|
|
|
8
8
|
await runInit({ nameArg: args[0] });
|
|
9
9
|
}).command("init", "Scaffold a new project").argument("[name]", "Project name (directory name)").action(async (_options, args) => {
|
|
10
10
|
await runInit({ nameArg: args[0] });
|
|
11
|
+
}).command("ci", "Add CI/release workflows to an existing project").action(async () => {
|
|
12
|
+
await runCi();
|
|
11
13
|
}).run();
|
|
12
14
|
}
|
|
13
15
|
main();
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
//#region src/commands/ci.d.ts
|
|
2
|
+
declare function runCi(): Promise<undefined>;
|
|
3
|
+
//#endregion
|
|
1
4
|
//#region src/commands/init.d.ts
|
|
2
5
|
declare function runInit({
|
|
3
6
|
nameArg
|
|
@@ -5,4 +8,4 @@ declare function runInit({
|
|
|
5
8
|
nameArg?: string;
|
|
6
9
|
}): Promise<void>;
|
|
7
10
|
//#endregion
|
|
8
|
-
export { runInit };
|
|
11
|
+
export { runCi, runInit };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as runInit } from "./init-
|
|
1
|
+
import { n as runCi, t as runInit } from "./init-DXlH6jJs.mjs";
|
|
2
2
|
|
|
3
|
-
export { runInit };
|
|
3
|
+
export { runCi, runInit };
|
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/lib/fs.ts
|
|
9
|
+
async function writeText(filePath, contents) {
|
|
10
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
11
|
+
await writeFile(filePath, contents, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/templates.ts
|
|
16
|
+
function editorconfigTemplate() {
|
|
17
|
+
return [
|
|
18
|
+
"root = true",
|
|
19
|
+
"",
|
|
20
|
+
"[*]",
|
|
21
|
+
"charset = utf-8",
|
|
22
|
+
"end_of_line = lf",
|
|
23
|
+
"indent_style = space",
|
|
24
|
+
"indent_size = 2",
|
|
25
|
+
"insert_final_newline = true",
|
|
26
|
+
"trim_trailing_whitespace = true",
|
|
27
|
+
"",
|
|
28
|
+
"[*.md]",
|
|
29
|
+
"trim_trailing_whitespace = false",
|
|
30
|
+
""
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
function gitignoreTemplate() {
|
|
34
|
+
return [
|
|
35
|
+
"node_modules",
|
|
36
|
+
"dist",
|
|
37
|
+
"coverage",
|
|
38
|
+
"*.log",
|
|
39
|
+
".DS_Store",
|
|
40
|
+
".env",
|
|
41
|
+
".env.*",
|
|
42
|
+
""
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
function gitattributesTemplate() {
|
|
46
|
+
return ["* text=auto eol=lf", ""].join("\n");
|
|
47
|
+
}
|
|
48
|
+
function tsconfigTemplate() {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
compilerOptions: {
|
|
51
|
+
target: "ES2022",
|
|
52
|
+
module: "ESNext",
|
|
53
|
+
moduleResolution: "Bundler",
|
|
54
|
+
strict: true,
|
|
55
|
+
skipLibCheck: true,
|
|
56
|
+
noEmit: true
|
|
57
|
+
},
|
|
58
|
+
include: ["src"]
|
|
59
|
+
}, null, 2) + "\n";
|
|
60
|
+
}
|
|
61
|
+
function srcIndexTemplate() {
|
|
62
|
+
return [
|
|
63
|
+
"export function hello(name: string) {",
|
|
64
|
+
" return `Hello, ${name}`;",
|
|
65
|
+
"}",
|
|
66
|
+
""
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
function srcVitestTemplate() {
|
|
70
|
+
return [
|
|
71
|
+
"import { describe, expect, it } from \"vitest\";",
|
|
72
|
+
"import { hello } from \"./index.js\";",
|
|
73
|
+
"",
|
|
74
|
+
"describe(\"hello\", () => {",
|
|
75
|
+
" it(\"greets\", () => {",
|
|
76
|
+
" expect(hello(\"world\")).toBe(\"Hello, world\");",
|
|
77
|
+
" });",
|
|
78
|
+
"});",
|
|
79
|
+
""
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
function readmeTemplate(projectName) {
|
|
83
|
+
return [
|
|
84
|
+
`# ${projectName}`,
|
|
85
|
+
"",
|
|
86
|
+
"Generated by `frontpl`.",
|
|
87
|
+
""
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
function oxlintConfigTemplate({ useVitest }) {
|
|
91
|
+
return JSON.stringify({
|
|
92
|
+
$schema: "https://json.schemastore.org/oxlintrc.json",
|
|
93
|
+
env: {
|
|
94
|
+
browser: true,
|
|
95
|
+
es2022: true
|
|
96
|
+
}
|
|
97
|
+
}, null, 2) + "\n";
|
|
98
|
+
}
|
|
99
|
+
function oxfmtConfigTemplate() {
|
|
100
|
+
return JSON.stringify({ $schema: "https://json.schemastore.org/oxfmtrc.json" }, null, 2) + "\n";
|
|
101
|
+
}
|
|
102
|
+
function tsdownConfigTemplate() {
|
|
103
|
+
return [
|
|
104
|
+
"import { defineConfig } from \"tsdown\";",
|
|
105
|
+
"",
|
|
106
|
+
"export default defineConfig({",
|
|
107
|
+
" entry: [\"src/index.ts\"],",
|
|
108
|
+
" platform: \"browser\"",
|
|
109
|
+
"});",
|
|
110
|
+
""
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
function packageJsonTemplate(opts) {
|
|
114
|
+
const scripts = { typecheck: "tsc --noEmit" };
|
|
115
|
+
if (opts.useOxlint) {
|
|
116
|
+
const oxlintCmd = [
|
|
117
|
+
"oxlint",
|
|
118
|
+
opts.useVitest ? "--vitest-plugin" : void 0,
|
|
119
|
+
"--type-aware",
|
|
120
|
+
"--type-check"
|
|
121
|
+
].filter(Boolean).join(" ");
|
|
122
|
+
scripts.lint = oxlintCmd;
|
|
123
|
+
scripts["lint:fix"] = `${oxlintCmd} --fix`;
|
|
124
|
+
}
|
|
125
|
+
if (opts.useOxfmt) {
|
|
126
|
+
scripts.format = "oxfmt";
|
|
127
|
+
scripts["format:check"] = "oxfmt --check";
|
|
128
|
+
scripts.fmt = "oxfmt";
|
|
129
|
+
scripts["fmt:check"] = "oxfmt --check";
|
|
130
|
+
}
|
|
131
|
+
if (opts.useVitest) scripts.test = "vitest";
|
|
132
|
+
if (opts.useTsdown) scripts.build = "tsdown";
|
|
133
|
+
const devDependencies = { typescript: opts.typescriptVersion };
|
|
134
|
+
if (opts.useOxlint) {
|
|
135
|
+
if (opts.oxlintVersion) devDependencies.oxlint = opts.oxlintVersion;
|
|
136
|
+
if (opts.oxlintTsgolintVersion) devDependencies["oxlint-tsgolint"] = opts.oxlintTsgolintVersion;
|
|
137
|
+
}
|
|
138
|
+
if (opts.useOxfmt && opts.oxfmtVersion) devDependencies.oxfmt = opts.oxfmtVersion;
|
|
139
|
+
if (opts.useVitest && opts.vitestVersion) devDependencies.vitest = opts.vitestVersion;
|
|
140
|
+
if (opts.useTsdown && opts.tsdownVersion) devDependencies.tsdown = opts.tsdownVersion;
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
name: opts.name,
|
|
143
|
+
version: "0.0.0",
|
|
144
|
+
private: true,
|
|
145
|
+
type: "module",
|
|
146
|
+
scripts,
|
|
147
|
+
devDependencies,
|
|
148
|
+
packageManager: opts.packageManager
|
|
149
|
+
}, null, 2) + "\n";
|
|
150
|
+
}
|
|
151
|
+
function githubCliCiWorkflowTemplate(opts) {
|
|
152
|
+
const ref = opts.workflowsRef ?? "v1";
|
|
153
|
+
const installCommand = opts.installCommand?.trim();
|
|
154
|
+
const lintCommand = opts.lintCommand?.trim();
|
|
155
|
+
const formatCheckCommand = opts.formatCheckCommand?.trim();
|
|
156
|
+
const testCommand = opts.testCommand?.trim();
|
|
157
|
+
return [
|
|
158
|
+
"name: CI",
|
|
159
|
+
"",
|
|
160
|
+
"on:",
|
|
161
|
+
" push:",
|
|
162
|
+
" branches: [main]",
|
|
163
|
+
" pull_request:",
|
|
164
|
+
" branches: [main]",
|
|
165
|
+
"",
|
|
166
|
+
"jobs:",
|
|
167
|
+
" ci:",
|
|
168
|
+
` uses: kingsword09/workflows/.github/workflows/cli-ci.yml@${ref}`,
|
|
169
|
+
" with:",
|
|
170
|
+
` packageManager: ${opts.packageManager}`,
|
|
171
|
+
` nodeVersion: ${opts.nodeVersion}`,
|
|
172
|
+
` workingDirectory: ${opts.workingDirectory}`,
|
|
173
|
+
` runLint: ${opts.runLint}`,
|
|
174
|
+
` runFormatCheck: ${opts.runFormatCheck}`,
|
|
175
|
+
` runTests: ${opts.runTests}`,
|
|
176
|
+
...installCommand ? [` installCommand: ${yamlString(installCommand)}`] : [],
|
|
177
|
+
...lintCommand ? [` lintCommand: ${yamlString(lintCommand)}`] : [],
|
|
178
|
+
...formatCheckCommand ? [` formatCheckCommand: ${yamlString(formatCheckCommand)}`] : [],
|
|
179
|
+
...testCommand ? [` testCommand: ${yamlString(testCommand)}`] : [],
|
|
180
|
+
""
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
function githubCliReleaseWorkflowTemplate(opts) {
|
|
184
|
+
const ref = opts.workflowsRef ?? "v1";
|
|
185
|
+
const trustedPublishing = opts.trustedPublishing;
|
|
186
|
+
const needsNpmToken = opts.packageManager !== "deno" && trustedPublishing === false;
|
|
187
|
+
return [
|
|
188
|
+
"name: Release",
|
|
189
|
+
"",
|
|
190
|
+
"on:",
|
|
191
|
+
" push:",
|
|
192
|
+
" branches: [main]",
|
|
193
|
+
"",
|
|
194
|
+
"jobs:",
|
|
195
|
+
" release:",
|
|
196
|
+
" permissions:",
|
|
197
|
+
" contents: write",
|
|
198
|
+
" id-token: write",
|
|
199
|
+
` uses: kingsword09/workflows/.github/workflows/cli-release.yml@${ref}`,
|
|
200
|
+
" with:",
|
|
201
|
+
` packageManager: ${opts.packageManager}`,
|
|
202
|
+
` nodeVersion: ${opts.nodeVersion}`,
|
|
203
|
+
` workingDirectory: ${opts.workingDirectory}`,
|
|
204
|
+
...trustedPublishing === void 0 ? [] : [` trustedPublishing: ${trustedPublishing}`],
|
|
205
|
+
...needsNpmToken ? [" secrets:", " NPM_TOKEN: ${{ secrets.NPM_TOKEN }}"] : [],
|
|
206
|
+
""
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function githubCliReleaseTagWorkflowTemplate(opts) {
|
|
210
|
+
const ref = opts.workflowsRef ?? "v1";
|
|
211
|
+
const trustedPublishing = opts.trustedPublishing;
|
|
212
|
+
const needsNpmToken = opts.packageManager !== "deno" && trustedPublishing === false;
|
|
213
|
+
return [
|
|
214
|
+
"name: Release",
|
|
215
|
+
"",
|
|
216
|
+
"on:",
|
|
217
|
+
" push:",
|
|
218
|
+
" tags: [v*.*.*]",
|
|
219
|
+
"",
|
|
220
|
+
"jobs:",
|
|
221
|
+
" release:",
|
|
222
|
+
" permissions:",
|
|
223
|
+
" contents: write",
|
|
224
|
+
" id-token: write",
|
|
225
|
+
` uses: kingsword09/workflows/.github/workflows/cli-release-tag.yml@${ref}`,
|
|
226
|
+
" with:",
|
|
227
|
+
` packageManager: ${opts.packageManager}`,
|
|
228
|
+
` nodeVersion: ${opts.nodeVersion}`,
|
|
229
|
+
` workingDirectory: ${opts.workingDirectory}`,
|
|
230
|
+
...trustedPublishing === void 0 ? [] : [` trustedPublishing: ${trustedPublishing}`],
|
|
231
|
+
...needsNpmToken ? [" secrets:", " NPM_TOKEN: ${{ secrets.NPM_TOKEN }}"] : [],
|
|
232
|
+
""
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
235
|
+
function githubCliReleaseBothWorkflowTemplate(opts) {
|
|
236
|
+
const ref = opts.workflowsRef ?? "v1";
|
|
237
|
+
const trustedPublishing = opts.trustedPublishing;
|
|
238
|
+
const needsNpmToken = opts.packageManager !== "deno" && trustedPublishing === false;
|
|
239
|
+
return [
|
|
240
|
+
"name: Release",
|
|
241
|
+
"",
|
|
242
|
+
"on:",
|
|
243
|
+
" push:",
|
|
244
|
+
" branches: [main]",
|
|
245
|
+
" tags: [v*.*.*]",
|
|
246
|
+
"",
|
|
247
|
+
"jobs:",
|
|
248
|
+
" release_tag:",
|
|
249
|
+
" if: startsWith(github.ref, 'refs/tags/')",
|
|
250
|
+
" permissions:",
|
|
251
|
+
" contents: write",
|
|
252
|
+
" id-token: write",
|
|
253
|
+
` uses: kingsword09/workflows/.github/workflows/cli-release-tag.yml@${ref}`,
|
|
254
|
+
" with:",
|
|
255
|
+
` packageManager: ${opts.packageManager}`,
|
|
256
|
+
` nodeVersion: ${opts.nodeVersion}`,
|
|
257
|
+
` workingDirectory: ${opts.workingDirectory}`,
|
|
258
|
+
...trustedPublishing === void 0 ? [] : [` trustedPublishing: ${trustedPublishing}`],
|
|
259
|
+
...needsNpmToken ? [" secrets:", " NPM_TOKEN: ${{ secrets.NPM_TOKEN }}"] : [],
|
|
260
|
+
"",
|
|
261
|
+
" release_commit:",
|
|
262
|
+
" if: startsWith(github.ref, 'refs/heads/')",
|
|
263
|
+
" permissions:",
|
|
264
|
+
" contents: write",
|
|
265
|
+
" id-token: write",
|
|
266
|
+
` uses: kingsword09/workflows/.github/workflows/cli-release.yml@${ref}`,
|
|
267
|
+
" with:",
|
|
268
|
+
` packageManager: ${opts.packageManager}`,
|
|
269
|
+
` nodeVersion: ${opts.nodeVersion}`,
|
|
270
|
+
` workingDirectory: ${opts.workingDirectory}`,
|
|
271
|
+
...trustedPublishing === void 0 ? [] : [` trustedPublishing: ${trustedPublishing}`],
|
|
272
|
+
...needsNpmToken ? [" secrets:", " NPM_TOKEN: ${{ secrets.NPM_TOKEN }}"] : [],
|
|
273
|
+
""
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function yamlString(value) {
|
|
277
|
+
return JSON.stringify(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/lib/utils.ts
|
|
282
|
+
async function pathExists(pathname) {
|
|
283
|
+
try {
|
|
284
|
+
await access(pathname);
|
|
285
|
+
return true;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/commands/ci.ts
|
|
293
|
+
async function runCi() {
|
|
294
|
+
try {
|
|
295
|
+
intro("frontpl (ci)");
|
|
296
|
+
const rootDir = process.cwd();
|
|
297
|
+
const detectedPackageManager = await detectPackageManager(rootDir);
|
|
298
|
+
const packageManager = await select({
|
|
299
|
+
message: detectedPackageManager ? `Package manager (detected: ${detectedPackageManager})` : "Package manager",
|
|
300
|
+
initialValue: detectedPackageManager ?? "pnpm",
|
|
301
|
+
options: [
|
|
302
|
+
{
|
|
303
|
+
value: "npm",
|
|
304
|
+
label: "npm"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
value: "yarn",
|
|
308
|
+
label: "yarn"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
value: "pnpm",
|
|
312
|
+
label: "pnpm"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
value: "bun",
|
|
316
|
+
label: "bun"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
value: "deno",
|
|
320
|
+
label: "deno"
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
if (isCancel(packageManager)) return abort();
|
|
325
|
+
const candidates = await listPackageCandidates(rootDir, packageManager);
|
|
326
|
+
if (candidates.length === 0) {
|
|
327
|
+
cancel("No package found. Run this command in a project root (with package.json or deno.json).");
|
|
328
|
+
process.exitCode = 1;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const initialWorkingDirectory = await detectWorkingDirectory(rootDir, candidates);
|
|
332
|
+
const workingDirectory = candidates.length === 1 ? candidates[0] : await select({
|
|
333
|
+
message: "Working directory (package folder)",
|
|
334
|
+
initialValue: initialWorkingDirectory,
|
|
335
|
+
options: candidates.map((c) => ({
|
|
336
|
+
value: c,
|
|
337
|
+
label: c
|
|
338
|
+
}))
|
|
339
|
+
});
|
|
340
|
+
if (isCancel(workingDirectory)) return abort();
|
|
341
|
+
const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
|
|
342
|
+
const nodeVersionText = await text({
|
|
343
|
+
message: "Node.js major version (for GitHub Actions)",
|
|
344
|
+
initialValue: String(nodeVersionDefault),
|
|
345
|
+
validate: (value) => {
|
|
346
|
+
const major = Number.parseInt(value.trim(), 10);
|
|
347
|
+
if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
if (isCancel(nodeVersionText)) return abort();
|
|
351
|
+
const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
|
|
352
|
+
const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
|
|
353
|
+
const addRelease = await confirm({
|
|
354
|
+
message: "Add release workflow too?",
|
|
355
|
+
initialValue: true
|
|
356
|
+
});
|
|
357
|
+
if (isCancel(addRelease)) return abort();
|
|
358
|
+
const releaseMode = addRelease ? await select({
|
|
359
|
+
message: "Release workflows",
|
|
360
|
+
initialValue: "tag",
|
|
361
|
+
options: [
|
|
362
|
+
{
|
|
363
|
+
value: "tag",
|
|
364
|
+
label: "Tag push (vX.Y.Z) — recommended"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
value: "commit",
|
|
368
|
+
label: "Release commit (chore(release): vX.Y.Z) — legacy"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
value: "both",
|
|
372
|
+
label: "Both (tag + commit)"
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}) : void 0;
|
|
376
|
+
if (isCancel(releaseMode)) return abort();
|
|
377
|
+
const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
|
|
378
|
+
message: "Release: npm trusted publishing (OIDC)?",
|
|
379
|
+
initialValue: true
|
|
380
|
+
}) : void 0;
|
|
381
|
+
if (isCancel(trustedPublishing)) return abort();
|
|
382
|
+
const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
|
|
383
|
+
const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
|
|
384
|
+
if (!await confirmOverwriteIfExists(ciWorkflowPath, ".github/workflows/ci.yml")) {
|
|
385
|
+
cancel("Skipped CI workflow");
|
|
386
|
+
process.exitCode = 0;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await writeText(ciWorkflowPath, githubCliCiWorkflowTemplate({
|
|
390
|
+
packageManager,
|
|
391
|
+
nodeVersion,
|
|
392
|
+
workingDirectory,
|
|
393
|
+
runLint,
|
|
394
|
+
runFormatCheck,
|
|
395
|
+
runTests,
|
|
396
|
+
lintCommand,
|
|
397
|
+
formatCheckCommand,
|
|
398
|
+
testCommand
|
|
399
|
+
}));
|
|
400
|
+
if (addRelease) {
|
|
401
|
+
if (await confirmOverwriteIfExists(releaseWorkflowPath, ".github/workflows/release.yml")) await writeText(releaseWorkflowPath, (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
|
|
402
|
+
packageManager,
|
|
403
|
+
nodeVersion,
|
|
404
|
+
workingDirectory,
|
|
405
|
+
trustedPublishing
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
outro(addRelease ? "Done. Generated CI + release workflows in .github/workflows/." : "Done. Generated CI workflow in .github/workflows/.");
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err instanceof CancelledError) return;
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
var CancelledError = class extends Error {
|
|
415
|
+
constructor() {
|
|
416
|
+
super("Cancelled");
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
function abort(opts = {}) {
|
|
420
|
+
cancel(opts.message ?? "Cancelled");
|
|
421
|
+
process.exitCode = opts.exitCode ?? 0;
|
|
422
|
+
throw new CancelledError();
|
|
423
|
+
}
|
|
424
|
+
async function confirmOverwriteIfExists(absPath, label) {
|
|
425
|
+
if (!await pathExists(absPath)) return true;
|
|
426
|
+
const overwrite = await confirm({
|
|
427
|
+
message: `Overwrite existing ${label}?`,
|
|
428
|
+
initialValue: true
|
|
429
|
+
});
|
|
430
|
+
if (isCancel(overwrite)) return abort();
|
|
431
|
+
return overwrite;
|
|
432
|
+
}
|
|
433
|
+
function isPackageManager(value) {
|
|
434
|
+
return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
|
|
435
|
+
}
|
|
436
|
+
async function detectPackageManager(rootDir) {
|
|
437
|
+
const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
|
|
438
|
+
if (pmField) {
|
|
439
|
+
const pm = pmField.split("@")[0] ?? "";
|
|
440
|
+
if (isPackageManager(pm)) return pm;
|
|
441
|
+
}
|
|
442
|
+
const candidates = [];
|
|
443
|
+
if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
|
|
444
|
+
if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
|
|
445
|
+
if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
|
|
446
|
+
if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
|
|
447
|
+
if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
|
|
448
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
449
|
+
}
|
|
450
|
+
async function listPackageCandidates(rootDir, packageManager) {
|
|
451
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
452
|
+
if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
|
|
453
|
+
if (packageManager === "deno" && (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc")))) candidates.add(".");
|
|
454
|
+
for (const base of ["packages", "apps"]) {
|
|
455
|
+
const baseDir = path.join(rootDir, base);
|
|
456
|
+
if (!await pathExists(baseDir)) continue;
|
|
457
|
+
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
if (!entry.isDirectory()) continue;
|
|
460
|
+
if (await pathExists(path.join(baseDir, entry.name, "package.json"))) candidates.add(path.posix.join(base, entry.name));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return [...candidates];
|
|
464
|
+
}
|
|
465
|
+
async function detectWorkingDirectory(rootDir, candidates) {
|
|
466
|
+
if (candidates.length === 1) return candidates[0];
|
|
467
|
+
const rootScripts = (await readPackageJson(path.join(rootDir, "package.json")))?.scripts ?? {};
|
|
468
|
+
const rootHasScripts = Object.keys(rootScripts).length > 0;
|
|
469
|
+
const nonRoot = candidates.filter((c) => c !== ".");
|
|
470
|
+
if (!rootHasScripts && nonRoot.length === 1) return nonRoot[0];
|
|
471
|
+
return ".";
|
|
472
|
+
}
|
|
473
|
+
async function detectNodeMajorVersion(rootDir) {
|
|
474
|
+
for (const file of [".nvmrc", ".node-version"]) {
|
|
475
|
+
const filePath = path.join(rootDir, file);
|
|
476
|
+
if (!await pathExists(filePath)) continue;
|
|
477
|
+
const major = parseMajorVersion((await readFile(filePath, "utf8")).split("\n")[0]?.trim() ?? "");
|
|
478
|
+
if (major) return major;
|
|
479
|
+
}
|
|
480
|
+
const engine = (await readPackageJson(path.join(rootDir, "package.json")))?.engines?.node;
|
|
481
|
+
if (!engine) return;
|
|
482
|
+
const match = engine.match(/([0-9]{2,})/);
|
|
483
|
+
if (!match) return;
|
|
484
|
+
return Number.parseInt(match[1], 10);
|
|
485
|
+
}
|
|
486
|
+
function parseMajorVersion(input) {
|
|
487
|
+
const trimmed = input.trim().replace(/^v/, "");
|
|
488
|
+
const major = Number.parseInt(trimmed.split(".")[0] ?? "", 10);
|
|
489
|
+
if (!Number.isFinite(major) || major <= 0) return;
|
|
490
|
+
return major;
|
|
491
|
+
}
|
|
492
|
+
async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
|
|
493
|
+
if (packageManager === "deno") return {
|
|
494
|
+
runLint: true,
|
|
495
|
+
runFormatCheck: true,
|
|
496
|
+
runTests: true
|
|
497
|
+
};
|
|
498
|
+
const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
|
|
499
|
+
if (!pkg) return abort({
|
|
500
|
+
message: `Missing package.json in ${workingDirectory}`,
|
|
501
|
+
exitCode: 1
|
|
502
|
+
});
|
|
503
|
+
const scripts = pkg.scripts ?? {};
|
|
504
|
+
const hasLint = typeof scripts.lint === "string";
|
|
505
|
+
const hasTest = typeof scripts.test === "string";
|
|
506
|
+
const hasFormatCheck = typeof scripts["format:check"] === "string";
|
|
507
|
+
const hasFmtCheck = typeof scripts["fmt:check"] === "string";
|
|
508
|
+
const runLintDefault = hasLint;
|
|
509
|
+
const runFormatCheckDefault = hasFormatCheck || hasFmtCheck;
|
|
510
|
+
const runTestsDefault = hasTest;
|
|
511
|
+
const runLint = await confirm({
|
|
512
|
+
message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
|
|
513
|
+
initialValue: runLintDefault
|
|
514
|
+
});
|
|
515
|
+
if (isCancel(runLint)) return abort();
|
|
516
|
+
const runFormatCheck = await confirm({
|
|
517
|
+
message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
|
|
518
|
+
initialValue: runFormatCheckDefault
|
|
519
|
+
});
|
|
520
|
+
if (isCancel(runFormatCheck)) return abort();
|
|
521
|
+
const runTests = await confirm({
|
|
522
|
+
message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
|
|
523
|
+
initialValue: runTestsDefault
|
|
524
|
+
});
|
|
525
|
+
if (isCancel(runTests)) return abort();
|
|
526
|
+
return {
|
|
527
|
+
runLint,
|
|
528
|
+
runFormatCheck,
|
|
529
|
+
runTests,
|
|
530
|
+
lintCommand: runLint && !hasLint ? await promptCommand("Lint command", pmRun(packageManager, "lint")) : void 0,
|
|
531
|
+
formatCheckCommand: runFormatCheck && !hasFormatCheck ? hasFmtCheck ? pmRun(packageManager, "fmt:check") : await promptCommand("Format check command", pmRun(packageManager, "format:check")) : void 0,
|
|
532
|
+
testCommand: runTests && !hasTest ? await promptCommand("Test command", pmRun(packageManager, "test")) : void 0
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function promptCommand(message, initialValue) {
|
|
536
|
+
const value = await text({
|
|
537
|
+
message,
|
|
538
|
+
initialValue,
|
|
539
|
+
validate: (v) => !v.trim() ? "Command is required" : void 0
|
|
540
|
+
});
|
|
541
|
+
if (isCancel(value)) return abort();
|
|
542
|
+
return String(value).trim();
|
|
543
|
+
}
|
|
544
|
+
function pmRun(pm, script) {
|
|
545
|
+
switch (pm) {
|
|
546
|
+
case "npm": return `npm run ${script}`;
|
|
547
|
+
case "pnpm": return `pnpm run ${script}`;
|
|
548
|
+
case "yarn": return `yarn ${script}`;
|
|
549
|
+
case "bun": return `bun run ${script}`;
|
|
550
|
+
case "deno": return script;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function readPackageJson(filePath) {
|
|
554
|
+
try {
|
|
555
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
556
|
+
} catch {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/lib/exec.ts
|
|
563
|
+
async function exec(command, args, opts = {}) {
|
|
564
|
+
const resolved = resolveCommand$1(command);
|
|
565
|
+
return new Promise((resolve) => {
|
|
566
|
+
const child = spawn(resolved, args, {
|
|
567
|
+
cwd: opts.cwd,
|
|
568
|
+
stdio: "inherit",
|
|
569
|
+
shell: false,
|
|
570
|
+
env: process.env
|
|
571
|
+
});
|
|
572
|
+
child.on("close", (code) => resolve({ ok: code === 0 }));
|
|
573
|
+
child.on("error", () => resolve({ ok: false }));
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function resolveCommand$1(command) {
|
|
577
|
+
if (process.platform !== "win32") return command;
|
|
578
|
+
if (command === "npm") return "npm.cmd";
|
|
579
|
+
if (command === "pnpm") return "pnpm.cmd";
|
|
580
|
+
if (command === "yarn") return "yarn.cmd";
|
|
581
|
+
return command;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/lib/versions.ts
|
|
586
|
+
async function detectPackageManagerVersion(pm) {
|
|
587
|
+
switch (pm) {
|
|
588
|
+
case "npm": return (await execCapture("npm", ["--version"])).stdout.trim() || void 0;
|
|
589
|
+
case "pnpm": return (await execCapture("pnpm", ["--version"])).stdout.trim() || void 0;
|
|
590
|
+
case "yarn": return (await execCapture("yarn", ["--version"])).stdout.trim() || void 0;
|
|
591
|
+
case "bun": return (await execCapture("bun", ["--version"])).stdout.trim() || void 0;
|
|
592
|
+
case "deno": return ((await execCapture("deno", ["--version"])).stdout.trim().split("\n")[0] ?? "").match(/deno\\s+([0-9]+\\.[0-9]+\\.[0-9]+)/)?.[1];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function execCapture(command, args) {
|
|
596
|
+
const resolved = resolveCommand(command);
|
|
597
|
+
return new Promise((resolve) => {
|
|
598
|
+
const child = spawn(resolved, args, {
|
|
599
|
+
cwd: os.tmpdir(),
|
|
600
|
+
stdio: [
|
|
601
|
+
"ignore",
|
|
602
|
+
"pipe",
|
|
603
|
+
"ignore"
|
|
604
|
+
],
|
|
605
|
+
shell: false,
|
|
606
|
+
env: process.env
|
|
607
|
+
});
|
|
608
|
+
const chunks = [];
|
|
609
|
+
child.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
|
|
610
|
+
child.on("close", (code) => {
|
|
611
|
+
resolve({
|
|
612
|
+
ok: code === 0,
|
|
613
|
+
stdout: Buffer.concat(chunks).toString("utf8")
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
child.on("error", () => resolve({
|
|
617
|
+
ok: false,
|
|
618
|
+
stdout: ""
|
|
619
|
+
}));
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function resolveCommand(command) {
|
|
623
|
+
if (process.platform !== "win32") return command;
|
|
624
|
+
if (command === "npm") return "npm.cmd";
|
|
625
|
+
if (command === "pnpm") return "pnpm.cmd";
|
|
626
|
+
if (command === "yarn") return "yarn.cmd";
|
|
627
|
+
return command;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/commands/init.ts
|
|
632
|
+
async function runInit({ nameArg }) {
|
|
633
|
+
intro("frontpl");
|
|
634
|
+
const projectName = await text({
|
|
635
|
+
message: "Project name",
|
|
636
|
+
initialValue: nameArg ?? "my-frontend",
|
|
637
|
+
validate: validateProjectName
|
|
638
|
+
});
|
|
639
|
+
if (isCancel(projectName)) return onCancel();
|
|
640
|
+
const packageManager = await select({
|
|
641
|
+
message: "Package manager",
|
|
642
|
+
initialValue: "pnpm",
|
|
643
|
+
options: [
|
|
644
|
+
{
|
|
645
|
+
value: "npm",
|
|
646
|
+
label: "npm"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
value: "yarn",
|
|
650
|
+
label: "yarn"
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
value: "pnpm",
|
|
654
|
+
label: "pnpm"
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
value: "bun",
|
|
658
|
+
label: "bun"
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
value: "deno",
|
|
662
|
+
label: "deno"
|
|
663
|
+
}
|
|
664
|
+
]
|
|
665
|
+
});
|
|
666
|
+
if (isCancel(packageManager)) return onCancel();
|
|
667
|
+
const pnpmWorkspace = packageManager === "pnpm" ? await confirm({
|
|
668
|
+
message: "pnpm workspace mode (monorepo skeleton)?",
|
|
669
|
+
initialValue: false
|
|
670
|
+
}) : false;
|
|
671
|
+
if (isCancel(pnpmWorkspace)) return onCancel();
|
|
672
|
+
const useOxlint = await confirm({
|
|
673
|
+
message: "Enable oxlint (type-aware + type-check via tsgolint)?",
|
|
674
|
+
initialValue: true
|
|
675
|
+
});
|
|
676
|
+
if (isCancel(useOxlint)) return onCancel();
|
|
677
|
+
const useOxfmt = await confirm({
|
|
678
|
+
message: "Enable oxfmt (code formatting)?",
|
|
679
|
+
initialValue: true
|
|
680
|
+
});
|
|
681
|
+
if (isCancel(useOxfmt)) return onCancel();
|
|
682
|
+
const useVitest = await confirm({
|
|
683
|
+
message: "Add Vitest?",
|
|
684
|
+
initialValue: false
|
|
685
|
+
});
|
|
686
|
+
if (isCancel(useVitest)) return onCancel();
|
|
687
|
+
const useTsdown = await confirm({
|
|
688
|
+
message: "Add tsdown build?",
|
|
689
|
+
initialValue: true
|
|
690
|
+
});
|
|
691
|
+
if (isCancel(useTsdown)) return onCancel();
|
|
692
|
+
const initGit = await confirm({
|
|
693
|
+
message: "Initialize a git repository?",
|
|
694
|
+
initialValue: true
|
|
695
|
+
});
|
|
696
|
+
if (isCancel(initGit)) return onCancel();
|
|
697
|
+
const githubActions = await select({
|
|
698
|
+
message: "GitHub Actions workflows",
|
|
699
|
+
initialValue: "ci",
|
|
700
|
+
options: [
|
|
701
|
+
{
|
|
702
|
+
value: "none",
|
|
703
|
+
label: "None"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
value: "ci",
|
|
707
|
+
label: "CI only"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
value: "ci+release",
|
|
711
|
+
label: "CI + release"
|
|
712
|
+
}
|
|
713
|
+
]
|
|
714
|
+
});
|
|
715
|
+
if (isCancel(githubActions)) return onCancel();
|
|
716
|
+
const releaseMode = githubActions === "ci+release" ? await select({
|
|
717
|
+
message: "Release workflows",
|
|
718
|
+
initialValue: "tag",
|
|
719
|
+
options: [
|
|
720
|
+
{
|
|
721
|
+
value: "tag",
|
|
722
|
+
label: "Tag push (vX.Y.Z) — recommended"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
value: "commit",
|
|
726
|
+
label: "Release commit (chore(release): vX.Y.Z) — legacy"
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
value: "both",
|
|
730
|
+
label: "Both (tag + commit)"
|
|
731
|
+
}
|
|
732
|
+
]
|
|
733
|
+
}) : void 0;
|
|
734
|
+
if (isCancel(releaseMode)) return onCancel();
|
|
735
|
+
const trustedPublishing = githubActions === "ci+release" && packageManager !== "deno" ? await confirm({
|
|
736
|
+
message: "Release: npm trusted publishing (OIDC)?",
|
|
737
|
+
initialValue: true
|
|
738
|
+
}) : void 0;
|
|
739
|
+
if (isCancel(trustedPublishing)) return onCancel();
|
|
740
|
+
const rootDir = path.resolve(process.cwd(), projectName);
|
|
741
|
+
if (await pathExists(rootDir)) {
|
|
742
|
+
cancel(`Directory already exists: ${rootDir}`);
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const pkgDir = pnpmWorkspace ? path.join(rootDir, "packages", projectName) : rootDir;
|
|
747
|
+
const pmVersion = await detectPackageManagerVersion(packageManager);
|
|
748
|
+
const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
|
|
749
|
+
await mkdir(path.join(pkgDir, "src"), { recursive: true });
|
|
750
|
+
await Promise.all([
|
|
751
|
+
writeText(path.join(rootDir, ".editorconfig"), editorconfigTemplate()),
|
|
752
|
+
writeText(path.join(rootDir, ".gitignore"), gitignoreTemplate()),
|
|
753
|
+
writeText(path.join(rootDir, ".gitattributes"), gitattributesTemplate())
|
|
754
|
+
]);
|
|
755
|
+
if (pnpmWorkspace) {
|
|
756
|
+
await writeText(path.join(rootDir, "pnpm-workspace.yaml"), [
|
|
757
|
+
"packages:",
|
|
758
|
+
" - \"packages/*\"",
|
|
759
|
+
""
|
|
760
|
+
].join("\n"));
|
|
761
|
+
await writeText(path.join(rootDir, "package.json"), JSON.stringify({
|
|
762
|
+
name: projectName,
|
|
763
|
+
private: true,
|
|
764
|
+
packageManager: packageManagerField
|
|
765
|
+
}, null, 2) + "\n");
|
|
766
|
+
}
|
|
767
|
+
await Promise.all([
|
|
768
|
+
writeText(path.join(pkgDir, "README.md"), readmeTemplate(projectName)),
|
|
769
|
+
writeText(path.join(pkgDir, "src/index.ts"), srcIndexTemplate()),
|
|
770
|
+
writeText(path.join(pkgDir, "tsconfig.json"), tsconfigTemplate()),
|
|
771
|
+
writeText(path.join(pkgDir, "package.json"), packageJsonTemplate({
|
|
772
|
+
name: projectName,
|
|
773
|
+
packageManager: packageManagerField,
|
|
774
|
+
typescriptVersion: "latest",
|
|
775
|
+
useOxlint,
|
|
776
|
+
oxlintVersion: "latest",
|
|
777
|
+
oxlintTsgolintVersion: "latest",
|
|
778
|
+
useOxfmt,
|
|
779
|
+
oxfmtVersion: "latest",
|
|
780
|
+
useVitest,
|
|
781
|
+
vitestVersion: "latest",
|
|
782
|
+
useTsdown,
|
|
783
|
+
tsdownVersion: "latest"
|
|
784
|
+
}))
|
|
785
|
+
]);
|
|
786
|
+
if (useOxlint) await writeText(path.join(pkgDir, ".oxlintrc.json"), oxlintConfigTemplate({ useVitest }));
|
|
787
|
+
if (useOxfmt) await writeText(path.join(pkgDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
|
|
788
|
+
if (useVitest) await writeText(path.join(pkgDir, "src/index.test.ts"), srcVitestTemplate());
|
|
789
|
+
if (useTsdown) await writeText(path.join(pkgDir, "tsdown.config.ts"), tsdownConfigTemplate());
|
|
790
|
+
if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
|
|
791
|
+
if (githubActions !== "none") {
|
|
792
|
+
const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
|
|
793
|
+
await writeText(path.join(rootDir, ".github/workflows/ci.yml"), githubCliCiWorkflowTemplate({
|
|
794
|
+
packageManager,
|
|
795
|
+
nodeVersion: 22,
|
|
796
|
+
workingDirectory,
|
|
797
|
+
runLint: useOxlint,
|
|
798
|
+
runFormatCheck: useOxfmt,
|
|
799
|
+
runTests: useVitest
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
if (githubActions === "ci+release") {
|
|
803
|
+
const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
|
|
804
|
+
await writeText(path.join(rootDir, ".github/workflows/release.yml"), (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
|
|
805
|
+
packageManager,
|
|
806
|
+
nodeVersion: 22,
|
|
807
|
+
workingDirectory,
|
|
808
|
+
trustedPublishing
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
const canInstall = Boolean(pmVersion);
|
|
812
|
+
let installOk = false;
|
|
813
|
+
if (canInstall) {
|
|
814
|
+
const installSpinner = spinner();
|
|
815
|
+
installSpinner.start(`Installing dependencies with ${packageManager}`);
|
|
816
|
+
installOk = (await exec(packageManager, ["install"], { cwd: rootDir })).ok;
|
|
817
|
+
installSpinner.stop(installOk ? "Dependencies installed" : "Install failed (skipped)");
|
|
818
|
+
}
|
|
819
|
+
if (initGit) await exec("git", ["init"], { cwd: rootDir });
|
|
820
|
+
outro(`Done. Next:\n cd ${projectName}${!canInstall ? `\n (${packageManager} not found, run install manually)` : !installOk ? `\n (${packageManager} install failed, run install manually)` : ""}\n ${nextStepHint(packageManager)}`);
|
|
821
|
+
}
|
|
822
|
+
function validateProjectName(value) {
|
|
823
|
+
const name = value.trim();
|
|
824
|
+
if (!name) return "Project name is required";
|
|
825
|
+
if (name.length > 214) return "Project name is too long";
|
|
826
|
+
if (name.startsWith(".")) return "Project name cannot start with '.'";
|
|
827
|
+
if (name.startsWith("_")) return "Project name cannot start with '_'";
|
|
828
|
+
if (/[A-Z]/.test(name)) return "Use lowercase letters only";
|
|
829
|
+
if (!/^[a-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
|
|
830
|
+
}
|
|
831
|
+
function onCancel() {
|
|
832
|
+
cancel("Cancelled");
|
|
833
|
+
process.exitCode = 0;
|
|
834
|
+
}
|
|
835
|
+
function nextStepHint(pm) {
|
|
836
|
+
switch (pm) {
|
|
837
|
+
case "npm": return "npm run typecheck";
|
|
838
|
+
case "pnpm": return "pnpm run typecheck";
|
|
839
|
+
case "yarn": return "yarn typecheck";
|
|
840
|
+
case "bun": return "bun run typecheck";
|
|
841
|
+
case "deno": return "deno task typecheck # (or run the package.json scripts with your preferred runner)";
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
//#endregion
|
|
846
|
+
export { runCi as n, runInit as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontpl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Interactive CLI to scaffold standardized frontend project templates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"types": "./dist/index.d.mts",
|
|
31
31
|
"exports": {
|
|
32
32
|
".": {
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
33
34
|
"import": "./dist/index.mjs",
|
|
34
|
-
"require": "./dist/index.mjs"
|
|
35
|
-
"types": "./dist/index.d.mts"
|
|
35
|
+
"require": "./dist/index.mjs"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
package/dist/init-Cva-s-yN.mjs
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
2
|
-
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
//#region src/lib/exec.ts
|
|
9
|
-
async function exec(command, args, opts = {}) {
|
|
10
|
-
const resolved = resolveCommand$1(command);
|
|
11
|
-
return new Promise((resolve) => {
|
|
12
|
-
const child = spawn(resolved, args, {
|
|
13
|
-
cwd: opts.cwd,
|
|
14
|
-
stdio: "inherit",
|
|
15
|
-
shell: false,
|
|
16
|
-
env: process.env
|
|
17
|
-
});
|
|
18
|
-
child.on("close", (code) => resolve({ ok: code === 0 }));
|
|
19
|
-
child.on("error", () => resolve({ ok: false }));
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
function resolveCommand$1(command) {
|
|
23
|
-
if (process.platform !== "win32") return command;
|
|
24
|
-
if (command === "npm") return "npm.cmd";
|
|
25
|
-
if (command === "pnpm") return "pnpm.cmd";
|
|
26
|
-
if (command === "yarn") return "yarn.cmd";
|
|
27
|
-
return command;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/lib/versions.ts
|
|
32
|
-
async function detectPackageManagerVersion(pm) {
|
|
33
|
-
switch (pm) {
|
|
34
|
-
case "npm": return (await execCapture("npm", ["--version"])).stdout.trim() || void 0;
|
|
35
|
-
case "pnpm": return (await execCapture("pnpm", ["--version"])).stdout.trim() || void 0;
|
|
36
|
-
case "yarn": return (await execCapture("yarn", ["--version"])).stdout.trim() || void 0;
|
|
37
|
-
case "bun": return (await execCapture("bun", ["--version"])).stdout.trim() || void 0;
|
|
38
|
-
case "deno": return ((await execCapture("deno", ["--version"])).stdout.trim().split("\n")[0] ?? "").match(/deno\\s+([0-9]+\\.[0-9]+\\.[0-9]+)/)?.[1];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async function execCapture(command, args) {
|
|
42
|
-
const resolved = resolveCommand(command);
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
const child = spawn(resolved, args, {
|
|
45
|
-
cwd: os.tmpdir(),
|
|
46
|
-
stdio: [
|
|
47
|
-
"ignore",
|
|
48
|
-
"pipe",
|
|
49
|
-
"ignore"
|
|
50
|
-
],
|
|
51
|
-
shell: false,
|
|
52
|
-
env: process.env
|
|
53
|
-
});
|
|
54
|
-
const chunks = [];
|
|
55
|
-
child.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
|
|
56
|
-
child.on("close", (code) => {
|
|
57
|
-
resolve({
|
|
58
|
-
ok: code === 0,
|
|
59
|
-
stdout: Buffer.concat(chunks).toString("utf8")
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
child.on("error", () => resolve({
|
|
63
|
-
ok: false,
|
|
64
|
-
stdout: ""
|
|
65
|
-
}));
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function resolveCommand(command) {
|
|
69
|
-
if (process.platform !== "win32") return command;
|
|
70
|
-
if (command === "npm") return "npm.cmd";
|
|
71
|
-
if (command === "pnpm") return "pnpm.cmd";
|
|
72
|
-
if (command === "yarn") return "yarn.cmd";
|
|
73
|
-
return command;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/lib/templates.ts
|
|
78
|
-
function editorconfigTemplate() {
|
|
79
|
-
return [
|
|
80
|
-
"root = true",
|
|
81
|
-
"",
|
|
82
|
-
"[*]",
|
|
83
|
-
"charset = utf-8",
|
|
84
|
-
"end_of_line = lf",
|
|
85
|
-
"indent_style = space",
|
|
86
|
-
"indent_size = 2",
|
|
87
|
-
"insert_final_newline = true",
|
|
88
|
-
"trim_trailing_whitespace = true",
|
|
89
|
-
"",
|
|
90
|
-
"[*.md]",
|
|
91
|
-
"trim_trailing_whitespace = false",
|
|
92
|
-
""
|
|
93
|
-
].join("\n");
|
|
94
|
-
}
|
|
95
|
-
function gitignoreTemplate() {
|
|
96
|
-
return [
|
|
97
|
-
"node_modules",
|
|
98
|
-
"dist",
|
|
99
|
-
"coverage",
|
|
100
|
-
"*.log",
|
|
101
|
-
".DS_Store",
|
|
102
|
-
".env",
|
|
103
|
-
".env.*",
|
|
104
|
-
""
|
|
105
|
-
].join("\n");
|
|
106
|
-
}
|
|
107
|
-
function gitattributesTemplate() {
|
|
108
|
-
return ["* text=auto eol=lf", ""].join("\n");
|
|
109
|
-
}
|
|
110
|
-
function tsconfigTemplate() {
|
|
111
|
-
return JSON.stringify({
|
|
112
|
-
compilerOptions: {
|
|
113
|
-
target: "ES2022",
|
|
114
|
-
module: "ESNext",
|
|
115
|
-
moduleResolution: "Bundler",
|
|
116
|
-
strict: true,
|
|
117
|
-
skipLibCheck: true,
|
|
118
|
-
noEmit: true
|
|
119
|
-
},
|
|
120
|
-
include: ["src"]
|
|
121
|
-
}, null, 2) + "\n";
|
|
122
|
-
}
|
|
123
|
-
function srcIndexTemplate() {
|
|
124
|
-
return [
|
|
125
|
-
"export function hello(name: string) {",
|
|
126
|
-
" return `Hello, ${name}`;",
|
|
127
|
-
"}",
|
|
128
|
-
""
|
|
129
|
-
].join("\n");
|
|
130
|
-
}
|
|
131
|
-
function srcVitestTemplate() {
|
|
132
|
-
return [
|
|
133
|
-
"import { describe, expect, it } from \"vitest\";",
|
|
134
|
-
"import { hello } from \"./index.js\";",
|
|
135
|
-
"",
|
|
136
|
-
"describe(\"hello\", () => {",
|
|
137
|
-
" it(\"greets\", () => {",
|
|
138
|
-
" expect(hello(\"world\")).toBe(\"Hello, world\");",
|
|
139
|
-
" });",
|
|
140
|
-
"});",
|
|
141
|
-
""
|
|
142
|
-
].join("\n");
|
|
143
|
-
}
|
|
144
|
-
function readmeTemplate(projectName) {
|
|
145
|
-
return [
|
|
146
|
-
`# ${projectName}`,
|
|
147
|
-
"",
|
|
148
|
-
"Generated by `frontpl`.",
|
|
149
|
-
""
|
|
150
|
-
].join("\n");
|
|
151
|
-
}
|
|
152
|
-
function oxlintConfigTemplate({ useVitest }) {
|
|
153
|
-
return JSON.stringify({
|
|
154
|
-
$schema: "https://json.schemastore.org/oxlintrc.json",
|
|
155
|
-
env: {
|
|
156
|
-
browser: true,
|
|
157
|
-
es2022: true
|
|
158
|
-
}
|
|
159
|
-
}, null, 2) + "\n";
|
|
160
|
-
}
|
|
161
|
-
function oxfmtConfigTemplate() {
|
|
162
|
-
return JSON.stringify({ $schema: "https://json.schemastore.org/oxfmtrc.json" }, null, 2) + "\n";
|
|
163
|
-
}
|
|
164
|
-
function tsdownConfigTemplate() {
|
|
165
|
-
return [
|
|
166
|
-
"import { defineConfig } from \"tsdown\";",
|
|
167
|
-
"",
|
|
168
|
-
"export default defineConfig({",
|
|
169
|
-
" entry: [\"src/index.ts\"],",
|
|
170
|
-
" platform: \"browser\"",
|
|
171
|
-
"});",
|
|
172
|
-
""
|
|
173
|
-
].join("\n");
|
|
174
|
-
}
|
|
175
|
-
function packageJsonTemplate(opts) {
|
|
176
|
-
const scripts = { typecheck: "tsc --noEmit" };
|
|
177
|
-
if (opts.useOxlint) {
|
|
178
|
-
const oxlintCmd = [
|
|
179
|
-
"oxlint",
|
|
180
|
-
opts.useVitest ? "--vitest-plugin" : void 0,
|
|
181
|
-
"--type-aware",
|
|
182
|
-
"--type-check"
|
|
183
|
-
].filter(Boolean).join(" ");
|
|
184
|
-
scripts.lint = oxlintCmd;
|
|
185
|
-
scripts["lint:fix"] = `${oxlintCmd} --fix`;
|
|
186
|
-
}
|
|
187
|
-
if (opts.useOxfmt) {
|
|
188
|
-
scripts.fmt = "oxfmt";
|
|
189
|
-
scripts["fmt:check"] = "oxfmt --check";
|
|
190
|
-
}
|
|
191
|
-
if (opts.useVitest) scripts.test = "vitest";
|
|
192
|
-
if (opts.useTsdown) scripts.build = "tsdown";
|
|
193
|
-
const devDependencies = { typescript: opts.typescriptVersion };
|
|
194
|
-
if (opts.useOxlint) {
|
|
195
|
-
if (opts.oxlintVersion) devDependencies.oxlint = opts.oxlintVersion;
|
|
196
|
-
if (opts.oxlintTsgolintVersion) devDependencies["oxlint-tsgolint"] = opts.oxlintTsgolintVersion;
|
|
197
|
-
}
|
|
198
|
-
if (opts.useOxfmt && opts.oxfmtVersion) devDependencies.oxfmt = opts.oxfmtVersion;
|
|
199
|
-
if (opts.useVitest && opts.vitestVersion) devDependencies.vitest = opts.vitestVersion;
|
|
200
|
-
if (opts.useTsdown && opts.tsdownVersion) devDependencies.tsdown = opts.tsdownVersion;
|
|
201
|
-
return JSON.stringify({
|
|
202
|
-
name: opts.name,
|
|
203
|
-
version: "0.0.0",
|
|
204
|
-
private: true,
|
|
205
|
-
type: "module",
|
|
206
|
-
scripts,
|
|
207
|
-
devDependencies,
|
|
208
|
-
packageManager: opts.packageManager
|
|
209
|
-
}, null, 2) + "\n";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
//#endregion
|
|
213
|
-
//#region src/lib/utils.ts
|
|
214
|
-
async function pathExists(pathname) {
|
|
215
|
-
try {
|
|
216
|
-
await access(pathname);
|
|
217
|
-
return true;
|
|
218
|
-
} catch {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
//#endregion
|
|
224
|
-
//#region src/commands/init.ts
|
|
225
|
-
async function runInit({ nameArg }) {
|
|
226
|
-
intro("frontpl");
|
|
227
|
-
const projectName = await text({
|
|
228
|
-
message: "Project name",
|
|
229
|
-
initialValue: nameArg ?? "my-frontend",
|
|
230
|
-
validate: validateProjectName
|
|
231
|
-
});
|
|
232
|
-
if (isCancel(projectName)) return onCancel();
|
|
233
|
-
const packageManager = await select({
|
|
234
|
-
message: "Package manager",
|
|
235
|
-
initialValue: "pnpm",
|
|
236
|
-
options: [
|
|
237
|
-
{
|
|
238
|
-
value: "npm",
|
|
239
|
-
label: "npm"
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
value: "yarn",
|
|
243
|
-
label: "yarn"
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
value: "pnpm",
|
|
247
|
-
label: "pnpm"
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
value: "bun",
|
|
251
|
-
label: "bun"
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
value: "deno",
|
|
255
|
-
label: "deno"
|
|
256
|
-
}
|
|
257
|
-
]
|
|
258
|
-
});
|
|
259
|
-
if (isCancel(packageManager)) return onCancel();
|
|
260
|
-
const pnpmWorkspace = packageManager === "pnpm" ? await confirm({
|
|
261
|
-
message: "pnpm workspace mode (monorepo skeleton)?",
|
|
262
|
-
initialValue: false
|
|
263
|
-
}) : false;
|
|
264
|
-
if (isCancel(pnpmWorkspace)) return onCancel();
|
|
265
|
-
const useOxlint = await confirm({
|
|
266
|
-
message: "Enable oxlint (type-aware + type-check via tsgolint)?",
|
|
267
|
-
initialValue: true
|
|
268
|
-
});
|
|
269
|
-
if (isCancel(useOxlint)) return onCancel();
|
|
270
|
-
const useOxfmt = await confirm({
|
|
271
|
-
message: "Enable oxfmt (code formatting)?",
|
|
272
|
-
initialValue: true
|
|
273
|
-
});
|
|
274
|
-
if (isCancel(useOxfmt)) return onCancel();
|
|
275
|
-
const useVitest = await confirm({
|
|
276
|
-
message: "Add Vitest?",
|
|
277
|
-
initialValue: false
|
|
278
|
-
});
|
|
279
|
-
if (isCancel(useVitest)) return onCancel();
|
|
280
|
-
const useTsdown = await confirm({
|
|
281
|
-
message: "Add tsdown build?",
|
|
282
|
-
initialValue: true
|
|
283
|
-
});
|
|
284
|
-
if (isCancel(useTsdown)) return onCancel();
|
|
285
|
-
const initGit = await confirm({
|
|
286
|
-
message: "Initialize a git repository?",
|
|
287
|
-
initialValue: true
|
|
288
|
-
});
|
|
289
|
-
if (isCancel(initGit)) return onCancel();
|
|
290
|
-
const rootDir = path.resolve(process.cwd(), projectName);
|
|
291
|
-
if (await pathExists(rootDir)) {
|
|
292
|
-
cancel(`Directory already exists: ${rootDir}`);
|
|
293
|
-
process.exitCode = 1;
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const pkgDir = pnpmWorkspace ? path.join(rootDir, "packages", projectName) : rootDir;
|
|
297
|
-
const pmVersion = await detectPackageManagerVersion(packageManager);
|
|
298
|
-
const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
|
|
299
|
-
await mkdir(path.join(pkgDir, "src"), { recursive: true });
|
|
300
|
-
await Promise.all([
|
|
301
|
-
writeText(path.join(rootDir, ".editorconfig"), editorconfigTemplate()),
|
|
302
|
-
writeText(path.join(rootDir, ".gitignore"), gitignoreTemplate()),
|
|
303
|
-
writeText(path.join(rootDir, ".gitattributes"), gitattributesTemplate())
|
|
304
|
-
]);
|
|
305
|
-
if (pnpmWorkspace) {
|
|
306
|
-
await writeText(path.join(rootDir, "pnpm-workspace.yaml"), [
|
|
307
|
-
"packages:",
|
|
308
|
-
" - \"packages/*\"",
|
|
309
|
-
""
|
|
310
|
-
].join("\n"));
|
|
311
|
-
await writeText(path.join(rootDir, "package.json"), JSON.stringify({
|
|
312
|
-
name: projectName,
|
|
313
|
-
private: true,
|
|
314
|
-
packageManager: packageManagerField
|
|
315
|
-
}, null, 2) + "\n");
|
|
316
|
-
}
|
|
317
|
-
await Promise.all([
|
|
318
|
-
writeText(path.join(pkgDir, "README.md"), readmeTemplate(projectName)),
|
|
319
|
-
writeText(path.join(pkgDir, "src/index.ts"), srcIndexTemplate()),
|
|
320
|
-
writeText(path.join(pkgDir, "tsconfig.json"), tsconfigTemplate()),
|
|
321
|
-
writeText(path.join(pkgDir, "package.json"), packageJsonTemplate({
|
|
322
|
-
name: projectName,
|
|
323
|
-
packageManager: packageManagerField,
|
|
324
|
-
typescriptVersion: "latest",
|
|
325
|
-
useOxlint,
|
|
326
|
-
oxlintVersion: "latest",
|
|
327
|
-
oxlintTsgolintVersion: "latest",
|
|
328
|
-
useOxfmt,
|
|
329
|
-
oxfmtVersion: "latest",
|
|
330
|
-
useVitest,
|
|
331
|
-
vitestVersion: "latest",
|
|
332
|
-
useTsdown,
|
|
333
|
-
tsdownVersion: "latest"
|
|
334
|
-
}))
|
|
335
|
-
]);
|
|
336
|
-
if (useOxlint) await writeText(path.join(pkgDir, ".oxlintrc.json"), oxlintConfigTemplate({ useVitest }));
|
|
337
|
-
if (useOxfmt) await writeText(path.join(pkgDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
|
|
338
|
-
if (useVitest) await writeText(path.join(pkgDir, "src/index.test.ts"), srcVitestTemplate());
|
|
339
|
-
if (useTsdown) await writeText(path.join(pkgDir, "tsdown.config.ts"), tsdownConfigTemplate());
|
|
340
|
-
if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
|
|
341
|
-
const canInstall = Boolean(pmVersion);
|
|
342
|
-
let installOk = false;
|
|
343
|
-
if (canInstall) {
|
|
344
|
-
const installSpinner = spinner();
|
|
345
|
-
installSpinner.start(`Installing dependencies with ${packageManager}`);
|
|
346
|
-
installOk = (await exec(packageManager, ["install"], { cwd: rootDir })).ok;
|
|
347
|
-
installSpinner.stop(installOk ? "Dependencies installed" : "Install failed (skipped)");
|
|
348
|
-
}
|
|
349
|
-
if (initGit) await exec("git", ["init"], { cwd: rootDir });
|
|
350
|
-
outro(`Done. Next:\n cd ${projectName}${!canInstall ? `\n (${packageManager} not found, run install manually)` : !installOk ? `\n (${packageManager} install failed, run install manually)` : ""}\n ${nextStepHint(packageManager)}`);
|
|
351
|
-
}
|
|
352
|
-
function validateProjectName(value) {
|
|
353
|
-
const name = value.trim();
|
|
354
|
-
if (!name) return "Project name is required";
|
|
355
|
-
if (name.length > 214) return "Project name is too long";
|
|
356
|
-
if (name.startsWith(".")) return "Project name cannot start with '.'";
|
|
357
|
-
if (name.startsWith("_")) return "Project name cannot start with '_'";
|
|
358
|
-
if (/[A-Z]/.test(name)) return "Use lowercase letters only";
|
|
359
|
-
if (!/^[a-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
|
|
360
|
-
}
|
|
361
|
-
function onCancel() {
|
|
362
|
-
cancel("Cancelled");
|
|
363
|
-
process.exitCode = 0;
|
|
364
|
-
}
|
|
365
|
-
async function writeText(filePath, contents) {
|
|
366
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
367
|
-
await writeFile(filePath, contents, "utf8");
|
|
368
|
-
}
|
|
369
|
-
function nextStepHint(pm) {
|
|
370
|
-
switch (pm) {
|
|
371
|
-
case "npm": return "npm run typecheck";
|
|
372
|
-
case "pnpm": return "pnpm run typecheck";
|
|
373
|
-
case "yarn": return "yarn typecheck";
|
|
374
|
-
case "bun": return "bun run typecheck";
|
|
375
|
-
case "deno": return "deno task typecheck # (or run the package.json scripts with your preferred runner)";
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
//#endregion
|
|
380
|
-
export { runInit as t };
|