create-harper 0.0.2 → 0.0.3

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.
Files changed (71) hide show
  1. package/README.md +5 -0
  2. package/index.js +37 -214
  3. package/lib/constants/renameFiles.js +1 -0
  4. package/lib/fs/applyAndWriteTemplateFile.js +5 -5
  5. package/lib/fs/crawlTemplateDir.js +3 -3
  6. package/lib/pkg/toValidPackageName.js +2 -1
  7. package/lib/steps/getEnvVars.js +79 -0
  8. package/lib/steps/getImmediate.js +29 -0
  9. package/lib/steps/getPackageName.js +38 -0
  10. package/lib/steps/getProjectName.js +40 -0
  11. package/lib/steps/getTemplate.js +65 -0
  12. package/lib/steps/handleExistingDir.js +61 -0
  13. package/lib/steps/scaffoldProject.js +40 -0
  14. package/lib/steps/showOutro.js +30 -0
  15. package/package.json +12 -8
  16. package/template-barebones/README.md +1 -1
  17. package/template-barebones/_env +3 -0
  18. package/template-barebones/_env.example +3 -0
  19. package/template-barebones/package.json +1 -1
  20. package/template-react/README.md +1 -1
  21. package/template-react/_aiignore +1 -0
  22. package/template-react/_env +3 -0
  23. package/template-react/_env.example +3 -0
  24. package/template-react/_github/workflow/deploy.yaml +1 -1
  25. package/template-react/_gitignore +147 -0
  26. package/template-react/graphql.config.yml +3 -0
  27. package/template-react/index.html +1 -1
  28. package/template-react/package.json +5 -4
  29. package/template-react-ts/README.md +1 -1
  30. package/template-react-ts/_aiignore +1 -0
  31. package/template-react-ts/_env +3 -0
  32. package/template-react-ts/_env.example +3 -0
  33. package/template-react-ts/_github/workflow/deploy.yaml +1 -1
  34. package/template-react-ts/_gitignore +147 -0
  35. package/template-react-ts/graphql.config.yml +3 -0
  36. package/template-react-ts/index.html +1 -1
  37. package/template-react-ts/package.json +5 -4
  38. package/template-studio/README.md +1 -1
  39. package/template-studio/_aiignore +1 -0
  40. package/template-studio/_gitignore +147 -0
  41. package/template-studio/graphql.config.yml +3 -0
  42. package/template-studio/package.json +1 -1
  43. package/template-studio/web/index.html +2 -2
  44. package/template-studio-ts/README.md +1 -1
  45. package/template-studio-ts/_aiignore +1 -0
  46. package/template-studio-ts/_gitignore +147 -0
  47. package/template-studio-ts/graphql.config.yml +3 -0
  48. package/template-studio-ts/package.json +1 -1
  49. package/template-studio-ts/web/index.html +2 -2
  50. package/template-vanilla/README.md +1 -1
  51. package/template-vanilla/_aiignore +1 -0
  52. package/template-vanilla/_env +3 -0
  53. package/template-vanilla/_env.example +3 -0
  54. package/template-vanilla/_gitignore +147 -0
  55. package/template-vanilla/graphql.config.yml +3 -0
  56. package/template-vanilla/package.json +1 -1
  57. package/template-vanilla/web/index.html +2 -2
  58. package/template-vanilla-ts/README.md +1 -1
  59. package/template-vanilla-ts/_aiignore +1 -0
  60. package/template-vanilla-ts/_env +3 -0
  61. package/template-vanilla-ts/_env.example +3 -0
  62. package/template-vanilla-ts/_gitignore +147 -0
  63. package/template-vanilla-ts/graphql.config.yml +3 -0
  64. package/template-vanilla-ts/package.json +1 -1
  65. package/template-vanilla-ts/web/index.html +2 -2
  66. package/lib/fs/editFile.js +0 -6
  67. package/lib/pkg/getFullCustomCommand.js +0 -47
  68. package/template-shared/_env.example +0 -3
  69. /package/{template-shared → template-barebones}/_aiignore +0 -0
  70. /package/{template-shared → template-barebones}/_gitignore +0 -0
  71. /package/{template-shared → template-barebones}/graphql.config.yml +0 -0
