create-esmx 3.0.0-rc.104

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/README.zh-CN.md +52 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.integration.test.d.ts +1 -0
  6. package/dist/cli.integration.test.mjs +238 -0
  7. package/dist/cli.mjs +166 -0
  8. package/dist/create.d.ts +2 -0
  9. package/dist/create.mjs +6 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/project.d.ts +5 -0
  13. package/dist/project.mjs +46 -0
  14. package/dist/project.test.d.ts +1 -0
  15. package/dist/project.test.mjs +155 -0
  16. package/dist/template.d.ts +17 -0
  17. package/dist/template.mjs +76 -0
  18. package/dist/template.test.d.ts +1 -0
  19. package/dist/template.test.mjs +106 -0
  20. package/dist/types.d.ts +30 -0
  21. package/dist/types.mjs +0 -0
  22. package/dist/utils/index.d.ts +3 -0
  23. package/dist/utils/index.mjs +7 -0
  24. package/dist/utils/package-manager.d.ts +10 -0
  25. package/dist/utils/package-manager.mjs +49 -0
  26. package/dist/utils/package-manager.test.d.ts +4 -0
  27. package/dist/utils/package-manager.test.mjs +275 -0
  28. package/dist/utils/project-name.d.ts +48 -0
  29. package/dist/utils/project-name.mjs +42 -0
  30. package/dist/utils/project-name.test.d.ts +1 -0
  31. package/dist/utils/project-name.test.mjs +332 -0
  32. package/dist/utils/template.d.ts +19 -0
  33. package/dist/utils/template.mjs +8 -0
  34. package/dist/utils/template.test.d.ts +4 -0
  35. package/dist/utils/template.test.mjs +150 -0
  36. package/package.json +75 -0
  37. package/src/cli.integration.test.ts +289 -0
  38. package/src/cli.ts +214 -0
  39. package/src/create.ts +8 -0
  40. package/src/index.ts +3 -0
  41. package/src/project.test.ts +200 -0
  42. package/src/project.ts +75 -0
  43. package/src/template.test.ts +135 -0
  44. package/src/template.ts +117 -0
  45. package/src/types.ts +32 -0
  46. package/src/utils/index.ts +11 -0
  47. package/src/utils/package-manager.test.ts +540 -0
  48. package/src/utils/package-manager.ts +92 -0
  49. package/src/utils/project-name.test.ts +441 -0
  50. package/src/utils/project-name.ts +101 -0
  51. package/src/utils/template.test.ts +234 -0
  52. package/src/utils/template.ts +34 -0
  53. package/template/react-csr/README.md +81 -0
  54. package/template/react-csr/package.json +29 -0
  55. package/template/react-csr/src/app.css +98 -0
  56. package/template/react-csr/src/app.tsx +26 -0
  57. package/template/react-csr/src/components/hello-world.css +48 -0
  58. package/template/react-csr/src/components/hello-world.tsx +29 -0
  59. package/template/react-csr/src/create-app.tsx +9 -0
  60. package/template/react-csr/src/entry.client.ts +13 -0
  61. package/template/react-csr/src/entry.node.ts +35 -0
  62. package/template/react-csr/src/entry.server.tsx +27 -0
  63. package/template/react-csr/tsconfig.json +27 -0
  64. package/template/react-ssr/README.md +81 -0
  65. package/template/react-ssr/package.json +29 -0
  66. package/template/react-ssr/src/app.css +98 -0
  67. package/template/react-ssr/src/app.tsx +26 -0
  68. package/template/react-ssr/src/components/hello-world.css +48 -0
  69. package/template/react-ssr/src/components/hello-world.tsx +29 -0
  70. package/template/react-ssr/src/create-app.tsx +9 -0
  71. package/template/react-ssr/src/entry.client.ts +13 -0
  72. package/template/react-ssr/src/entry.node.ts +32 -0
  73. package/template/react-ssr/src/entry.server.tsx +36 -0
  74. package/template/react-ssr/tsconfig.json +27 -0
  75. package/template/shared-modules/README.md +85 -0
  76. package/template/shared-modules/package.json +28 -0
  77. package/template/shared-modules/src/entry.client.ts +50 -0
  78. package/template/shared-modules/src/entry.node.ts +67 -0
  79. package/template/shared-modules/src/entry.server.ts +299 -0
  80. package/template/shared-modules/src/index.ts +3 -0
  81. package/template/shared-modules/src/vue/index.ts +1 -0
  82. package/template/shared-modules/src/vue2/index.ts +1 -0
  83. package/template/shared-modules/tsconfig.json +26 -0
  84. package/template/vue-csr/README.md +80 -0
  85. package/template/vue-csr/package.json +26 -0
  86. package/template/vue-csr/src/app.vue +127 -0
  87. package/template/vue-csr/src/components/hello-world.vue +77 -0
  88. package/template/vue-csr/src/create-app.ts +9 -0
  89. package/template/vue-csr/src/entry.client.ts +5 -0
  90. package/template/vue-csr/src/entry.node.ts +35 -0
  91. package/template/vue-csr/src/entry.server.ts +26 -0
  92. package/template/vue-csr/tsconfig.json +26 -0
  93. package/template/vue-ssr/README.md +80 -0
  94. package/template/vue-ssr/package.json +27 -0
  95. package/template/vue-ssr/src/app.vue +127 -0
  96. package/template/vue-ssr/src/components/hello-world.vue +77 -0
  97. package/template/vue-ssr/src/create-app.ts +9 -0
  98. package/template/vue-ssr/src/entry.client.ts +5 -0
  99. package/template/vue-ssr/src/entry.node.ts +37 -0
  100. package/template/vue-ssr/src/entry.server.ts +30 -0
  101. package/template/vue-ssr/tsconfig.json +26 -0
  102. package/template/vue2-csr/README.md +80 -0
  103. package/template/vue2-csr/package.json +26 -0
  104. package/template/vue2-csr/src/app.vue +127 -0
  105. package/template/vue2-csr/src/components/hello-world.vue +77 -0
  106. package/template/vue2-csr/src/create-app.ts +11 -0
  107. package/template/vue2-csr/src/entry.client.ts +5 -0
  108. package/template/vue2-csr/src/entry.node.ts +35 -0
  109. package/template/vue2-csr/src/entry.server.ts +26 -0
  110. package/template/vue2-csr/tsconfig.json +26 -0
  111. package/template/vue2-ssr/README.md +80 -0
  112. package/template/vue2-ssr/package.json +27 -0
  113. package/template/vue2-ssr/src/app.vue +127 -0
  114. package/template/vue2-ssr/src/components/hello-world.vue +77 -0
  115. package/template/vue2-ssr/src/create-app.ts +11 -0
  116. package/template/vue2-ssr/src/entry.client.ts +5 -0
  117. package/template/vue2-ssr/src/entry.node.ts +32 -0
  118. package/template/vue2-ssr/src/entry.server.ts +37 -0
  119. package/template/vue2-ssr/tsconfig.json +26 -0
