frontpl 0.1.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 ADDED
@@ -0,0 +1,11 @@
1
+ # frontpl
2
+
3
+ Interactive CLI to scaffold standardized frontend project templates.
4
+
5
+ ## Development
6
+
7
+ ```sh
8
+ pnpm install
9
+ pnpm run build
10
+ node dist/cli.mjs --help
11
+ ```
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { t as runInit } from "./init-Cva-s-yN.mjs";
3
+ import bin from "tiny-bin";
4
+
5
+ //#region src/cli.ts
6
+ async function main() {
7
+ await bin("frontpl", "Scaffold standardized frontend templates").argument("[name]", "Project name (directory name)").action(async (_options, args) => {
8
+ await runInit({ nameArg: args[0] });
9
+ }).command("init", "Scaffold a new project").argument("[name]", "Project name (directory name)").action(async (_options, args) => {
10
+ await runInit({ nameArg: args[0] });
11
+ }).run();
12
+ }
13
+ main();
14
+
15
+ //#endregion
16
+ export { };
@@ -0,0 +1,8 @@
1
+ //#region src/commands/init.d.ts
2
+ declare function runInit({
3
+ nameArg
4
+ }: {
5
+ nameArg?: string;
6
+ }): Promise<void>;
7
+ //#endregion
8
+ export { runInit };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { t as runInit } from "./init-Cva-s-yN.mjs";
2
+
3
+ export { runInit };
@@ -0,0 +1,380 @@
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 };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "frontpl",
3
+ "version": "0.1.0",
4
+ "description": "Interactive CLI to scaffold standardized frontend project templates.",
5
+ "keywords": [
6
+ "cli",
7
+ "frontend",
8
+ "generator",
9
+ "scaffold",
10
+ "template"
11
+ ],
12
+ "homepage": "https://github.com/kingsword09/frontpl#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/kingsword09/frontpl/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Kingsword kingsword09 <kingsword09@gmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kingsword09/frontpl.git"
21
+ },
22
+ "bin": {
23
+ "frontpl": "./dist/cli.mjs"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "main": "dist/index.mjs",
30
+ "types": "dist/index.d.mts",
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "dev": "tsdown --watch",
34
+ "start": "node dist/cli.mjs",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "lint": "oxlint --type-aware --type-check",
37
+ "lint:fix": "oxlint --type-aware --type-check --fix",
38
+ "format": "oxfmt",
39
+ "format:check": "oxfmt --check",
40
+ "fmt": "pnpm run format",
41
+ "fmt:check": "pnpm run format:check",
42
+ "prepublishOnly": "pnpm run build"
43
+ },
44
+ "dependencies": {
45
+ "@clack/prompts": "^0.11.0",
46
+ "tiny-bin": "^1.11.3"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.0.10",
50
+ "oxfmt": "^0.26.0",
51
+ "oxlint": "^1.41.0",
52
+ "oxlint-tsgolint": "^0.11.1",
53
+ "tsdown": "^0.20.1",
54
+ "typescript": "^5.9.3"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.0.0"
58
+ },
59
+ "packageManager": "pnpm@10.28.1"
60
+ }