package/README.md CHANGED
@@ -78,3 +78,8 @@ Currently supported template presets include:
78
78
  - `qwik-ts`
79
79
 
80
80
  You can use `.` for the project name to scaffold in the current directory.
81
+
82
+ ## Shout Out
83
+
84
+ This project is based largely on the prior work of the Vite team on Create Vite:
85
+ https://github.com/vitejs/vite/tree/main/packages/create-vite
package/index.js CHANGED
@@ -1,43 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import * as prompts from '@clack/prompts';
3
3
  import { determineAgent } from '@vercel/detect-agent';
4
- import spawn from 'cross-spawn';
5
4
  import mri from 'mri';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { defaultTargetDir } from './lib/constants/defaultTargetDir.js';
10
- import { FRAMEWORKS } from './lib/constants/frameworks.js';
11
5
  import { helpMessage } from './lib/constants/helpMessage.js';
12
- import { TEMPLATES } from './lib/constants/templates.js';
13
- import { crawlTemplateDir } from './lib/fs/crawlTemplateDir.js';
14
- import { emptyDir } from './lib/fs/emptyDir.js';
15
6
  import { formatTargetDir } from './lib/fs/formatTargetDir.js';
16
- import { isEmpty } from './lib/fs/isEmpty.js';
17
- import { install } from './lib/install.js';
18
- import { getFullCustomCommand } from './lib/pkg/getFullCustomCommand.js';
19
- import { getInstallCommand } from './lib/pkg/getInstallCommand.js';
20
- import { getRunCommand } from './lib/pkg/getRunCommand.js';
21
- import { isValidPackageName } from './lib/pkg/isValidPackageName.js';
22
7
  import { pkgFromUserAgent } from './lib/pkg/pkgFromUserAgent.js';
23
- import { toValidPackageName } from './lib/pkg/toValidPackageName.js';
24
- import { start } from './lib/start.js';
8
+ import { getEnvVars } from './lib/steps/getEnvVars.js';
9
+ import { getImmediate } from './lib/steps/getImmediate.js';
10
+ import { getPackageName } from './lib/steps/getPackageName.js';
11
+ import { getProjectName } from './lib/steps/getProjectName.js';
12
+ import { getTemplate } from './lib/steps/getTemplate.js';
13
+ import { handleExistingDir } from './lib/steps/handleExistingDir.js';
14
+ import { scaffoldProject } from './lib/steps/scaffoldProject.js';
15
+ import { showOutro } from './lib/steps/showOutro.js';
25
16
 
26
17
  const argv = mri(process.argv.slice(2), {
27
18
  boolean: ['help', 'overwrite', 'immediate', 'interactive'],
28
19
  alias: { h: 'help', t: 'template', i: 'immediate' },
29
- string: ['template'],
20
+ string: ['template', 'cli-target-username', 'cli-target'],
30
21
  });
31
- const cwd = process.cwd();
32
22
 
33
23
  init().catch((e) => {
34
24
  console.error(e);
35
25
  });
36
26
 
37
27
  async function init() {
38
- const argTargetDir = argv._[0]
39
- ? formatTargetDir(String(argv._[0]))
40
- : undefined;
28
+ const argTargetDir = argv._[0] ? formatTargetDir(String(argv._[0])) : undefined;
41
29
  const argTemplate = argv.template;
42
30
  const argOverwrite = argv.overwrite;
43
31
  const argImmediate = argv.immediate;
@@ -59,207 +47,42 @@ async function init() {
59
47
  );
60
48
  }
61
49
 
62
- const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
63
50
  const cancel = () => prompts.cancel('Operation cancelled');
64
51
 
65
- // 1. Get project name and target dir
66
- let targetDir = argTargetDir;
67
- let projectName = targetDir;
68
- if (!targetDir) {
69
- if (interactive) {
70
- projectName = await prompts.text({
71
- message: 'Project name:',
72
- defaultValue: defaultTargetDir,
73
- placeholder: defaultTargetDir,
74
- validate: (value) => {
75
- return !value || formatTargetDir(value).length > 0
76
- ? undefined
77
- : 'Invalid project name';
78
- },
79
- });
80
- if (prompts.isCancel(projectName)) { return cancel(); }
81
- targetDir = formatTargetDir(projectName);
82
- } else {
83
- targetDir = defaultTargetDir;
84
- }
85
- }
52
+ // 1. Get the project name and target directory
53
+ const projectNameResult = await getProjectName(argTargetDir, interactive);
54
+ if (projectNameResult.cancelled) { return cancel(); }
55
+ const { projectName, targetDir } = projectNameResult;
86
56
 
