@cloudwerk/create-app 0.0.1
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/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +42 -0
- package/template/_gitignore +5 -0
- package/template/app/routes/route.ts +6 -0
- package/template/cloudwerk.config.ts +5 -0
- package/template/package.json.tmpl +23 -0
- package/template/tsconfig.json +14 -0
- package/template/wrangler.toml.tmpl +6 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
|
|
7
|
+
// src/scaffold.ts
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import fs2 from "fs-extra";
|
|
11
|
+
|
|
12
|
+
// src/validate.ts
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var NPM_NAME_REGEX = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
16
|
+
function validateProjectName(name) {
|
|
17
|
+
if (!name || name.trim().length === 0) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: "Project name cannot be empty"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const trimmedName = name.trim();
|
|
24
|
+
if (trimmedName.includes("..") || trimmedName.includes("/") || trimmedName.includes("\\")) {
|
|
25
|
+
if (!trimmedName.startsWith("@") || (trimmedName.match(/\//g) || []).length > 1) {
|
|
26
|
+
return {
|
|
27
|
+
valid: false,
|
|
28
|
+
error: "Project name cannot contain path traversal characters (.. or / or \\)"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const reservedNames = ["node_modules", ".git", ".svn", ".hg", "package.json"];
|
|
33
|
+
if (reservedNames.includes(trimmedName)) {
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: `Project name "${trimmedName}" is reserved`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (trimmedName !== trimmedName.toLowerCase()) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
error: "Project name must be lowercase"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!NPM_NAME_REGEX.test(trimmedName)) {
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
error: "Project name must be a valid npm package name (lowercase letters, numbers, hyphens, dots, underscores)"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
53
|
+
function validateDirectory(dirPath) {
|
|
54
|
+
if (fs.existsSync(dirPath)) {
|
|
55
|
+
const stats = fs.statSync(dirPath);
|
|
56
|
+
if (stats.isDirectory()) {
|
|
57
|
+
const contents = fs.readdirSync(dirPath);
|
|
58
|
+
if (contents.length > 0) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: `Directory "${path.basename(dirPath)}" already exists and is not empty`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { valid: true };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: `"${path.basename(dirPath)}" already exists and is not a directory`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { valid: true };
|
|
72
|
+
}
|
|
73
|
+
function validateProject(projectName, targetDir) {
|
|
74
|
+
const nameResult = validateProjectName(projectName);
|
|
75
|
+
if (!nameResult.valid) {
|
|
76
|
+
return nameResult;
|
|
77
|
+
}
|
|
78
|
+
const resolvedDir = targetDir || path.resolve(process.cwd(), projectName);
|
|
79
|
+
return validateDirectory(resolvedDir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/utils.ts
|
|
83
|
+
import pc from "picocolors";
|
|
84
|
+
function detectPackageManager() {
|
|
85
|
+
const userAgent = process.env.npm_config_user_agent || process.env.USER_AGENT;
|
|
86
|
+
if (userAgent) {
|
|
87
|
+
if (userAgent.includes("pnpm")) {
|
|
88
|
+
return "pnpm";
|
|
89
|
+
}
|
|
90
|
+
if (userAgent.includes("yarn")) {
|
|
91
|
+
return "yarn";
|
|
92
|
+
}
|
|
93
|
+
if (userAgent.includes("bun")) {
|
|
94
|
+
return "bun";
|
|
95
|
+
}
|
|
96
|
+
if (userAgent.includes("npm")) {
|
|
97
|
+
return "npm";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "npm";
|
|
101
|
+
}
|
|
102
|
+
function getInstallCommand(pm) {
|
|
103
|
+
switch (pm) {
|
|
104
|
+
case "yarn":
|
|
105
|
+
return "yarn";
|
|
106
|
+
case "pnpm":
|
|
107
|
+
return "pnpm install";
|
|
108
|
+
case "bun":
|
|
109
|
+
return "bun install";
|
|
110
|
+
default:
|
|
111
|
+
return "npm install";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getDevCommand(pm) {
|
|
115
|
+
switch (pm) {
|
|
116
|
+
case "yarn":
|
|
117
|
+
return "yarn dev";
|
|
118
|
+
case "pnpm":
|
|
119
|
+
return "pnpm dev";
|
|
120
|
+
case "bun":
|
|
121
|
+
return "bun dev";
|
|
122
|
+
default:
|
|
123
|
+
return "npm run dev";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var logger = {
|
|
127
|
+
/**
|
|
128
|
+
* Log an info message with blue prefix.
|
|
129
|
+
*/
|
|
130
|
+
info(message) {
|
|
131
|
+
console.log(pc.blue("info") + " " + message);
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* Log a success message with green prefix.
|
|
135
|
+
*/
|
|
136
|
+
success(message) {
|
|
137
|
+
console.log(pc.green("success") + " " + message);
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Log an error message with red prefix.
|
|
141
|
+
*/
|
|
142
|
+
error(message) {
|
|
143
|
+
console.log(pc.red("error") + " " + message);
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* Log a warning message with yellow prefix.
|
|
147
|
+
*/
|
|
148
|
+
warn(message) {
|
|
149
|
+
console.log(pc.yellow("warn") + " " + message);
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* Log a plain message without prefix.
|
|
153
|
+
*/
|
|
154
|
+
log(message) {
|
|
155
|
+
console.log(message);
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Log an empty line.
|
|
159
|
+
*/
|
|
160
|
+
blank() {
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function printSuccessBanner(projectName, projectPath, pm) {
|
|
165
|
+
const installCmd = getInstallCommand(pm);
|
|
166
|
+
const devCmd = getDevCommand(pm);
|
|
167
|
+
logger.blank();
|
|
168
|
+
logger.success(`Created ${pc.bold(projectName)} at ${pc.cyan(projectPath)}`);
|
|
169
|
+
logger.blank();
|
|
170
|
+
logger.log(pc.dim(" Next steps:"));
|
|
171
|
+
logger.blank();
|
|
172
|
+
logger.log(` ${pc.dim("$")} ${pc.cyan(`cd ${projectName}`)}`);
|
|
173
|
+
logger.log(` ${pc.dim("$")} ${pc.cyan(installCmd)}`);
|
|
174
|
+
logger.log(` ${pc.dim("$")} ${pc.cyan(devCmd)}`);
|
|
175
|
+
logger.blank();
|
|
176
|
+
logger.log(pc.dim(" Happy hacking!"));
|
|
177
|
+
logger.blank();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/scaffold.ts
|
|
181
|
+
var PACKAGE_VERSION = "0.0.1";
|
|
182
|
+
function getTemplateDir() {
|
|
183
|
+
const __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
return path2.resolve(__dirname, "..", "template");
|
|
185
|
+
}
|
|
186
|
+
function processTemplate(content, values) {
|
|
187
|
+
return content.replace(/\{\{name\}\}/g, values.name).replace(/\{\{coreVersion\}\}/g, values.coreVersion).replace(/\{\{cliVersion\}\}/g, values.cliVersion);
|
|
188
|
+
}
|
|
189
|
+
function isTemplateFile(fileName) {
|
|
190
|
+
return fileName.endsWith(".tmpl");
|
|
191
|
+
}
|
|
192
|
+
function getOutputFileName(fileName) {
|
|
193
|
+
if (fileName === "_gitignore") {
|
|
194
|
+
return ".gitignore";
|
|
195
|
+
}
|
|
196
|
+
if (fileName.endsWith(".tmpl")) {
|
|
197
|
+
return fileName.slice(0, -5);
|
|
198
|
+
}
|
|
199
|
+
return fileName;
|
|
200
|
+
}
|
|
201
|
+
async function scaffold(projectName, options = {}) {
|
|
202
|
+
const targetDir = options.targetDir || path2.resolve(process.cwd(), projectName);
|
|
203
|
+
const validation = validateProject(projectName, targetDir);
|
|
204
|
+
if (!validation.valid) {
|
|
205
|
+
throw new Error(validation.error);
|
|
206
|
+
}
|
|
207
|
+
const templateDir = getTemplateDir();
|
|
208
|
+
if (!fs2.existsSync(templateDir)) {
|
|
209
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
210
|
+
}
|
|
211
|
+
const templateValues = {
|
|
212
|
+
name: projectName,
|
|
213
|
+
coreVersion: PACKAGE_VERSION,
|
|
214
|
+
cliVersion: PACKAGE_VERSION
|
|
215
|
+
};
|
|
216
|
+
logger.info(`Creating ${projectName}...`);
|
|
217
|
+
try {
|
|
218
|
+
await fs2.ensureDir(targetDir);
|
|
219
|
+
await copyTemplateRecursive(templateDir, targetDir, templateValues);
|
|
220
|
+
const pm = detectPackageManager();
|
|
221
|
+
printSuccessBanner(projectName, targetDir, pm);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (fs2.existsSync(targetDir)) {
|
|
224
|
+
try {
|
|
225
|
+
await fs2.remove(targetDir);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function copyTemplateRecursive(srcDir, destDir, values) {
|
|
233
|
+
const entries = await fs2.readdir(srcDir, { withFileTypes: true });
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
236
|
+
const outputName = getOutputFileName(entry.name);
|
|
237
|
+
const destPath = path2.join(destDir, outputName);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
await fs2.ensureDir(destPath);
|
|
240
|
+
await copyTemplateRecursive(srcPath, destPath, values);
|
|
241
|
+
} else if (entry.isFile()) {
|
|
242
|
+
if (isTemplateFile(entry.name)) {
|
|
243
|
+
const content = await fs2.readFile(srcPath, "utf-8");
|
|
244
|
+
const processed = processTemplate(content, values);
|
|
245
|
+
await fs2.writeFile(destPath, processed, "utf-8");
|
|
246
|
+
} else {
|
|
247
|
+
await fs2.copyFile(srcPath, destPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/index.ts
|
|
254
|
+
var require2 = createRequire(import.meta.url);
|
|
255
|
+
var pkg = require2("../package.json");
|
|
256
|
+
program.name("create-cloudwerk-app").description("Create a new Cloudwerk app").version(pkg.version).argument("<project-name>", "Name of the project to create").action(async (projectName) => {
|
|
257
|
+
try {
|
|
258
|
+
await scaffold(projectName);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.error(error.message);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudwerk/create-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Create a new Cloudwerk app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-cloudwerk-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"template"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
22
|
+
"test": "vitest --run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest --run --coverage"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"fs-extra": "^11.0.0",
|
|
29
|
+
"picocolors": "^1.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/fs-extra": "^11.0.0",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"@vitest/coverage-v8": "^1.0.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "cloudwerk dev",
|
|
8
|
+
"build": "cloudwerk build",
|
|
9
|
+
"deploy": "wrangler deploy"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@cloudwerk/core": "^{{coreVersion}}",
|
|
13
|
+
"@cloudwerk/cli": "^{{cliVersion}}",
|
|
14
|
+
"hono": "^4.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.0.0",
|
|
18
|
+
"wrangler": "^3.0.0"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./app"
|
|
12
|
+
},
|
|
13
|
+
"include": ["app/**/*", "cloudwerk.config.ts"]
|
|
14
|
+
}
|