@jtl-software/create-cloud-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.js +156 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/scaffold.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
function toPascalCase(kebab) {
|
|
13
|
+
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
14
|
+
}
|
|
15
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".svg"]);
|
|
16
|
+
function copyDir(src, dest, replacements) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
19
|
+
if (entry.name === "package.json") continue;
|
|
20
|
+
const srcPath = path.join(src, entry.name);
|
|
21
|
+
let destName = entry.name;
|
|
22
|
+
if (destName.startsWith("_")) {
|
|
23
|
+
destName = destName.startsWith("_git") ? `.${destName.slice(1)}` : destName.slice(1);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
26
|
+
destName = destName.replaceAll(key, value);
|
|
27
|
+
}
|
|
28
|
+
const destPath = path.join(dest, destName);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
copyDir(srcPath, destPath, replacements);
|
|
31
|
+
} else {
|
|
32
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
33
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
34
|
+
fs.copyFileSync(srcPath, destPath);
|
|
35
|
+
} else {
|
|
36
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
37
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
38
|
+
content = content.replaceAll(key, value);
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(destPath, content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function resolveTemplatePackage(packageName) {
|
|
46
|
+
const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
|
|
47
|
+
return path.dirname(pkgJsonPath);
|
|
48
|
+
}
|
|
49
|
+
var TEMPLATE_PACKAGES = {
|
|
50
|
+
shared: "@jtl-software/cloud-app-template-shared",
|
|
51
|
+
"react-tailwind": "@jtl-software/cloud-app-template-frontend-react",
|
|
52
|
+
"node-express": "@jtl-software/cloud-app-template-backend-node",
|
|
53
|
+
dotnet: "@jtl-software/cloud-app-template-backend-dotnet"
|
|
54
|
+
};
|
|
55
|
+
async function scaffold({ appName, description, backend, frontend, targetDir: customTargetDir }) {
|
|
56
|
+
const targetDir = customTargetDir ?? path.resolve(process.cwd(), appName);
|
|
57
|
+
const pascalName = toPascalCase(appName);
|
|
58
|
+
const replacements = {
|
|
59
|
+
"{{APP_NAME}}": appName,
|
|
60
|
+
"{{APP_NAME_PASCAL}}": pascalName,
|
|
61
|
+
"{{APP_DESCRIPTION}}": description
|
|
62
|
+
};
|
|
63
|
+
if (fs.existsSync(targetDir)) {
|
|
64
|
+
throw new Error(`Directory "${appName}" already exists`);
|
|
65
|
+
}
|
|
66
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
67
|
+
copyDir(resolveTemplatePackage(TEMPLATE_PACKAGES.shared), targetDir, replacements);
|
|
68
|
+
const frontendDest = path.join(targetDir, "packages", "frontend");
|
|
69
|
+
copyDir(resolveTemplatePackage(TEMPLATE_PACKAGES[frontend]), frontendDest, replacements);
|
|
70
|
+
if (backend === "node-express") {
|
|
71
|
+
const backendDest = path.join(targetDir, "packages", "backend");
|
|
72
|
+
copyDir(resolveTemplatePackage(TEMPLATE_PACKAGES["node-express"]), backendDest, replacements);
|
|
73
|
+
} else if (backend === "dotnet") {
|
|
74
|
+
const backendDest = path.join(targetDir, "backend");
|
|
75
|
+
const dotnetReplacements = { ...replacements, HelloWorldApp: pascalName };
|
|
76
|
+
copyDir(resolveTemplatePackage(TEMPLATE_PACKAGES.dotnet), backendDest, dotnetReplacements);
|
|
77
|
+
}
|
|
78
|
+
const workspaces = backend === "node-express" ? ["packages/*"] : ["packages/frontend"];
|
|
79
|
+
const rootPackageJson = {
|
|
80
|
+
name: appName,
|
|
81
|
+
version: "1.0.0",
|
|
82
|
+
private: true,
|
|
83
|
+
workspaces,
|
|
84
|
+
scripts: {
|
|
85
|
+
dev: "turbo dev",
|
|
86
|
+
build: "turbo build",
|
|
87
|
+
lint: "turbo lint",
|
|
88
|
+
test: "turbo test"
|
|
89
|
+
},
|
|
90
|
+
packageManager: "npm@10.8.2",
|
|
91
|
+
devDependencies: {
|
|
92
|
+
turbo: "^2.5.5"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify(rootPackageJson, null, 2) + "\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/index.ts
|
|
99
|
+
async function main() {
|
|
100
|
+
console.log();
|
|
101
|
+
p.intro(pc.bgCyan(pc.black(" create-jtl-app ")));
|
|
102
|
+
const answers = await p.group(
|
|
103
|
+
{
|
|
104
|
+
appName: () => p.text({
|
|
105
|
+
message: "App name",
|
|
106
|
+
placeholder: "my-jtl-app",
|
|
107
|
+
validate: (value) => {
|
|
108
|
+
if (!value) return "App name is required";
|
|
109
|
+
if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
description: () => p.text({
|
|
113
|
+
message: "Description",
|
|
114
|
+
placeholder: "A JTL Platform app"
|
|
115
|
+
}),
|
|
116
|
+
backend: () => p.select({
|
|
117
|
+
message: "Backend",
|
|
118
|
+
options: [
|
|
119
|
+
{ value: "node-express", label: "Node.js", hint: "Express + TypeScript" },
|
|
120
|
+
{ value: "dotnet", label: ".NET", hint: "ASP.NET Core + FastEndpoints" }
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
frontend: () => p.select({
|
|
124
|
+
message: "Frontend",
|
|
125
|
+
options: [{ value: "react-tailwind", label: "React", hint: "Vite + Tailwind + JTL UI" }]
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
onCancel: () => {
|
|
130
|
+
p.cancel("Cancelled.");
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
const s = p.spinner();
|
|
136
|
+
s.start("Scaffolding project");
|
|
137
|
+
await scaffold({
|
|
138
|
+
appName: answers.appName,
|
|
139
|
+
description: answers.description || "A JTL Platform app",
|
|
140
|
+
backend: answers.backend,
|
|
141
|
+
frontend: answers.frontend
|
|
142
|
+
});
|
|
143
|
+
s.stop("Project scaffolded");
|
|
144
|
+
p.note(
|
|
145
|
+
[
|
|
146
|
+
`${pc.cyan("cd")} ${answers.appName}`,
|
|
147
|
+
`${pc.cyan("npm install")}`,
|
|
148
|
+
`${pc.cyan("npm run dev")}`,
|
|
149
|
+
"",
|
|
150
|
+
pc.dim("Then register your manifest in the Partner Portal.")
|
|
151
|
+
].join("\n"),
|
|
152
|
+
"Next steps"
|
|
153
|
+
);
|
|
154
|
+
p.outro(pc.green("Done! Happy building."));
|
|
155
|
+
}
|
|
156
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jtl-software/create-cloud-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-cloud-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"lint": "eslint src/"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clack/prompts": "^0.10.0",
|
|
20
|
+
"@jtl-software/cloud-app-template-frontend-react": "*",
|
|
21
|
+
"@jtl-software/cloud-app-template-backend-node": "*",
|
|
22
|
+
"@jtl-software/cloud-app-template-backend-dotnet": "*",
|
|
23
|
+
"@jtl-software/cloud-app-template-shared": "*",
|
|
24
|
+
"picocolors": "^1.1.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.14.0",
|
|
28
|
+
"tsup": "^8.4.0",
|
|
29
|
+
"tsx": "^4.19.3",
|
|
30
|
+
"typescript": "^5.8.3",
|
|
31
|
+
"vitest": "^2.1.8"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|