87
- // 2. Handle directory if exist and not empty
88
- if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
89
- let overwrite = argOverwrite
90
- ? 'yes'
91
- : undefined;
92
- if (!overwrite) {
93
- if (interactive) {
94
- const res = await prompts.select({
95
- message: (targetDir === '.'
96
- ? 'Current directory'
97
- : `Target directory "${targetDir}"`)
98
- + ` is not empty. Please choose how to proceed:`,
99
- options: [
100
- {
101
- label: 'Cancel operation',
102
- value: 'no',
103
- },
104
- {
105
- label: 'Remove existing files and continue',
106
- value: 'yes',
107
- },
108
- {
109
- label: 'Ignore files and continue',
110
- value: 'ignore',
111
- },
112
- ],
113
- });
114
- if (prompts.isCancel(res)) { return cancel(); }
115
- overwrite = res;
116
- } else {
117
- overwrite = 'no';
118
- }
119
- }
57
+ // 2. Handle if the directory exists and isn't empty
58
+ const handleExistingDirResult = await handleExistingDir(targetDir, argOverwrite, interactive);
59
+ if (handleExistingDirResult.cancelled) { return cancel(); }
120
60
 
121
- switch (overwrite) {
122
- case 'yes':
123
- emptyDir(targetDir);
124
- break;
125
- case 'no':
126
- cancel();
127
- return;
128
- }
129
- }
130
-
131
- // 3. Get package name
132
- let packageName = path.basename(path.resolve(targetDir));
133
- if (!isValidPackageName(packageName)) {
134
- if (interactive) {
135
- const packageNameResult = await prompts.text({
136
- message: 'Package name:',
137
- defaultValue: toValidPackageName(packageName),
138
- placeholder: toValidPackageName(packageName),
139
- validate(dir) {
140
- if (dir && !isValidPackageName(dir)) {
141
- return 'Invalid package.json name';
142
- }
143
- },
144
- });
145
- if (prompts.isCancel(packageNameResult)) { return cancel(); }
146
- packageName = packageNameResult;
147
- } else {
148
- packageName = toValidPackageName(packageName);
149
- }
150
- }
61
+ // 3. Get the package name
62
+ const packageNameResult = await getPackageName(targetDir, interactive);
63
+ if (packageNameResult.cancelled) { return cancel(); }
64
+ const { packageName } = packageNameResult;
151
65
 
152
66
  // 4. Choose a framework and variant
153
- let template = argTemplate;
154
- let hasInvalidArgTemplate = false;
155
- if (argTemplate && !TEMPLATES.includes(argTemplate)) {
156
- template = undefined;
157
- hasInvalidArgTemplate = true;
158
- }
159
- if (!template) {
160
- if (interactive) {
161
- const framework = await prompts.select({
162
- message: hasInvalidArgTemplate
163
- ? `"${argTemplate}" isn't a valid template. Please choose from below: `
164
- : 'Select a framework:',
165
- options: FRAMEWORKS
166
- .filter(framework => !framework.hidden)
167
- .map((framework) => {
168
- const frameworkColor = framework.color;
169
- return {
170
- label: frameworkColor(framework.display || framework.name),
171
- value: framework,
172
- };
173
- }),
174
- });
175
- if (prompts.isCancel(framework)) { return cancel(); }
176
-
177
- const variant = framework.variants.length === 1
178
- ? framework.variants[0].name
179
- : await prompts.select({
180
- message: 'Select a variant:',
181
- options: framework.variants.map((variant) => {
182
- const variantColor = variant.color;
183
- const command = variant.customCommand
184
- ? getFullCustomCommand(variant.customCommand, pkgInfo).replace(
185
- / TARGET_DIR$/,
186
- '',
187
- )
188
- : undefined;
189
- return {
190
- label: variantColor(variant.display || variant.name),
191
- value: variant.name,
192
- hint: command,
193
- };
194
- }),
195
- });
196
- if (prompts.isCancel(variant)) { return cancel(); }
197
-
198
- template = variant;
199
- } else {
200
- template = 'vanilla-ts';
201
- }
202
- }
67
+ const templateResult = await getTemplate(argTemplate, interactive);
68
+ if (templateResult.cancelled) { return cancel(); }
69
+ const { template } = templateResult;
203
70
 