@@ -0,0 +1,200 @@
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
+ async function createTempDir(prefix = 'esmx-unit-test-'): Promise<string> {
11
+ return mkdtemp(join(tmpdir(), prefix));
12
+ }
13
+
14
+ async function cleanupTempDir(tmpDir: string): Promise<void> {
15
+ await rm(tmpDir, { recursive: true, force: true });
16
+ }
17
+
18
+ describe('project unit tests', () => {
19
+ let tmpDir: string;
20
+
21
+ beforeEach(async () => {
22
+ tmpDir = await createTempDir();
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await cleanupTempDir(tmpDir);
27
+ });
28
+
29
+ it('should handle isDirectoryEmpty edge cases', async () => {
30
+ const hiddenFilesDir = join(tmpDir, 'hidden-files-dir');
31
+ await mkdir(hiddenFilesDir, { recursive: true });
32
+ await writeFile(join(hiddenFilesDir, '.hidden-file'), 'hidden content');
33
+ await writeFile(join(hiddenFilesDir, '.gitignore'), 'node_modules/');
34
+
35
+ const projectNameInput = 'hidden-files-dir';
36
+ const { name, root } = formatProjectName(projectNameInput, tmpDir);
37
+
38
+ // Create project from template
39
+ await createProjectFromTemplate(root, 'vue2-csr', tmpDir, false, {
40
+ projectName: name,
41
+ esmxVersion: getEsmxVersion(),
42
+ installCommand: 'npm install',
43
+ devCommand: 'npm run dev',
44
+ buildCommand: 'npm run build',
45
+ startCommand: 'npm start',
46
+ buildTypeCommand: 'npm run build:type',
47
+ lintTypeCommand: 'npm run lint:type'
48
+ });
49
+
50
+ // Should succeed because hidden files are ignored
51
+ expect(existsSync(join(hiddenFilesDir, 'package.json'))).toBe(true);
52
+ });
53
+
54
+ it('should handle directory creation for nested paths', async () => {
55
+ const deepPath = join(
56
+ tmpDir,
57
+ 'very',
58
+ 'deep',
59
+ 'nested',
60
+ 'path',
61
+ 'project'
62
+ );
63
+
64
+ // Get project name and target directory
65
+ const projectNameInput = 'very/deep/nested/path/project';
66
+ const { name, root } = formatProjectName(projectNameInput, tmpDir);
67
+
68
+ // Create project from template
69
+ await createProjectFromTemplate(root, 'vue2-csr', tmpDir, false, {
70
+ projectName: name,
71
+ esmxVersion: getEsmxVersion(),
72
+ installCommand: 'npm install',
73
+ devCommand: 'npm run dev',
74
+ buildCommand: 'npm run build',
75
+ startCommand: 'npm start',
76
+ buildTypeCommand: 'npm run build:type',
77
+ lintTypeCommand: 'npm run lint:type'
78
+ });
79
+
80
+ expect(existsSync(deepPath)).toBe(true);
81
+ expect(existsSync(join(deepPath, 'package.json'))).toBe(true);
82
+ });
83
+
84
+ it('should handle file copy with template variable replacement', async () => {
85
+ const projectPath = join(tmpDir, 'variable-test');
86
+
87
+ // Get project name and target directory
88
+ const projectNameInput = 'variable-test';
89
+ const { name, root } = formatProjectName(projectNameInput, tmpDir);
90
+
91
+ // Create project from template
92
+ await createProjectFromTemplate(root, 'vue2-csr', tmpDir, false, {
93
+ projectName: name,
94
+ esmxVersion: getEsmxVersion(),
95
+ installCommand: 'npm install',
96
+ devCommand: 'npm run dev',
97
+ buildCommand: 'npm run build',
98
+ startCommand: 'npm start',
99
+ buildTypeCommand: 'npm run build:type',
100
+ lintTypeCommand: 'npm run lint:type'
101
+ });
102
+
103
+ // Verify that package.json contains replaced variables
104
+ const packageJsonPath = join(projectPath, 'package.json');
105
+ expect(existsSync(packageJsonPath)).toBe(true);
106
+
107
+ const packageContent = require('node:fs').readFileSync(
108
+ packageJsonPath,
109
+ 'utf-8'
110
+ );
111
+ const packageJson = JSON.parse(packageContent);
112
+ expect(packageJson.name).toBe('variable-test');
113
+ });
114
+
115
+ it('should handle empty directory detection correctly', async () => {
116
+ // Test completely empty directory
117
+ const emptyDir = join(tmpDir, 'empty-dir');
118
+ await mkdir(emptyDir, { recursive: true });
119
+
120
+ // Get project name and target directory
121
+ const projectNameInput = 'empty-dir';
122
+ const { name, root } = formatProjectName(projectNameInput, tmpDir);
123
+
124
+ // Create project from template
125
+ await createProjectFromTemplate(root, 'vue2-csr', tmpDir, false, {
126
+ projectName: name,
127
+ esmxVersion: getEsmxVersion(),
128
+ installCommand: 'npm install',
129
+ devCommand: 'npm run dev',
130
+ buildCommand: 'npm run build',
131
+ startCommand: 'npm start',
132
+ buildTypeCommand: 'npm run build:type',
133
+ lintTypeCommand: 'npm run lint:type'
134
+ });
135
+
136
+ expect(existsSync(join(emptyDir, 'package.json'))).toBe(true);
137
+ });
138
+
139
+ it('should handle mixed file types in directory', async () => {
140
+ // Test directory with mix of hidden and non-hidden files
141
+ const mixedDir = join(tmpDir, 'mixed-dir');
142
+ await mkdir(mixedDir, { recursive: true });
143
+ await writeFile(join(mixedDir, '.dotfile'), 'hidden');
144
+ await writeFile(join(mixedDir, 'regular-file.txt'), 'visible');
145
+
146
+ // Get project name and target directory
147
+ const projectNameInput = 'mixed-dir';
148
+ const { name, root } = formatProjectName(projectNameInput, tmpDir);
149
+
150
+ // Create project from template with force flag
151
+ await createProjectFromTemplate(
152
+ root,
153
+ 'vue2-csr',
154
+ tmpDir,
155
+ true, // force flag
156
+ {
157
+ projectName: name,
158
+ esmxVersion: getEsmxVersion(),
159
+ installCommand: 'npm install',
160
+ devCommand: 'npm run dev',
161
+ buildCommand: 'npm run build',
162
+ startCommand: 'npm start',
163
+ buildTypeCommand: 'npm run build:type',
164
+ lintTypeCommand: 'npm run lint:type'
165
+ }
166
+ );
167
+
168
+ expect(existsSync(join(mixedDir, 'package.json'))).toBe(true);
169
+ });
170
+
171
+ it('should handle special characters in project names', async () => {
172
+ const specialNames = [
173
+ 'project-with-dashes',
174
+ 'project_with_underscores',
175
+ 'project.with.dots'
176
+ ];
177
+
178
+ for (const projectName of specialNames) {
179
+ const projectPath = join(tmpDir, projectName);
180
+
181
+ // Get project name and target directory
182
+ const { name, root } = formatProjectName(projectName, tmpDir);
183
+
184
+ // Create project from template
185
+ await createProjectFromTemplate(root, 'vue2-csr', tmpDir, false, {
186
+ projectName: name,
187
+ esmxVersion: getEsmxVersion(),
188
+ installCommand: 'npm install',
189
+ devCommand: 'npm run dev',
190
+ buildCommand: 'npm run build',
191
+ startCommand: 'npm start',
192
+ buildTypeCommand: 'npm run build:type',
193
+ lintTypeCommand: 'npm run lint:type'
194
+ });
195
+
196
+ expect(existsSync(projectPath)).toBe(true);
197
+ expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
198
+ }
199
+ });
200
+ });
package/src/project.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, isAbsolute, 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 = isAbsolute(targetDir)
22
+ ? targetDir
23
+ : targetDir === '.'
24
+ ? workingDir
25
+ : resolve(workingDir, targetDir);
26
+
27
+ if (!existsSync(templatePath)) {
28
+ throw new Error(`Template "${templateType}" not found`);
29
+ }
30
+
31
+ // Handle directory existence and overwrite confirmation
32
+ if (targetDir !== '.' && existsSync(targetPath)) {
33
+ if (!isDirectoryEmpty(targetPath)) {
34
+ if (!force) {
35
+ const shouldOverwrite = await confirm({
36
+ message: `Directory "${targetDir}" is not empty. Do you want to overwrite it?`
37
+ });
38
+
39
+ if (isCancel(shouldOverwrite)) {
40
+ cancel('Operation cancelled');
41
+ return;
42
+ }
43
+
44
+ if (!shouldOverwrite) {
45
+ throw new Error('Operation cancelled by user');
46
+ }
47
+ }
48
+
49
+ // Files will be overwritten during copyTemplateFiles
50
+ }
51
+ } else if (targetDir !== '.') {
52
+ mkdirSync(targetPath, { recursive: true });
53
+ }
54
+
55
+ // Handle current directory case
56
+ if (targetDir === '.' && !isDirectoryEmpty(targetPath)) {
57
+ if (!force) {
58
+ const shouldOverwrite = await confirm({
59
+ message:
60
+ 'Current directory is not empty. Do you want to overwrite existing files?'
61
+ });
62
+
63
+ if (isCancel(shouldOverwrite)) {
64
+ cancel('Operation cancelled');
65
+ return;
66
+ }
67
+
68
+ if (!shouldOverwrite) {
69
+ throw new Error('Operation cancelled by user');
70
+ }
71
+ }
72
+ }
73
+
74
+ copyTemplateFiles(templatePath, targetPath, variables);
75
+ }
@@ -0,0 +1,135 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { mkdir, mkdtemp, rm } 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 {
7
+ copyTemplateFiles,
8
+ getAvailableTemplates,
9
+ getEsmxVersion,
10
+ isDirectoryEmpty
11
+ } from './template';
12
+
13
+ // Test utilities
14
+ async function createTempDir(prefix = 'esmx-template-test-'): Promise<string> {
15
+ return mkdtemp(join(tmpdir(), prefix));
16
+ }
17
+
18
+ async function cleanupTempDir(tempDir: string): Promise<void> {
19
+ try {
20
+ await rm(tempDir, { recursive: true, force: true });
21
+ } catch (error) {
22
+ console.warn(`Failed to cleanup temp directory: ${tempDir}`, error);
23
+ }
24
+ }
25
+
26
+ describe('template unit tests', () => {
27
+ let tmpDir: string;
28
+
29
+ beforeEach(async () => {
30
+ tmpDir = await createTempDir();
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await cleanupTempDir(tmpDir);
35
+ });
36
+
37
+ it('should get Esmx version from package.json', () => {
38
+ const version = getEsmxVersion();
39
+ // 版本可能是 x.y.z 格式,也可能是 x.y.z-rc.n 格式,或 'latest'
40
+ expect(typeof version).toBe('string');
41
+ expect(version.length).toBeGreaterThan(0);
42
+ });
43
+
44
+ it('should get available templates', () => {
45
+ const templates = getAvailableTemplates();
46
+ expect(templates.length).toBeGreaterThan(0);
47
+ expect(templates[0]).toHaveProperty('folder');
48
+ expect(templates[0]).toHaveProperty('name');
49
+ expect(templates[0]).toHaveProperty('description');
50
+ });
51
+
52
+ it('should detect empty directory', async () => {
53
+ // Create empty directory
54
+ const emptyDir = join(tmpDir, 'empty');
55
+ await mkdir(emptyDir);
56
+ expect(isDirectoryEmpty(emptyDir)).toBe(true);
57
+
58
+ // Create directory with only hidden files
59
+ const hiddenDir = join(tmpDir, 'hidden');
60
+ await mkdir(hiddenDir);
61
+ writeFileSync(join(hiddenDir, '.hiddenfile'), 'hidden');
62
+ expect(isDirectoryEmpty(hiddenDir)).toBe(true);
63
+
64
+ // Create directory with visible files
65
+ const nonEmptyDir = join(tmpDir, 'non-empty');
66
+ await mkdir(nonEmptyDir);
67
+ writeFileSync(join(nonEmptyDir, 'visible.txt'), 'visible');
68
+ expect(isDirectoryEmpty(nonEmptyDir)).toBe(false);
69
+ });
70
+
71
+ it('should copy template files with variable replacement', () => {
72
+ // Create a simple template
73
+ const templateDir = join(tmpDir, 'template');
74
+ const targetDir = join(tmpDir, 'target');
75
+
76
+ mkdirSync(templateDir, { recursive: true });
77
+ mkdirSync(join(templateDir, 'src'), { recursive: true });
78
+ mkdirSync(targetDir, { recursive: true });
79
+
80
+ // Create template files with variables
81
+ writeFileSync(
82
+ join(templateDir, 'package.json'),
83
+ JSON.stringify({
84
+ name: '{{projectName}}',
85
+ version: '1.0.0',
86
+ dependencies: {
87
+ esmx: '{{esmxVersion}}'
88
+ },
89
+ scripts: {
90
+ dev: '{{devCommand}}',
91
+ build: '{{buildCommand}}'
92
+ }
93
+ })
94
+ );
95
+
96
+ writeFileSync(
97
+ join(templateDir, 'src', 'index.ts'),
98
+ 'console.log("Welcome to {{projectName}}!");'
99
+ );
100
+
101
+ // Copy with variable replacement
102
+ copyTemplateFiles(templateDir, targetDir, {
103
+ projectName: 'test-project',
104
+ esmxVersion: '1.2.3',
105
+ devCommand: 'npm run dev',
106
+ buildCommand: 'npm run build',
107
+ installCommand: 'npm install',
108
+ startCommand: 'npm start',
109
+ buildTypeCommand: 'npm run build:type',
110
+ lintTypeCommand: 'npm run lint:type'
111
+ });
112
+
113
+ // Check that files were created
114
+ expect(existsSync(join(targetDir, 'package.json'))).toBe(true);
115
+ expect(existsSync(join(targetDir, 'src', 'index.ts'))).toBe(true);
116
+
117
+ // Check variable replacement in package.json
118
+ const packageJson = JSON.parse(
119
+ require('node:fs').readFileSync(
120
+ join(targetDir, 'package.json'),
121
+ 'utf-8'
122
+ )
123
+ );
124
+ expect(packageJson.name).toBe('test-project');
125
+ expect(packageJson.dependencies.esmx).toBe('1.2.3');
126
+ expect(packageJson.scripts.dev).toBe('npm run dev');
127
+
128
+ // Check variable replacement in source file
129
+ const indexContent = require('node:fs').readFileSync(
130
+ join(targetDir, 'src', 'index.ts'),
131
+ 'utf-8'
132
+ );
133
+ expect(indexContent).toBe('console.log("Welcome to test-project!");');
134
+ });
135
+ });
@@ -0,0 +1,117 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ statSync,
7
+ writeFileSync
8
+ } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import type { TemplateInfo, TemplateVariables } from './types';
12
+ import { replaceTemplateVariables } from './utils/index';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ /**
17
+ * Get version of esmx from package.json
18
+ */
19
+ export function getEsmxVersion(): string {
20
+ try {
21
+ const packageJsonPath = resolve(__dirname, '../package.json');
22
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
23
+
24
+ return packageJson.version || 'latest';
25
+ } catch (error) {
26
+ console.warn('Failed to read esmx version, using latest version');
27
+ return 'latest';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Get list of available templates
33
+ */
34
+ export function getAvailableTemplates(): TemplateInfo[] {
35
+ const templateDir = resolve(__dirname, '../template');
36
+
37
+ const templates: TemplateInfo[] = [];
38
+ const templateFolders = readdirSync(templateDir, { withFileTypes: true })
39
+ .filter((dirent) => dirent.isDirectory())
40
+ .map((dirent) => dirent.name);
41
+
42
+ for (const folder of templateFolders) {
43
+ // Use folder name as display name
44
+ const name = folder;
45
+
46
+ // Try to read description from package.json
47
+ const packageJsonPath = resolve(templateDir, folder, 'package.json');
48
+ let description = `${name} template`;
49
+
50
+ if (existsSync(packageJsonPath)) {
51
+ try {
52
+ const packageJson = JSON.parse(
53
+ readFileSync(packageJsonPath, 'utf-8')
54
+ );
55
+ if (packageJson.description) {
56
+ description = packageJson.description;
57
+ }
58
+ templates.push({
59
+ folder,
60
+ name,
61
+ description
62
+ });
63
+ } catch (error) {
64
+ // JSON parsing failed, skip this template
65
+ console.warn(
66
+ `Warning: Failed to parse package.json for template '${folder}', skipping.`
67
+ );
68
+ }
69
+ }
70
+ }
71
+
72
+ // Sort by name alphabetically
73
+ return templates.sort((a, b) => a.name.localeCompare(b.name));
74
+ }
75
+
76
+ /**
77
+ * Check if directory is empty (ignoring hidden files)
78
+ */
79
+ export function isDirectoryEmpty(dirPath: string): boolean {
80
+ if (!existsSync(dirPath)) {
81
+ return true;
82
+ }
83
+
84
+ const files = readdirSync(dirPath);
85
+ // Only consider non-hidden files and directories
86
+ const nonHiddenFiles = files.filter((file) => !file.startsWith('.'));
87
+ return nonHiddenFiles.length === 0;
88
+ }
89
+
90
+ /**
91
+ * Copy template files to target directory with variable replacement
92
+ */
93
+ export function copyTemplateFiles(
94
+ templatePath: string,
95
+ targetPath: string,
96
+ variables: TemplateVariables
97
+ ): void {
98
+ const files = readdirSync(templatePath);
99
+
100
+ for (const file of files) {
101
+ const filePath = join(templatePath, file);
102
+ const targetFilePath = join(targetPath, file);
103
+ const stat = statSync(filePath);
104
+
105
+ if (stat.isDirectory()) {
106
+ mkdirSync(targetFilePath, { recursive: true });
107
+ copyTemplateFiles(filePath, targetFilePath, variables);
108
+ } else {
109
+ let content = readFileSync(filePath, 'utf-8');
110
+
111
+ // Replace all template variables using the utility function
112
+ content = replaceTemplateVariables(content, variables);
113
+
114
+ writeFileSync(targetFilePath, content);
115
+ }
116
+ }
117
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Options for project creation
3
+ */
4
+ export interface CliOptions {
5
+ argv?: string[]; // Command line arguments
6
+ cwd?: string; // Working directory
7
+ userAgent?: string; // Package manager user agent
8
+ version?: string; // Esmx version override
9
+ }
10
+
11
+ /**
12
+ * Template information structure
13
+ */
14
+ export interface TemplateInfo {
15
+ folder: string;
16
+ name: string;
17
+ description: string;
18
+ }
19
+
20
+ /**
21
+ * Variables used in templates for replacement
22
+ */
23
+ export interface TemplateVariables extends Record<string, string> {
24
+ projectName: string;
25
+ esmxVersion: string;
26
+ installCommand: string;
27
+ devCommand: string;
28
+ buildCommand: string;
29
+ startCommand: string;
30
+ buildTypeCommand: string;
31
+ lintTypeCommand: string;
32
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ type CommandType,
3
+ getCommand
4
+ } from './package-manager';
5
+
6
+ export {
7
+ formatProjectName,
8
+ type ProjectNameResult
9
+ } from './project-name';
10
+
11
+ export { replaceTemplateVariables } from './template';