@citruslime/create-boilerplate 3.0.0-beta.2 → 3.0.0-beta.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.
- package/README.md +7 -7
- package/main.js +353 -314
- package/package.json +3 -4
- package/template/README.md +4 -4
- package/template/_editorconfig +5 -0
- package/template/_gitattributes +5 -2
- package/template/_gitignore +6 -2
- package/template/_husky/post-checkout +4 -0
- package/template/_husky/post-merge +4 -0
- package/template/_husky/pre-commit +4 -0
- package/template/apps/app/_lintstagedrc.js +5 -0
- package/template/apps/app/_stylelint.config.js +4 -0
- package/template/apps/app/eslint.config.js +39 -0
- package/template/apps/app/index.html +17 -0
- package/template/apps/app/package.json +23 -0
- package/template/{src → apps/app/src}/app.vue +1 -7
- package/template/apps/app/src/components/.gitkeep +0 -0
- package/template/apps/app/src/main.css +6 -0
- package/template/apps/app/src/main.ts +17 -0
- package/template/{src/pages/dashboard/index.vue → apps/app/src/pages/dashboard.vue} +4 -6
- package/template/{src/router/index.ts → apps/app/src/router.ts} +12 -7
- package/template/apps/app/src/state/auth.ts +11 -0
- package/template/apps/app/src/utils/.gitkeep +0 -0
- package/template/{tsconfig.app.json → apps/app/tsconfig.app.json} +7 -9
- package/template/{tsconfig.json → apps/app/tsconfig.json} +1 -4
- package/template/apps/app/tsconfig.node.json +13 -0
- package/template/{vite.config.ts → apps/app/vite.config.ts} +57 -27
- package/template/package.json +11 -10
- package/template/packages/config-eslint/_lintstagedrc.js +4 -0
- package/template/packages/config-eslint/eslint.config.js +1 -0
- package/template/packages/config-eslint/package.json +10 -0
- package/template/packages/config-stylelint/_lintstagedrc.js +4 -0
- package/template/packages/config-stylelint/eslint.config.js +1 -0
- package/template/packages/config-stylelint/package.json +13 -0
- package/template/packages/config-typescript/node.json +17 -0
- package/template/packages/config-typescript/package.json +9 -0
- package/template/packages/config-typescript/vue.json +18 -0
- package/template/packages/utils/_lintstagedrc.js +4 -0
- package/template/packages/utils/eslint.config.js +1 -0
- package/template/packages/utils/exports-list.json +1 -0
- package/template/packages/utils/exports-list.ts +95 -0
- package/template/packages/utils/package.json +43 -0
- package/template/packages/utils/plugin.ts +55 -0
- package/template/packages/utils/resolver.ts +38 -0
- package/template/packages/utils/src/api/api.ts +6 -0
- package/template/packages/utils/src/api/endpoints.ts +18 -0
- package/template/packages/utils/src/api/errors.ts +19 -0
- package/template/packages/utils/src/api/models/app-info.ts +5 -0
- package/template/packages/utils/src/index.ts +2 -0
- package/template/packages/utils/tsconfig.build.json +19 -0
- package/template/packages/utils/tsconfig.json +11 -0
- package/template/packages/utils/tsconfig.lib.json +15 -0
- package/template/packages/utils/tsconfig.node.json +21 -0
- package/template/packages/utils/vite.config.ts +31 -0
- package/template/pnpm-workspace.yaml +11 -0
- package/template/{.vscode/template.code-workspace → template.code-workspace} +26 -7
- package/template/turbo.json +24 -0
- package/hooks/post-checkout +0 -8
- package/hooks/post-merge +0 -8
- package/hooks/pre-commit +0 -8
- package/template/.vscode/extensions.json +0 -14
- package/template/.vscode/settings.json +0 -40
- package/template/_lintstagedrc.js +0 -5
- package/template/_npmrc +0 -3
- package/template/eslint.config.js +0 -11
- package/template/index.html +0 -13
- package/template/postcss.config.js +0 -6
- package/template/src/main.ts +0 -15
- package/template/src/state/auth.ts +0 -13
- package/template/tailwind.config.ts +0 -9
- package/template/tsconfig.node.json +0 -17
- package/template/tsconfig.vitest.json +0 -13
- /package/template/{_stylelintignore → apps/app/_stylelintignore} +0 -0
- /package/template/{env.d.ts → apps/app/env.d.ts} +0 -0
- /package/template/{public → apps/app/public}/favicon.ico +0 -0
- /package/template/{_stylelint.config.js → packages/config-stylelint/index.js} +0 -0
package/main.js
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
4
|
-
import {
|
|
2
|
+
import { exec, execSync, spawn } from 'node:child_process';
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { userInfo } from 'node:os';
|
|
5
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath, URL } from 'node:url';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
bold,
|
|
13
|
+
cyan,
|
|
14
|
+
dim,
|
|
15
|
+
green,
|
|
16
|
+
lightBlue,
|
|
17
|
+
magenta,
|
|
18
|
+
red
|
|
19
|
+
} from 'kolorist';
|
|
8
20
|
import parseArgs from 'minimist';
|
|
9
21
|
import { default as prompts } from 'prompts';
|
|
10
22
|
|
|
11
|
-
const argv = parseArgs(process.argv.slice(2)
|
|
23
|
+
const argv = parseArgs(process.argv.slice(2), {
|
|
24
|
+
alias: { v: 'verbose' },
|
|
25
|
+
boolean: ['verbose']
|
|
26
|
+
});
|
|
12
27
|
const cwd = process.cwd();
|
|
13
28
|
const codeDir = fileURLToPath(new URL('./', import.meta.url));
|
|
29
|
+
const verbose = Boolean(argv.verbose);
|
|
14
30
|
|
|
15
31
|
const placeholdersToReplace = {
|
|
16
32
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
17
33
|
'[[PACKAGE_NAME]]': '',
|
|
18
|
-
'[[
|
|
19
|
-
'[[
|
|
20
|
-
'[[
|
|
21
|
-
'\'[[PROXY]]\'': '{}',
|
|
22
|
-
'\'[[FRONTEND_PORT]]\'': 0
|
|
34
|
+
'[[GIT_ROOT]]': '',
|
|
35
|
+
'[[DEFAULT_USERNAME]]': '',
|
|
36
|
+
'\'[[PROXY]]\'': ''
|
|
23
37
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
24
38
|
};
|
|
25
39
|
|
|
@@ -30,303 +44,248 @@ const filesToRename = {
|
|
|
30
44
|
'_lintstagedrc.js': '.lintstagedrc.js',
|
|
31
45
|
_stylelintignore: '.stylelintignore',
|
|
32
46
|
'_stylelint.config.js': 'stylelint.config.js',
|
|
33
|
-
|
|
34
|
-
_npmrc: '.npmrc'
|
|
47
|
+
_husky: '.husky'
|
|
35
48
|
};
|
|
36
49
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
name: 'pinia'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'pinia-plugin-persistedstate'
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: 'vue'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'vue-i18n'
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: 'vue-router'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: '@citruslime/config',
|
|
64
|
-
dev: true
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: '@citruslime/theme',
|
|
68
|
-
dev: true
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: '@tsconfig/node20',
|
|
72
|
-
dev: true
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: '@types/jsdom',
|
|
76
|
-
dev: true
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: '@types/luxon',
|
|
80
|
-
dev: true
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: '@types/node',
|
|
84
|
-
dev: true
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: '@vitejs/plugin-vue',
|
|
88
|
-
dev: true
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: '@vue/tsconfig',
|
|
92
|
-
dev: true
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: 'husky',
|
|
96
|
-
dev: true
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'lint-staged',
|
|
100
|
-
dev: true
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'postcss',
|
|
104
|
-
dev: true
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: 'npm-run-all',
|
|
108
|
-
dev: true
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: 'typescript',
|
|
112
|
-
dev: true
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: 'unplugin-auto-import',
|
|
116
|
-
dev: true
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: 'unplugin-vue-components',
|
|
120
|
-
dev: true
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
name: 'unplugin-vue-router',
|
|
124
|
-
dev: true
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'vite',
|
|
128
|
-
dev: true
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: 'vite-plugin-mkcert',
|
|
132
|
-
dev: true
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: 'vue-tsc',
|
|
136
|
-
dev: true
|
|
137
|
-
}
|
|
50
|
+
const spinnerFrames = [
|
|
51
|
+
'⠋',
|
|
52
|
+
'⠙',
|
|
53
|
+
'⠹',
|
|
54
|
+
'⠸',
|
|
55
|
+
'⠼',
|
|
56
|
+
'⠴',
|
|
57
|
+
'⠦',
|
|
58
|
+
'⠧',
|
|
59
|
+
'⠇',
|
|
60
|
+
'⠏'
|
|
138
61
|
];
|
|
139
62
|
|
|
140
63
|
/**
|
|
141
|
-
*
|
|
64
|
+
* Short banner (similar in spirit to create-vite / npm init style CLIs).
|
|
65
|
+
*/
|
|
66
|
+
function printBanner () {
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(` ${bold(cyan('Citrus-Lime Vue Template'))}`);
|
|
69
|
+
|
|
70
|
+
if (!verbose) {
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(dim('Tip: pass --verbose for further details.'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Runs task with a spinner (interactive TTY) or plain lines
|
|
80
|
+
* (CI, piped stdout, or --verbose so child process output does not garble the spinner).
|
|
81
|
+
*
|
|
82
|
+
* @param {string} label Step description.
|
|
83
|
+
* @param {() => void | Promise<void>} callback Work to run (sync or async).
|
|
84
|
+
* @throws {Error} An error if the work fails.
|
|
85
|
+
*/
|
|
86
|
+
async function runStep (label, callback) {
|
|
87
|
+
const plain = !process.stdout.isTTY || verbose;
|
|
88
|
+
|
|
89
|
+
if (plain) {
|
|
90
|
+
console.log(`${lightBlue('›')} ${label}`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await Promise.resolve(callback());
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.log(`${red('✖')} ${label}`);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`${green('✓')} ${label}`);
|
|
101
|
+
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let frame = 0;
|
|
106
|
+
const interval = setInterval(() => {
|
|
107
|
+
const f = spinnerFrames[frame++ % spinnerFrames.length];
|
|
108
|
+
|
|
109
|
+
process.stdout.write(`\r${dim('◆')} ${magenta(f)} ${label}\x1b[K`);
|
|
110
|
+
}, 50);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await Promise.resolve(callback());
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
process.stdout.write('\r\x1b[K');
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearInterval(interval);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.stdout.write(`\r${dim('◆')} ${green('✓')} ${label}\x1b[K\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Initialise the new project.
|
|
128
|
+
*
|
|
129
|
+
* @throws {Error} Operation cancelled error.
|
|
142
130
|
*/
|
|
143
131
|
async function init () {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.trim();
|
|
132
|
+
printBanner();
|
|
133
|
+
|
|
134
|
+
const packageDir = argv._[0];
|
|
148
135
|
|
|
149
136
|
const {
|
|
150
137
|
packageName,
|
|
151
|
-
|
|
152
|
-
backendPort,
|
|
153
|
-
packageManager
|
|
138
|
+
backendPort
|
|
154
139
|
} = await prompts([
|
|
155
140
|
{
|
|
156
141
|
type: 'text',
|
|
157
142
|
name: 'packageName',
|
|
158
143
|
message: 'Enter a name for the project:',
|
|
159
|
-
validate: value => /^
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
type: packageDir ? null : 'text',
|
|
163
|
-
name: 'packageDir',
|
|
164
|
-
message: 'Enter a directory for the project:',
|
|
165
|
-
initial: '.',
|
|
166
|
-
onState: state => packageDir = state.value
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
type: () => directoryValid(packageDir) ? null : 'confirm',
|
|
170
|
-
name: 'empty',
|
|
171
|
-
message: () => `${packageDir === '.' ? 'Current directory' : `Target directory "${packageDir}"`} is not empty. Remove existing files and continue?`
|
|
144
|
+
validate: value => /^[a-z0-9-~][a-z0-9-._~]*$/.test(value) || 'Invalid project name (lowercase, no scope)'
|
|
172
145
|
},
|
|
173
146
|
{
|
|
174
|
-
type
|
|
175
|
-
if (empty === false) {
|
|
176
|
-
cancel();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
},
|
|
181
|
-
name: 'emptyConfirmation'
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
type: 'confirm',
|
|
185
|
-
name: 'hasApi',
|
|
186
|
-
message: 'Setup backend proxy?',
|
|
187
|
-
initial: true
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
type: prev => prev ? 'text' : null,
|
|
147
|
+
type: 'number',
|
|
191
148
|
name: 'backendPort',
|
|
192
149
|
message: 'Enter the port that the backend server runs on:',
|
|
193
|
-
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
type: 'select',
|
|
197
|
-
name: 'packageManager',
|
|
198
|
-
message: 'Select a package manager:',
|
|
199
|
-
choices: [
|
|
200
|
-
{
|
|
201
|
-
title: 'pnpm',
|
|
202
|
-
value: 'pnpm'
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
title: 'yarn',
|
|
206
|
-
value: 'yarn'
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
title: 'npm',
|
|
210
|
-
value: 'npm'
|
|
211
|
-
}
|
|
212
|
-
],
|
|
213
|
-
initial: 0
|
|
150
|
+
validate: value => (Number.isInteger(value) && value >= 1024 && value <= 65535) || 'Enter a valid port (1024-65535)'
|
|
214
151
|
}
|
|
215
152
|
], {
|
|
216
|
-
onCancel
|
|
153
|
+
onCancel () {
|
|
154
|
+
throw new Error('Operation was cancelled.');
|
|
155
|
+
}
|
|
217
156
|
});
|
|
218
157
|
|
|
219
|
-
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(dim('—'.repeat(42)));
|
|
160
|
+
console.log();
|
|
220
161
|
|
|
221
|
-
|
|
162
|
+
const targetDir = (packageDir ?? packageName).toString();
|
|
222
163
|
|
|
223
|
-
|
|
224
|
-
prepareDir(join(cwd, packageDir), empty);
|
|
225
|
-
copyTemplate(packageDir);
|
|
164
|
+
const projectDir = resolve(cwd, targetDir);
|
|
226
165
|
|
|
227
|
-
|
|
228
|
-
|
|
166
|
+
await runStep('Preparing project directory', () => {
|
|
167
|
+
if (!existsSync(projectDir)) {
|
|
168
|
+
mkdirSync(projectDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
229
171
|
|
|
230
|
-
|
|
231
|
-
|
|
172
|
+
const gitRoot = getGitRoot(projectDir);
|
|
173
|
+
const gitRootRelative = (relative(projectDir, gitRoot ?? projectDir) || '.').replaceAll('\\', '/');
|
|
232
174
|
|
|
233
|
-
|
|
234
|
-
}
|
|
175
|
+
setReplacements(packageName, backendPort, gitRootRelative);
|
|
235
176
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
177
|
+
await runStep('Scaffolding template files', () => {
|
|
178
|
+
copyTemplate(projectDir, gitRoot ?? projectDir);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!gitRoot) {
|
|
182
|
+
await runStep('Initialising Git repository', () => execAsync('git init', {
|
|
183
|
+
cwd: projectDir,
|
|
184
|
+
stdio: 'ignore'
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await installDependencies(projectDir, packageName);
|
|
189
|
+
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(bold(green('All set.')));
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(dim('Your new project is ready. Next:'));
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(` ${cyan('$')} ${dim('cd')} ${targetDir}`);
|
|
196
|
+
console.log(` ${cyan('$')} ${dim('pnpm dev')}`);
|
|
197
|
+
console.log();
|
|
250
198
|
}
|
|
251
199
|
|
|
252
200
|
/**
|
|
253
|
-
* Set the dynamic values for the
|
|
201
|
+
* Set the dynamic values for the placeholder replacements.
|
|
254
202
|
*
|
|
255
|
-
* @param {string} packageName The name of the
|
|
256
|
-
* @param {string} packageDir The directory of the package being created.
|
|
257
|
-
* @param {string} rootDir The relative path of the root of the repository.
|
|
203
|
+
* @param {string} packageName The name of the project.
|
|
258
204
|
* @param {number} backendPort The port that the backend server runs on.
|
|
259
|
-
* @param
|
|
205
|
+
* @param {string} gitRoot The relative path from the project directory to the Git root.
|
|
260
206
|
*/
|
|
261
|
-
function setReplacements (packageName,
|
|
262
|
-
const husky = relative(join(cwd, packageDir, '.vscode'), join(rootDir, '.husky'));
|
|
263
|
-
const repoRoot = relative(join(cwd, packageDir), rootDir);
|
|
264
|
-
const projDir = relative(rootDir, join(cwd, packageDir));
|
|
265
|
-
|
|
207
|
+
function setReplacements (packageName, backendPort, gitRoot) {
|
|
266
208
|
filesToRename['template.code-workspace'] = `${packageName}.code-workspace`;
|
|
267
209
|
placeholdersToReplace['[[PACKAGE_NAME]]'] = packageName;
|
|
268
|
-
placeholdersToReplace['[[
|
|
269
|
-
placeholdersToReplace['[[
|
|
270
|
-
placeholdersToReplace['[[
|
|
271
|
-
|
|
272
|
-
placeholdersToReplace['[[PACKAGE_MANAGER]]'] = packageManager;
|
|
273
|
-
|
|
274
|
-
if (backendPort !== 0) {
|
|
275
|
-
placeholdersToReplace['\'[[PROXY]]\''] = `{
|
|
210
|
+
placeholdersToReplace['[[GIT_ROOT]]'] = gitRoot;
|
|
211
|
+
placeholdersToReplace['[[DEFAULT_USERNAME]]'] = getDefaultUsername();
|
|
212
|
+
placeholdersToReplace['\'[[PROXY]]\''] = `{
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
276
214
|
'/api': {
|
|
277
215
|
target: 'https://localhost:${backendPort}',
|
|
278
216
|
secure: false,
|
|
279
|
-
rewrite: (path) => path.replace('/api', '')
|
|
217
|
+
rewrite: (path: string): string => path.replace('/api', '')
|
|
280
218
|
}
|
|
281
219
|
}`;
|
|
282
|
-
}
|
|
283
220
|
}
|
|
284
221
|
|
|
285
222
|
/**
|
|
286
|
-
*
|
|
223
|
+
* Gets username to use as default for the project.
|
|
287
224
|
*
|
|
288
|
-
* @
|
|
289
|
-
* @param {boolean} empty If the current contents needs to be emptied.
|
|
225
|
+
* @returns {string} Git `user.name`, if set; otherwise, the OS username.
|
|
290
226
|
*/
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
227
|
+
function getDefaultUsername () {
|
|
228
|
+
try {
|
|
229
|
+
const username = execSync('git config user.name', {
|
|
230
|
+
cwd: process.cwd(),
|
|
231
|
+
encoding: 'utf-8',
|
|
232
|
+
stdio: [
|
|
233
|
+
'ignore',
|
|
234
|
+
'pipe',
|
|
235
|
+
'ignore'
|
|
236
|
+
]
|
|
237
|
+
}).trim();
|
|
238
|
+
|
|
239
|
+
if (username) {
|
|
240
|
+
return username;
|
|
241
|
+
}
|
|
299
242
|
|
|
300
|
-
|
|
243
|
+
return userInfo().username;
|
|
301
244
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
mkdirSync(path);
|
|
245
|
+
catch {
|
|
246
|
+
// Git is missing, not on PATH, or config is unreadable.
|
|
306
247
|
}
|
|
248
|
+
|
|
249
|
+
return 'Unknown';
|
|
307
250
|
}
|
|
308
251
|
|
|
309
252
|
/**
|
|
310
|
-
*
|
|
253
|
+
* Returns the root directory of the Git repository containing the given
|
|
254
|
+
* directory, or null if the directory is not inside a Git repository.
|
|
311
255
|
*
|
|
312
|
-
* @param {string}
|
|
256
|
+
* @param {string} dir The directory to check.
|
|
257
|
+
* @returns {string | null} The Git root path, or null.
|
|
313
258
|
*/
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
259
|
+
function getGitRoot (dir) {
|
|
260
|
+
try {
|
|
261
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
262
|
+
cwd: dir,
|
|
263
|
+
stdio: [
|
|
264
|
+
'ignore',
|
|
265
|
+
'pipe',
|
|
266
|
+
'ignore'
|
|
267
|
+
],
|
|
268
|
+
encoding: 'utf-8'
|
|
269
|
+
}).trim();
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
318
274
|
}
|
|
319
275
|
|
|
320
276
|
/**
|
|
321
|
-
* Copies the
|
|
277
|
+
* Copies the template directory into the project directory.
|
|
278
|
+
* The `_husky` directory is copied to the Git root so that hooks are installed into the correct location.
|
|
322
279
|
*
|
|
323
|
-
* @param {string}
|
|
280
|
+
* @param {string} projectDir The directory of the new project.
|
|
281
|
+
* @param {string} gitRoot The root of the Git repository.
|
|
324
282
|
*/
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
const huskyDir = join(rootDir, '.husky');
|
|
283
|
+
function copyTemplate (projectDir, gitRoot) {
|
|
284
|
+
const templateDir = join(codeDir, 'template');
|
|
328
285
|
|
|
329
|
-
|
|
286
|
+
for (const item of readdirSync(templateDir)) {
|
|
287
|
+
copy(templateDir, item === '_husky' ? gitRoot : projectDir, item);
|
|
288
|
+
}
|
|
330
289
|
}
|
|
331
290
|
|
|
332
291
|
/**
|
|
@@ -337,118 +296,198 @@ function copyHooks (rootDir) {
|
|
|
337
296
|
* @param {string} item The item to copy.
|
|
338
297
|
*/
|
|
339
298
|
function copy (source, destination, item) {
|
|
299
|
+
if (item === '.gitkeep') {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
340
303
|
const sourceItem = join(source, item);
|
|
341
304
|
const info = statSync(sourceItem);
|
|
342
305
|
|
|
343
306
|
if (info.isDirectory()) {
|
|
344
|
-
const
|
|
307
|
+
const renamedItem = filesToRename[item] ?? item;
|
|
308
|
+
const destinationDir = join(destination, renamedItem);
|
|
345
309
|
|
|
346
310
|
mkdirSync(destinationDir, { recursive: true });
|
|
347
311
|
|
|
348
|
-
|
|
312
|
+
for (const child of readdirSync(sourceItem)) {
|
|
313
|
+
copy(sourceItem, destinationDir, child);
|
|
314
|
+
}
|
|
349
315
|
}
|
|
350
316
|
else {
|
|
351
|
-
const destinationFile =
|
|
317
|
+
const destinationFile = join(destination, filesToRename[item] ?? item);
|
|
352
318
|
|
|
353
319
|
if (existsSync(destinationFile)) {
|
|
354
|
-
|
|
320
|
+
if (verbose) {
|
|
321
|
+
console.log(`${dim('skip')} ${item}`);
|
|
322
|
+
}
|
|
355
323
|
}
|
|
356
|
-
else {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
324
|
+
else if (extname(item).toLowerCase() === '.ico') {
|
|
325
|
+
if (verbose) {
|
|
326
|
+
console.log(`${dim('copy')} ${item}`);
|
|
327
|
+
}
|
|
360
328
|
|
|
361
|
-
|
|
329
|
+
copyFileSync(sourceItem, destinationFile);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
if (verbose) {
|
|
333
|
+
console.log(`${dim('copy')} ${item}`);
|
|
334
|
+
}
|
|
362
335
|
|
|
363
|
-
let contents = readFileSync(sourceItem,
|
|
336
|
+
let contents = readFileSync(sourceItem, { encoding: 'utf-8' });
|
|
364
337
|
|
|
365
338
|
for (const [
|
|
366
339
|
key,
|
|
367
340
|
value
|
|
368
341
|
] of Object.entries(placeholdersToReplace)) {
|
|
369
|
-
contents = contents.
|
|
342
|
+
contents = contents.replaceAll(key, value);
|
|
370
343
|
}
|
|
371
344
|
|
|
372
|
-
writeFileSync(destinationFile, contents,
|
|
345
|
+
writeFileSync(destinationFile, contents, { encoding: 'utf-8' });
|
|
373
346
|
}
|
|
374
347
|
}
|
|
375
348
|
}
|
|
376
349
|
|
|
377
350
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
351
|
+
* Dependency definitions per workspace.
|
|
352
|
+
* The major version constraint ensures pnpm resolves to the latest minor/patch
|
|
353
|
+
* and writes the full ^x.y.z version into package.json.
|
|
381
354
|
*/
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
355
|
+
const dependencies = {
|
|
356
|
+
root: {
|
|
357
|
+
dev: [
|
|
358
|
+
'eslint@9',
|
|
359
|
+
'husky@9',
|
|
360
|
+
'lint-staged@16',
|
|
361
|
+
'stylelint@17',
|
|
362
|
+
'turbo@2'
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
app: {
|
|
366
|
+
prod: [
|
|
367
|
+
'@citruslime/ui@4',
|
|
368
|
+
'@citruslime/vue-utils@2',
|
|
369
|
+
'@vueuse/core@14',
|
|
370
|
+
'luxon@3',
|
|
371
|
+
'pinia@3',
|
|
372
|
+
'pinia-plugin-persistedstate@4',
|
|
373
|
+
'vue@3',
|
|
374
|
+
'vue-i18n@11',
|
|
375
|
+
'vue-router@5'
|
|
376
|
+
],
|
|
377
|
+
dev: [
|
|
378
|
+
'@citruslime/theme@2',
|
|
379
|
+
'@intlify/unplugin-vue-i18n@11',
|
|
380
|
+
'@tailwindcss/vite@4',
|
|
381
|
+
'@types/jsdom@27',
|
|
382
|
+
'@types/luxon@3',
|
|
383
|
+
'@types/node@25',
|
|
384
|
+
'@vitejs/plugin-vue@6',
|
|
385
|
+
'jsdom@28',
|
|
386
|
+
'npm-run-all@4',
|
|
387
|
+
'typescript@5',
|
|
388
|
+
'unplugin-auto-import@21',
|
|
389
|
+
'unplugin-vue-components@32',
|
|
390
|
+
'vite@8',
|
|
391
|
+
'vite-plugin-mkcert@1',
|
|
392
|
+
'vite-plugin-vue-devtools@8',
|
|
393
|
+
'vue-tsc@3'
|
|
394
|
+
]
|
|
395
|
+
},
|
|
396
|
+
'config-eslint': {
|
|
397
|
+
prod: ['@citruslime/config@2']
|
|
398
|
+
},
|
|
399
|
+
'config-stylelint': {
|
|
400
|
+
prod: ['@citruslime/config@2']
|
|
401
|
+
},
|
|
402
|
+
'config-typescript': {
|
|
403
|
+
prod: [
|
|
404
|
+
'@tsconfig/node24@24',
|
|
405
|
+
'@vue/tsconfig@0'
|
|
406
|
+
]
|
|
407
|
+
},
|
|
408
|
+
utils: {
|
|
409
|
+
prod: [
|
|
410
|
+
'@citruslime/vue-utils@2',
|
|
411
|
+
'unimport@6'
|
|
412
|
+
],
|
|
413
|
+
dev: [
|
|
414
|
+
'@types/node@25',
|
|
415
|
+
'npm-run-all@4',
|
|
416
|
+
'typescript@5',
|
|
417
|
+
'unplugin-auto-import@21',
|
|
418
|
+
'vite@8',
|
|
419
|
+
'vue-tsc@3'
|
|
420
|
+
]
|
|
401
421
|
}
|
|
402
|
-
|
|
403
|
-
execSync(`${prefix} ${dependencies}`);
|
|
404
|
-
|
|
405
|
-
print(lightBlue('Running final install...'), true);
|
|
406
|
-
|
|
407
|
-
execSync(`${packageManager} install`);
|
|
408
|
-
}
|
|
422
|
+
};
|
|
409
423
|
|
|
410
424
|
/**
|
|
411
|
-
*
|
|
425
|
+
* Runs a shell command. Use this instead of promisified `exec` when `stdio` must be
|
|
426
|
+
* `'inherit'` — `exec` always buffers child output and does not honour `stdio` for streaming.
|
|
412
427
|
*
|
|
413
|
-
* @param {string}
|
|
414
|
-
* @param {
|
|
428
|
+
* @param {string} command Full command line (shell parsing).
|
|
429
|
+
* @param {string} cwd Working directory.
|
|
430
|
+
* @returns {Promise<void>}
|
|
415
431
|
*/
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
432
|
+
function runShellCommand (command, cwd) {
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const child = spawn(command, {
|
|
435
|
+
cwd,
|
|
436
|
+
shell: true,
|
|
437
|
+
stdio: verbose ? 'inherit' : 'ignore',
|
|
438
|
+
windowsHide: true
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
child.on('error', reject);
|
|
442
|
+
child.on('close', (code, signal) => {
|
|
443
|
+
if (code === 0) {
|
|
444
|
+
resolve();
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
reject(new Error(`Command failed with exit code ${code}${signal ? ` (${signal})` : ''}: ${command}`));
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
422
451
|
}
|
|
423
452
|
|
|
424
453
|
/**
|
|
425
|
-
*
|
|
454
|
+
* Installs all project dependencies using pnpm add with major version constraints.
|
|
455
|
+
* Pnpm resolves each to the latest matching version and writes ^x.y.z into package.json.
|
|
426
456
|
*
|
|
427
|
-
* @
|
|
457
|
+
* @param {string} projectDir The project directory.
|
|
458
|
+
* @param {string} packageName The name of the project used as the npm scope.
|
|
459
|
+
* @returns {Promise<void>}
|
|
428
460
|
*/
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
}
|
|
461
|
+
async function installDependencies (projectDir, packageName) {
|
|
462
|
+
const runPnpm = cmd => runShellCommand(cmd, projectDir);
|
|
432
463
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
464
|
+
await runStep('pnpm install (workspace)', () => runPnpm('pnpm install --ignore-scripts'));
|
|
465
|
+
|
|
466
|
+
await runStep('Adding root DevDependencies', () => runPnpm(`pnpm add -D -w ${dependencies.root.dev.join(' ')}`));
|
|
467
|
+
|
|
468
|
+
for (const [
|
|
469
|
+
workspace,
|
|
470
|
+
deps
|
|
471
|
+
] of Object.entries(dependencies).filter(([k]) => k !== 'root')) {
|
|
472
|
+
const filter = `--filter @${packageName}/${workspace}`;
|
|
473
|
+
|
|
474
|
+
if (deps.prod?.length) {
|
|
475
|
+
await runStep(`Dependencies · @${packageName}/${workspace}`, () => runPnpm(`pnpm ${filter} add ${deps.prod.join(' ')}`));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (deps.dev?.length) {
|
|
479
|
+
await runStep(`DevDependencies · @${packageName}/${workspace}`, () => runPnpm(`pnpm ${filter} add -D ${deps.dev.join(' ')}`));
|
|
480
|
+
}
|
|
443
481
|
}
|
|
444
482
|
|
|
445
|
-
|
|
483
|
+
await runStep('pnpm install (final lockfile)', () => runPnpm('pnpm install'));
|
|
446
484
|
}
|
|
447
485
|
|
|
448
486
|
try {
|
|
449
487
|
await init();
|
|
450
488
|
}
|
|
451
489
|
catch (e) {
|
|
452
|
-
|
|
490
|
+
console.error();
|
|
491
|
+
console.error(`${red('✖')} ${e.message}`);
|
|
453
492
|
process.exit(1);
|
|
454
493
|
}
|