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.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/README.zh-CN.md +52 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.integration.test.d.ts +1 -0
- package/dist/cli.integration.test.mjs +238 -0
- package/dist/cli.mjs +166 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.mjs +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +2 -0
- package/dist/project.d.ts +5 -0
- package/dist/project.mjs +46 -0
- package/dist/project.test.d.ts +1 -0
- package/dist/project.test.mjs +155 -0
- package/dist/template.d.ts +17 -0
- package/dist/template.mjs +76 -0
- package/dist/template.test.d.ts +1 -0
- package/dist/template.test.mjs +106 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.mjs +0 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.mjs +7 -0
- package/dist/utils/package-manager.d.ts +10 -0
- package/dist/utils/package-manager.mjs +49 -0
- package/dist/utils/package-manager.test.d.ts +4 -0
- package/dist/utils/package-manager.test.mjs +275 -0
- package/dist/utils/project-name.d.ts +48 -0
- package/dist/utils/project-name.mjs +42 -0
- package/dist/utils/project-name.test.d.ts +1 -0
- package/dist/utils/project-name.test.mjs +332 -0
- package/dist/utils/template.d.ts +19 -0
- package/dist/utils/template.mjs +8 -0
- package/dist/utils/template.test.d.ts +4 -0
- package/dist/utils/template.test.mjs +150 -0
- package/package.json +75 -0
- package/src/cli.integration.test.ts +289 -0
- package/src/cli.ts +214 -0
- package/src/create.ts +8 -0
- package/src/index.ts +3 -0
- package/src/project.test.ts +200 -0
- package/src/project.ts +75 -0
- package/src/template.test.ts +135 -0
- package/src/template.ts +117 -0
- package/src/types.ts +32 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/package-manager.test.ts +540 -0
- package/src/utils/package-manager.ts +92 -0
- package/src/utils/project-name.test.ts +441 -0
- package/src/utils/project-name.ts +101 -0
- package/src/utils/template.test.ts +234 -0
- package/src/utils/template.ts +34 -0
- package/template/react-csr/README.md +81 -0
- package/template/react-csr/package.json +29 -0
- package/template/react-csr/src/app.css +98 -0
- package/template/react-csr/src/app.tsx +26 -0
- package/template/react-csr/src/components/hello-world.css +48 -0
- package/template/react-csr/src/components/hello-world.tsx +29 -0
- package/template/react-csr/src/create-app.tsx +9 -0
- package/template/react-csr/src/entry.client.ts +13 -0
- package/template/react-csr/src/entry.node.ts +35 -0
- package/template/react-csr/src/entry.server.tsx +27 -0
- package/template/react-csr/tsconfig.json +27 -0
- package/template/react-ssr/README.md +81 -0
- package/template/react-ssr/package.json +29 -0
- package/template/react-ssr/src/app.css +98 -0
- package/template/react-ssr/src/app.tsx +26 -0
- package/template/react-ssr/src/components/hello-world.css +48 -0
- package/template/react-ssr/src/components/hello-world.tsx +29 -0
- package/template/react-ssr/src/create-app.tsx +9 -0
- package/template/react-ssr/src/entry.client.ts +13 -0
- package/template/react-ssr/src/entry.node.ts +32 -0
- package/template/react-ssr/src/entry.server.tsx +36 -0
- package/template/react-ssr/tsconfig.json +27 -0
- package/template/shared-modules/README.md +85 -0
- package/template/shared-modules/package.json +28 -0
- package/template/shared-modules/src/entry.client.ts +50 -0
- package/template/shared-modules/src/entry.node.ts +67 -0
- package/template/shared-modules/src/entry.server.ts +299 -0
- package/template/shared-modules/src/index.ts +3 -0
- package/template/shared-modules/src/vue/index.ts +1 -0
- package/template/shared-modules/src/vue2/index.ts +1 -0
- package/template/shared-modules/tsconfig.json +26 -0
- package/template/vue-csr/README.md +80 -0
- package/template/vue-csr/package.json +26 -0
- package/template/vue-csr/src/app.vue +127 -0
- package/template/vue-csr/src/components/hello-world.vue +77 -0
- package/template/vue-csr/src/create-app.ts +9 -0
- package/template/vue-csr/src/entry.client.ts +5 -0
- package/template/vue-csr/src/entry.node.ts +35 -0
- package/template/vue-csr/src/entry.server.ts +26 -0
- package/template/vue-csr/tsconfig.json +26 -0
- package/template/vue-ssr/README.md +80 -0
- package/template/vue-ssr/package.json +27 -0
- package/template/vue-ssr/src/app.vue +127 -0
- package/template/vue-ssr/src/components/hello-world.vue +77 -0
- package/template/vue-ssr/src/create-app.ts +9 -0
- package/template/vue-ssr/src/entry.client.ts +5 -0
- package/template/vue-ssr/src/entry.node.ts +37 -0
- package/template/vue-ssr/src/entry.server.ts +30 -0
- package/template/vue-ssr/tsconfig.json +26 -0
- package/template/vue2-csr/README.md +80 -0
- package/template/vue2-csr/package.json +26 -0
- package/template/vue2-csr/src/app.vue +127 -0
- package/template/vue2-csr/src/components/hello-world.vue +77 -0
- package/template/vue2-csr/src/create-app.ts +11 -0
- package/template/vue2-csr/src/entry.client.ts +5 -0
- package/template/vue2-csr/src/entry.node.ts +35 -0
- package/template/vue2-csr/src/entry.server.ts +26 -0
- package/template/vue2-csr/tsconfig.json +26 -0
- package/template/vue2-ssr/README.md +80 -0
- package/template/vue2-ssr/package.json +27 -0
- package/template/vue2-ssr/src/app.vue +127 -0
- package/template/vue2-ssr/src/components/hello-world.vue +77 -0
- package/template/vue2-ssr/src/create-app.ts +11 -0
- package/template/vue2-ssr/src/entry.client.ts +5 -0
- package/template/vue2-ssr/src/entry.node.ts +32 -0
- package/template/vue2-ssr/src/entry.server.ts +37 -0
- 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
|
+
});
|
package/src/template.ts
ADDED
|
@@ -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
|
+
}
|