71
+ // 5. Should we do a package manager installation?
72
+ const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
204
73
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
74
+ const immediateResult = await getImmediate(argImmediate, interactive, pkgManager);
75
+ if (immediateResult.cancelled) { return cancel(); }
76
+ const { immediate } = immediateResult;
205
77
 
206
- const root = path.join(cwd, targetDir);
78
+ // 6. Get environment variables for .env file
79
+ const envVarsResult = await getEnvVars(argv, interactive, template);
80
+ if (envVarsResult.cancelled) { return cancel(); }
81
+ const { envVars } = envVarsResult;
207
82
 
208
- const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {};
83
+ // 7. Write out the contents based on all prior steps.
84
+ const root = scaffoldProject(targetDir, projectName, packageName, template, envVars);
209
85
 
210
- if (customCommand) {
211
- const fullCustomCommand = getFullCustomCommand(customCommand, pkgInfo);
212
-
213
- const [command, ...args] = fullCustomCommand.split(' ');
214
- // we replace TARGET_DIR here because targetDir may include a space
215
- const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', () => targetDir));
216
- const { status } = spawn.sync(command, replacedArgs, {
217
- stdio: 'inherit',
218
- });
219
- process.exit(status ?? 0);
220
- }
221
-
222
- // 5. Ask about immediate install and package manager
223
- let immediate = argImmediate;
224
- if (immediate === undefined) {
225
- if (interactive) {
226
- const immediateResult = await prompts.confirm({
227
- message: `Install with ${pkgManager} and start now?`,
228
- });
229
- if (prompts.isCancel(immediateResult)) { return cancel(); }
230
- immediate = immediateResult;
231
- } else {
232
- immediate = false;
233
- }
234
- }
235
-
236
- // Only create a directory for built-in templates, not for customCommand
237
- fs.mkdirSync(root, { recursive: true });
238
- prompts.log.step(`Scaffolding project in ${root}...`);
239
-
240
- const context = {
241
- projectName,
242
- packageName,
243
- };
244
-
245
- const templateSharedDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-shared`);
246
- crawlTemplateDir(root, templateSharedDir, context);
247
-
248
- const templateDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-${template}`);
249
- crawlTemplateDir(root, templateDir, context);
250
-
251
- if (immediate) {
252
- install(root, pkgManager);
253
- start(root, pkgManager);
254
- } else {
255
- let doneMessage = '';
256
- const cdProjectName = path.relative(cwd, root);
257
- doneMessage += `Done. Now run:\n`;
258
- if (root !== cwd) {
259
- doneMessage += `\n cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`;
260
- }
261
- doneMessage += `\n ${getInstallCommand(pkgManager).join(' ')}`;
262
- doneMessage += `\n ${getRunCommand(pkgManager, 'dev').join(' ')}`;
263
- prompts.outro(doneMessage);
264
- }
86
+ // 8. Log out the next steps.
87
+ showOutro(root, pkgManager, immediate);
265
88
  }
@@ -3,4 +3,5 @@ export const renameFiles = {
3
3
  _gitignore: '.gitignore',
4
4
  _github: '.github',
5
5
  '_env.example': '.env.example',
6
+ '_env': '.env',
6
7
  };
@@ -1,9 +1,9 @@
1
- import template from 'lodash/template.js';
2
1
  import fs from 'node:fs';
3
2
 
