create-esmx 3.0.0-rc.34 → 3.0.0-rc.36

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/src/index.ts CHANGED
@@ -1,391 +1 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- readdirSync,
8
- statSync,
9
- writeFileSync
10
- } from 'node:fs';
11
- import { dirname, join, resolve } from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
- import {
14
- cancel,
15
- confirm,
16
- intro,
17
- isCancel,
18
- log,
19
- note,
20
- outro,
21
- select,
22
- text
23
- } from '@clack/prompts';
24
- import minimist from 'minimist';
25
- import color from 'picocolors';
26
- import {
27
- formatProjectName,
28
- getCommand,
29
- replaceTemplateVariables
30
- } from './utils/index';
31
-
32
- const __dirname = dirname(fileURLToPath(import.meta.url));
33
-
34
- interface CreateProjectOptions {
35
- argv?: string[]; // Command line arguments
36
- cwd?: string; // Working directory
37
- userAgent?: string; // Package manager user agent
38
- }
39
-
40
- function getEsmxVersion(): string {
41
- try {
42
- const packageJsonPath = resolve(__dirname, '../package.json');
43
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
44
-
45
- return packageJson.version || 'latest';
46
- } catch (error) {
47
- console.warn('Failed to read esmx version, using latest version');
48
- return 'latest';
49
- }
50
- }
51
-
52
- interface TemplateInfo {
53
- folder: string;
54
- name: string;
55
- description: string;
56
- }
57
-
58
- function getAvailableTemplates(): TemplateInfo[] {
59
- const templateDir = resolve(__dirname, '../template');
60
-
61
- const templates: TemplateInfo[] = [];
62
- const templateFolders = readdirSync(templateDir, { withFileTypes: true })
63
- .filter((dirent) => dirent.isDirectory())
64
- .map((dirent) => dirent.name);
65
-
66
- for (const folder of templateFolders) {
67
- // Use folder name as display name
68
- const name = folder;
69
-
70
- // Try to read description from package.json
71
- const packageJsonPath = resolve(templateDir, folder, 'package.json');
72
- let description = `${name} template`;
73
-
74
- if (existsSync(packageJsonPath)) {
75
- try {
76
- const packageJson = JSON.parse(
77
- readFileSync(packageJsonPath, 'utf-8')
78
- );
79
- if (packageJson.description) {
80
- description = packageJson.description;
81
- }
82
- templates.push({
83
- folder,
84
- name,
85
- description
86
- });
87
- } catch (error) {
88
- // JSON parsing failed, skip this template
89
- console.warn(
90
- `Warning: Failed to parse package.json for template '${folder}', skipping.`
91
- );
92
- }
93
- }
94
- }
95
-
96
- // Sort by name alphabetically
97
- return templates.sort((a, b) => a.name.localeCompare(b.name));
98
- }
99
-
100
- interface TemplateVariables extends Record<string, string> {
101
- projectName: string;
102
- esmxVersion: string;
103
- installCommand: string;
104
- devCommand: string;
105
- buildCommand: string;
106
- startCommand: string;
107
- buildTypeCommand: string;
108
- lintTypeCommand: string;
109
- }
110
-
111
- function showHelp(userAgent?: string): void {
112
- const createCmd = getCommand('create', userAgent);
113
-
114
- console.log(`
115
- ${color.reset(color.bold(color.blue('🚀 Create Esmx Project')))}
116
-
117
- ${color.bold('Usage:')}
118
- ${createCmd} [project-name]
119
- ${createCmd} [project-name] [options]
120
-
121
- ${color.bold('Options:')}
122
- -t, --template <template> Template to use (default: vue2)
123
- -n, --name <name> Project name or path
124
- -f, --force Force overwrite existing directory
125
- -h, --help Show help information
126
- -v, --version Show version number
127
-
128
- ${color.bold('Examples:')}
129
- ${createCmd} my-project
130
- ${createCmd} my-project -t vue2
131
- ${createCmd} my-project --force
132
- ${createCmd} . -f -t vue2
133
-
134
- ${color.bold('Available Templates:')}
135
- ${getAvailableTemplates()
136
- .map((t) => ` ${t.folder.padEnd(25)} ${t.description}`)
137
- .join('\n')}
138
-
139
- For more information, visit: ${color.cyan('https://esmnext.com')}
140
- `);
141
- }
142
-
143
- export async function createProject(
144
- options: CreateProjectOptions = {}
145
- ): Promise<void> {
146
- const { argv, cwd, userAgent } = options;
147
- const commandLineArgs = argv || process.argv.slice(2);
148
- const workingDir = cwd || process.cwd();
149
-
150
- const parsedArgs = minimist(commandLineArgs, {
151
- string: ['template', 'name'],
152
- boolean: ['help', 'version', 'force'],
153
- alias: {
154
- t: 'template',
155
- n: 'name',
156
- f: 'force',
157
- h: 'help',
158
- v: 'version'
159
- }
160
- });
161
-
162
- if (parsedArgs.help) {
163
- showHelp(userAgent);
164
- return;
165
- }
166
-
167
- if (parsedArgs.version) {
168
- console.log(getEsmxVersion());
169
- return;
170
- }
171
-
172
- console.log();
173
- intro(
174
- color.reset(
175
- color.bold(color.blue('🚀 Welcome to Esmx Project Creator!'))
176
- )
177
- );
178
-
179
- const projectNameInput = await getProjectName(
180
- parsedArgs.name,
181
- parsedArgs._[0]
182
- );
183
- if (isCancel(projectNameInput)) {
184
- cancel('Operation cancelled');
185
- return;
186
- }
187
-
188
- const { packageName, targetDir } = formatProjectName(
189
- projectNameInput,
190
- workingDir
191
- );
192
-
193
- const templateType = await getTemplateType(parsedArgs.template);
194
- if (isCancel(templateType)) {
195
- cancel('Operation cancelled');
196
- return;
197
- }
198
-
199
- const installCommand = getCommand('install', userAgent);
200
- const devCommand = getCommand('dev', userAgent);
201
- const buildCommand = getCommand('build', userAgent);
202
- const startCommand = getCommand('start', userAgent);
203
- const buildTypeCommand = getCommand('build:type', userAgent);
204
- const lintTypeCommand = getCommand('lint:type', userAgent);
205
-
206
- await createProjectFromTemplate(
207
- targetDir,
208
- templateType,
209
- workingDir,
210
- parsedArgs.force,
211
- {
212
- projectName: packageName,
213
- esmxVersion: getEsmxVersion(),
214
- installCommand,
215
- devCommand,
216
- buildCommand,
217
- startCommand,
218
- buildTypeCommand,
219
- lintTypeCommand
220
- }
221
- );
222
- const installCmd = installCommand;
223
- const devCmd = devCommand;
224
-
225
- const nextSteps = [
226
- color.reset(`1. ${color.cyan(`cd ${targetDir}`)}`),
227
- color.reset(`2. ${color.cyan(installCmd)}`),
228
- color.reset(`3. ${color.cyan('git init')} ${color.gray('(optional)')}`),
229
- color.reset(`4. ${color.cyan(devCmd)}`)
230
- ];
231
-
232
- note(nextSteps.join('\n'), 'Next steps');
233
-
234
- outro(color.reset(color.green('Happy coding! 🎉')));
235
- }
236
-
237
- async function getProjectName(
238
- argName?: string,
239
- positionalName?: string
240
- ): Promise<string | symbol> {
241
- const providedName = argName || positionalName;
242
- if (providedName) {
243
- return providedName;
244
- }
245
-
246
- const projectName = await text({
247
- message: 'Project name or path:',
248
- placeholder: 'my-esmx-project',
249
- validate: (value: string) => {
250
- if (!value.trim()) {
251
- return 'Project name or path is required';
252
- }
253
- if (!/^[a-zA-Z0-9_.\/@-]+$/.test(value.trim())) {
254
- return 'Project name or path should only contain letters, numbers, hyphens, underscores, dots, and slashes';
255
- }
256
- }
257
- });
258
-
259
- return String(projectName).trim();
260
- }
261
-
262
- async function getTemplateType(argTemplate?: string): Promise<string | symbol> {
263
- const availableTemplates = getAvailableTemplates();
264
-
265
- if (
266
- argTemplate &&
267
- availableTemplates.some((t) => t.folder === argTemplate)
268
- ) {
269
- return argTemplate;
270
- }
271
-
272
- const options = availableTemplates.map((t) => ({
273
- label: color.reset(color.gray(`${t.folder} - `) + color.bold(t.name)),
274
- value: t.folder,
275
- hint: t.description
276
- }));
277
-
278
- const template = await select({
279
- message: 'Select a template:',
280
- options: options
281
- });
282
-
283
- return template as string | symbol;
284
- }
285
-
286
- function isDirectoryEmpty(dirPath: string): boolean {
287
- if (!existsSync(dirPath)) {
288
- return true;
289
- }
290
-
291
- const files = readdirSync(dirPath);
292
- // Only consider non-hidden files and directories
293
- const nonHiddenFiles = files.filter((file) => !file.startsWith('.'));
294
- return nonHiddenFiles.length === 0;
295
- }
296
-
297
- async function createProjectFromTemplate(
298
- targetDir: string,
299
- templateType: string,
300
- workingDir: string,
301
- force: boolean,
302
- variables: TemplateVariables
303
- ): Promise<void> {
304
- const templatePath = resolve(__dirname, '../template', templateType);
305
- const targetPath =
306
- targetDir === '.' ? workingDir : resolve(workingDir, targetDir);
307
-
308
- if (!existsSync(templatePath)) {
309
- throw new Error(`Template "${templateType}" not found`);
310
- }
311
-
312
- // Handle directory existence and overwrite confirmation
313
- if (targetDir !== '.' && existsSync(targetPath)) {
314
- if (!isDirectoryEmpty(targetPath)) {
315
- if (!force) {
316
- const shouldOverwrite = await confirm({
317
- message: `Directory "${targetDir}" is not empty. Do you want to overwrite it?`
318
- });
319
-
320
- if (isCancel(shouldOverwrite)) {
321
- cancel('Operation cancelled');
322
- return;
323
- }
324
-
325
- if (!shouldOverwrite) {
326
- throw new Error('Operation cancelled by user');
327
- }
328
- }
329
-
330
- // Files will be overwritten during copyTemplateFiles
331
- }
332
- } else if (targetDir !== '.') {
333
- mkdirSync(targetPath, { recursive: true });
334
- }
335
-
336
- // Handle current directory case
337
- if (targetDir === '.' && !isDirectoryEmpty(targetPath)) {
338
- if (!force) {
339
- const shouldOverwrite = await confirm({
340
- message:
341
- 'Current directory is not empty. Do you want to overwrite existing files?'
342
- });
343
-
344
- if (isCancel(shouldOverwrite)) {
345
- cancel('Operation cancelled');
346
- return;
347
- }
348
-
349
- if (!shouldOverwrite) {
350
- throw new Error('Operation cancelled by user');
351
- }
352
- }
353
- }
354
-
355
- copyTemplateFiles(templatePath, targetPath, variables);
356
- }
357
-
358
- function copyTemplateFiles(
359
- templatePath: string,
360
- targetPath: string,
361
- variables: TemplateVariables
362
- ): void {
363
- const files = readdirSync(templatePath);
364
-
365
- for (const file of files) {
366
- const filePath = join(templatePath, file);
367
- const targetFilePath = join(targetPath, file);
368
- const stat = statSync(filePath);
369
-
370
- if (stat.isDirectory()) {
371
- mkdirSync(targetFilePath, { recursive: true });
372
- copyTemplateFiles(filePath, targetFilePath, variables);
373
- } else {
374
- let content = readFileSync(filePath, 'utf-8');
375
-
376
- // Replace all template variables using the utility function
377
- content = replaceTemplateVariables(content, variables);
378
-
379
- writeFileSync(targetFilePath, content);
380
- }
381
- }
382
- }
383
-
384
- export default createProject;
385
-
386
- if (import.meta.url === `file://${process.argv[1]}`) {
387
- createProject().catch((error) => {
388
- console.error('Error creating project:', error);
389
- process.exit(1);
390
- });
391
- }
1
+ export { cli } from './cli';
@@ -0,0 +1,225 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { createProjectFromTemplate } from './project';
7
+ import { getEsmxVersion } from './template';
8
+ import { formatProjectName } from './utils/index';
9
+
10
+ // Test utilities
11
+ async function createTempDir(prefix = 'esmx-unit-test-'): Promise<string> {
12
+ return mkdtemp(join(tmpdir(), prefix));
13
+ }
14
+
15
+ async function cleanupTempDir(tempDir: string): Promise<void> {
16
+ try {
17
+ await rm(tempDir, { recursive: true, force: true });
18
+ } catch (error) {
19
+ console.warn(`Failed to cleanup temp directory: ${tempDir}`, error);
20
+ }
21
+ }
22
+
23
+ describe('project unit tests', () => {
24
+ let tmpDir: string;
25
+
26
+ beforeEach(async () => {
27
+ tmpDir = await createTempDir();
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await cleanupTempDir(tmpDir);
32
+ });
33
+
34
+ it('should handle isDirectoryEmpty edge cases', async () => {
35
+ // Test with directory containing only hidden files
36
+ const hiddenFilesDir = join(tmpDir, 'hidden-files-dir');
37
+ await mkdir(hiddenFilesDir, { recursive: true });
38
+ await writeFile(join(hiddenFilesDir, '.hidden-file'), 'hidden content');
39
+ await writeFile(join(hiddenFilesDir, '.gitignore'), 'node_modules/');
40
+
41
+ // Get project name and target directory
42
+ const projectNameInput = 'hidden-files-dir';
43
+ const { packageName, targetDir } = formatProjectName(
44
+ projectNameInput,
45
+ tmpDir
46
+ );
47
+
48
+ // Create project from template
49
+ await createProjectFromTemplate(targetDir, 'vue2', tmpDir, false, {
50
+ projectName: packageName,
51
+ esmxVersion: getEsmxVersion(),
52
+ installCommand: 'npm install',
53
+ devCommand: 'npm run dev',
54
+ buildCommand: 'npm run build',
55
+ startCommand: 'npm start',
56
+ buildTypeCommand: 'npm run build:type',
57
+ lintTypeCommand: 'npm run lint:type'
58
+ });
59
+
60
+ // Should succeed because hidden files are ignored
61
+ expect(existsSync(join(hiddenFilesDir, 'package.json'))).toBe(true);
62
+ });
63
+
64
+ it('should handle directory creation for nested paths', async () => {
65
+ const deepPath = join(
66
+ tmpDir,
67
+ 'very',
68
+ 'deep',
69
+ 'nested',
70
+ 'path',
71
+ 'project'
72
+ );
73
+
74
+ // Get project name and target directory
75
+ const projectNameInput = 'very/deep/nested/path/project';
76
+ const { packageName, targetDir } = formatProjectName(
77
+ projectNameInput,
78
+ tmpDir
79
+ );
80
+
81
+ // Create project from template
82
+ await createProjectFromTemplate(targetDir, 'vue2', tmpDir, false, {
83
+ projectName: packageName,
84
+ esmxVersion: getEsmxVersion(),
85
+ installCommand: 'npm install',
86
+ devCommand: 'npm run dev',
87
+ buildCommand: 'npm run build',
88
+ startCommand: 'npm start',
89
+ buildTypeCommand: 'npm run build:type',
90
+ lintTypeCommand: 'npm run lint:type'
91
+ });
92
+
93
+ expect(existsSync(deepPath)).toBe(true);
94
+ expect(existsSync(join(deepPath, 'package.json'))).toBe(true);
95
+ });
96
+
97
+ it('should handle file copy with template variable replacement', async () => {
98
+ const projectPath = join(tmpDir, 'variable-test');
99
+
100
+ // Get project name and target directory
101
+ const projectNameInput = 'variable-test';
102
+ const { packageName, targetDir } = formatProjectName(
103
+ projectNameInput,
104
+ tmpDir
105
+ );
106
+
107
+ // Create project from template
108
+ await createProjectFromTemplate(targetDir, 'vue2', tmpDir, false, {
109
+ projectName: packageName,
110
+ esmxVersion: getEsmxVersion(),
111
+ installCommand: 'npm install',
112
+ devCommand: 'npm run dev',
113
+ buildCommand: 'npm run build',
114
+ startCommand: 'npm start',
115
+ buildTypeCommand: 'npm run build:type',
116
+ lintTypeCommand: 'npm run lint:type'
117
+ });
118
+
119
+ // Verify that package.json contains replaced variables
120
+ const packageJsonPath = join(projectPath, 'package.json');
121
+ expect(existsSync(packageJsonPath)).toBe(true);
122
+
123
+ const packageContent = require('node:fs').readFileSync(
124
+ packageJsonPath,
125
+ 'utf-8'
126
+ );
127
+ const packageJson = JSON.parse(packageContent);
128
+ expect(packageJson.name).toBe('variable-test');
129
+ });
130
+
131
+ it('should handle empty directory detection correctly', async () => {
132
+ // Test completely empty directory
133
+ const emptyDir = join(tmpDir, 'empty-dir');
134
+ await mkdir(emptyDir, { recursive: true });
135
+
136
+ // Get project name and target directory
137
+ const projectNameInput = 'empty-dir';
138
+ const { packageName, targetDir } = formatProjectName(
139
+ projectNameInput,
140
+ tmpDir
141
+ );
142
+
143
+ // Create project from template
144
+ await createProjectFromTemplate(targetDir, 'vue2', tmpDir, false, {
145
+ projectName: packageName,
146
+ esmxVersion: getEsmxVersion(),
147
+ installCommand: 'npm install',
148
+ devCommand: 'npm run dev',
149
+ buildCommand: 'npm run build',
150
+ startCommand: 'npm start',
151
+ buildTypeCommand: 'npm run build:type',
152
+ lintTypeCommand: 'npm run lint:type'
153
+ });
154
+
155
+ expect(existsSync(join(emptyDir, 'package.json'))).toBe(true);
156
+ });
157
+
158
+ it('should handle mixed file types in directory', async () => {
159
+ // Test directory with mix of hidden and non-hidden files
160
+ const mixedDir = join(tmpDir, 'mixed-dir');
161
+ await mkdir(mixedDir, { recursive: true });
162
+ await writeFile(join(mixedDir, '.dotfile'), 'hidden');
163
+ await writeFile(join(mixedDir, 'regular-file.txt'), 'visible');
164
+
165
+ // Get project name and target directory
166
+ const projectNameInput = 'mixed-dir';
167
+ const { packageName, targetDir } = formatProjectName(
168
+ projectNameInput,
169
+ tmpDir
170
+ );
171
+
172
+ // Create project from template with force flag
173
+ await createProjectFromTemplate(
174
+ targetDir,
175
+ 'vue2',
176
+ tmpDir,
177
+ true, // force flag
178
+ {
179
+ projectName: packageName,
180
+ esmxVersion: getEsmxVersion(),
181
+ installCommand: 'npm install',
182
+ devCommand: 'npm run dev',
183
+ buildCommand: 'npm run build',
184
+ startCommand: 'npm start',
185
+ buildTypeCommand: 'npm run build:type',
186
+ lintTypeCommand: 'npm run lint:type'
187
+ }
188
+ );
189
+
190
+ expect(existsSync(join(mixedDir, 'package.json'))).toBe(true);
191
+ });
192
+
193
+ it('should handle special characters in project names', async () => {
194
+ const specialNames = [
195
+ 'project-with-dashes',
196
+ 'project_with_underscores',
197
+ 'project.with.dots'
198
+ ];
199
+
200
+ for (const projectName of specialNames) {
201
+ const projectPath = join(tmpDir, projectName);
202
+
203
+ // Get project name and target directory
204
+ const { packageName, targetDir } = formatProjectName(
205
+ projectName,
206
+ tmpDir
207
+ );
208
+
209
+ // Create project from template
210
+ await createProjectFromTemplate(targetDir, 'vue2', tmpDir, false, {
211
+ projectName: packageName,
212
+ esmxVersion: getEsmxVersion(),
213
+ installCommand: 'npm install',
214
+ devCommand: 'npm run dev',
215
+ buildCommand: 'npm run build',
216
+ startCommand: 'npm start',
217
+ buildTypeCommand: 'npm run build:type',
218
+ lintTypeCommand: 'npm run lint:type'
219
+ });
220
+
221
+ expect(existsSync(projectPath)).toBe(true);
222
+ expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
223
+ }
224
+ });
225
+ });
package/src/project.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { cancel, confirm, isCancel } from '@clack/prompts';
5
+ import { copyTemplateFiles, isDirectoryEmpty } from './template';
6
+ import type { TemplateVariables } from './types';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ /**
11
+ * Create a project from template
12
+ */
13
+ export async function createProjectFromTemplate(
14
+ targetDir: string,
15
+ templateType: string,
16
+ workingDir: string,
17
+ force: boolean,
18
+ variables: TemplateVariables
19
+ ): Promise<void> {
20
+ const templatePath = resolve(__dirname, '../template', templateType);
21
+ const targetPath =
22
+ targetDir === '.' ? workingDir : resolve(workingDir, targetDir);
23
+
24
+ if (!existsSync(templatePath)) {
25
+ throw new Error(`Template "${templateType}" not found`);
26
+ }
27
+
28
+ // Handle directory existence and overwrite confirmation
29
+ if (targetDir !== '.' && existsSync(targetPath)) {
30
+ if (!isDirectoryEmpty(targetPath)) {
31
+ if (!force) {
32
+ const shouldOverwrite = await confirm({
33
+ message: `Directory "${targetDir}" is not empty. Do you want to overwrite it?`
34
+ });
35
+
36
+ if (isCancel(shouldOverwrite)) {
37
+ cancel('Operation cancelled');
38
+ return;
39
+ }
40
+
41
+ if (!shouldOverwrite) {
42
+ throw new Error('Operation cancelled by user');
43
+ }
44
+ }
45
+
46
+ // Files will be overwritten during copyTemplateFiles
47
+ }
48
+ } else if (targetDir !== '.') {
49
+ mkdirSync(targetPath, { recursive: true });
50
+ }
51
+
52
+ // Handle current directory case
53
+ if (targetDir === '.' && !isDirectoryEmpty(targetPath)) {
54
+ if (!force) {
55
+ const shouldOverwrite = await confirm({
56
+ message:
57
+ 'Current directory is not empty. Do you want to overwrite existing files?'
58
+ });
59
+
60
+ if (isCancel(shouldOverwrite)) {
61
+ cancel('Operation cancelled');
62
+ return;
63
+ }
64
+
65
+ if (!shouldOverwrite) {
66
+ throw new Error('Operation cancelled by user');
67
+ }
68
+ }
69
+ }
70
+
71
+ copyTemplateFiles(templatePath, targetPath, variables);
72
+ }