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.
@@ -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
+ 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 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,31 @@
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
+ }
9
+
10
+ /**
11
+ * Template information structure
12
+ */
13
+ export interface TemplateInfo {
14
+ folder: string;
15
+ name: string;
16
+ description: string;
17
+ }
18
+
19
+ /**
20
+ * Variables used in templates for replacement
21
+ */
22
+ export interface TemplateVariables extends Record<string, string> {
23
+ projectName: string;
24
+ esmxVersion: string;
25
+ installCommand: string;
26
+ devCommand: string;
27
+ buildCommand: string;
28
+ startCommand: string;
29
+ buildTypeCommand: string;
30
+ lintTypeCommand: string;
31
+ }
@@ -170,7 +170,7 @@ Esmx version: {{esmxVersion}}`;
170
170
  "scripts": {
171
171
  "dev": "esmx dev",
172
172
  "build": "esmx build",
173
- "start": "esmx start"
173
+ "start": "NODE_ENV=production node dist/index.mjs"
174
174
  },
175
175
  "dependencies": {
176
176
  "esmx": "{{esmxVersion}}"
@@ -60,7 +60,7 @@ Visit http://localhost:3000 to see the development environment.
60
60
  │ │ └── hello-world.vue # Example component with counter functionality
61
61
  │ ├── create-app.ts # Vue instance creation
62
62
  │ ├── entry.client.ts # Client-side entry
63
- │ ├── entry.node.ts # Server-side rendering entry
63
+ │ ├── entry.node.ts # Node.js environment entry point
64
64
  │ └── entry.server.ts # Server-side rendering functions
65
65
  ├── package.json
66
66
  ├── tsconfig.json
@@ -8,7 +8,7 @@
8
8
  "dev": "esmx dev",
9
9
  "build": "esmx build",
10
10
  "preview": "esmx preview",
11
- "start": "esmx start",
11
+ "start": "NODE_ENV=production node dist/index.mjs",
12
12
  "lint:type": "vue-tsc --noEmit",
13
13
  "build:type": "vue-tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist/src && tsc-alias -p tsconfig.json --outDir dist/src"
14
14
  },
@@ -28,11 +28,9 @@ const count = ref<number>(0);
28
28
  <style scoped>
29
29
  .card {
30
30
  padding: 2em;
31
- border: 1px solid var(--border-color);
32
31
  border-radius: 12px;
33
32
  margin: 2.5em 0;
34
33
  background-color: var(--bg-card);
35
- box-shadow: 0 4px 12px var(--shadow-color);
36
34
  }
37
35
 
38
36
  button {
@@ -1,123 +0,0 @@
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 { createProject } from "./index.mjs";
7
- async function createTempDir(prefix = "esmx-unit-test-") {
8
- return mkdtemp(join(tmpdir(), prefix));
9
- }
10
- async function cleanupTempDir(tempDir) {
11
- try {
12
- await rm(tempDir, { recursive: true, force: true });
13
- } catch (error) {
14
- console.warn(`Failed to cleanup temp directory: ${tempDir}`, error);
15
- }
16
- }
17
- describe("createProject unit tests", () => {
18
- let tmpDir;
19
- beforeEach(async () => {
20
- tmpDir = await createTempDir();
21
- });
22
- afterEach(async () => {
23
- await cleanupTempDir(tmpDir);
24
- });
25
- it("should handle isDirectoryEmpty edge cases", async () => {
26
- const hiddenFilesDir = join(tmpDir, "hidden-files-dir");
27
- await mkdir(hiddenFilesDir, { recursive: true });
28
- await writeFile(join(hiddenFilesDir, ".hidden-file"), "hidden content");
29
- await writeFile(join(hiddenFilesDir, ".gitignore"), "node_modules/");
30
- await createProject({
31
- argv: ["hidden-files-dir", "--template", "vue2"],
32
- cwd: tmpDir,
33
- userAgent: "npm/test"
34
- });
35
- expect(existsSync(join(hiddenFilesDir, "package.json"))).toBe(true);
36
- });
37
- it("should handle directory creation for nested paths", async () => {
38
- const deepPath = join(
39
- tmpDir,
40
- "very",
41
- "deep",
42
- "nested",
43
- "path",
44
- "project"
45
- );
46
- await createProject({
47
- argv: ["very/deep/nested/path/project", "--template", "vue2"],
48
- cwd: tmpDir,
49
- userAgent: "npm/test"
50
- });
51
- expect(existsSync(deepPath)).toBe(true);
52
- expect(existsSync(join(deepPath, "package.json"))).toBe(true);
53
- });
54
- it("should handle file copy with template variable replacement", async () => {
55
- const projectPath = join(tmpDir, "variable-test");
56
- await createProject({
57
- argv: ["variable-test", "--template", "vue2"],
58
- cwd: tmpDir,
59
- userAgent: "npm/test"
60
- });
61
- const packageJsonPath = join(projectPath, "package.json");
62
- expect(existsSync(packageJsonPath)).toBe(true);
63
- const packageContent = require("node:fs").readFileSync(
64
- packageJsonPath,
65
- "utf-8"
66
- );
67
- const packageJson = JSON.parse(packageContent);
68
- expect(packageJson.name).toBe("variable-test");
69
- });
70
- it("should handle empty directory detection correctly", async () => {
71
- const emptyDir = join(tmpDir, "empty-dir");
72
- await mkdir(emptyDir, { recursive: true });
73
- await createProject({
74
- argv: ["empty-dir", "--template", "vue2"],
75
- cwd: tmpDir,
76
- userAgent: "npm/test"
77
- });
78
- expect(existsSync(join(emptyDir, "package.json"))).toBe(true);
79
- });
80
- it("should handle mixed file types in directory", async () => {
81
- const mixedDir = join(tmpDir, "mixed-dir");
82
- await mkdir(mixedDir, { recursive: true });
83
- await writeFile(join(mixedDir, ".dotfile"), "hidden");
84
- await writeFile(join(mixedDir, "regular-file.txt"), "visible");
85
- await createProject({
86
- argv: ["mixed-dir", "--template", "vue2", "--force"],
87
- cwd: tmpDir,
88
- userAgent: "npm/test"
89
- });
90
- expect(existsSync(join(mixedDir, "package.json"))).toBe(true);
91
- });
92
- it("should handle various package manager user agents", async () => {
93
- const testCases = ["npm", "yarn", "pnpm", "bun"];
94
- for (const userAgent of testCases) {
95
- const projectName = `test-${userAgent}`;
96
- const projectPath = join(tmpDir, projectName);
97
- await createProject({
98
- argv: [projectName, "--template", "vue2"],
99
- cwd: tmpDir,
100
- userAgent: `${userAgent}/test-version`
101
- });
102
- expect(existsSync(projectPath)).toBe(true);
103
- expect(existsSync(join(projectPath, "package.json"))).toBe(true);
104
- }
105
- });
106
- it("should handle special characters in project names", async () => {
107
- const specialNames = [
108
- "project-with-dashes",
109
- "project_with_underscores",
110
- "project.with.dots"
111
- ];
112
- for (const projectName of specialNames) {
113
- const projectPath = join(tmpDir, projectName);
114
- await createProject({
115
- argv: [projectName, "--template", "vue2"],
116
- cwd: tmpDir,
117
- userAgent: "npm/test"
118
- });
119
- expect(existsSync(projectPath)).toBe(true);
120
- expect(existsSync(join(projectPath, "package.json"))).toBe(true);
121
- }
122
- });
123
- });
package/src/index.test.ts DELETED
@@ -1,159 +0,0 @@
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 { createProject } from './index';
7
-
8
- // Test utilities
9
- async function createTempDir(prefix = 'esmx-unit-test-'): Promise<string> {
10
- return mkdtemp(join(tmpdir(), prefix));
11
- }
12
-
13
- async function cleanupTempDir(tempDir: string): Promise<void> {
14
- try {
15
- await rm(tempDir, { recursive: true, force: true });
16
- } catch (error) {
17
- console.warn(`Failed to cleanup temp directory: ${tempDir}`, error);
18
- }
19
- }
20
-
21
- describe('createProject unit tests', () => {
22
- let tmpDir: string;
23
-
24
- beforeEach(async () => {
25
- tmpDir = await createTempDir();
26
- });
27
-
28
- afterEach(async () => {
29
- await cleanupTempDir(tmpDir);
30
- });
31
-
32
- it('should handle isDirectoryEmpty edge cases', async () => {
33
- // Test with directory containing only hidden files
34
- const hiddenFilesDir = join(tmpDir, 'hidden-files-dir');
35
- await mkdir(hiddenFilesDir, { recursive: true });
36
- await writeFile(join(hiddenFilesDir, '.hidden-file'), 'hidden content');
37
- await writeFile(join(hiddenFilesDir, '.gitignore'), 'node_modules/');
38
-
39
- await createProject({
40
- argv: ['hidden-files-dir', '--template', 'vue2'],
41
- cwd: tmpDir,
42
- userAgent: 'npm/test'
43
- });
44
-
45
- // Should succeed because hidden files are ignored
46
- expect(existsSync(join(hiddenFilesDir, 'package.json'))).toBe(true);
47
- });
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
-
59
- await createProject({
60
- argv: ['very/deep/nested/path/project', '--template', 'vue2'],
61
- cwd: tmpDir,
62
- userAgent: 'npm/test'
63
- });
64
-
65
- expect(existsSync(deepPath)).toBe(true);
66
- expect(existsSync(join(deepPath, 'package.json'))).toBe(true);
67
- });
68
-
69
- it('should handle file copy with template variable replacement', async () => {
70
- const projectPath = join(tmpDir, 'variable-test');
71
-
72
- await createProject({
73
- argv: ['variable-test', '--template', 'vue2'],
74
- cwd: tmpDir,
75
- userAgent: 'npm/test'
76
- });
77
-
78
- // Verify that package.json contains replaced variables
79
- const packageJsonPath = join(projectPath, 'package.json');
80
- expect(existsSync(packageJsonPath)).toBe(true);
81
-
82
- const packageContent = require('node:fs').readFileSync(
83
- packageJsonPath,
84
- 'utf-8'
85
- );
86
- const packageJson = JSON.parse(packageContent);
87
- expect(packageJson.name).toBe('variable-test');
88
- });
89
-
90
- it('should handle empty directory detection correctly', async () => {
91
- // Test completely empty directory
92
- const emptyDir = join(tmpDir, 'empty-dir');
93
- await mkdir(emptyDir, { recursive: true });
94
-
95
- await createProject({
96
- argv: ['empty-dir', '--template', 'vue2'],
97
- cwd: tmpDir,
98
- userAgent: 'npm/test'
99
- });
100
-
101
- expect(existsSync(join(emptyDir, 'package.json'))).toBe(true);
102
- });
103
-
104
- it('should handle mixed file types in directory', async () => {
105
- // Test directory with mix of hidden and non-hidden files
106
- const mixedDir = join(tmpDir, 'mixed-dir');
107
- await mkdir(mixedDir, { recursive: true });
108
- await writeFile(join(mixedDir, '.dotfile'), 'hidden');
109
- await writeFile(join(mixedDir, 'regular-file.txt'), 'visible');
110
-
111
- // This should require force flag since directory is not empty
112
- await createProject({
113
- argv: ['mixed-dir', '--template', 'vue2', '--force'],
114
- cwd: tmpDir,
115
- userAgent: 'npm/test'
116
- });
117
-
118
- expect(existsSync(join(mixedDir, 'package.json'))).toBe(true);
119
- });
120
-
121
- it('should handle various package manager user agents', async () => {
122
- const testCases = ['npm', 'yarn', 'pnpm', 'bun'];
123
-
124
- for (const userAgent of testCases) {
125
- const projectName = `test-${userAgent}`;
126
- const projectPath = join(tmpDir, projectName);
127
-
128
- await createProject({
129
- argv: [projectName, '--template', 'vue2'],
130
- cwd: tmpDir,
131
- userAgent: `${userAgent}/test-version`
132
- });
133
-
134
- expect(existsSync(projectPath)).toBe(true);
135
- expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
136
- }
137
- });
138
-
139
- it('should handle special characters in project names', async () => {
140
- const specialNames = [
141
- 'project-with-dashes',
142
- 'project_with_underscores',
143
- 'project.with.dots'
144
- ];
145
-
146
- for (const projectName of specialNames) {
147
- const projectPath = join(tmpDir, projectName);
148
-
149
- await createProject({
150
- argv: [projectName, '--template', 'vue2'],
151
- cwd: tmpDir,
152
- userAgent: 'npm/test'
153
- });
154
-
155
- expect(existsSync(projectPath)).toBe(true);
156
- expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
157
- }
158
- });
159
- });