@jtl-software/create-cloud-app 0.0.1 → 0.0.3
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 +43 -0
- package/dist/index.js +143 -41
- package/package.json +7 -10
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @jtl-software/create-cloud-app
|
|
2
|
+
|
|
3
|
+
Scaffold a [JTL Platform](https://www.jtl-software.com/) cloud app in seconds.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create @jtl-software/cloud-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI prompts you for:
|
|
12
|
+
|
|
13
|
+
- **App name** — directory name, package name, and manifest identifier
|
|
14
|
+
- **Description** — placed in the app manifest
|
|
15
|
+
- **Backend** — Node.js (Express + TypeScript) or .NET (ASP.NET Core + FastEndpoints)
|
|
16
|
+
- **Frontend** — React (Vite + Tailwind + JTL Platform UI)
|
|
17
|
+
|
|
18
|
+
Then start developing:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd my-app
|
|
22
|
+
npm install
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What's included
|
|
27
|
+
|
|
28
|
+
- Pre-configured monorepo with [Turborepo](https://turbo.build/)
|
|
29
|
+
- Frontend with welcome pages explaining each app mode and manifest mapping
|
|
30
|
+
- Backend with JWT verification, tenant connection, and ERP API proxy
|
|
31
|
+
- Ready-to-register `manifest.json`
|
|
32
|
+
|
|
33
|
+
## After scaffolding
|
|
34
|
+
|
|
35
|
+
1. Register your manifest in the [Partner Portal](https://partner.jtl-cloud.com/)
|
|
36
|
+
2. Add your Client ID and Secret to the backend config
|
|
37
|
+
3. Install the app from [JTL-Cloud Hub](https://hub.jtl-cloud.com/) under "Apps in development"
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
- [JTL Developer Platform](https://developer.jtl-software.com/)
|
|
42
|
+
- [Partner Portal](https://partner.jtl-cloud.com/)
|
|
43
|
+
- [JTL-Cloud Hub](https://hub.jtl-cloud.com/)
|
package/dist/index.js
CHANGED
|
@@ -7,20 +7,45 @@ import pc from "picocolors";
|
|
|
7
7
|
// src/scaffold.ts
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import { createRequire } from "module";
|
|
11
|
-
var require2 = createRequire(import.meta.url);
|
|
12
10
|
function toPascalCase(kebab) {
|
|
13
11
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
14
12
|
}
|
|
15
|
-
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
16
|
-
|
|
13
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
14
|
+
".png",
|
|
15
|
+
".jpg",
|
|
16
|
+
".jpeg",
|
|
17
|
+
".gif",
|
|
18
|
+
".ico",
|
|
19
|
+
".woff",
|
|
20
|
+
".woff2",
|
|
21
|
+
".ttf",
|
|
22
|
+
".eot",
|
|
23
|
+
".svg"
|
|
24
|
+
]);
|
|
25
|
+
var PACKAGE_JSON_STRIP_FIELDS = [
|
|
26
|
+
"cloudAppTemplate",
|
|
27
|
+
"publishConfig",
|
|
28
|
+
"description"
|
|
29
|
+
];
|
|
30
|
+
function transformPackageJson(content, appName, templateType, replacements) {
|
|
31
|
+
const pkg = JSON.parse(content);
|
|
32
|
+
pkg.name = `${appName}-${templateType}`;
|
|
33
|
+
for (const field of PACKAGE_JSON_STRIP_FIELDS) {
|
|
34
|
+
delete pkg[field];
|
|
35
|
+
}
|
|
36
|
+
let result = JSON.stringify(pkg, null, 2);
|
|
37
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
38
|
+
result = result.replaceAll(key, value);
|
|
39
|
+
}
|
|
40
|
+
return result + "\n";
|
|
41
|
+
}
|
|
42
|
+
function copyDir(src, dest, replacements, templateType, appName) {
|
|
17
43
|
fs.mkdirSync(dest, { recursive: true });
|
|
18
44
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
19
|
-
if (entry.name === "package.json") continue;
|
|
20
45
|
const srcPath = path.join(src, entry.name);
|
|
21
46
|
let destName = entry.name;
|
|
22
|
-
if (destName.startsWith("
|
|
23
|
-
destName =
|
|
47
|
+
if (destName.startsWith("_git")) {
|
|
48
|
+
destName = `.${destName.slice(1)}`;
|
|
24
49
|
}
|
|
25
50
|
for (const [key, value] of Object.entries(replacements)) {
|
|
26
51
|
destName = destName.replaceAll(key, value);
|
|
@@ -28,6 +53,12 @@ function copyDir(src, dest, replacements) {
|
|
|
28
53
|
const destPath = path.join(dest, destName);
|
|
29
54
|
if (entry.isDirectory()) {
|
|
30
55
|
copyDir(srcPath, destPath, replacements);
|
|
56
|
+
} else if (entry.name === "package.json" && templateType && appName) {
|
|
57
|
+
const content = fs.readFileSync(srcPath, "utf-8");
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
destPath,
|
|
60
|
+
transformPackageJson(content, appName, templateType, replacements)
|
|
61
|
+
);
|
|
31
62
|
} else {
|
|
32
63
|
const ext = path.extname(entry.name).toLowerCase();
|
|
33
64
|
if (BINARY_EXTENSIONS.has(ext)) {
|
|
@@ -42,20 +73,29 @@ function copyDir(src, dest, replacements) {
|
|
|
42
73
|
}
|
|
43
74
|
}
|
|
44
75
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
76
|
+
function buildReplacements(base, template) {
|
|
77
|
+
const extra = template.meta.extraReplacements ?? {};
|
|
78
|
+
const resolved = {};
|
|
79
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
80
|
+
let resolvedValue = value;
|
|
81
|
+
for (const [baseKey, baseValue] of Object.entries(base)) {
|
|
82
|
+
resolvedValue = resolvedValue.replaceAll(baseKey, baseValue);
|
|
83
|
+
}
|
|
84
|
+
resolved[key] = resolvedValue;
|
|
85
|
+
}
|
|
86
|
+
return { ...base, ...resolved };
|
|
48
87
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
async function scaffold({
|
|
89
|
+
appName,
|
|
90
|
+
description,
|
|
91
|
+
backend,
|
|
92
|
+
frontend,
|
|
93
|
+
shared,
|
|
94
|
+
targetDir: customTargetDir
|
|
95
|
+
}) {
|
|
56
96
|
const targetDir = customTargetDir ?? path.resolve(process.cwd(), appName);
|
|
57
97
|
const pascalName = toPascalCase(appName);
|
|
58
|
-
const
|
|
98
|
+
const baseReplacements = {
|
|
59
99
|
"{{APP_NAME}}": appName,
|
|
60
100
|
"{{APP_NAME_PASCAL}}": pascalName,
|
|
61
101
|
"{{APP_DESCRIPTION}}": description
|
|
@@ -64,23 +104,30 @@ async function scaffold({ appName, description, backend, frontend, targetDir: cu
|
|
|
64
104
|
throw new Error(`Directory "${appName}" already exists`);
|
|
65
105
|
}
|
|
66
106
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
67
|
-
copyDir(
|
|
107
|
+
copyDir(shared.dir, targetDir, baseReplacements);
|
|
68
108
|
const frontendDest = path.join(targetDir, "packages", "frontend");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
109
|
+
const frontendReplacements = buildReplacements(baseReplacements, frontend);
|
|
110
|
+
copyDir(
|
|
111
|
+
frontend.dir,
|
|
112
|
+
frontendDest,
|
|
113
|
+
frontendReplacements,
|
|
114
|
+
frontend.meta.type,
|
|
115
|
+
appName
|
|
116
|
+
);
|
|
117
|
+
const backendDest = path.join(targetDir, "packages", "backend");
|
|
118
|
+
const backendReplacements = buildReplacements(baseReplacements, backend);
|
|
119
|
+
copyDir(
|
|
120
|
+
backend.dir,
|
|
121
|
+
backendDest,
|
|
122
|
+
backendReplacements,
|
|
123
|
+
backend.meta.type,
|
|
124
|
+
appName
|
|
125
|
+
);
|
|
79
126
|
const rootPackageJson = {
|
|
80
127
|
name: appName,
|
|
81
128
|
version: "1.0.0",
|
|
82
129
|
private: true,
|
|
83
|
-
workspaces,
|
|
130
|
+
workspaces: ["packages/*"],
|
|
84
131
|
scripts: {
|
|
85
132
|
dev: "turbo dev",
|
|
86
133
|
build: "turbo build",
|
|
@@ -92,13 +139,55 @@ async function scaffold({ appName, description, backend, frontend, targetDir: cu
|
|
|
92
139
|
turbo: "^2.5.5"
|
|
93
140
|
}
|
|
94
141
|
};
|
|
95
|
-
fs.writeFileSync(
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(targetDir, "package.json"),
|
|
144
|
+
JSON.stringify(rootPackageJson, null, 2) + "\n"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/templates.ts
|
|
149
|
+
import fs2 from "fs";
|
|
150
|
+
import path2 from "path";
|
|
151
|
+
import { createRequire } from "module";
|
|
152
|
+
var require2 = createRequire(import.meta.url);
|
|
153
|
+
function discoverTemplates() {
|
|
154
|
+
const cliPkgPath = require2.resolve("@jtl-software/create-cloud-app/package.json");
|
|
155
|
+
const cliPkg = JSON.parse(fs2.readFileSync(cliPkgPath, "utf-8"));
|
|
156
|
+
const deps = cliPkg.dependencies ?? {};
|
|
157
|
+
const templates = [];
|
|
158
|
+
for (const packageName of Object.keys(deps)) {
|
|
159
|
+
if (!packageName.startsWith("@jtl-software/cloud-app-template-")) continue;
|
|
160
|
+
try {
|
|
161
|
+
const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
|
|
162
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
|
|
163
|
+
const meta = pkg.cloudAppTemplate;
|
|
164
|
+
if (!meta?.type) continue;
|
|
165
|
+
templates.push({
|
|
166
|
+
packageName,
|
|
167
|
+
dir: path2.dirname(pkgJsonPath),
|
|
168
|
+
meta
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return templates;
|
|
174
|
+
}
|
|
175
|
+
function getTemplatesByType(templates, type) {
|
|
176
|
+
return templates.filter((t) => t.meta.type === type);
|
|
96
177
|
}
|
|
97
178
|
|
|
98
179
|
// src/index.ts
|
|
99
180
|
async function main() {
|
|
100
181
|
console.log();
|
|
101
|
-
p.intro(pc.bgCyan(pc.black(" create-
|
|
182
|
+
p.intro(pc.bgCyan(pc.black(" create-cloud-app ")));
|
|
183
|
+
const templates = discoverTemplates();
|
|
184
|
+
const backends = getTemplatesByType(templates, "backend");
|
|
185
|
+
const frontends = getTemplatesByType(templates, "frontend");
|
|
186
|
+
const shared = templates.find((t) => t.meta.type === "shared");
|
|
187
|
+
if (!backends.length || !frontends.length || !shared) {
|
|
188
|
+
p.cancel("No templates found. Ensure template packages are installed.");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
102
191
|
const answers = await p.group(
|
|
103
192
|
{
|
|
104
193
|
appName: () => p.text({
|
|
@@ -106,7 +195,8 @@ async function main() {
|
|
|
106
195
|
placeholder: "my-jtl-app",
|
|
107
196
|
validate: (value) => {
|
|
108
197
|
if (!value) return "App name is required";
|
|
109
|
-
if (!/^[a-z0-9-]+$/.test(value))
|
|
198
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
199
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
110
200
|
}
|
|
111
201
|
}),
|
|
112
202
|
description: () => p.text({
|
|
@@ -115,14 +205,19 @@ async function main() {
|
|
|
115
205
|
}),
|
|
116
206
|
backend: () => p.select({
|
|
117
207
|
message: "Backend",
|
|
118
|
-
options:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
208
|
+
options: backends.map((t) => ({
|
|
209
|
+
value: t.packageName,
|
|
210
|
+
label: t.meta.label ?? t.packageName,
|
|
211
|
+
hint: t.meta.hint
|
|
212
|
+
}))
|
|
122
213
|
}),
|
|
123
|
-
frontend: () => p.select({
|
|
214
|
+
frontend: () => frontends.length === 1 ? Promise.resolve(frontends[0].packageName) : p.select({
|
|
124
215
|
message: "Frontend",
|
|
125
|
-
options:
|
|
216
|
+
options: frontends.map((t) => ({
|
|
217
|
+
value: t.packageName,
|
|
218
|
+
label: t.meta.label ?? t.packageName,
|
|
219
|
+
hint: t.meta.hint
|
|
220
|
+
}))
|
|
126
221
|
})
|
|
127
222
|
},
|
|
128
223
|
{
|
|
@@ -132,13 +227,20 @@ async function main() {
|
|
|
132
227
|
}
|
|
133
228
|
}
|
|
134
229
|
);
|
|
230
|
+
const selectedBackend = templates.find(
|
|
231
|
+
(t) => t.packageName === answers.backend
|
|
232
|
+
);
|
|
233
|
+
const selectedFrontend = templates.find(
|
|
234
|
+
(t) => t.packageName === answers.frontend
|
|
235
|
+
);
|
|
135
236
|
const s = p.spinner();
|
|
136
237
|
s.start("Scaffolding project");
|
|
137
238
|
await scaffold({
|
|
138
239
|
appName: answers.appName,
|
|
139
240
|
description: answers.description || "A JTL Platform app",
|
|
140
|
-
backend:
|
|
141
|
-
frontend:
|
|
241
|
+
backend: selectedBackend,
|
|
242
|
+
frontend: selectedFrontend,
|
|
243
|
+
shared
|
|
142
244
|
});
|
|
143
245
|
s.stop("Project scaffolded");
|
|
144
246
|
p.note(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/create-cloud-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -11,24 +11,21 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsup src/index.ts --format esm --clean",
|
|
14
|
-
"dev": "tsx src/index.ts"
|
|
15
|
-
"test": "vitest run",
|
|
16
|
-
"lint": "eslint src/"
|
|
14
|
+
"dev": "tsx src/index.ts"
|
|
17
15
|
},
|
|
18
16
|
"dependencies": {
|
|
19
17
|
"@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": "
|
|
18
|
+
"@jtl-software/cloud-app-template-frontend-react": "0.0.3",
|
|
19
|
+
"@jtl-software/cloud-app-template-backend-node": "0.0.3",
|
|
20
|
+
"@jtl-software/cloud-app-template-backend-dotnet": "0.0.3",
|
|
21
|
+
"@jtl-software/cloud-app-template-shared": "0.0.3",
|
|
24
22
|
"picocolors": "^1.1.1"
|
|
25
23
|
},
|
|
26
24
|
"devDependencies": {
|
|
27
25
|
"@types/node": "^22.14.0",
|
|
28
26
|
"tsup": "^8.4.0",
|
|
29
27
|
"tsx": "^4.19.3",
|
|
30
|
-
"typescript": "^5.8.3"
|
|
31
|
-
"vitest": "^2.1.8"
|
|
28
|
+
"typescript": "^5.8.3"
|
|
32
29
|
},
|
|
33
30
|
"publishConfig": {
|
|
34
31
|
"access": "public"
|