@smi-digital/create-smi-app 1.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/bin/cli.js +2 -0
- package/dist/index.js +633 -0
- package/package.json +76 -0
- package/templates/.husky/commit-msg +1 -0
- package/templates/.husky/pre-commit +1 -0
- package/templates/base/commitlint.config.js.template +1 -0
- package/templates/base/dependency-cruiser.cjs.template +373 -0
- package/templates/base/env.template +0 -0
- package/templates/base/eslint.config.js.template +38 -0
- package/templates/base/gitignore.template +2 -0
- package/templates/base/knip.json.template +11 -0
- package/templates/base/package-lock.json.template +4674 -0
- package/templates/base/package.json.template +48 -0
- package/templates/base/prettierignore.template +4 -0
- package/templates/base/prettierrc.template +1 -0
- package/templates/base/tsconfig.json.template +45 -0
- package/templates/integrations/strapi-astro/.github/workflows/deploy.yml.template +26 -0
- package/templates/integrations/strapi-astro/.github/workflows/pr-validation.yml.template +14 -0
- package/templates/integrations/strapi-astro/integration.config.json +57 -0
package/bin/cli.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// src/modules/greetings.ts
|
|
2
|
+
import figlet from "figlet";
|
|
3
|
+
import gradient from "gradient-string";
|
|
4
|
+
import chalkAnimation from "chalk-animation";
|
|
5
|
+
|
|
6
|
+
// src/modules/common.ts
|
|
7
|
+
var primary1 = "#8e2de2";
|
|
8
|
+
var primary2 = "#ff00d4";
|
|
9
|
+
|
|
10
|
+
// src/modules/greetings.ts
|
|
11
|
+
var smiGradient = gradient([primary1, primary2]);
|
|
12
|
+
function greet() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const asciiText = figlet.textSync("SMI - Digital", {
|
|
15
|
+
font: "Slant"
|
|
16
|
+
});
|
|
17
|
+
const styledText = smiGradient(asciiText);
|
|
18
|
+
const anim = chalkAnimation.neon(styledText);
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
anim.stop();
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
resolve();
|
|
23
|
+
}, 100);
|
|
24
|
+
}, 2e3);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/modules/questions.ts
|
|
29
|
+
import chalk from "chalk";
|
|
30
|
+
import { confirm, select, input, checkbox } from "@inquirer/prompts";
|
|
31
|
+
|
|
32
|
+
// src/modules/scaffoldRegistry.ts
|
|
33
|
+
var PROJECT_TYPE_CHOICES = [
|
|
34
|
+
{ value: "monorepo", name: "Monorepo" },
|
|
35
|
+
{ value: "singleProject", name: "Single Project" }
|
|
36
|
+
];
|
|
37
|
+
var TOOL_CHOICES = [
|
|
38
|
+
{ value: "basic", name: "Basic" },
|
|
39
|
+
{ value: "none", name: "None" }
|
|
40
|
+
];
|
|
41
|
+
var SINGLE_PROJECT_FRAMEWORK_CHOICES = [
|
|
42
|
+
{ value: "astro", name: "Astro" },
|
|
43
|
+
{ value: "react", name: "React" },
|
|
44
|
+
{ value: "none", name: "None" }
|
|
45
|
+
];
|
|
46
|
+
var FRONTEND_FRAMEWORK_CHOICES = [
|
|
47
|
+
{ value: "astro", name: "Astro" },
|
|
48
|
+
{ value: "react", name: "React" },
|
|
49
|
+
{ value: "none", name: "None" }
|
|
50
|
+
];
|
|
51
|
+
var BACKEND_FRAMEWORK_CHOICES = [
|
|
52
|
+
{ value: "strapi", name: "Strapi" },
|
|
53
|
+
{ value: "express", name: "Express" },
|
|
54
|
+
{ value: "none", name: "None" }
|
|
55
|
+
];
|
|
56
|
+
var FRAMEWORK_GENERATORS = {
|
|
57
|
+
astro: {
|
|
58
|
+
displayName: "Astro",
|
|
59
|
+
command: "npm",
|
|
60
|
+
getArgs(directory) {
|
|
61
|
+
return [
|
|
62
|
+
"create",
|
|
63
|
+
"astro@latest",
|
|
64
|
+
directory,
|
|
65
|
+
"--",
|
|
66
|
+
"--template",
|
|
67
|
+
"minimal",
|
|
68
|
+
"--install",
|
|
69
|
+
"--git",
|
|
70
|
+
"false",
|
|
71
|
+
"--yes"
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
strapi: {
|
|
76
|
+
displayName: "Strapi",
|
|
77
|
+
command: "npx",
|
|
78
|
+
getArgs(directory) {
|
|
79
|
+
return [
|
|
80
|
+
"create-strapi@latest",
|
|
81
|
+
directory,
|
|
82
|
+
"--quickstart",
|
|
83
|
+
"--skip-cloud",
|
|
84
|
+
"--typescript",
|
|
85
|
+
"--no-run"
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function asFramework(value) {
|
|
91
|
+
if (value === "astro" || value === "strapi" || value === "react" || value === "express" || value === "none") {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
return "none";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/modules/integrations.ts
|
|
98
|
+
import { readFile, readdir } from "fs/promises";
|
|
99
|
+
import { join } from "path";
|
|
100
|
+
async function loadIntegrationConfig(templatesDir, integrationKey) {
|
|
101
|
+
const configPath = join(
|
|
102
|
+
templatesDir,
|
|
103
|
+
"integrations",
|
|
104
|
+
integrationKey,
|
|
105
|
+
"integration.config.json"
|
|
106
|
+
);
|
|
107
|
+
const rawContent = await readFile(configPath, "utf8");
|
|
108
|
+
return JSON.parse(rawContent);
|
|
109
|
+
}
|
|
110
|
+
function matchesIntegration(selection, config) {
|
|
111
|
+
const { match } = config;
|
|
112
|
+
if (selection.projectType !== match.projectType) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (match.framework && selection.framework !== match.framework) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (match.frontendFramework && selection.frontendFramework !== match.frontendFramework) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (match.backendFramework && selection.backendFramework !== match.backendFramework) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
async function getIntegrationForSelection(templatesDir, selection) {
|
|
127
|
+
const integrationsRoot = join(templatesDir, "integrations");
|
|
128
|
+
const entries = await readdir(integrationsRoot, { withFileTypes: true });
|
|
129
|
+
const folders = entries.filter((entry) => entry.isDirectory());
|
|
130
|
+
const findSequentially = async (index) => {
|
|
131
|
+
const folder = folders[index];
|
|
132
|
+
if (!folder) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const config = await loadIntegrationConfig(templatesDir, folder.name);
|
|
136
|
+
if (matchesIntegration(selection, config)) {
|
|
137
|
+
return { key: folder.name, config };
|
|
138
|
+
}
|
|
139
|
+
return findSequentially(index + 1);
|
|
140
|
+
};
|
|
141
|
+
return findSequentially(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/modules/questions.ts
|
|
145
|
+
var primary12 = chalk.hex(primary1);
|
|
146
|
+
var primary22 = chalk.hex(primary2);
|
|
147
|
+
var smiTheme = {
|
|
148
|
+
prefix: {
|
|
149
|
+
idle: primary12("?"),
|
|
150
|
+
done: primary22("\u2714")
|
|
151
|
+
},
|
|
152
|
+
style: {
|
|
153
|
+
highlight: (text) => primary12(text),
|
|
154
|
+
answer: (text) => primary22(text)
|
|
155
|
+
},
|
|
156
|
+
icon: {
|
|
157
|
+
cursor: primary12("\u276F")
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
async function askIntegrationInputs(inputConfig, index = 0, answers = {}) {
|
|
161
|
+
const current = inputConfig[index];
|
|
162
|
+
if (!current) {
|
|
163
|
+
return answers;
|
|
164
|
+
}
|
|
165
|
+
if (current.type === "confirm") {
|
|
166
|
+
const confirmConfig = {
|
|
167
|
+
message: current.message,
|
|
168
|
+
theme: smiTheme
|
|
169
|
+
};
|
|
170
|
+
if (typeof current.default === "boolean") {
|
|
171
|
+
confirmConfig.default = current.default;
|
|
172
|
+
}
|
|
173
|
+
const result2 = await confirm(confirmConfig);
|
|
174
|
+
return askIntegrationInputs(inputConfig, index + 1, {
|
|
175
|
+
...answers,
|
|
176
|
+
[current.key]: result2
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const inputConfigData = {
|
|
180
|
+
message: current.message,
|
|
181
|
+
validate(value) {
|
|
182
|
+
if (current.required && value.trim().length === 0) {
|
|
183
|
+
return "This value is required.";
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
},
|
|
187
|
+
theme: smiTheme
|
|
188
|
+
};
|
|
189
|
+
if (typeof current.default === "string") {
|
|
190
|
+
inputConfigData.default = current.default;
|
|
191
|
+
}
|
|
192
|
+
const result = await input(inputConfigData);
|
|
193
|
+
return askIntegrationInputs(inputConfig, index + 1, {
|
|
194
|
+
...answers,
|
|
195
|
+
[current.key]: result
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function askQuestions(templatesDir) {
|
|
199
|
+
const projectName = await input({
|
|
200
|
+
message: "What is the Project Name?",
|
|
201
|
+
theme: smiTheme
|
|
202
|
+
});
|
|
203
|
+
const tools = await checkbox({
|
|
204
|
+
message: "Which Tools to install?",
|
|
205
|
+
choices: TOOL_CHOICES,
|
|
206
|
+
theme: smiTheme
|
|
207
|
+
});
|
|
208
|
+
const projectType = await select({
|
|
209
|
+
message: "What type of Project?",
|
|
210
|
+
choices: PROJECT_TYPE_CHOICES,
|
|
211
|
+
theme: smiTheme
|
|
212
|
+
});
|
|
213
|
+
let frameworkAnswers;
|
|
214
|
+
let configureCd = false;
|
|
215
|
+
let integrationKey;
|
|
216
|
+
let integrationInputs = {};
|
|
217
|
+
if (projectType === "singleProject") {
|
|
218
|
+
const framework = await select({
|
|
219
|
+
message: "What Framework?",
|
|
220
|
+
choices: SINGLE_PROJECT_FRAMEWORK_CHOICES,
|
|
221
|
+
theme: smiTheme
|
|
222
|
+
});
|
|
223
|
+
frameworkAnswers = { framework };
|
|
224
|
+
} else {
|
|
225
|
+
const frontendFramework = await select({
|
|
226
|
+
message: "What frontend Framework?",
|
|
227
|
+
choices: FRONTEND_FRAMEWORK_CHOICES,
|
|
228
|
+
theme: smiTheme
|
|
229
|
+
});
|
|
230
|
+
const backendFramework = await select({
|
|
231
|
+
message: "What backend Framework?",
|
|
232
|
+
choices: BACKEND_FRAMEWORK_CHOICES,
|
|
233
|
+
theme: smiTheme
|
|
234
|
+
});
|
|
235
|
+
frameworkAnswers = { frontendFramework, backendFramework };
|
|
236
|
+
const integration = await getIntegrationForSelection(templatesDir, {
|
|
237
|
+
projectType,
|
|
238
|
+
frontendFramework,
|
|
239
|
+
backendFramework
|
|
240
|
+
});
|
|
241
|
+
integrationKey = integration?.key;
|
|
242
|
+
if (integration?.config.cd) {
|
|
243
|
+
configureCd = await confirm({
|
|
244
|
+
message: integration.config.cd.question,
|
|
245
|
+
theme: smiTheme
|
|
246
|
+
});
|
|
247
|
+
if (configureCd) {
|
|
248
|
+
integrationInputs = await askIntegrationInputs(
|
|
249
|
+
integration.config.cd.inputs
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const confirmation = await confirm({
|
|
255
|
+
message: "Should the Project be created with the selected Options?",
|
|
256
|
+
theme: smiTheme
|
|
257
|
+
});
|
|
258
|
+
const result = {
|
|
259
|
+
projectName,
|
|
260
|
+
projectType,
|
|
261
|
+
tools,
|
|
262
|
+
configureCd,
|
|
263
|
+
integrationInputs,
|
|
264
|
+
...frameworkAnswers,
|
|
265
|
+
confirmation
|
|
266
|
+
};
|
|
267
|
+
if (integrationKey) {
|
|
268
|
+
return {
|
|
269
|
+
...result,
|
|
270
|
+
integrationKey
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/modules/init.ts
|
|
277
|
+
import { fileURLToPath } from "url";
|
|
278
|
+
import { join as join2, dirname } from "path";
|
|
279
|
+
import { access } from "fs/promises";
|
|
280
|
+
async function resolveTemplatesDir(currentDir) {
|
|
281
|
+
const candidates = [
|
|
282
|
+
join2(currentDir, "../../templates"),
|
|
283
|
+
join2(currentDir, "../templates")
|
|
284
|
+
];
|
|
285
|
+
const checks = await Promise.all(
|
|
286
|
+
candidates.map(async (candidate) => {
|
|
287
|
+
try {
|
|
288
|
+
await access(join2(candidate, "base"));
|
|
289
|
+
return true;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
const index = checks.findIndex(Boolean);
|
|
296
|
+
if (index !== -1) {
|
|
297
|
+
const templatesPath = candidates[index];
|
|
298
|
+
if (templatesPath) {
|
|
299
|
+
return templatesPath;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Templates directory not found. Tried: ${candidates.join(", ")}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
async function init() {
|
|
307
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
308
|
+
const templatesDir = await resolveTemplatesDir(__dirname);
|
|
309
|
+
const targetDir = process.cwd();
|
|
310
|
+
return { targetDir, templatesDir };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/modules/createScaffold.ts
|
|
314
|
+
import { access as access2, mkdir as mkdir2 } from "fs/promises";
|
|
315
|
+
import { join as join5 } from "path";
|
|
316
|
+
|
|
317
|
+
// src/modules/scaffoldActions/createTools.ts
|
|
318
|
+
import { cp, readdir as readdir2 } from "fs/promises";
|
|
319
|
+
import { join as join3 } from "path";
|
|
320
|
+
var templateRenameMap = {
|
|
321
|
+
"env.template": ".env",
|
|
322
|
+
"gitignore.template": ".gitignore",
|
|
323
|
+
"prettierignore.template": ".prettierignore",
|
|
324
|
+
"prettierrc.template": ".prettierrc",
|
|
325
|
+
"dependency-cruiser.cjs.template": ".dependency-cruiser.cjs"
|
|
326
|
+
};
|
|
327
|
+
function getTargetFileName(sourceName) {
|
|
328
|
+
if (templateRenameMap[sourceName]) {
|
|
329
|
+
return templateRenameMap[sourceName];
|
|
330
|
+
}
|
|
331
|
+
if (sourceName.endsWith(".template")) {
|
|
332
|
+
return sourceName.slice(0, -".template".length);
|
|
333
|
+
}
|
|
334
|
+
return sourceName;
|
|
335
|
+
}
|
|
336
|
+
async function createTools(tools, targetDir, templatesDir) {
|
|
337
|
+
if (!tools.includes("basic")) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const baseToolsDir = join3(templatesDir, "base");
|
|
341
|
+
const huskyDir = join3(templatesDir, ".husky");
|
|
342
|
+
const entries = await readdir2(baseToolsDir, { withFileTypes: true });
|
|
343
|
+
await Promise.all(
|
|
344
|
+
entries.map(
|
|
345
|
+
(entry) => cp(
|
|
346
|
+
join3(baseToolsDir, entry.name),
|
|
347
|
+
join3(targetDir, getTargetFileName(entry.name)),
|
|
348
|
+
{
|
|
349
|
+
recursive: true
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
await cp(huskyDir, join3(targetDir, ".husky"), {
|
|
355
|
+
recursive: true
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/modules/scaffoldActions/progress.ts
|
|
360
|
+
import { spawn } from "child_process";
|
|
361
|
+
import chalk2 from "chalk";
|
|
362
|
+
var primary = chalk2.hex(primary1);
|
|
363
|
+
var accent = chalk2.hex(primary2);
|
|
364
|
+
var muted = chalk2.gray;
|
|
365
|
+
var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
366
|
+
function formatDuration(durationMs) {
|
|
367
|
+
if (durationMs < 1e3) {
|
|
368
|
+
return `${durationMs}ms`;
|
|
369
|
+
}
|
|
370
|
+
return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
371
|
+
}
|
|
372
|
+
async function runStep(label, task) {
|
|
373
|
+
const start = Date.now();
|
|
374
|
+
const canAnimate = Boolean(process.stdout.isTTY);
|
|
375
|
+
let frameIndex = 0;
|
|
376
|
+
let spinnerTimer;
|
|
377
|
+
const renderSpinner = () => {
|
|
378
|
+
const frame = spinnerFrames[frameIndex % spinnerFrames.length];
|
|
379
|
+
frameIndex += 1;
|
|
380
|
+
process.stdout.write(`\r${primary(frame)} ${accent(label)}`);
|
|
381
|
+
};
|
|
382
|
+
if (canAnimate) {
|
|
383
|
+
renderSpinner();
|
|
384
|
+
spinnerTimer = setInterval(() => {
|
|
385
|
+
renderSpinner();
|
|
386
|
+
}, 90);
|
|
387
|
+
} else {
|
|
388
|
+
console.log(primary("\u25B6"), accent(label));
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const result = await task();
|
|
392
|
+
const duration = Date.now() - start;
|
|
393
|
+
if (spinnerTimer) {
|
|
394
|
+
clearInterval(spinnerTimer);
|
|
395
|
+
process.stdout.write("\r\x1B[2K");
|
|
396
|
+
}
|
|
397
|
+
console.log(
|
|
398
|
+
accent("\u2714"),
|
|
399
|
+
`${label} ${muted(`(${formatDuration(duration)})`)}`
|
|
400
|
+
);
|
|
401
|
+
return result;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const duration = Date.now() - start;
|
|
404
|
+
if (spinnerTimer) {
|
|
405
|
+
clearInterval(spinnerTimer);
|
|
406
|
+
process.stdout.write("\r\x1B[2K");
|
|
407
|
+
}
|
|
408
|
+
console.error(
|
|
409
|
+
chalk2.red("\u2716"),
|
|
410
|
+
`${label} ${muted(`(${formatDuration(duration)})`)}`
|
|
411
|
+
);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function runCommandQuiet(command, args, cwd) {
|
|
416
|
+
await new Promise((resolve, reject) => {
|
|
417
|
+
let stderr = "";
|
|
418
|
+
const child = spawn(command, args, {
|
|
419
|
+
cwd,
|
|
420
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
421
|
+
});
|
|
422
|
+
child.stdout?.on("data", () => {
|
|
423
|
+
});
|
|
424
|
+
child.stderr?.on("data", (chunk) => {
|
|
425
|
+
stderr += chunk.toString();
|
|
426
|
+
});
|
|
427
|
+
child.on("error", (error) => {
|
|
428
|
+
reject(error);
|
|
429
|
+
});
|
|
430
|
+
child.on("close", (code) => {
|
|
431
|
+
if (code === 0) {
|
|
432
|
+
resolve();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const stderrTail = stderr.trim().split("\n").slice(-12).join("\n");
|
|
436
|
+
const details = stderrTail ? `
|
|
437
|
+
${stderrTail}` : "";
|
|
438
|
+
reject(
|
|
439
|
+
new Error(
|
|
440
|
+
`Command failed with exit code ${code}: ${command} ${args.join(" ")}${details}`
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/modules/scaffoldActions/createApps.ts
|
|
448
|
+
async function createFrameworkApp(projectRoot, target) {
|
|
449
|
+
const generator = FRAMEWORK_GENERATORS[target.framework];
|
|
450
|
+
if (!generator) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
await runStep(
|
|
454
|
+
`Creating ${generator.displayName} ${target.label}`,
|
|
455
|
+
async () => runCommandQuiet(
|
|
456
|
+
generator.command,
|
|
457
|
+
generator.getArgs(target.directory),
|
|
458
|
+
projectRoot
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
async function createApps(projectRoot, targets) {
|
|
463
|
+
const runSequentially = async (index) => {
|
|
464
|
+
const target = targets[index];
|
|
465
|
+
if (!target) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
await createFrameworkApp(projectRoot, target);
|
|
469
|
+
await runSequentially(index + 1);
|
|
470
|
+
};
|
|
471
|
+
await runSequentially(0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/modules/scaffoldActions/createIntegrations.ts
|
|
475
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
476
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
477
|
+
function toTemplateToken(key) {
|
|
478
|
+
const normalized = key.replaceAll(/[^a-zA-Z0-9]+/gv, "_").replaceAll(/^_+|_+$/gv, "").toUpperCase();
|
|
479
|
+
return `__${normalized}__`;
|
|
480
|
+
}
|
|
481
|
+
function applyTemplateVariables(content, values) {
|
|
482
|
+
let result = content;
|
|
483
|
+
for (const [key, value] of Object.entries(values)) {
|
|
484
|
+
result = result.replaceAll(toTemplateToken(key), String(value));
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
async function copyTemplateFile(sourcePath, targetPath, values) {
|
|
489
|
+
const rawContent = await readFile2(sourcePath, "utf8");
|
|
490
|
+
const content = applyTemplateVariables(rawContent, values);
|
|
491
|
+
await mkdir(dirname2(targetPath), { recursive: true });
|
|
492
|
+
await writeFile(targetPath, content, "utf8");
|
|
493
|
+
}
|
|
494
|
+
async function createIntegrations(options, projectRoot, templatesDir) {
|
|
495
|
+
const { projectType } = options;
|
|
496
|
+
let { integrationKey } = options;
|
|
497
|
+
if (!integrationKey) {
|
|
498
|
+
const selection = projectType === "singleProject" ? {
|
|
499
|
+
projectType,
|
|
500
|
+
..."framework" in options && options.framework ? { framework: options.framework } : {}
|
|
501
|
+
} : {
|
|
502
|
+
projectType,
|
|
503
|
+
..."frontendFramework" in options && options.frontendFramework ? { frontendFramework: options.frontendFramework } : {},
|
|
504
|
+
..."backendFramework" in options && options.backendFramework ? { backendFramework: options.backendFramework } : {}
|
|
505
|
+
};
|
|
506
|
+
const integration = await getIntegrationForSelection(
|
|
507
|
+
templatesDir,
|
|
508
|
+
selection
|
|
509
|
+
);
|
|
510
|
+
integrationKey = integration?.key;
|
|
511
|
+
}
|
|
512
|
+
if (!integrationKey) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const integrationConfig = await loadIntegrationConfig(
|
|
516
|
+
templatesDir,
|
|
517
|
+
integrationKey
|
|
518
|
+
);
|
|
519
|
+
const integrationRoot = join4(templatesDir, "integrations", integrationKey);
|
|
520
|
+
const filesToApply = [integrationConfig.workflows.prValidation];
|
|
521
|
+
if (options.configureCd && integrationConfig.workflows.deploy) {
|
|
522
|
+
filesToApply.push(integrationConfig.workflows.deploy);
|
|
523
|
+
}
|
|
524
|
+
const applyFile = async (file) => runStep(
|
|
525
|
+
`Adding ${file.target}`,
|
|
526
|
+
async () => copyTemplateFile(
|
|
527
|
+
join4(integrationRoot, file.template),
|
|
528
|
+
join4(projectRoot, file.target),
|
|
529
|
+
options.integrationInputs
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
const applySequentially = async (index) => {
|
|
533
|
+
const file = filesToApply[index];
|
|
534
|
+
if (!file) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await applyFile(file);
|
|
538
|
+
await applySequentially(index + 1);
|
|
539
|
+
};
|
|
540
|
+
await applySequentially(0);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/modules/createScaffold.ts
|
|
544
|
+
import chalk3 from "chalk";
|
|
545
|
+
var accent2 = chalk3.hex(primary2);
|
|
546
|
+
function getAppTargets(options) {
|
|
547
|
+
if (options.projectType === "singleProject") {
|
|
548
|
+
return [
|
|
549
|
+
{
|
|
550
|
+
framework: asFramework(options.framework),
|
|
551
|
+
directory: ".",
|
|
552
|
+
label: "project"
|
|
553
|
+
}
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
return [
|
|
557
|
+
{
|
|
558
|
+
framework: asFramework(options.frontendFramework),
|
|
559
|
+
directory: "frontend",
|
|
560
|
+
label: "frontend"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
framework: asFramework(options.backendFramework),
|
|
564
|
+
directory: "backend",
|
|
565
|
+
label: "backend"
|
|
566
|
+
}
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
async function createProjectStructure(options, projectRoot) {
|
|
570
|
+
if (options.projectType !== "monorepo") {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
await Promise.all([
|
|
574
|
+
mkdir2(join5(projectRoot, "backend"), { recursive: true }),
|
|
575
|
+
mkdir2(join5(projectRoot, "frontend"), { recursive: true })
|
|
576
|
+
]);
|
|
577
|
+
}
|
|
578
|
+
async function initializeGitRepository(projectRoot) {
|
|
579
|
+
const gitDir = join5(projectRoot, ".git");
|
|
580
|
+
try {
|
|
581
|
+
await access2(gitDir);
|
|
582
|
+
return;
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
await runCommandQuiet("git", ["init"], projectRoot);
|
|
587
|
+
} catch {
|
|
588
|
+
console.warn(
|
|
589
|
+
"Git is not available. Husky hooks will activate after running git init."
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function createScaffold(options, targetDir, templatesDir) {
|
|
594
|
+
const projectName = options.projectName.trim();
|
|
595
|
+
if (!projectName) {
|
|
596
|
+
throw new Error("Project name cannot be empty.");
|
|
597
|
+
}
|
|
598
|
+
console.log();
|
|
599
|
+
const projectRoot = join5(targetDir, projectName);
|
|
600
|
+
await runStep(
|
|
601
|
+
"Creating project folder",
|
|
602
|
+
async () => mkdir2(projectRoot, { recursive: true })
|
|
603
|
+
);
|
|
604
|
+
await runStep(
|
|
605
|
+
"Preparing project structure",
|
|
606
|
+
async () => createProjectStructure(options, projectRoot)
|
|
607
|
+
);
|
|
608
|
+
if (options.tools.includes("basic")) {
|
|
609
|
+
await runStep(
|
|
610
|
+
"Initializing Git repository",
|
|
611
|
+
async () => initializeGitRepository(projectRoot)
|
|
612
|
+
);
|
|
613
|
+
await runStep(
|
|
614
|
+
"Copying base tools",
|
|
615
|
+
async () => createTools(options.tools, projectRoot, templatesDir)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
await createApps(projectRoot, getAppTargets(options));
|
|
619
|
+
await createIntegrations(options, projectRoot, templatesDir);
|
|
620
|
+
console.log(accent2(`\u25C6 Project scaffolded at ${projectRoot}`));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/index.ts
|
|
624
|
+
async function main() {
|
|
625
|
+
const { targetDir, templatesDir } = await init();
|
|
626
|
+
await greet();
|
|
627
|
+
const answers = await askQuestions(templatesDir);
|
|
628
|
+
if (!answers.confirmation) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
await createScaffold(answers, targetDir, templatesDir);
|
|
632
|
+
}
|
|
633
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smi-digital/create-smi-app",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-smi-app": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"registry": "https://registry.npmjs.org/"
|
|
17
|
+
},
|
|
18
|
+
"imports": {
|
|
19
|
+
"#src/*": "./src/*",
|
|
20
|
+
"#dist/*": "./dist/*"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
24
|
+
"dev": "npx tsx src/index.ts",
|
|
25
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prettier": "npx prettier --write .",
|
|
28
|
+
"lint": "npx eslint .",
|
|
29
|
+
"prepare": "husky",
|
|
30
|
+
"knip": "knip",
|
|
31
|
+
"depcruise": "depcruise . --exclude \"node_modules|dist|.husky|test\"",
|
|
32
|
+
"depcheck": "npm run knip && npm run depcruise",
|
|
33
|
+
"check": "npm run depcheck && npm run lint && npm run typecheck",
|
|
34
|
+
"prepack": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "ISC",
|
|
38
|
+
"type": "module",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@commitlint/cli": "^20.4.4",
|
|
41
|
+
"@commitlint/config-conventional": "^20.4.4",
|
|
42
|
+
"@eslint/js": "^10.0.1",
|
|
43
|
+
"@eslint/json": "^1.1.0",
|
|
44
|
+
"@types/chalk-animation": "^1.6.3",
|
|
45
|
+
"@types/node": "^25.5.0",
|
|
46
|
+
"dependency-cruiser": "^17.3.9",
|
|
47
|
+
"eslint": "^10.0.3",
|
|
48
|
+
"eslint-config-prettier": "^10.1.8",
|
|
49
|
+
"eslint-config-xo": "^0.50.0",
|
|
50
|
+
"globals": "^17.4.0",
|
|
51
|
+
"husky": "^9.1.7",
|
|
52
|
+
"knip": "^5.86.0",
|
|
53
|
+
"lint-staged": "^16.3.3",
|
|
54
|
+
"prettier": "3.8.1",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"tsx": "^4.21.0",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"typescript-eslint": "^8.57.0"
|
|
59
|
+
},
|
|
60
|
+
"lint-staged": {
|
|
61
|
+
"*.{js,jsx,ts,tsx,mjs,cjs,astro}": [
|
|
62
|
+
"eslint --fix",
|
|
63
|
+
"prettier --write"
|
|
64
|
+
],
|
|
65
|
+
"*.{json,md}": [
|
|
66
|
+
"prettier --write"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@inquirer/prompts": "^8.3.0",
|
|
71
|
+
"chalk": "^5.6.2",
|
|
72
|
+
"chalk-animation": "^2.0.3",
|
|
73
|
+
"figlet": "^1.11.0",
|
|
74
|
+
"gradient-string": "^3.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|