create-harper 0.0.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 +80 -0
- package/index.js +265 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# create-harper <a href="https://npmjs.com/package/create-harper"><img src="https://img.shields.io/npm/v/create-harper" alt="npm package"></a>
|
|
2
|
+
|
|
3
|
+
## Scaffolding Your First Harper Project
|
|
4
|
+
|
|
5
|
+
> **Compatibility Note:**
|
|
6
|
+
> Harper requires [Node.js](https://nodejs.org/en/) version 20.19+, 22.12+. However, some templates require a higher Node.js version to work, please upgrade if your package manager warns about it.
|
|
7
|
+
|
|
8
|
+
With NPM:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm create harper@latest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
With Yarn:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
yarn create harper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
With PNPM:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm create harper
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
With Bun:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun create harper
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With Deno:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
deno init --npm harper
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then follow the prompts!
|
|
39
|
+
|
|
40
|
+
You can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a Harper + Vue project, run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# npm 7+, extra double-dash is needed:
|
|
44
|
+
npm create harper@latest my-vue-app -- --template vue
|
|
45
|
+
|
|
46
|
+
# yarn
|
|
47
|
+
yarn create harper my-vue-app --template vue
|
|
48
|
+
|
|
49
|
+
# pnpm
|
|
50
|
+
pnpm create harper my-vue-app --template vue
|
|
51
|
+
|
|
52
|
+
# Bun
|
|
53
|
+
bun create harper my-vue-app --template vue
|
|
54
|
+
|
|
55
|
+
# Deno
|
|
56
|
+
deno init --npm harper my-vue-app --template vue
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Currently supported template presets include:
|
|
60
|
+
|
|
61
|
+
- `vanilla`
|
|
62
|
+
- `vanilla-ts`
|
|
63
|
+
- `vue`
|
|
64
|
+
- `vue-ts`
|
|
65
|
+
- `react`
|
|
66
|
+
- `react-ts`
|
|
67
|
+
- `react-swc`
|
|
68
|
+
- `react-swc-ts`
|
|
69
|
+
- `preact`
|
|
70
|
+
- `preact-ts`
|
|
71
|
+
- `lit`
|
|
72
|
+
- `lit-ts`
|
|
73
|
+
- `svelte`
|
|
74
|
+
- `svelte-ts`
|
|
75
|
+
- `solid`
|
|
76
|
+
- `solid-ts`
|
|
77
|
+
- `qwik`
|
|
78
|
+
- `qwik-ts`
|
|
79
|
+
|
|
80
|
+
You can use `.` for the project name to scaffold in the current directory.
|
package/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as prompts from '@clack/prompts';
|
|
3
|
+
import { determineAgent } from '@vercel/detect-agent';
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
|
+
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
|
+
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
|
+
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
|
+
import { pkgFromUserAgent } from './lib/pkg/pkgFromUserAgent.js';
|
|
23
|
+
import { toValidPackageName } from './lib/pkg/toValidPackageName.js';
|
|
24
|
+
import { start } from './lib/start.js';
|
|
25
|
+
|
|
26
|
+
const argv = mri(process.argv.slice(2), {
|
|
27
|
+
boolean: ['help', 'overwrite', 'immediate', 'interactive'],
|
|
28
|
+
alias: { h: 'help', t: 'template', i: 'immediate' },
|
|
29
|
+
string: ['template'],
|
|
30
|
+
});
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
|
|
33
|
+
init().catch((e) => {
|
|
34
|
+
console.error(e);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function init() {
|
|
38
|
+
const argTargetDir = argv._[0]
|
|
39
|
+
? formatTargetDir(String(argv._[0]))
|
|
40
|
+
: undefined;
|
|
41
|
+
const argTemplate = argv.template;
|
|
42
|
+
const argOverwrite = argv.overwrite;
|
|
43
|
+
const argImmediate = argv.immediate;
|
|
44
|
+
const argInteractive = argv.interactive;
|
|
45
|
+
|
|
46
|
+
const help = argv.help;
|
|
47
|
+
if (help) {
|
|
48
|
+
console.log(helpMessage);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const interactive = argInteractive ?? process.stdin.isTTY;
|
|
53
|
+
|
|
54
|
+
// Detect AI agent environment for better agent experience (AX)
|
|
55
|
+
const { isAgent } = await determineAgent();
|
|
56
|
+
if (isAgent && interactive) {
|
|
57
|
+
console.log(
|
|
58
|
+
'\nTo create in one go, run: create-harper <DIRECTORY> --no-interactive --template <TEMPLATE>\n',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
|
|
63
|
+
const cancel = () => prompts.cancel('Operation cancelled');
|
|
64
|
+
|
|
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
|
+
}
|
|
86
|
+
|
|
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
|
+
}
|
|
120
|
+
|
|
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
|
+
}
|
|
151
|
+
|
|
152
|
+
// 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
|
+
}
|
|
203
|
+
|
|
204
|
+
const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
|
|
205
|
+
|
|
206
|
+
const root = path.join(cwd, targetDir);
|
|
207
|
+
|
|
208
|
+
const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {};
|
|
209
|
+
|
|
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
|
+
}
|
|
265
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-harper",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "HarperDB",
|
|
7
|
+
"email": "support@harperdb.io"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-harper": "index.js",
|
|
11
|
+
"cha": "index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"template-*"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"lint": "oxlint .",
|
|
19
|
+
"lint:fix": "oxlint . --fix",
|
|
20
|
+
"format": "dprint check",
|
|
21
|
+
"format:fix": "dprint fmt",
|
|
22
|
+
"format:staged": "dprint check --staged",
|
|
23
|
+
"test": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/harperfast/create-harper.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/harperfast/create-harper/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/harperfast",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^1.0.0-alpha.9",
|
|
38
|
+
"@vercel/detect-agent": "^1.0.0",
|
|
39
|
+
"cross-spawn": "^7.0.6",
|
|
40
|
+
"lodash": "^4.17.21",
|
|
41
|
+
"mri": "^1.2.0",
|
|
42
|
+
"picocolors": "^1.1.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"dprint": "^0.51.1",
|
|
46
|
+
"harperdb": "^4.7.15",
|
|
47
|
+
"oxlint": "^1.38.0",
|
|
48
|
+
"vitest": "^4.0.17"
|
|
49
|
+
}
|
|
50
|
+
}
|