create-icebreaker 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,33 @@
1
1
  # create-icebreaker
2
2
 
3
- One-shot bootstrapper for the icebreaker monorepo template. It clones the template repo, removes `.git`, and optionally runs the built-in cleanup before you install dependencies.
3
+ Interactive bootstrapper for the icebreaker monorepo template. It scaffolds from the npm package and trims templates based on your selections.
4
4
 
5
5
  ## Usage
6
6
 
7
- - `pnpm create icebreaker my-app`
8
- - `npm create icebreaker@latest my-app`
7
+ - `pnpm create icebreaker`
8
+ - `npm create icebreaker@latest`
9
9
 
10
10
  By default this:
11
11
 
12
- - clones `sonofmagic/monorepo-template` (branch `main`) into `my-app`
13
- - strips the git history
14
- - runs `pnpm dlx @icebreakers/monorepo@latest clean --yes`
12
+ - uses npm templates by default (no GitHub needed)
13
+ - asks for the project directory (defaults to `icebreaker-monorepo`)
14
+ - asks which templates to keep (default keeps none)
15
+ - updates the root `package.json` name
16
+
17
+ ## Templates
18
+
19
+ Available template keys:
20
+
21
+ - `unbuild`
22
+ - `tsup`
23
+ - `tsdown`
24
+ - `vue-lib`
25
+ - `vue-hono`
26
+ - `hono-server`
27
+ - `vitepress`
28
+ - `cli`
15
29
 
16
30
  ## Flags
17
31
 
18
- - `--repo <git-url-or-owner/name>`: clone a different repo
19
- - `--branch <branch-or-tag>`: choose a branch or tag
20
- - `--no-clean`: skip running `monorepo clean`
21
- - `--include-private`: include private packages when cleaning
32
+ - `--templates <list>`: comma-separated template keys or indexes to keep (e.g. `tsup,vue-hono` or `1,5`)
22
33
  - `--force`: overwrite a non-empty target directory
