@guchen_0521/create-temp 0.1.6 → 0.1.7

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/lib/scaffold.js CHANGED
@@ -3,8 +3,10 @@ import {
3
3
  mkdir,
4
4
  mkdtemp,
5
5
  readdir,
6
+ readFile,
6
7
  rm,
7
8
  stat,
9
+ writeFile,
8
10
  } from "node:fs/promises";
9
11
  import { execFile } from "node:child_process";
10
12
  import { promisify } from "node:util";
@@ -29,6 +31,10 @@ const GIT_SSH_COMMAND =
29
31
  "ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=6";
30
32
  const PROMPT_CANCELLED_MESSAGE = "Prompt cancelled.";
31
33
  const PROMPT_BACK_VALUE = "__back";
34
+ const OPTIONAL_PACKAGES_PATH = new URL(
35
+ "../optional-packages.json",
36
+ import.meta.url
37
+ );
32
38
 
33
39
  function assertInteractive() {
34
40
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -38,6 +44,10 @@ function assertInteractive() {
38
44
  }
39
45
  }
40
46
 
47
+ function isInteractive() {
48
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
49
+ }
50
+
41
51
  async function cloneTemplateRepo() {
42
52
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "template-cli-"));
43
53
  const repoDir = path.join(tempDir, `repo-${Date.now().toString(36)}`);
@@ -111,6 +121,112 @@ function normalizeTemplateKey(templateKey) {
111
121
  .replace(/^\.\/+/, "");
112
122
  }
113
123
 
