create-esmx 3.0.0-rc.35 → 3.0.0-rc.37
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 +52 -0
- package/README.zh-CN.md +52 -0
- package/dist/cli.d.ts +5 -2
- package/dist/{integration.test.mjs → cli.integration.test.mjs} +94 -27
- package/dist/cli.mjs +156 -6
- package/dist/create.d.ts +2 -0
- package/dist/create.mjs +6 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.mjs +1 -276
- package/dist/project.d.ts +5 -0
- package/dist/project.mjs +46 -0
- package/dist/project.test.mjs +177 -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 +29 -0
- package/dist/types.mjs +0 -0
- package/dist/utils/template.test.mjs +1 -1
- package/package.json +4 -4
- package/src/{integration.test.ts → cli.integration.test.ts} +107 -52
- package/src/cli.ts +198 -6
- package/src/create.ts +8 -0
- package/src/index.ts +1 -384
- package/src/project.test.ts +225 -0
- package/src/project.ts +72 -0
- package/src/template.test.ts +135 -0
- package/src/template.ts +117 -0
- package/src/types.ts +31 -0
- package/src/utils/template.test.ts +1 -1
- package/template/vue2/README.md +1 -1
- package/template/vue2/package.json +1 -1
- package/template/vue2/src/components/hello-world.vue +0 -2
- package/dist/index.test.mjs +0 -123
- package/src/index.test.ts +0 -159
- /package/dist/{index.test.d.ts → cli.integration.test.d.ts} +0 -0
- /package/dist/{integration.test.d.ts → project.test.d.ts} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,276 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
statSync,
|
|
8
|
-
writeFileSync
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import {
|
|
13
|
-
cancel,
|
|
14
|
-
confirm,
|
|
15
|
-
intro,
|
|
16
|
-
isCancel,
|
|
17
|
-
note,
|
|
18
|
-
outro,
|
|
19
|
-
select,
|
|
20
|
-
text
|
|
21
|
-
} from "@clack/prompts";
|
|
22
|
-
import minimist from "minimist";
|
|
23
|
-
import color from "picocolors";
|
|
24
|
-
import {
|
|
25
|
-
formatProjectName,
|
|
26
|
-
getCommand,
|
|
27
|
-
replaceTemplateVariables
|
|
28
|
-
} from "./utils/index.mjs";
|
|
29
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
function getEsmxVersion() {
|
|
31
|
-
try {
|
|
32
|
-
const packageJsonPath = resolve(__dirname, "../package.json");
|
|
33
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
34
|
-
return packageJson.version || "latest";
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.warn("Failed to read esmx version, using latest version");
|
|
37
|
-
return "latest";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function getAvailableTemplates() {
|
|
41
|
-
const templateDir = resolve(__dirname, "../template");
|
|
42
|
-
const templates = [];
|
|
43
|
-
const templateFolders = readdirSync(templateDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
44
|
-
for (const folder of templateFolders) {
|
|
45
|
-
const name = folder;
|
|
46
|
-
const packageJsonPath = resolve(templateDir, folder, "package.json");
|
|
47
|
-
let description = `${name} template`;
|
|
48
|
-
if (existsSync(packageJsonPath)) {
|
|
49
|
-
try {
|
|
50
|
-
const packageJson = JSON.parse(
|
|
51
|
-
readFileSync(packageJsonPath, "utf-8")
|
|
52
|
-
);
|
|
53
|
-
if (packageJson.description) {
|
|
54
|
-
description = packageJson.description;
|
|
55
|
-
}
|
|
56
|
-
templates.push({
|
|
57
|
-
folder,
|
|
58
|
-
name,
|
|
59
|
-
description
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.warn(
|
|
63
|
-
`Warning: Failed to parse package.json for template '${folder}', skipping.`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return templates.sort((a, b) => a.name.localeCompare(b.name));
|
|
69
|
-
}
|
|
70
|
-
function showHelp(userAgent) {
|
|
71
|
-
const createCmd = getCommand("create", userAgent);
|
|
72
|
-
console.log(`
|
|
73
|
-
${color.reset(color.bold(color.blue("\u{1F680} Create Esmx Project")))}
|
|
74
|
-
|
|
75
|
-
${color.bold("Usage:")}
|
|
76
|
-
${createCmd} [project-name]
|
|
77
|
-
${createCmd} [project-name] [options]
|
|
78
|
-
|
|
79
|
-
${color.bold("Options:")}
|
|
80
|
-
-t, --template <template> Template to use (default: vue2)
|
|
81
|
-
-n, --name <name> Project name or path
|
|
82
|
-
-f, --force Force overwrite existing directory
|
|
83
|
-
-h, --help Show help information
|
|
84
|
-
-v, --version Show version number
|
|
85
|
-
|
|
86
|
-
${color.bold("Examples:")}
|
|
87
|
-
${createCmd} my-project
|
|
88
|
-
${createCmd} my-project -t vue2
|
|
89
|
-
${createCmd} my-project --force
|
|
90
|
-
${createCmd} . -f -t vue2
|
|
91
|
-
|
|
92
|
-
${color.bold("Available Templates:")}
|
|
93
|
-
${getAvailableTemplates().map((t) => ` ${t.folder.padEnd(25)} ${t.description}`).join("\n")}
|
|
94
|
-
|
|
95
|
-
For more information, visit: ${color.cyan("https://esmnext.com")}
|
|
96
|
-
`);
|
|
97
|
-
}
|
|
98
|
-
export async function createProject(options = {}) {
|
|
99
|
-
const { argv, cwd, userAgent } = options;
|
|
100
|
-
const commandLineArgs = argv || process.argv.slice(2);
|
|
101
|
-
const workingDir = cwd || process.cwd();
|
|
102
|
-
const parsedArgs = minimist(commandLineArgs, {
|
|
103
|
-
string: ["template", "name"],
|
|
104
|
-
boolean: ["help", "version", "force"],
|
|
105
|
-
alias: {
|
|
106
|
-
t: "template",
|
|
107
|
-
n: "name",
|
|
108
|
-
f: "force",
|
|
109
|
-
h: "help",
|
|
110
|
-
v: "version"
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (parsedArgs.help) {
|
|
114
|
-
showHelp(userAgent);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (parsedArgs.version) {
|
|
118
|
-
console.log(getEsmxVersion());
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
console.log();
|
|
122
|
-
intro(
|
|
123
|
-
color.reset(
|
|
124
|
-
color.bold(color.blue("\u{1F680} Welcome to Esmx Project Creator!"))
|
|
125
|
-
)
|
|
126
|
-
);
|
|
127
|
-
const projectNameInput = await getProjectName(
|
|
128
|
-
parsedArgs.name,
|
|
129
|
-
parsedArgs._[0]
|
|
130
|
-
);
|
|
131
|
-
if (isCancel(projectNameInput)) {
|
|
132
|
-
cancel("Operation cancelled");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const { packageName, targetDir } = formatProjectName(
|
|
136
|
-
projectNameInput,
|
|
137
|
-
workingDir
|
|
138
|
-
);
|
|
139
|
-
const templateType = await getTemplateType(parsedArgs.template);
|
|
140
|
-
if (isCancel(templateType)) {
|
|
141
|
-
cancel("Operation cancelled");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const installCommand = getCommand("install", userAgent);
|
|
145
|
-
const devCommand = getCommand("dev", userAgent);
|
|
146
|
-
const buildCommand = getCommand("build", userAgent);
|
|
147
|
-
const startCommand = getCommand("start", userAgent);
|
|
148
|
-
const buildTypeCommand = getCommand("build:type", userAgent);
|
|
149
|
-
const lintTypeCommand = getCommand("lint:type", userAgent);
|
|
150
|
-
await createProjectFromTemplate(
|
|
151
|
-
targetDir,
|
|
152
|
-
templateType,
|
|
153
|
-
workingDir,
|
|
154
|
-
parsedArgs.force,
|
|
155
|
-
{
|
|
156
|
-
projectName: packageName,
|
|
157
|
-
esmxVersion: getEsmxVersion(),
|
|
158
|
-
installCommand,
|
|
159
|
-
devCommand,
|
|
160
|
-
buildCommand,
|
|
161
|
-
startCommand,
|
|
162
|
-
buildTypeCommand,
|
|
163
|
-
lintTypeCommand
|
|
164
|
-
}
|
|
165
|
-
);
|
|
166
|
-
const installCmd = installCommand;
|
|
167
|
-
const devCmd = devCommand;
|
|
168
|
-
const nextSteps = [
|
|
169
|
-
color.reset(`1. ${color.cyan(`cd ${targetDir}`)}`),
|
|
170
|
-
color.reset(`2. ${color.cyan(installCmd)}`),
|
|
171
|
-
color.reset(`3. ${color.cyan("git init")} ${color.gray("(optional)")}`),
|
|
172
|
-
color.reset(`4. ${color.cyan(devCmd)}`)
|
|
173
|
-
];
|
|
174
|
-
note(nextSteps.join("\n"), "Next steps");
|
|
175
|
-
outro(color.reset(color.green("Happy coding! \u{1F389}")));
|
|
176
|
-
}
|
|
177
|
-
async function getProjectName(argName, positionalName) {
|
|
178
|
-
const providedName = argName || positionalName;
|
|
179
|
-
if (providedName) {
|
|
180
|
-
return providedName;
|
|
181
|
-
}
|
|
182
|
-
const projectName = await text({
|
|
183
|
-
message: "Project name or path:",
|
|
184
|
-
placeholder: "my-esmx-project",
|
|
185
|
-
validate: (value) => {
|
|
186
|
-
if (!value.trim()) {
|
|
187
|
-
return "Project name or path is required";
|
|
188
|
-
}
|
|
189
|
-
if (!/^[a-zA-Z0-9_.\/@-]+$/.test(value.trim())) {
|
|
190
|
-
return "Project name or path should only contain letters, numbers, hyphens, underscores, dots, and slashes";
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
return String(projectName).trim();
|
|
195
|
-
}
|
|
196
|
-
async function getTemplateType(argTemplate) {
|
|
197
|
-
const availableTemplates = getAvailableTemplates();
|
|
198
|
-
if (argTemplate && availableTemplates.some((t) => t.folder === argTemplate)) {
|
|
199
|
-
return argTemplate;
|
|
200
|
-
}
|
|
201
|
-
const options = availableTemplates.map((t) => ({
|
|
202
|
-
label: color.reset(color.gray(`${t.folder} - `) + color.bold(t.name)),
|
|
203
|
-
value: t.folder,
|
|
204
|
-
hint: t.description
|
|
205
|
-
}));
|
|
206
|
-
const template = await select({
|
|
207
|
-
message: "Select a template:",
|
|
208
|
-
options
|
|
209
|
-
});
|
|
210
|
-
return template;
|
|
211
|
-
}
|
|
212
|
-
function isDirectoryEmpty(dirPath) {
|
|
213
|
-
if (!existsSync(dirPath)) {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
const files = readdirSync(dirPath);
|
|
217
|
-
const nonHiddenFiles = files.filter((file) => !file.startsWith("."));
|
|
218
|
-
return nonHiddenFiles.length === 0;
|
|
219
|
-
}
|
|
220
|
-
async function createProjectFromTemplate(targetDir, templateType, workingDir, force, variables) {
|
|
221
|
-
const templatePath = resolve(__dirname, "../template", templateType);
|
|
222
|
-
const targetPath = targetDir === "." ? workingDir : resolve(workingDir, targetDir);
|
|
223
|
-
if (!existsSync(templatePath)) {
|
|
224
|
-
throw new Error(`Template "${templateType}" not found`);
|
|
225
|
-
}
|
|
226
|
-
if (targetDir !== "." && existsSync(targetPath)) {
|
|
227
|
-
if (!isDirectoryEmpty(targetPath)) {
|
|
228
|
-
if (!force) {
|
|
229
|
-
const shouldOverwrite = await confirm({
|
|
230
|
-
message: `Directory "${targetDir}" is not empty. Do you want to overwrite it?`
|
|
231
|
-
});
|
|
232
|
-
if (isCancel(shouldOverwrite)) {
|
|
233
|
-
cancel("Operation cancelled");
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (!shouldOverwrite) {
|
|
237
|
-
throw new Error("Operation cancelled by user");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
} else if (targetDir !== ".") {
|
|
242
|
-
mkdirSync(targetPath, { recursive: true });
|
|
243
|
-
}
|
|
244
|
-
if (targetDir === "." && !isDirectoryEmpty(targetPath)) {
|
|
245
|
-
if (!force) {
|
|
246
|
-
const shouldOverwrite = await confirm({
|
|
247
|
-
message: "Current directory is not empty. Do you want to overwrite existing files?"
|
|
248
|
-
});
|
|
249
|
-
if (isCancel(shouldOverwrite)) {
|
|
250
|
-
cancel("Operation cancelled");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (!shouldOverwrite) {
|
|
254
|
-
throw new Error("Operation cancelled by user");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
copyTemplateFiles(templatePath, targetPath, variables);
|
|
259
|
-
}
|
|
260
|
-
function copyTemplateFiles(templatePath, targetPath, variables) {
|
|
261
|
-
const files = readdirSync(templatePath);
|
|
262
|
-
for (const file of files) {
|
|
263
|
-
const filePath = join(templatePath, file);
|
|
264
|
-
const targetFilePath = join(targetPath, file);
|
|
265
|
-
const stat = statSync(filePath);
|
|
266
|
-
if (stat.isDirectory()) {
|
|
267
|
-
mkdirSync(targetFilePath, { recursive: true });
|
|
268
|
-
copyTemplateFiles(filePath, targetFilePath, variables);
|
|
269
|
-
} else {
|
|
270
|
-
let content = readFileSync(filePath, "utf-8");
|
|
271
|
-
content = replaceTemplateVariables(content, variables);
|
|
272
|
-
writeFileSync(targetFilePath, content);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
export default createProject;
|
|
1
|
+
export { cli } from "./cli.mjs";
|
package/dist/project.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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.mjs";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export async function createProjectFromTemplate(targetDir, templateType, workingDir, force, variables) {
|
|
8
|
+
const templatePath = resolve(__dirname, "../template", templateType);
|
|
9
|
+
const targetPath = targetDir === "." ? workingDir : resolve(workingDir, targetDir);
|
|
10
|
+
if (!existsSync(templatePath)) {
|
|
11
|
+
throw new Error(`Template "${templateType}" not found`);
|
|
12
|
+
}
|
|
13
|
+
if (targetDir !== "." && existsSync(targetPath)) {
|
|
14
|
+
if (!isDirectoryEmpty(targetPath)) {
|
|
15
|
+
if (!force) {
|
|
16
|
+
const shouldOverwrite = await confirm({
|
|
17
|
+
message: `Directory "${targetDir}" is not empty. Do you want to overwrite it?`
|
|
18
|
+
});
|
|
19
|
+
if (isCancel(shouldOverwrite)) {
|
|
20
|
+
cancel("Operation cancelled");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!shouldOverwrite) {
|
|
24
|
+
throw new Error("Operation cancelled by user");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (targetDir !== ".") {
|
|
29
|
+
mkdirSync(targetPath, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
if (targetDir === "." && !isDirectoryEmpty(targetPath)) {
|
|
32
|
+
if (!force) {
|
|
33
|
+
const shouldOverwrite = await confirm({
|
|
34
|
+
message: "Current directory is not empty. Do you want to overwrite existing files?"
|
|
35
|
+
});
|
|
36
|
+
if (isCancel(shouldOverwrite)) {
|
|
37
|
+
cancel("Operation cancelled");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!shouldOverwrite) {
|
|
41
|
+
throw new Error("Operation cancelled by user");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
copyTemplateFiles(templatePath, targetPath, variables);
|
|
46
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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.mjs";
|
|
7
|
+
import { getEsmxVersion } from "./template.mjs";
|
|
8
|
+
import { formatProjectName } from "./utils/index.mjs";
|
|
9
|
+
async function createTempDir(prefix = "esmx-unit-test-") {
|
|
10
|
+
return mkdtemp(join(tmpdir(), prefix));
|
|
11
|
+
}
|
|
12
|
+
async function cleanupTempDir(tempDir) {
|
|
13
|
+
try {
|
|
14
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn(`Failed to cleanup temp directory: ${tempDir}`, error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
describe("project unit tests", () => {
|
|
20
|
+
let tmpDir;
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tmpDir = await createTempDir();
|
|
23
|
+
});
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await cleanupTempDir(tmpDir);
|
|
26
|
+
});
|
|
27
|
+
it("should handle isDirectoryEmpty edge cases", async () => {
|
|
28
|
+
const hiddenFilesDir = join(tmpDir, "hidden-files-dir");
|
|
29
|
+
await mkdir(hiddenFilesDir, { recursive: true });
|
|
30
|
+
await writeFile(join(hiddenFilesDir, ".hidden-file"), "hidden content");
|
|
31
|
+
await writeFile(join(hiddenFilesDir, ".gitignore"), "node_modules/");
|
|
32
|
+
const projectNameInput = "hidden-files-dir";
|
|
33
|
+
const { packageName, targetDir } = formatProjectName(
|
|
34
|
+
projectNameInput,
|
|
35
|
+
tmpDir
|
|
36
|
+
);
|
|
37
|
+
await createProjectFromTemplate(targetDir, "vue2", tmpDir, false, {
|
|
38
|
+
projectName: packageName,
|
|
39
|
+
esmxVersion: getEsmxVersion(),
|
|
40
|
+
installCommand: "npm install",
|
|
41
|
+
devCommand: "npm run dev",
|
|
42
|
+
buildCommand: "npm run build",
|
|
43
|
+
startCommand: "npm start",
|
|
44
|
+
buildTypeCommand: "npm run build:type",
|
|
45
|
+
lintTypeCommand: "npm run lint:type"
|
|
46
|
+
});
|
|
47
|
+
expect(existsSync(join(hiddenFilesDir, "package.json"))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("should handle directory creation for nested paths", async () => {
|
|
50
|
+
const deepPath = join(
|
|
51
|
+
tmpDir,
|
|
52
|
+
"very",
|
|
53
|
+
"deep",
|
|
54
|
+
"nested",
|
|
55
|
+
"path",
|
|
56
|
+
"project"
|
|
57
|
+
);
|
|
58
|
+
const projectNameInput = "very/deep/nested/path/project";
|
|
59
|
+
const { packageName, targetDir } = formatProjectName(
|
|
60
|
+
projectNameInput,
|
|
61
|
+
tmpDir
|
|
62
|
+
);
|
|
63
|
+
await createProjectFromTemplate(targetDir, "vue2", tmpDir, false, {
|
|
64
|
+
projectName: packageName,
|
|
65
|
+
esmxVersion: getEsmxVersion(),
|
|
66
|
+
installCommand: "npm install",
|
|
67
|
+
devCommand: "npm run dev",
|
|
68
|
+
buildCommand: "npm run build",
|
|
69
|
+
startCommand: "npm start",
|
|
70
|
+
buildTypeCommand: "npm run build:type",
|
|
71
|
+
lintTypeCommand: "npm run lint:type"
|
|
72
|
+
});
|
|
73
|
+
expect(existsSync(deepPath)).toBe(true);
|
|
74
|
+
expect(existsSync(join(deepPath, "package.json"))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("should handle file copy with template variable replacement", async () => {
|
|
77
|
+
const projectPath = join(tmpDir, "variable-test");
|
|
78
|
+
const projectNameInput = "variable-test";
|
|
79
|
+
const { packageName, targetDir } = formatProjectName(
|
|
80
|
+
projectNameInput,
|
|
81
|
+
tmpDir
|
|
82
|
+
);
|
|
83
|
+
await createProjectFromTemplate(targetDir, "vue2", tmpDir, false, {
|
|
84
|
+
projectName: packageName,
|
|
85
|
+
esmxVersion: getEsmxVersion(),
|
|
86
|
+
installCommand: "npm install",
|
|
87
|
+
devCommand: "npm run dev",
|
|
88
|
+
buildCommand: "npm run build",
|
|
89
|
+
startCommand: "npm start",
|
|
90
|
+
buildTypeCommand: "npm run build:type",
|
|
91
|
+
lintTypeCommand: "npm run lint:type"
|
|
92
|
+
});
|
|
93
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
94
|
+
expect(existsSync(packageJsonPath)).toBe(true);
|
|
95
|
+
const packageContent = require("node:fs").readFileSync(
|
|
96
|
+
packageJsonPath,
|
|
97
|
+
"utf-8"
|
|
98
|
+
);
|
|
99
|
+
const packageJson = JSON.parse(packageContent);
|
|
100
|
+
expect(packageJson.name).toBe("variable-test");
|
|
101
|
+
});
|
|
102
|
+
it("should handle empty directory detection correctly", async () => {
|
|
103
|
+
const emptyDir = join(tmpDir, "empty-dir");
|
|
104
|
+
await mkdir(emptyDir, { recursive: true });
|
|
105
|
+
const projectNameInput = "empty-dir";
|
|
106
|
+
const { packageName, targetDir } = formatProjectName(
|
|
107
|
+
projectNameInput,
|
|
108
|
+
tmpDir
|
|
109
|
+
);
|
|
110
|
+
await createProjectFromTemplate(targetDir, "vue2", tmpDir, false, {
|
|
111
|
+
projectName: packageName,
|
|
112
|
+
esmxVersion: getEsmxVersion(),
|
|
113
|
+
installCommand: "npm install",
|
|
114
|
+
devCommand: "npm run dev",
|
|
115
|
+
buildCommand: "npm run build",
|
|
116
|
+
startCommand: "npm start",
|
|
117
|
+
buildTypeCommand: "npm run build:type",
|
|
118
|
+
lintTypeCommand: "npm run lint:type"
|
|
119
|
+
});
|
|
120
|
+
expect(existsSync(join(emptyDir, "package.json"))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
it("should handle mixed file types in directory", async () => {
|
|
123
|
+
const mixedDir = join(tmpDir, "mixed-dir");
|
|
124
|
+
await mkdir(mixedDir, { recursive: true });
|
|
125
|
+
await writeFile(join(mixedDir, ".dotfile"), "hidden");
|
|
126
|
+
await writeFile(join(mixedDir, "regular-file.txt"), "visible");
|
|
127
|
+
const projectNameInput = "mixed-dir";
|
|
128
|
+
const { packageName, targetDir } = formatProjectName(
|
|
129
|
+
projectNameInput,
|
|
130
|
+
tmpDir
|
|
131
|
+
);
|
|
132
|
+
await createProjectFromTemplate(
|
|
133
|
+
targetDir,
|
|
134
|
+
"vue2",
|
|
135
|
+
tmpDir,
|
|
136
|
+
true,
|
|
137
|
+
// force flag
|
|
138
|
+
{
|
|
139
|
+
projectName: packageName,
|
|
140
|
+
esmxVersion: getEsmxVersion(),
|
|
141
|
+
installCommand: "npm install",
|
|
142
|
+
devCommand: "npm run dev",
|
|
143
|
+
buildCommand: "npm run build",
|
|
144
|
+
startCommand: "npm start",
|
|
145
|
+
buildTypeCommand: "npm run build:type",
|
|
146
|
+
lintTypeCommand: "npm run lint:type"
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
expect(existsSync(join(mixedDir, "package.json"))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it("should handle special characters in project names", async () => {
|
|
152
|
+
const specialNames = [
|
|
153
|
+
"project-with-dashes",
|
|
154
|
+
"project_with_underscores",
|
|
155
|
+
"project.with.dots"
|
|
156
|
+
];
|
|
157
|
+
for (const projectName of specialNames) {
|
|
158
|
+
const projectPath = join(tmpDir, projectName);
|
|
159
|
+
const { packageName, targetDir } = formatProjectName(
|
|
160
|
+
projectName,
|
|
161
|
+
tmpDir
|
|
162
|
+
);
|
|
163
|
+
await createProjectFromTemplate(targetDir, "vue2", tmpDir, false, {
|
|
164
|
+
projectName: packageName,
|
|
165
|
+
esmxVersion: getEsmxVersion(),
|
|
166
|
+
installCommand: "npm install",
|
|
167
|
+
devCommand: "npm run dev",
|
|
168
|
+
buildCommand: "npm run build",
|
|
169
|
+
startCommand: "npm start",
|
|
170
|
+
buildTypeCommand: "npm run build:type",
|
|
171
|
+
lintTypeCommand: "npm run lint:type"
|
|
172
|
+
});
|
|
173
|
+
expect(existsSync(projectPath)).toBe(true);
|
|
174
|
+
expect(existsSync(join(projectPath, "package.json"))).toBe(true);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TemplateInfo, TemplateVariables } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get version of esmx from package.json
|
|
4
|
+
*/
|
|
5
|
+
export declare function getEsmxVersion(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Get list of available templates
|
|
8
|
+
*/
|
|
9
|
+
export declare function getAvailableTemplates(): TemplateInfo[];
|
|
10
|
+
/**
|
|
11
|
+
* Check if directory is empty (ignoring hidden files)
|
|
12
|
+
*/
|
|
13
|
+
export declare function isDirectoryEmpty(dirPath: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Copy template files to target directory with variable replacement
|
|
16
|
+
*/
|
|
17
|
+
export declare function copyTemplateFiles(templatePath: string, targetPath: string, variables: TemplateVariables): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { replaceTemplateVariables } from "./utils/index.mjs";
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export function getEsmxVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const packageJsonPath = resolve(__dirname, "../package.json");
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
17
|
+
return packageJson.version || "latest";
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.warn("Failed to read esmx version, using latest version");
|
|
20
|
+
return "latest";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function getAvailableTemplates() {
|
|
24
|
+
const templateDir = resolve(__dirname, "../template");
|
|
25
|
+
const templates = [];
|
|
26
|
+
const templateFolders = readdirSync(templateDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
27
|
+
for (const folder of templateFolders) {
|
|
28
|
+
const name = folder;
|
|
29
|
+
const packageJsonPath = resolve(templateDir, folder, "package.json");
|
|
30
|
+
let description = `${name} template`;
|
|
31
|
+
if (existsSync(packageJsonPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const packageJson = JSON.parse(
|
|
34
|
+
readFileSync(packageJsonPath, "utf-8")
|
|
35
|
+
);
|
|
36
|
+
if (packageJson.description) {
|
|
37
|
+
description = packageJson.description;
|
|
38
|
+
}
|
|
39
|
+
templates.push({
|
|
40
|
+
folder,
|
|
41
|
+
name,
|
|
42
|
+
description
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`Warning: Failed to parse package.json for template '${folder}', skipping.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return templates.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
export function isDirectoryEmpty(dirPath) {
|
|
54
|
+
if (!existsSync(dirPath)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const files = readdirSync(dirPath);
|
|
58
|
+
const nonHiddenFiles = files.filter((file) => !file.startsWith("."));
|
|
59
|
+
return nonHiddenFiles.length === 0;
|
|
60
|
+
}
|
|
61
|
+
export function copyTemplateFiles(templatePath, targetPath, variables) {
|
|
62
|
+
const files = readdirSync(templatePath);
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const filePath = join(templatePath, file);
|
|
65
|
+
const targetFilePath = join(targetPath, file);
|
|
66
|
+
const stat = statSync(filePath);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
mkdirSync(targetFilePath, { recursive: true });
|
|
69
|
+
copyTemplateFiles(filePath, targetFilePath, variables);
|
|
70
|
+
} else {
|
|
71
|
+
let content = readFileSync(filePath, "utf-8");
|
|
72
|
+
content = replaceTemplateVariables(content, variables);
|
|
73
|
+
writeFileSync(targetFilePath, content);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|