@jtl-software/create-cloud-app 0.0.2 → 0.0.4

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 +121 -62
  3. package/package.json +5 -5
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,8 +7,6 @@ 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
  }
@@ -24,14 +22,30 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
24
22
  ".eot",
25
23
  ".svg"
26
24
  ]);
27
- function copyDir(src, dest, replacements) {
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) {
28
43
  fs.mkdirSync(dest, { recursive: true });
29
44
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
30
- if (entry.name === "package.json") continue;
31
45
  const srcPath = path.join(src, entry.name);
32
46
  let destName = entry.name;
33
- if (destName.startsWith("_")) {
34
- destName = destName.startsWith("_git") ? `.${destName.slice(1)}` : destName.slice(1);
47
+ if (destName.startsWith("_git")) {
48
+ destName = `.${destName.slice(1)}`;
35
49
  }
36
50
  for (const [key, value] of Object.entries(replacements)) {
37
51
  destName = destName.replaceAll(key, value);
@@ -39,6 +53,12 @@ function copyDir(src, dest, replacements) {
39
53
  const destPath = path.join(dest, destName);
40
54
  if (entry.isDirectory()) {
41
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
+ );
42
62
  } else {
43
63
  const ext = path.extname(entry.name).toLowerCase();
44
64
  if (BINARY_EXTENSIONS.has(ext)) {
@@ -53,26 +73,29 @@ function copyDir(src, dest, replacements) {
53
73
  }
54
74
  }
55
75
  }
56
- function resolveTemplatePackage(packageName) {
57
- const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
58
- 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 };
59
87
  }
60
- var TEMPLATE_PACKAGES = {
61
- shared: "@jtl-software/cloud-app-template-shared",
62
- "react-tailwind": "@jtl-software/cloud-app-template-frontend-react",
63
- "node-express": "@jtl-software/cloud-app-template-backend-node",
64
- dotnet: "@jtl-software/cloud-app-template-backend-dotnet"
65
- };
66
88
  async function scaffold({
67
89
  appName,
68
90
  description,
69
91
  backend,
70
92
  frontend,
93
+ shared,
71
94
  targetDir: customTargetDir
72
95
  }) {
73
96
  const targetDir = customTargetDir ?? path.resolve(process.cwd(), appName);
74
97
  const pascalName = toPascalCase(appName);
75
- const replacements = {
98
+ const baseReplacements = {
76
99
  "{{APP_NAME}}": appName,
77
100
  "{{APP_NAME_PASCAL}}": pascalName,
78
101
  "{{APP_DESCRIPTION}}": description
@@ -81,39 +104,38 @@ async function scaffold({
81
104
  throw new Error(`Directory "${appName}" already exists`);
82
105
  }
83
106
  fs.mkdirSync(targetDir, { recursive: true });
84
- copyDir(
85
- resolveTemplatePackage(TEMPLATE_PACKAGES.shared),
86
- targetDir,
87
- replacements
88
- );
107
+ copyDir(shared.dir, targetDir, baseReplacements);
89
108
  const frontendDest = path.join(targetDir, "packages", "frontend");
109
+ const frontendReplacements = buildReplacements(baseReplacements, frontend);
90
110
  copyDir(
91
- resolveTemplatePackage(TEMPLATE_PACKAGES[frontend]),
111
+ frontend.dir,
92
112
  frontendDest,
93
- replacements
113
+ frontendReplacements,
114
+ frontend.meta.type,
115
+ appName
94
116
  );
95
- if (backend === "node-express") {
96
- const backendDest = path.join(targetDir, "packages", "backend");
97
- copyDir(
98
- resolveTemplatePackage(TEMPLATE_PACKAGES["node-express"]),
99
- backendDest,
100
- replacements
101
- );
102
- } else if (backend === "dotnet") {
103
- const backendDest = path.join(targetDir, "backend");
104
- const dotnetReplacements = { ...replacements, HelloWorldApp: pascalName };
105
- copyDir(
106
- resolveTemplatePackage(TEMPLATE_PACKAGES.dotnet),
107
- backendDest,
108
- dotnetReplacements
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
+ );
126
+ const runtimeVersions = backend.meta.runtimeVersions ?? {};
127
+ if (Object.keys(runtimeVersions).length > 0) {
128
+ const toolVersions = Object.entries(runtimeVersions).map(([tool, version]) => `${tool} ${version}`).join("\n");
129
+ fs.writeFileSync(
130
+ path.join(targetDir, ".tool-versions"),
131
+ toolVersions + "\n"
109
132
  );
110
133
  }
111
- const workspaces = backend === "node-express" ? ["packages/*"] : ["packages/frontend"];
112
134
  const rootPackageJson = {
113
135
  name: appName,
114
136
  version: "1.0.0",
115
137
  private: true,
116
- workspaces,
138
+ workspaces: ["packages/*"],
117
139
  scripts: {
118
140
  dev: "turbo dev",
119
141
  build: "turbo build",
@@ -131,10 +153,49 @@ async function scaffold({
131
153
  );
132
154
  }
133
155
 
156
+ // src/templates.ts
157
+ import fs2 from "fs";
158
+ import path2 from "path";
159
+ import { createRequire } from "module";
160
+ var require2 = createRequire(import.meta.url);
161
+ function discoverTemplates() {
162
+ const cliPkgPath = require2.resolve("@jtl-software/create-cloud-app/package.json");
163
+ const cliPkg = JSON.parse(fs2.readFileSync(cliPkgPath, "utf-8"));
164
+ const deps = cliPkg.dependencies ?? {};
165
+ const templates = [];
166
+ for (const packageName of Object.keys(deps)) {
167
+ if (!packageName.startsWith("@jtl-software/cloud-app-template-")) continue;
168
+ try {
169
+ const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
170
+ const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
171
+ const meta = pkg.cloudAppTemplate;
172
+ if (!meta?.type) continue;
173
+ templates.push({
174
+ packageName,
175
+ dir: path2.dirname(pkgJsonPath),
176
+ meta
177
+ });
178
+ } catch {
179
+ }
180
+ }
181
+ return templates;
182
+ }
183
+ function getTemplatesByType(templates, type) {
184
+ return templates.filter((t) => t.meta.type === type);
185
+ }
186
+
134
187
  // src/index.ts
135
188
  async function main() {
136
189
  console.log();
137
- p.intro(pc.bgCyan(pc.black(" create-jtl-app ")));
190
+ p.intro(pc.bgCyan(pc.black(" create-cloud-app ")));
191
+ const templates = discoverTemplates();
192
+ const backends = getTemplatesByType(templates, "backend");
193
+ const frontends = getTemplatesByType(templates, "frontend");
194
+ const shared = templates.find((t) => t.meta.type === "shared");
195
+ if (!backends.length || !frontends.length || !shared) {
196
+ p.cancel("No templates found. Ensure template packages are installed.");
197
+ process.exit(1);
198
+ }
138
199
  const answers = await p.group(
139
200
  {
140
201
  appName: () => p.text({
@@ -152,28 +213,19 @@ async function main() {
152
213
  }),
153
214
  backend: () => p.select({
154
215
  message: "Backend",
155
- options: [
156
- {
157
- value: "node-express",
158
- label: "Node.js",
159
- hint: "Express + TypeScript"
160
- },
161
- {
162
- value: "dotnet",
163
- label: ".NET",
164
- hint: "ASP.NET Core + FastEndpoints"
165
- }
166
- ]
216
+ options: backends.map((t) => ({
217
+ value: t.packageName,
218
+ label: t.meta.label ?? t.packageName,
219
+ hint: t.meta.hint
220
+ }))
167
221
  }),
168
- frontend: () => p.select({
222
+ frontend: () => frontends.length === 1 ? Promise.resolve(frontends[0].packageName) : p.select({
169
223
  message: "Frontend",
170
- options: [
171
- {
172
- value: "react-tailwind",
173
- label: "React",
174
- hint: "Vite + Tailwind + JTL UI"
175
- }
176
- ]
224
+ options: frontends.map((t) => ({
225
+ value: t.packageName,
226
+ label: t.meta.label ?? t.packageName,
227
+ hint: t.meta.hint
228
+ }))
177
229
  })
178
230
  },
179
231
  {
@@ -183,13 +235,20 @@ async function main() {
183
235
  }
184
236
  }
185
237
  );
238
+ const selectedBackend = templates.find(
239
+ (t) => t.packageName === answers.backend
240
+ );
241
+ const selectedFrontend = templates.find(
242
+ (t) => t.packageName === answers.frontend
243
+ );
186
244
  const s = p.spinner();
187
245
  s.start("Scaffolding project");
188
246
  await scaffold({
189
247
  appName: answers.appName,
190
248
  description: answers.description || "A JTL Platform app",
191
- backend: answers.backend,
192
- frontend: answers.frontend
249
+ backend: selectedBackend,
250
+ frontend: selectedFrontend,
251
+ shared
193
252
  });
194
253
  s.stop("Project scaffolded");
195
254
  p.note(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/create-cloud-app",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "CLI tool for scaffolding JTL Platform cloud apps",
6
6
  "bin": {
@@ -15,10 +15,10 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@clack/prompts": "^0.10.0",
18
- "@jtl-software/cloud-app-template-frontend-react": "0.0.2",
19
- "@jtl-software/cloud-app-template-backend-node": "0.0.2",
20
- "@jtl-software/cloud-app-template-backend-dotnet": "0.0.2",
21
- "@jtl-software/cloud-app-template-shared": "0.0.2",
18
+ "@jtl-software/cloud-app-template-frontend-react": "0.0.4",
19
+ "@jtl-software/cloud-app-template-backend-node": "0.0.4",
20
+ "@jtl-software/cloud-app-template-backend-dotnet": "0.0.4",
21
+ "@jtl-software/cloud-app-template-shared": "0.0.3",
22
22
  "picocolors": "^1.1.1"
23
23
  },
24
24
  "devDependencies": {