mirror of
https://github.com/zuiidea/antd-admin
synced 2026-11-02 07:53:59 +00:00
merge: feat/create-antd-admin-cli (create-antd-admin CLI)
Made-with: Cursor
This commit is contained in:
@@ -49,6 +49,8 @@ Balanced admin monorepo: MSW mocks, minimal RBAC, full CRUD, and Playwright E2E
|
||||
|
||||
## Templates
|
||||
|
||||
`pnpm dlx create-antd-admin@latest`
|
||||
|
||||
| Directory | Use when |
|
||||
| -------------------- | -------- |
|
||||
| [`apps/with-lingui`](./apps/with-lingui/) | You want **LinguiJS** (en + zh), `.po` extract/compile, and a language switcher — this is the fuller reference stack. |
|
||||
|
||||
@@ -171,7 +171,8 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../..");
|
||||
/** From `packages/create/scripts/`, three levels up = monorepo root. */
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const appsDir = path.join(repoRoot, "apps");
|
||||
const outFile = path.resolve(__dirname, "../dist/examples.json");
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "template-placeholder",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "create-antd-admin",
|
||||
"version": "6.0.0",
|
||||
"description": "Create an Antd Admin app from official examples",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"create-antd-admin": "./dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm run build",
|
||||
"build": "tsup && node ./scripts/write-examples-json.mjs",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.10.1",
|
||||
"commander": "^14.0.0",
|
||||
"giget": "^3.2.0",
|
||||
"picocolors": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const appsDir = path.join(repoRoot, "apps");
|
||||
const outFile = path.resolve(__dirname, "../dist/examples.json");
|
||||
|
||||
const exclude = new Set(["docs"]);
|
||||
const names = fs
|
||||
.readdirSync(appsDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name)
|
||||
.filter((name) => !exclude.has(name))
|
||||
.sort();
|
||||
|
||||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||
fs.writeFileSync(outFile, JSON.stringify({ examples: names }, null, 2));
|
||||
console.log(`write-examples-json: wrote ${outFile}`, names);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command, Option } from "commander";
|
||||
import pc from "picocolors";
|
||||
import { runCreate } from "./commands/create.js";
|
||||
import { EXIT_ERROR, EXIT_SIGINT } from "./constants.js";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(
|
||||
path.join(path.dirname(fileURLToPath(import.meta.url)), "../package.json"),
|
||||
"utf8"
|
||||
)
|
||||
) as { version: string };
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("create-antd-admin")
|
||||
.description("Create an Antd Admin app from official examples")
|
||||
.version(pkg.version, "-v, --version")
|
||||
.argument("[project-directory]", "Directory for the new project")
|
||||
.option("-e, --example <name|url>", "Example name or template URL")
|
||||
.option("--example-path <path>", "Path inside repo when URL is ambiguous")
|
||||
.addOption(
|
||||
new Option("-m, --package-manager <pm>", "Package manager").choices([
|
||||
"npm",
|
||||
"yarn",
|
||||
"pnpm",
|
||||
"bun",
|
||||
])
|
||||
)
|
||||
.option("--skip-install", "Skip installing dependencies", false)
|
||||
.option("--skip-transforms", "Skip code transforms", false)
|
||||
.option("--no-git", "Skip git repository initialization")
|
||||
.action(
|
||||
async (
|
||||
projectDirectory: string | undefined,
|
||||
cmdOpts: {
|
||||
example?: string;
|
||||
examplePath?: string;
|
||||
packageManager?: "npm" | "yarn" | "pnpm" | "bun";
|
||||
skipInstall?: boolean;
|
||||
skipTransforms?: boolean;
|
||||
git?: boolean;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
await runCreate({
|
||||
projectDirectory,
|
||||
example: cmdOpts.example,
|
||||
examplePath: cmdOpts.examplePath,
|
||||
packageManager: cmdOpts.packageManager,
|
||||
skipInstall: Boolean(cmdOpts.skipInstall),
|
||||
skipTransforms: Boolean(cmdOpts.skipTransforms),
|
||||
git: cmdOpts.git !== false,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof CliError) {
|
||||
console.error(pc.red(e.message));
|
||||
process.exit(e.exitCode);
|
||||
}
|
||||
console.error(e);
|
||||
process.exit(EXIT_ERROR);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
process.exit(EXIT_SIGINT);
|
||||
});
|
||||
|
||||
program.parseAsync(process.argv).catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(EXIT_ERROR);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import pc from "picocolors";
|
||||
import { assertValidExample, loadExamplesManifest } from "../examples.js";
|
||||
import { downloadExampleToTemp } from "../download.js";
|
||||
import { installDependencies } from "../install.js";
|
||||
import {
|
||||
detectAvailableManagers,
|
||||
type PackageManagerName,
|
||||
} from "../package-manager.js";
|
||||
import { resolveTargetRoot } from "../paths.js";
|
||||
import {
|
||||
promptExample,
|
||||
promptPackageManager,
|
||||
promptPackageName,
|
||||
promptTargetDir,
|
||||
} from "../prompts.js";
|
||||
import { resolveExampleSource } from "../resolve-example-source.js";
|
||||
import { tryGitInit } from "../git.js";
|
||||
import { runTransforms } from "../transforms/run-transforms.js";
|
||||
|
||||
export async function runCreate(
|
||||
opts: Omit<import("../types.js").CreateCliOptions, "git"> & {
|
||||
projectDirectory?: string;
|
||||
git?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
const cwd = process.cwd();
|
||||
const manifest = loadExamplesManifest();
|
||||
|
||||
const exampleTrim = opts.example?.trim();
|
||||
const example = exampleTrim
|
||||
? exampleTrim
|
||||
: await promptExample(manifest);
|
||||
|
||||
if (!/^https?:\/\//i.test(example)) {
|
||||
assertValidExample(example, manifest);
|
||||
}
|
||||
|
||||
const projectDirRaw = opts.projectDirectory?.trim();
|
||||
const projectDir = projectDirRaw
|
||||
? projectDirRaw
|
||||
: await promptTargetDir();
|
||||
|
||||
const targetRoot = resolveTargetRoot(cwd, projectDir);
|
||||
fsSync.mkdirSync(targetRoot, { recursive: true });
|
||||
|
||||
const defaultPkgName =
|
||||
projectDir === "."
|
||||
? (() => {
|
||||
const normalized = path
|
||||
.basename(cwd)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || "antd-admin-app";
|
||||
})()
|
||||
: path.basename(targetRoot);
|
||||
|
||||
const packageName = await promptPackageName(defaultPkgName);
|
||||
|
||||
const packageManager: PackageManagerName =
|
||||
opts.packageManager ??
|
||||
(await promptPackageManager(detectAvailableManagers()));
|
||||
|
||||
if (opts.skipTransforms && opts.packageManager) {
|
||||
console.warn(
|
||||
pc.yellow(
|
||||
"--skip-transforms was used with an explicit package manager; package.json name may not be updated before install."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { gigetSource } = resolveExampleSource({
|
||||
example,
|
||||
examplePath: opts.examplePath,
|
||||
});
|
||||
|
||||
const tmp = await downloadExampleToTemp(gigetSource);
|
||||
try {
|
||||
fsSync.cpSync(tmp, targetRoot, { recursive: true, dereference: true });
|
||||
} finally {
|
||||
fsSync.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
await runTransforms({
|
||||
projectRoot: targetRoot,
|
||||
packageName,
|
||||
skip: opts.skipTransforms,
|
||||
});
|
||||
|
||||
if (!opts.skipInstall) {
|
||||
installDependencies(targetRoot, packageManager);
|
||||
}
|
||||
|
||||
tryGitInit(targetRoot, opts.git !== false);
|
||||
|
||||
console.log(pc.green(`Success! Project created at ${targetRoot}`));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const DEFAULT_GITHUB_REPO = "zuiidea/antd-admin";
|
||||
|
||||
/** Override full repo slug `owner/name` */
|
||||
export const ENV_REPO = "CREATE_ANTD_ADMIN_REPO";
|
||||
|
||||
/** Override ref: branch name, tag, or commit sha */
|
||||
export const ENV_REF = "CREATE_ANTD_ADMIN_REF";
|
||||
|
||||
export const EXIT_SUCCESS = 0;
|
||||
export const EXIT_ERROR = 1;
|
||||
export const EXIT_SIGINT = 130;
|
||||
@@ -0,0 +1,24 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { downloadTemplate } from "giget";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
export async function downloadExampleToTemp(
|
||||
gigetSource: string
|
||||
): Promise<string> {
|
||||
const dir = path.join(os.tmpdir(), `create-antd-admin-${randomUUID()}`);
|
||||
try {
|
||||
const { dir: outDir } = await downloadTemplate(gigetSource, {
|
||||
dir,
|
||||
forceClean: true,
|
||||
});
|
||||
return outDir;
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
throw new CliError(
|
||||
`Failed to download template.\nSource: ${gigetSource}\n${msg}`,
|
||||
"DOWNLOAD_FAILED"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export class CliError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code:
|
||||
| "INVALID_INPUT"
|
||||
| "DOWNLOAD_FAILED"
|
||||
| "TRANSFORM_FAILED"
|
||||
| "INSTALL_FAILED",
|
||||
public readonly exitCode = 1
|
||||
) {
|
||||
super(message);
|
||||
this.name = "CliError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
export type ExamplesManifest = { examples: string[] };
|
||||
|
||||
export function loadExamplesManifest(): string[] {
|
||||
const manifestPath = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"examples.json"
|
||||
);
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new CliError(
|
||||
`Missing ${manifestPath}. Run pnpm run build in packages/create.`,
|
||||
"INVALID_INPUT"
|
||||
);
|
||||
}
|
||||
const data = JSON.parse(
|
||||
fs.readFileSync(manifestPath, "utf8")
|
||||
) as ExamplesManifest;
|
||||
if (!Array.isArray(data.examples) || data.examples.length === 0) {
|
||||
throw new CliError("Invalid examples.json", "INVALID_INPUT");
|
||||
}
|
||||
return data.examples;
|
||||
}
|
||||
|
||||
export function assertValidExample(name: string, list: string[]): void {
|
||||
if (!list.includes(name)) {
|
||||
throw new CliError(
|
||||
`Unknown example "${name}". Valid: ${list.join(", ")}`,
|
||||
"INVALID_INPUT"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function tryGitInit(root: string, enabled: boolean): boolean {
|
||||
if (!enabled) {
|
||||
const gitDir = path.join(root, ".git");
|
||||
if (fs.existsSync(gitDir)) {
|
||||
fs.rmSync(gitDir, { recursive: true, force: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const init = spawnSync("git", ["init"], { cwd: root, stdio: "ignore" });
|
||||
if (init.status !== 0) return false;
|
||||
spawnSync("git", ["add", "-A"], { cwd: root, stdio: "ignore" });
|
||||
spawnSync(
|
||||
"git",
|
||||
["commit", "-m", "chore: initial commit from create-antd-admin"],
|
||||
{ cwd: root, stdio: "ignore" }
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import type { PackageManagerName } from "./package-manager.js";
|
||||
import { spawnInstall } from "./package-manager.js";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
export function installDependencies(
|
||||
cwd: string,
|
||||
manager: PackageManagerName
|
||||
): void {
|
||||
const { cmd, args } = spawnInstall(manager);
|
||||
const res = spawnSync(cmd, args, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, CI: "1" },
|
||||
});
|
||||
if (res.error || res.status !== 0) {
|
||||
throw new CliError(
|
||||
`${cmd} ${args.join(" ")} failed in ${cwd}`,
|
||||
"INSTALL_FAILED"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runTransforms } from "../transforms/run-transforms.js";
|
||||
|
||||
describe("create pipeline transforms", () => {
|
||||
it("renames package.json", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cad-"));
|
||||
const fixture = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../fixtures/minimal-app"
|
||||
);
|
||||
fs.cpSync(fixture, tmp, { recursive: true });
|
||||
await runTransforms({
|
||||
projectRoot: tmp,
|
||||
packageName: "my-fine-app",
|
||||
skip: false,
|
||||
});
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.join(tmp, "package.json"), "utf8")
|
||||
);
|
||||
expect(pkg.name).toBe("my-fine-app");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
|
||||
|
||||
const ORDER: PackageManagerName[] = ["pnpm", "npm", "yarn", "bun"];
|
||||
|
||||
export function isManagerAvailable(name: PackageManagerName): boolean {
|
||||
const result = spawnSync(name, ["--version"], { stdio: "ignore" });
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
export function detectAvailableManagers(): PackageManagerName[] {
|
||||
return ORDER.filter(isManagerAvailable);
|
||||
}
|
||||
|
||||
/** Install argv; cwd is applied by install.ts via spawnSync `cwd` option. */
|
||||
export function spawnInstall(name: PackageManagerName): {
|
||||
cmd: string;
|
||||
args: string[];
|
||||
} {
|
||||
switch (name) {
|
||||
case "pnpm":
|
||||
return { cmd: "pnpm", args: ["install"] };
|
||||
case "npm":
|
||||
return { cmd: "npm", args: ["install"] };
|
||||
case "yarn":
|
||||
return { cmd: "yarn", args: ["install"] };
|
||||
case "bun":
|
||||
return { cmd: "bun", args: ["install"] };
|
||||
default: {
|
||||
const _: never = name;
|
||||
throw new Error(`unsupported manager ${_}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
export function resolveTargetRoot(cwd: string, dir: string): string {
|
||||
const abs = path.resolve(cwd, dir);
|
||||
if (fs.existsSync(abs)) {
|
||||
const entries = fs.readdirSync(abs);
|
||||
if (entries.length > 0) {
|
||||
throw new CliError(
|
||||
`Target directory is not empty: ${abs}`,
|
||||
"INVALID_INPUT"
|
||||
);
|
||||
}
|
||||
}
|
||||
return abs;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { confirm, input, select } from "@inquirer/prompts";
|
||||
import type { PackageManagerName } from "./package-manager.js";
|
||||
import { CliError } from "./errors.js";
|
||||
import { normalizePackageName } from "./utils/normalize-package-name.js";
|
||||
|
||||
export async function promptExample(examples: string[]): Promise<string> {
|
||||
return select({
|
||||
message: "Which example do you want?",
|
||||
choices: examples.map((value) => ({ name: value, value })),
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptTargetDir(): Promise<string> {
|
||||
const raw = await input({
|
||||
message:
|
||||
"Project directory (relative path, or '.' for current directory)?",
|
||||
default: "my-antd-admin",
|
||||
});
|
||||
const dir = (raw.trim() || "my-antd-admin").replace(/\\/g, "/");
|
||||
if (dir === ".") {
|
||||
const ok = await confirm({
|
||||
message: "Scaffold into the current directory?",
|
||||
default: false,
|
||||
});
|
||||
if (!ok) {
|
||||
throw new CliError("Aborted.", "INVALID_INPUT");
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export async function promptPackageName(defaultName: string): Promise<string> {
|
||||
const raw = await input({
|
||||
message: "npm package name for package.json?",
|
||||
default: defaultName,
|
||||
validate: (value) => {
|
||||
try {
|
||||
normalizePackageName(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
return normalizePackageName(raw);
|
||||
}
|
||||
|
||||
export async function promptPackageManager(
|
||||
available: PackageManagerName[]
|
||||
): Promise<PackageManagerName> {
|
||||
if (available.length === 0) {
|
||||
throw new CliError(
|
||||
"No supported package manager found in PATH (pnpm, npm, yarn, bun).",
|
||||
"INVALID_INPUT"
|
||||
);
|
||||
}
|
||||
return select({
|
||||
message: "Which package manager do you want to use?",
|
||||
choices: available.map((value) => ({ name: value, value })),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { resolveExampleSource } from "./resolve-example-source.js";
|
||||
|
||||
describe("resolveExampleSource", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("resolves short example with default repo and env ref", () => {
|
||||
vi.stubEnv("CREATE_ANTD_ADMIN_REF", "main");
|
||||
const r = resolveExampleSource({ example: "basic" });
|
||||
expect(r.gigetSource).toBe("github:zuiidea/antd-admin/apps/basic#main");
|
||||
});
|
||||
|
||||
it("passes through https URL", () => {
|
||||
const url = "https://api.github.com/repos/zuiidea/antd-admin/tarball/main";
|
||||
const r = resolveExampleSource({ example: url });
|
||||
expect(r.gigetSource).toBe(url);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DEFAULT_GITHUB_REPO, ENV_REPO } from "./constants.js";
|
||||
import { CliError } from "./errors.js";
|
||||
import { resolveGitRef } from "./resolve-ref.js";
|
||||
|
||||
export type ResolvedExampleSource = { gigetSource: string; displayName: string };
|
||||
|
||||
function defaultRepo(): string {
|
||||
return process.env[ENV_REPO]?.trim() || DEFAULT_GITHUB_REPO;
|
||||
}
|
||||
|
||||
export function resolveExampleSource(input: {
|
||||
example?: string;
|
||||
examplePath?: string;
|
||||
}): ResolvedExampleSource {
|
||||
const example = input.example?.trim();
|
||||
if (!example) {
|
||||
throw new CliError("example is required", "INVALID_INPUT");
|
||||
}
|
||||
const { examplePath } = input;
|
||||
const ref = resolveGitRef();
|
||||
|
||||
if (/^https?:\/\//i.test(example)) {
|
||||
return { gigetSource: example, displayName: example };
|
||||
}
|
||||
|
||||
const repo = defaultRepo();
|
||||
|
||||
if (!example.includes("/")) {
|
||||
const sub = examplePath
|
||||
? `apps/${example}/${examplePath}`.replace(/\/+/g, "/")
|
||||
: `apps/${example}`;
|
||||
const gigetSource = `github:${repo}/${sub}#${ref}`;
|
||||
return { gigetSource, displayName: example };
|
||||
}
|
||||
|
||||
const pathPart = examplePath ? `${example}/${examplePath}` : example;
|
||||
const gigetSource = `github:${pathPart}#${ref}`;
|
||||
return { gigetSource, displayName: pathPart };
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { ENV_REF } from "./constants.js";
|
||||
|
||||
/**
|
||||
* Priority: CREATE_ANTD_ADMIN_REF env → read sibling package.json version as `v${version}` → `main` on failure.
|
||||
* When bundled to dist/cli.js, ../package.json is packages/create/package.json.
|
||||
*/
|
||||
export function resolveGitRef(): string {
|
||||
const env = process.env[ENV_REF]?.trim();
|
||||
if (env) return env;
|
||||
|
||||
try {
|
||||
const pkgPath = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
"package.json"
|
||||
);
|
||||
const raw = fs.readFileSync(pkgPath, "utf8");
|
||||
const { version } = JSON.parse(raw) as { version: string };
|
||||
return `v${version}`;
|
||||
} catch {
|
||||
return "main";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CliError } from "../errors.js";
|
||||
|
||||
export function transformPackageJsonName(
|
||||
projectRoot: string,
|
||||
name: string
|
||||
): void {
|
||||
const pkgPath = path.join(projectRoot, "package.json");
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
throw new CliError(`No package.json at ${pkgPath}`, "TRANSFORM_FAILED");
|
||||
}
|
||||
const raw = fs.readFileSync(pkgPath, "utf8");
|
||||
const json = JSON.parse(raw) as { name?: string };
|
||||
json.name = name;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + "\n");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { transformPackageJsonName } from "./package-json-name.js";
|
||||
|
||||
export async function runTransforms(opts: {
|
||||
projectRoot: string;
|
||||
packageName: string;
|
||||
skip: boolean;
|
||||
}): Promise<void> {
|
||||
if (opts.skip) return;
|
||||
transformPackageJsonName(opts.projectRoot, opts.packageName);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { PackageManagerName } from "./package-manager.js";
|
||||
|
||||
export type CreateCliOptions = {
|
||||
packageManager?: PackageManagerName;
|
||||
skipInstall: boolean;
|
||||
skipTransforms: boolean;
|
||||
example?: string;
|
||||
examplePath?: string;
|
||||
git: boolean;
|
||||
};
|
||||
|
||||
export type CreateContext = {
|
||||
example: string;
|
||||
targetDir: string;
|
||||
packageName: string;
|
||||
packageManager: PackageManagerName;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizePackageName } from "./normalize-package-name.js";
|
||||
|
||||
describe("normalizePackageName", () => {
|
||||
it("lowercases and replaces spaces", () => {
|
||||
expect(normalizePackageName("My App")).toBe("my-app");
|
||||
});
|
||||
|
||||
it("rejects leading dot", () => {
|
||||
expect(() => normalizePackageName(".bad")).toThrow(/cannot start/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CliError } from "../errors.js";
|
||||
|
||||
/**
|
||||
* npm package name rules (simplified): lowercase, URL-safe, no leading dot, segments 1-214.
|
||||
*/
|
||||
export function normalizePackageName(input: string): string {
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
throw new CliError("Project name cannot be empty", "INVALID_INPUT");
|
||||
}
|
||||
if (trimmed.startsWith(".") || trimmed.startsWith("_")) {
|
||||
throw new CliError(
|
||||
"Project name cannot start with . or _",
|
||||
"INVALID_INPUT"
|
||||
);
|
||||
}
|
||||
const safe = trimmed
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9-._~]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
if (!safe || safe.length > 214) {
|
||||
throw new CliError("Project name is invalid or too long", "INVALID_INPUT");
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true,
|
||||
"rootDir": ".",
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tsup.config.ts", "vitest.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/cli.ts"],
|
||||
format: ["esm"],
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
outDir: "dist",
|
||||
clean: true,
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
shims: false,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
Generated
+942
-25
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user