23
- - `--agent <pnpm|npm>`: force which tool to run the cleanup with
@@ -1,222 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process'
3
- import fs from 'node:fs/promises'
4
- import path from 'node:path'
5
- import process from 'node:process'
6
-
7
- const DEFAULT_REPO = 'sonofmagic/monorepo-template'
8
- const DEFAULT_BRANCH = 'main'
9
-
10
- function printHelp() {
11
- console.log([
12
- 'Usage: create-icebreaker [dir] [--repo <repo>] [--branch <branch>]',
13
- 'Options:',
14
- ' --repo <repo> GitHub repo or git url to clone (default sonofmagic/monorepo-template)',
15
- ' --branch <branch> Branch or tag to checkout (default main)',
16
- ' --no-clean Skip running monorepo clean after download',
17
- ' --include-private Run clean with private packages included',
18
- ' --force Remove existing target directory before cloning',
19
- ' --agent <pnpm|npm> Force package manager used for cleanup (default auto-detect)',
20
- ' -h, --help Show this help message',
21
- ].join('\n'))
22
- }
23
-
24
- function parseArgs(argv) {
25
- const options = {
26
- targetDir: 'icebreaker-monorepo',
27
- repo: DEFAULT_REPO,
28
- branch: DEFAULT_BRANCH,
29
- clean: true,
30
- force: false,
31
- includePrivate: false,
32
- agent: undefined,
33
- }
34
- const positionals = []
35
- for (let i = 0; i < argv.length; i++) {
36
- const arg = argv[i]
37
- if (arg === '--repo' && argv[i + 1]) {
38
- options.repo = argv[++i]
39
- continue
40
- }
41
- if (arg.startsWith('--repo=')) {
42
- options.repo = arg.split('=')[1] || options.repo
43
- continue
44
- }
45
- if (arg === '--branch' && argv[i + 1]) {
46
- options.branch = argv[++i]
47
- continue
48
- }
49
- if (arg.startsWith('--branch=')) {
50
- options.branch = arg.split('=')[1] || options.branch
51
- continue
52
- }
53
- if (arg === '--no-clean') {
54
- options.clean = false
55
- continue
56
- }
57
- if (arg === '--clean') {
58
- options.clean = true
59
- continue
60
- }
61
- if (arg === '--include-private') {
62
- options.includePrivate = true
63
- continue
64
- }
65
- if (arg === '--force') {
66
- options.force = true
67
- continue
68
- }
69
- if (arg === '--agent' && argv[i + 1]) {
70
- options.agent = argv[++i]
71
- continue
72
- }
73
- if (arg.startsWith('--agent=')) {
74
- options.agent = arg.split('=')[1] || options.agent
75
- continue
76
- }
77
- if (arg === '-h' || arg === '--help') {
78
- options.help = true
79
- continue
80
- }
81
- if (!arg.startsWith('-')) {
82
- positionals.push(arg)
83
- }
84
- }
85
- if (positionals.length) {
86
- options.targetDir = positionals[0]
87
- }
88
- return options
89
- }
90
-
91
- function normalizeRepo(repo) {
92
- if (repo.startsWith('http')) {
93
- return repo
94
- }
95
- if (repo.startsWith('gh:')) {
96
- return `https://github.com/${repo.slice(3)}.git`
97
- }
98
- if (/^[\w.-]+\/[\w.-]+$/.test(repo)) {
99
- return `https://github.com/${repo}.git`
100
- }
101
- return repo
102
- }
103
-
104
- async function isEmptyDir(dir) {
105
- try {
106
- const entries = await fs.readdir(dir)
107
- return entries.length === 0
108
- }
109
- catch (error) {
110
- if (error && error.code === 'ENOENT') {
111
- return true
112
- }
113
- throw error
114
- }
115
- }
116
-
117
- async function prepareTarget(dir, force) {
118
- const empty = await isEmptyDir(dir)
119
- if (empty) {
120
- await fs.mkdir(dir, { recursive: true })
121
- return
122
- }
123
- if (!force) {
124
- throw new Error(`Target directory ${dir} is not empty. Pass --force to overwrite.`)
125
- }
126
- await fs.rm(dir, { recursive: true, force: true })
127
- await fs.mkdir(dir, { recursive: true })
128
- }
129
-
130
- function runCommand(command, args, options) {
131
- return new Promise((resolve, reject) => {
132
- const child = spawn(command, args, {
133
- stdio: 'inherit',
134
- ...options,
135
- })
136
- child.on('error', reject)
137
- child.on('close', (code) => {
138
- if (code && code !== 0) {
139
- reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`))
140
- return
141
- }
142
- resolve()
143
- })
144
- })
145
- }
146
-
147
- async function cloneRepo(repo, branch, targetDir) {
148
- const normalized = normalizeRepo(repo)
149
- console.log(`Cloning ${normalized} (branch ${branch})...`)
150
- await runCommand('git', ['clone', '--depth', '1', '--branch', branch, normalized, targetDir])
151
- await fs.rm(path.join(targetDir, '.git'), { recursive: true, force: true })
152
- }
153
-
154
- function detectAgent(userAgent) {
155
- if (!userAgent) {
156
- return 'pnpm'
157
- }
158
- const first = userAgent.split(' ')[0] || ''
159
- if (first.startsWith('pnpm/')) {
160
- return 'pnpm'
161
- }
162
- if (first.startsWith('npm/')) {
163
- return 'npm'
164
- }
165
- return 'pnpm'
166
- }
167
-
168
- function getCleanCommand(agent, includePrivate) {
169
- const usePnpm = agent === 'pnpm'
170
- const runner = usePnpm ? 'pnpm' : 'npx'
171
- const args = usePnpm
172
- ? ['dlx', '@icebreakers/monorepo@latest', 'clean', '--yes']
173
- : ['--yes', '@icebreakers/monorepo@latest', 'clean', '--yes']
174
- if (includePrivate) {
175
- args.push('--include-private')
176
- }
177
- return { runner, args }
178
- }
179
-
180
- async function runClean(targetDir, agent, includePrivate) {
181
- const { runner, args } = getCleanCommand(agent, includePrivate)
182
- console.log(`Running ${runner} ${args.join(' ')} in ${targetDir}`)
183
- await runCommand(runner, args, { cwd: targetDir })
184
- }
185
-
186
- function printNextSteps(targetDir, cleanRan) {
187
- const relative = path.relative(process.cwd(), targetDir) || '.'
188
- console.log('\nAll set! Next steps:')
189
- console.log(` cd ${relative}`)
190
- if (!cleanRan) {
191
- console.log(' pnpm dlx @icebreakers/monorepo@latest clean --yes')
192
- }
193
- console.log(' pnpm install')
194
- console.log(' pnpm dev')
195
- }
196
-
197
- async function main() {
198
- const parsed = parseArgs(process.argv.slice(2))
199
- if (parsed.help) {
200
- printHelp()
201
- return
202
- }
203
- const targetDir = path.resolve(process.cwd(), parsed.targetDir)
204
- try {
205
- await prepareTarget(targetDir, parsed.force)
206
- await cloneRepo(parsed.repo, parsed.branch, targetDir)
207
- if (parsed.clean) {
208
- const agent = parsed.agent || detectAgent(process.env.npm_config_user_agent)
209
- await runClean(targetDir, agent, parsed.includePrivate)
210
- }
211
- else {
212
- console.log('Skip clean step per --no-clean')
213
- }
214
- printNextSteps(targetDir, parsed.clean)
215
- }
216
- catch (error) {
217
- console.error('[create-icebreaker]', error?.message || error)
218
- process.exitCode = 1
219
- }
220
- }
221
-
222
- main()
2
+ import '../dist/index.mjs'
@@ -0,0 +1 @@
1
+ export { };
package/dist/index.mjs ADDED
@@ -0,0 +1,186 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { Command, assetsDir, checkbox, input, shouldSkipTemplatePath, templateChoices, templatesDir, toWorkspaceGitignorePath } from "@icebreakers/monorepo-templates";
4
+ import fs from "node:fs/promises";
5
+
6
+ //#region src/constants.ts
7
+ const DEFAULT_TARGET = "icebreaker-monorepo";
8
+
9
+ //#endregion
10
+ //#region src/fs-utils.ts
11
+ async function isEmptyDir(dir) {
12
+ try {
13
+ return (await fs.readdir(dir)).length === 0;
14
+ } catch (error) {
15
+ if (error && error.code === "ENOENT") return true;
16
+ throw error;
17
+ }
18
+ }
19
+ async function prepareTarget(dir, force) {
20
+ if (await isEmptyDir(dir)) {
21
+ await fs.mkdir(dir, { recursive: true });
22
+ return;
23
+ }
24
+ if (!force) throw new Error(`Target directory ${dir} is not empty. Pass --force to overwrite.`);
25
+ await fs.rm(dir, {
26
+ recursive: true,
27
+ force: true
28
+ });
29
+ await fs.mkdir(dir, { recursive: true });
30
+ }
31
+ async function copyDirContents(sourceDir, targetDir, rootDir = sourceDir) {
32
+ await fs.mkdir(targetDir, { recursive: true });
33
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const from = path.join(sourceDir, entry.name);
36
+ if (shouldSkipTemplatePath(rootDir, from)) continue;
37
+ const targetName = toWorkspaceGitignorePath(entry.name);
38
+ const to = path.join(targetDir, targetName);
39
+ if (entry.isDirectory()) {
40
+ await copyDirContents(from, to, rootDir);
41
+ continue;
42
+ }
43
+ if (entry.isSymbolicLink()) {
44
+ const link = await fs.readlink(from);
45
+ await fs.symlink(link, to);
46
+ continue;
47
+ }
48
+ await fs.cp(from, to);
49
+ }
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/package-json.ts
54
+ async function updateRootPackageJson(targetDir, projectName) {
55
+ const pkgPath = path.join(targetDir, "package.json");
56
+ const raw = await fs.readFile(pkgPath, "utf8");
57
+ const pkg = JSON.parse(raw);
58
+ pkg.name = projectName;
59
+ const devDependencies = pkg.devDependencies;
60
+ if (devDependencies && devDependencies["@icebreakers/monorepo"]) {
61
+ delete devDependencies["@icebreakers/monorepo"];
62
+ if (Object.keys(devDependencies).length === 0) delete pkg.devDependencies;
63
+ }
64
+ const scripts = pkg.scripts;
65
+ if (scripts) {
66
+ const nextScripts = {};
67
+ for (const [key, value] of Object.entries(scripts)) {
68
+ if (typeof value === "string" && value.includes("monorepo")) continue;
69
+ nextScripts[key] = value;
70
+ }
71
+ if (Object.keys(nextScripts).length) pkg.scripts = nextScripts;
72
+ else delete pkg.scripts;
73
+ }
74
+ await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
75
+ }
76
+
77
+ //#endregion
78
+ //#region src/templates.ts
79
+ function parseTemplateInput(input$1) {
80
+ const selections = /* @__PURE__ */ new Set();
81
+ const unknown = [];
82
+ const tokens = input$1.split(/[,\\s]+/).map((token) => token.trim()).filter(Boolean);
83
+ for (const token of tokens) {
84
+ const normalized = token.toLowerCase();
85
+ if (/^\\d+$/.test(normalized)) {
86
+ const choice = templateChoices[Number(normalized) - 1];
87
+ if (choice) selections.add(choice.key);
88
+ else unknown.push(token);
89
+ continue;
90
+ }
91
+ const match = templateChoices.find((item) => item.key === normalized);
92
+ if (match) selections.add(match.key);
93
+ else unknown.push(token);
94
+ }
95
+ return {
96
+ selections: Array.from(selections),
97
+ unknown
98
+ };
99
+ }
100
+ function resolveTemplateSelections(input$1) {
101
+ if (!input$1) return {
102
+ selections: [],
103
+ unknown: []
104
+ };
105
+ return parseTemplateInput(input$1);
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/prompts.ts
110
+ async function promptTargetDir(defaultDir) {
111
+ return ((await input({
112
+ message: `项目创建目录(默认 ${defaultDir}):`,
113
+ default: defaultDir
114
+ }))?.trim?.() ?? defaultDir) || defaultDir;
115
+ }
116
+ async function promptTemplates() {
117
+ return await checkbox({
118
+ message: "请选择要保留的模板(默认不选择):",
119
+ choices: templateChoices.map((item) => ({
120
+ name: `${item.key} - ${item.label}`,
121
+ value: item.key
122
+ }))
123
+ });
124
+ }
125
+
126
+ //#endregion
127
+ //#region src/source-npm.ts
128
+ async function copySelectedTemplates(targetDir, selectedTemplates) {
129
+ if (!selectedTemplates.length) return;
130
+ const selected = new Set(selectedTemplates);
131
+ for (const template of templateChoices) {
132
+ if (!selected.has(template.key)) continue;
133
+ await copyDirContents(path.join(templatesDir, template.source), path.join(targetDir, template.target));
134
+ }
135
+ }
136
+ async function scaffoldFromNpm(targetDir, selectedTemplates) {
137
+ await copyDirContents(assetsDir, targetDir);
138
+ await copySelectedTemplates(targetDir, selectedTemplates);
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/index.ts
143
+ function printNextSteps(targetDir) {
144
+ const relative = path.relative(process.cwd(), targetDir) || ".";
145
+ process.stdout.write([
146
+ "",
147
+ "All set! Next steps:",
148
+ ` cd ${relative}`,
149
+ " pnpm install",
150
+ " pnpm dev",
151
+ ""
152
+ ].join("\n"));
153
+ }
154
+ async function runCreate(targetDirInput, options) {
155
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
156
+ let targetInput = targetDirInput || DEFAULT_TARGET;
157
+ if (isInteractive) targetInput = await promptTargetDir(targetInput);
158
+ let selectedTemplates = [];
159
+ if (options.templates) {
160
+ const { selections, unknown } = resolveTemplateSelections(options.templates);
161
+ if (unknown.length) process.stderr.write(`忽略未知模板:${unknown.join(", ")}\n`);
162
+ selectedTemplates = selections;
163
+ } else if (isInteractive) selectedTemplates = await promptTemplates();
164
+ const targetDir = path.resolve(process.cwd(), targetInput);
165
+ const projectName = path.basename(targetDir) || targetInput;
166
+ await prepareTarget(targetDir, Boolean(options.force));
167
+ await scaffoldFromNpm(targetDir, selectedTemplates);
168
+ await updateRootPackageJson(targetDir, projectName);
169
+ printNextSteps(targetDir);
170
+ }
171
+ const program = new Command();
172
+ program.name("create-icebreaker").description("Bootstrap the icebreaker monorepo template").argument("[dir]", "Target directory", DEFAULT_TARGET).option("-t, --templates <list>", "Comma-separated template keys or indexes to keep").option("-f, --force", "Remove existing target directory before scaffolding", false).action(async (dir, options) => {
173
+ try {
174
+ await runCreate(dir, options);
175
+ } catch (error) {
176
+ process.stderr.write(`[create-icebreaker] ${error instanceof Error ? error.message : error}\n`);
177
+ process.exitCode = 1;
178
+ }
179
+ });
180
+ program.parseAsync(process.argv).catch((error) => {
181
+ process.stderr.write(`[create-icebreaker] ${error instanceof Error ? error.message : error}\n`);
182
+ process.exitCode = 1;
183
+ });
184
+
185
+ //#endregion
186
+ export { };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-icebreaker",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.1.1",
5
5
  "description": "Bootstrap the icebreaker monorepo template via npm create/pnpm create with optional cleanup",
6
6
  "author": "ice breaker <1324318532@qq.com>",
7
7
  "license": "MIT",
@@ -24,15 +24,22 @@
24
24
  "create-icebreaker": "bin/create-icebreaker.js"
25
25
  },
26
26
  "files": [
27
- "bin"
27
+ "bin",
28
+ "dist"
28
29
  ],
29
30
  "engines": {
30
31
  "node": ">=18.0.0"
31
32
  },
33
+ "dependencies": {
34
+ "@icebreakers/monorepo-templates": "0.1.1"
35
+ },
32
36
  "publishConfig": {
33
37
  "access": "public"
34
38
  },
35
39
  "scripts": {
40
+ "dev": "tsdown --watch --sourcemap",
41
+ "build": "tsdown",
42
+ "typecheck": "tsc -p tsconfig.json",
36
43
  "lint": "eslint .",
37
44
  "test": "vitest run --passWithNoTests"
38
45
  }