124
+ async function loadOptionalPackageRules() {
125
+ try {
126
+ const raw = await readFile(OPTIONAL_PACKAGES_PATH, "utf8");
127
+ return JSON.parse(raw);
128
+ } catch (error) {
129
+ if (error && error.code === "ENOENT") {
130
+ return null;
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ function matchOptionalRule(rule, templateKey) {
137
+ if (!rule || !templateKey) {
138
+ return false;
139
+ }
140
+
141
+ const normalizedTemplate = normalizeTemplateKey(templateKey);
142
+
143
+ if (rule.match) {
144
+ return normalizeTemplateKey(rule.match) === normalizedTemplate;
145
+ }
146
+
147
+ if (rule.matchPrefix) {
148
+ const normalizedPrefix = normalizeTemplateKey(rule.matchPrefix);
149
+ const prefixWithSlash = normalizedPrefix.endsWith("/")
150
+ ? normalizedPrefix
151
+ : `${normalizedPrefix}/`;
152
+ return normalizedTemplate.startsWith(prefixWithSlash);
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ function collectOptionalPackages(rules, templateKey) {
159
+ if (!rules || !Array.isArray(rules.rules)) {
160
+ return [];
161
+ }
162
+
163
+ const collected = [];
164
+ const seen = new Set();
165
+
166
+ rules.rules.forEach((rule) => {
167
+ if (!matchOptionalRule(rule, templateKey)) {
168
+ return;
169
+ }
170
+
171
+ if (!Array.isArray(rule.packages)) {
172
+ return;
173
+ }
174
+
175
+ rule.packages.forEach((pkg) => {
176
+ if (!pkg || !pkg.name || typeof pkg.name !== "string") {
177
+ return;
178
+ }
179
+ if (seen.has(pkg.name)) {
180
+ return;
181
+ }
182
+ seen.add(pkg.name);
183
+ collected.push({
184
+ name: pkg.name,
185
+ dev: Boolean(pkg.dev),
186
+ version: pkg.version,
187
+ description: pkg.description,
188
+ });
189
+ });
190
+ });
191
+
192
+ return collected;
193
+ }
194
+
195
+ async function promptOptionalPackages(optionalPackages) {
196
+ if (!isInteractive() || optionalPackages.length === 0) {
197
+ return [];
198
+ }
199
+
200
+ const choices = optionalPackages.map((pkg) => ({
201
+ title: `${pkg.name}${pkg.dev ? " (dev)" : ""}${
202
+ pkg.description ? ` - ${pkg.description}` : ""
203
+ }`,
204
+ value: pkg.name,
205
+ }));
206
+
207
+ const response = await prompts(
208
+ {
209
+ type: "multiselect",
210
+ name: "selected",
211
+ message: "Select optional packages:",
212
+ choices,
213
+ },
214
+ {
215
+ onCancel: () => {
216
+ throw new Error(PROMPT_CANCELLED_MESSAGE);
217
+ },
218
+ }
219
+ );
220
+
221
+ const selected = Array.isArray(response.selected) ? response.selected : [];
222
+ if (selected.length === 0) {
223
+ return [];
224
+ }
225
+
226
+ const selectedSet = new Set(selected);
227
+ return optionalPackages.filter((pkg) => selectedSet.has(pkg.name));
228
+ }
229
+
114
230
  function buildTemplateTree(templates) {
115
231
  const root = { name: "", children: new Map(), template: null };
116
232
  templates.forEach((template) => {
@@ -248,6 +364,56 @@ async function copyDir(sourceDir, targetDir) {
248
364
  }
249
365
  }
250
366
 
367
+ async function applyOptionalPackages(targetPath, packages) {
368
+ if (!packages || packages.length === 0) {
369
+ return;
370
+ }
371
+
372
+ const packageJsonPath = path.join(targetPath, "package.json");
373
+ const raw = await readFile(packageJsonPath, "utf8");
374
+ const pkgJson = JSON.parse(raw);
375
+
376
+ const hadDependencies = Object.prototype.hasOwnProperty.call(
377
+ pkgJson,
378
+ "dependencies"
379
+ );
380
+ const hadDevDependencies = Object.prototype.hasOwnProperty.call(
381
+ pkgJson,
382
+ "devDependencies"
383
+ );
384
+
385
+ const dependencies = hadDependencies ? { ...pkgJson.dependencies } : {};
386
+ const devDependencies = hadDevDependencies ? { ...pkgJson.devDependencies } : {};
387
+
388
+ let changed = false;
389
+
390
+ packages.forEach((pkg) => {
391
+ if (dependencies[pkg.name] || devDependencies[pkg.name]) {
392
+ return;
393
+ }
394
+ const version = pkg.version || "latest";
395
+ if (pkg.dev) {
396
+ devDependencies[pkg.name] = version;
397
+ } else {
398
+ dependencies[pkg.name] = version;
399
+ }
400
+ changed = true;
401
+ });
402
+
403
+ if (!changed) {
404
+ return;
405
+ }
406
+
407
+ if (hadDependencies || Object.keys(dependencies).length > 0) {
408
+ pkgJson.dependencies = dependencies;
409
+ }
410
+ if (hadDevDependencies || Object.keys(devDependencies).length > 0) {
411
+ pkgJson.devDependencies = devDependencies;
412
+ }
413
+
414
+ await writeFile(packageJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
415
+ }
416
+
251
417
  export async function scaffold({ templateKey, targetDir }) {
252
418
  let repoDir = null;
253
419
  let tempDir = null;
@@ -300,11 +466,22 @@ export async function scaffold({ templateKey, targetDir }) {
300
466
  selectedTemplate = await promptSelectTemplateTree(templates);
301
467
  }
302
468
 
469
+ const optionalRules = await loadOptionalPackageRules();
470
+ const optionalPackages = collectOptionalPackages(
471
+ optionalRules,
472
+ selectedTemplate.key
473
+ );
474
+ const selectedOptionalPackages =
475
+ optionalPackages.length > 0
476
+ ? await promptOptionalPackages(optionalPackages)
477
+ : [];
478
+
303
479
  const resolvedTargetDir = targetDir || (await promptTargetDir());
304
480
  const targetPath = path.resolve(process.cwd(), resolvedTargetDir);
305
481
 
306
482
  await ensureEmptyDir(targetPath);
307
483
  await copyDir(selectedTemplate.dir, targetPath);
484
+ await applyOptionalPackages(targetPath, selectedOptionalPackages);
308
485
 
309
486
  if (tempDir) {
310
487
  await rm(tempDir, { recursive: true, force: true });
@@ -0,0 +1,30 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "match": "backend/express/express-no-ts",
5
+ "packages": [
6
+ {
7
+ "name": "sequelize",
8
+ "dev": false
9
+ },
10
+ {
11
+ "name": "mysql2",
12
+ "dev": false
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "matchPrefix": "front/web/vue3/",
18
+ "packages": [
19
+ {
20
+ "name": "axios",
21
+ "dev": false
22
+ },
23
+ {
24
+ "name": "eslint",
25
+ "dev": true
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guchen_0521/create-temp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Project scaffolding CLI (templates pulled from GitHub)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "lib",
12
+ "optional-packages.json",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],