@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.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/dist/index.js +143 -41
  3. 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([".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".svg"]);
16
- function copyDir(src, dest, replacements) {
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 = destName.startsWith("_git") ? `.${destName.slice(1)}` : destName.slice(1);
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 resolveTemplatePackage(packageName) {
46
- const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
47
- return path.dirname(pkgJsonPath);
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
- 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 }) {
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 replacements = {
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(resolveTemplatePackage(TEMPLATE_PACKAGES.shared), targetDir, replacements);
107
+ copyDir(shared.dir, targetDir, baseReplacements);
68
108
  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"];
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(path.join(targetDir, "package.json"), JSON.stringify(rootPackageJson, null, 2) + "\n");
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-jtl-app ")));
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)) return "Use lowercase letters, numbers, and hyphens only";
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
- { value: "node-express", label: "Node.js", hint: "Express + TypeScript" },
120
- { value: "dotnet", label: ".NET", hint: "ASP.NET Core + FastEndpoints" }
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: [{ value: "react-tailwind", label: "React", hint: "Vite + Tailwind + JTL UI" }]
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: answers.backend,
141
- frontend: answers.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.1",
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"