4
- export function applyAndWriteTemplateFile(targetPath, templatePath, context) {
5
- const templateContent = fs.readFileSync(templatePath, 'utf-8');
6
- const templateFn = template(templateContent, { interpolate: /<<([\s\S]+?)>>/g });
7
- const updatedContent = templateFn(context);
3
+ export function applyAndWriteTemplateFile(targetPath, templatePath, substitutions) {
4
+ let updatedContent = fs.readFileSync(templatePath, 'utf-8');
5
+ for (const variableName in substitutions) {
6
+ updatedContent = updatedContent.replaceAll(variableName, substitutions[variableName]);
7
+ }
8
8
  fs.writeFileSync(targetPath, updatedContent);
9
9
  }
@@ -3,16 +3,16 @@ import path from 'node:path';
3
3
  import { renameFiles } from '../constants/renameFiles.js';
4
4
  import { applyAndWriteTemplateFile } from './applyAndWriteTemplateFile.js';
5
5
 
6
- export function crawlTemplateDir(root, dir, context) {
6
+ export function crawlTemplateDir(root, dir, substitutions) {
7
7
  const files = fs.readdirSync(dir);
8
8
  for (const file of files) {
9
9
  const targetPath = path.join(root, renameFiles[file] ?? file);
10
10
  const templatePath = path.join(dir, file);
11
11
  if (fs.lstatSync(templatePath).isDirectory()) {
12
12
  fs.mkdirSync(targetPath, { recursive: true });
13
- crawlTemplateDir(targetPath, templatePath, context);
13
+ crawlTemplateDir(targetPath, templatePath, substitutions);
14
14
  } else {
15
- applyAndWriteTemplateFile(targetPath, templatePath, context);
15
+ applyAndWriteTemplateFile(targetPath, templatePath, substitutions);
16
16
  }
17
17
  }
18
18
  }
@@ -4,5 +4,6 @@ export function toValidPackageName(projectName) {
4
4
  .toLowerCase()
5
5
  .replace(/\s+/g, '-')
6
6
  .replace(/^[._]/, '')
7
- .replace(/[^a-z\d\-~]+/g, '-');
7
+ .replace(/[^a-z\d\-~]+/g, '-')
8
+ .replace(/-+$/, '');
8
9
  }
@@ -0,0 +1,79 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * Step 5: Get environment variables for .env file
8
+ * @param {any} argv
9
+ * @param {boolean} interactive
10
+ * @param {string} template
11
+ * @returns {Promise<{envVars: {username: string, target: string, password?: string}, cancelled: boolean}>}
12
+ */
13
+ export async function getEnvVars(argv, interactive, template) {
14
+ const templateDir = path.resolve(
15
+ fileURLToPath(import.meta.url),
16
+ '..',
17
+ '..',
18
+ '..',
19
+ `template-${template}`,
20
+ );
21
+ const hasEnvFile = fs.existsSync(path.join(templateDir, '_env'));
22
+
23
+ if (!hasEnvFile) {
24
+ return {
25
+ envVars: {},
26
+ cancelled: false,
27
+ };
28
+ }
29
+
30
+ let username = argv['cli-target-username'];
31
+ let target = argv['cli-target'];
32
+ let password = '';
33
+
34
+ if (interactive) {
35
+ if (!username) {
36
+ const usernameResult = await prompts.text({
37
+ message: 'CLI Target Username:',
38
+ placeholder: 'YOUR_CLUSTER_USERNAME',
39
+ });
40
+
41
+ if (prompts.isCancel(usernameResult)) {
42
+ return { envVars: {}, cancelled: true };
43
+ }
44
+ username = usernameResult;
45
+ }
46
+
47
+ if (!target) {
48
+ const targetResult = await prompts.text({
49
+ message: 'CLI Target URL:',
50
+ placeholder: 'YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE',
51
+ });
52
+
53
+ if (prompts.isCancel(targetResult)) {
54
+ return { envVars: {}, cancelled: true };
55
+ }
56
+ target = targetResult;
57
+ }
58
+
59
+ const passwordResult = await prompts.password({
60
+ message: 'CLI Target Password:',
61
+ });
62
+
63
+ if (prompts.isCancel(passwordResult)) {
64
+ return { envVars: {}, cancelled: true };
65
+ }
66
+ password = passwordResult;
67
+ } else {
68
+ prompts.log.warn('Non-interactive mode: Please update your .env to add your CLI_TARGET_PASSWORD on your own.');
69
+ }
70
+
71
+ return {
72
+ envVars: {
73
+ username: username || 'YOUR_CLUSTER_USERNAME',
74
+ target: target || 'YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE',
75
+ password: password || 'YOUR_CLUSTER_PASSWORD',
76
+ },
77
+ cancelled: false,
78
+ };
79
+ }
@@ -0,0 +1,29 @@
1
+ import * as prompts from '@clack/prompts';
2
+
3
+ /**
4
+ * Step 5: Ask about immediate install and package manager
5
+ * @param {boolean | undefined} argImmediate
6
+ * @param {boolean} interactive
7
+ * @param {string} pkgManager
8
+ * @returns {Promise<{immediate: boolean, cancelled: boolean}>}
9
+ */
10
+ export async function getImmediate(argImmediate, interactive, pkgManager) {
11
+ let immediate = argImmediate;
12
+
13
+ if (immediate === undefined) {
14
+ if (interactive) {
15
+ const immediateResult = await prompts.confirm({
16
+ message: `Install with ${pkgManager} and start now?`,
17
+ });
18
+
19
+ if (prompts.isCancel(immediateResult)) {
20
+ return { immediate: false, cancelled: true };
21
+ }
22
+ immediate = immediateResult;
23
+ } else {
24
+ immediate = false;
25
+ }
26
+ }
27
+
28
+ return { immediate, cancelled: false };
29
+ }
@@ -0,0 +1,38 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import path from 'node:path';
3
+ import { isValidPackageName } from '../pkg/isValidPackageName.js';
4
+ import { toValidPackageName } from '../pkg/toValidPackageName.js';
5
+
6
+ /**
7
+ * Step 3: Get package name
8
+ * @param {string} targetDir
9
+ * @param {boolean} interactive
10
+ * @returns {Promise<{packageName: string, cancelled: boolean}>}
11
+ */
12
+ export async function getPackageName(targetDir, interactive) {
13
+ let packageName = path.basename(path.resolve(targetDir));
14
+
15
+ if (!isValidPackageName(packageName)) {
16
+ if (interactive) {
17
+ const packageNameResult = await prompts.text({
18
+ message: 'Package name:',
19
+ defaultValue: toValidPackageName(packageName),
20
+ placeholder: toValidPackageName(packageName),
21
+ validate(dir) {
22
+ if (dir && !isValidPackageName(dir)) {
23
+ return 'Invalid package.json name';
24
+ }
25
+ },
26
+ });
27
+
28
+ if (prompts.isCancel(packageNameResult)) {
29
+ return { packageName: '', cancelled: true };
30
+ }
31
+ packageName = packageNameResult;
32
+ } else {
33
+ packageName = toValidPackageName(packageName);
34
+ }
35
+ }
36
+
37
+ return { packageName, cancelled: false };
38
+ }
@@ -0,0 +1,40 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import { defaultTargetDir } from '../constants/defaultTargetDir.js';
3
+ import { formatTargetDir } from '../fs/formatTargetDir.js';
4
+
5
+ /**
6
+ * Step 1: Get project name and target directory
7
+ * @param {string | undefined} argTargetDir
8
+ * @param {boolean} interactive
9
+ * @returns {Promise<{projectName: string, targetDir: string, cancelled: boolean}>}
10
+ */
11
+ export async function getProjectName(argTargetDir, interactive) {
12
+ let targetDir = argTargetDir;
13
+ let projectName = targetDir;
14
+
15
+ if (!targetDir) {
16
+ if (interactive) {
17
+ projectName = await prompts.text({
18
+ message: 'Project name:',
19
+ defaultValue: defaultTargetDir,
20
+ placeholder: defaultTargetDir,
21
+ validate: (value) => {
22
+ return !value || formatTargetDir(value).length > 0
23
+ ? undefined
24
+ : 'Invalid project name';
25
+ },
26
+ });
27
+
28
+ if (prompts.isCancel(projectName)) {
29
+ return { projectName: '', targetDir: '', cancelled: true };
30
+ }
31
+
32
+ targetDir = formatTargetDir(projectName);
33
+ } else {
34
+ targetDir = defaultTargetDir;
35
+ projectName = targetDir;
36
+ }
37
+ }
38
+
39
+ return { projectName, targetDir, cancelled: false };
40
+ }
@@ -0,0 +1,65 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import { FRAMEWORKS } from '../constants/frameworks.js';
3
+ import { TEMPLATES } from '../constants/templates.js';
4
+
5
+ /**
6
+ * Step 4: Choose a framework and variant
7
+ * @param {string | undefined} argTemplate
8
+ * @param {boolean} interactive
9
+ * @returns {Promise<{template: string, cancelled: boolean}>}
10
+ */
11
+ export async function getTemplate(argTemplate, interactive) {
12
+ let template = argTemplate;
13
+ let hasInvalidArgTemplate = false;
14
+
15
+ if (argTemplate && !TEMPLATES.includes(argTemplate)) {
16
+ template = undefined;
17
+ hasInvalidArgTemplate = true;
18
+ }
19
+
20
+ if (!template) {
21
+ if (interactive) {
22
+ const framework = await prompts.select({
23
+ message: hasInvalidArgTemplate
24
+ ? `"${argTemplate}" isn't a valid template. Please choose from below: `
25
+ : 'Select a framework:',
26
+ options: FRAMEWORKS
27
+ .filter(framework => !framework.hidden)
28
+ .map((framework) => {
29
+ const frameworkColor = framework.color;
30
+ return {
31
+ label: frameworkColor(framework.display),
32
+ value: framework,
33
+ };
34
+ }),
35
+ });
36
+
37
+ if (prompts.isCancel(framework)) {
38
+ return { template: '', cancelled: true };
39
+ }
40
+
41
+ const variant = framework.variants.length === 1
42
+ ? framework.variants[0].name
43
+ : await prompts.select({
44
+ message: 'Select a variant:',
45
+ options: framework.variants.map((variant) => {
46
+ const variantColor = variant.color;
47
+ return {
48
+ label: variantColor(variant.display || variant.name),
49
+ value: variant.name,
50
+ };
51
+ }),
52
+ });
53
+
54
+ if (prompts.isCancel(variant)) {
55
+ return { template: '', cancelled: true };
56
+ }
57
+
58
+ template = variant;
59
+ } else {
60
+ template = 'vanilla-ts';
61
+ }
62
+ }
63
+
64
+ return { template, cancelled: false };
65
+ }
@@ -0,0 +1,61 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import fs from 'node:fs';
3
+ import { emptyDir } from '../fs/emptyDir.js';
4
+ import { isEmpty } from '../fs/isEmpty.js';
5
+
6
+ /**
7
+ * Step 2: Handle directory if exist and not empty
8
+ * @param {string} targetDir
9
+ * @param {boolean | undefined} argOverwrite
10
+ * @param {boolean} interactive
11
+ * @returns {Promise<{cancelled: boolean}>}
12
+ */
13
+ export async function handleExistingDir(targetDir, argOverwrite, interactive) {
14
+ if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
15
+ let overwrite = argOverwrite
16
+ ? 'yes'
17
+ : undefined;
18
+
19
+ if (!overwrite) {
20
+ if (interactive) {
21
+ const res = await prompts.select({
22
+ message: (targetDir === '.'
23
+ ? 'Current directory'
24
+ : `Target directory "${targetDir}"`)
25
+ + ` is not empty. Please choose how to proceed:`,
26
+ options: [
27
+ {
28
+ label: 'Cancel operation',
29
+ value: 'no',
30
+ },
31
+ {
32
+ label: 'Remove existing files and continue',
33
+ value: 'yes',
34
+ },
35
+ {
36
+ label: 'Ignore files and continue',
37
+ value: 'ignore',
38
+ },
39
+ ],
40
+ });
41
+
42
+ if (prompts.isCancel(res)) {
43
+ return { cancelled: true };
44
+ }
45
+ overwrite = res;
46
+ } else {
47
+ overwrite = 'no';
48
+ }
49
+ }
50
+
51
+ switch (overwrite) {
52
+ case 'yes':
53
+ emptyDir(targetDir);
54
+ break;
55
+ case 'no':
56
+ return { cancelled: true };
57
+ }
58
+ }
59
+
60
+ return { cancelled: false };
61
+ }