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.
Files changed (3) hide show
  1. package/README.md +80 -0
  2. package/index.js +265 -0
  3. 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
+ }