stackkit 0.1.7 → 0.1.8
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/lib/env/env-editor.js +2 -18
- package/dist/lib/generation/code-generator.js +107 -20
- package/dist/lib/generation/generator-utils.js +18 -5
- package/modules/auth/authjs/generator.json +8 -8
- package/modules/auth/better-auth/files/lib/auth.ts +8 -4
- package/modules/auth/better-auth/files/lib/email-service.ts +1 -2
- package/modules/auth/better-auth/generator.json +9 -8
- package/modules/database/mongoose/generator.json +9 -7
- package/modules/database/prisma/generator.json +17 -12
- package/package.json +1 -1
|
@@ -8,8 +8,6 @@ exports.removeEnvVariables = removeEnvVariables;
|
|
|
8
8
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const logger_1 = require("../ui/logger");
|
|
11
|
-
const ENV_MARKER_START = "# StackKit:";
|
|
12
|
-
const ENV_MARKER_END = "# End StackKit";
|
|
13
11
|
async function addEnvVariables(projectRoot, variables, options = {}) {
|
|
14
12
|
const envExamplePath = path_1.default.join(projectRoot, ".env.example");
|
|
15
13
|
const envPath = path_1.default.join(projectRoot, ".env");
|
|
@@ -59,14 +57,11 @@ async function appendToEnvFile(filePath, variables, fileType, options = {}) {
|
|
|
59
57
|
if (content && !content.endsWith("\n")) {
|
|
60
58
|
content += "\n";
|
|
61
59
|
}
|
|
62
|
-
//
|
|
63
|
-
content += "\n";
|
|
64
|
-
content += `${ENV_MARKER_START} Added by StackKit\n`;
|
|
60
|
+
// Append variables
|
|
65
61
|
for (const variable of newVariables) {
|
|
66
62
|
const value = fileType === "example" ? variable.value || "" : variable.value || "";
|
|
67
63
|
content += `${variable.key}=${value}\n`;
|
|
68
64
|
}
|
|
69
|
-
content += `${ENV_MARKER_END}\n`;
|
|
70
65
|
await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
|
|
71
66
|
await fs_extra_1.default.writeFile(filePath, content, "utf-8");
|
|
72
67
|
}
|
|
@@ -86,23 +81,12 @@ async function removeFromEnvFile(filePath, keys) {
|
|
|
86
81
|
const content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
87
82
|
const lines = content.split("\n");
|
|
88
83
|
const newLines = [];
|
|
89
|
-
let inStackKitBlock = false;
|
|
90
84
|
for (const line of lines) {
|
|
91
|
-
if (line.includes(ENV_MARKER_START)) {
|
|
92
|
-
inStackKitBlock = true;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (line.includes(ENV_MARKER_END)) {
|
|
96
|
-
inStackKitBlock = false;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
85
|
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
100
86
|
if (match && keys.includes(match[1])) {
|
|
101
87
|
continue;
|
|
102
88
|
}
|
|
103
|
-
|
|
104
|
-
newLines.push(line);
|
|
105
|
-
}
|
|
89
|
+
newLines.push(line);
|
|
106
90
|
}
|
|
107
91
|
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
|
|
108
92
|
newLines.pop();
|
|
@@ -214,14 +214,14 @@ class AdvancedCodeGenerator {
|
|
|
214
214
|
const [, caseValue, caseContent] = caseMatch;
|
|
215
215
|
const cleanCaseValue = caseValue.trim().replace(/['"]/g, "");
|
|
216
216
|
if (cleanCaseValue === "default") {
|
|
217
|
-
defaultCase = caseContent;
|
|
217
|
+
defaultCase = caseContent.trim();
|
|
218
218
|
}
|
|
219
219
|
else if (actualVal === cleanCaseValue) {
|
|
220
|
-
result = caseContent;
|
|
220
|
+
result = caseContent.trim();
|
|
221
221
|
break;
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
return result || defaultCase || "";
|
|
224
|
+
return (result || defaultCase || "").trim();
|
|
225
225
|
});
|
|
226
226
|
// Handle variable replacement with advanced expressions
|
|
227
227
|
content = content.replace(/\{\{([^}]+)\}\}/g, (match, varExpr) => {
|
|
@@ -378,6 +378,18 @@ class AdvancedCodeGenerator {
|
|
|
378
378
|
!relativePath.startsWith("node_modules/"));
|
|
379
379
|
},
|
|
380
380
|
});
|
|
381
|
+
// If template provides a .env.example, create a .env from it when
|
|
382
|
+
// the target project does not already have a .env file.
|
|
383
|
+
try {
|
|
384
|
+
const envExampleSrc = path.join(templatePath, ".env.example");
|
|
385
|
+
const envDest = path.join(outputPath, ".env");
|
|
386
|
+
if ((await fs.pathExists(envExampleSrc)) && !(await fs.pathExists(envDest))) {
|
|
387
|
+
await fs.copy(envExampleSrc, envDest);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// ignore failures here — not critical
|
|
392
|
+
}
|
|
381
393
|
}
|
|
382
394
|
}
|
|
383
395
|
processOperationTemplates(operation, context) {
|
|
@@ -469,34 +481,107 @@ class AdvancedCodeGenerator {
|
|
|
469
481
|
switch (patchOp.type) {
|
|
470
482
|
case "add-import":
|
|
471
483
|
if (patchOp.imports) {
|
|
484
|
+
// Process imports and trim any accidental surrounding blank lines
|
|
472
485
|
const imports = patchOp.imports
|
|
473
486
|
.map((imp) => this.processTemplate(imp, context))
|
|
474
|
-
.join("\n")
|
|
475
|
-
|
|
487
|
+
.join("\n")
|
|
488
|
+
.replace(/^\n+/, "")
|
|
489
|
+
.replace(/\n+$/, "");
|
|
490
|
+
// Add imports at the top, after existing imports without introducing
|
|
491
|
+
// extra blank lines.
|
|
476
492
|
const lines = content.split("\n");
|
|
477
|
-
|
|
493
|
+
// Find the last import line index
|
|
494
|
+
let lastImportIndex = -1;
|
|
478
495
|
for (let i = 0; i < lines.length; i++) {
|
|
479
|
-
if (lines[i].trim().startsWith("import")
|
|
480
|
-
|
|
496
|
+
if (lines[i].trim().startsWith("import"))
|
|
497
|
+
lastImportIndex = i;
|
|
498
|
+
}
|
|
499
|
+
// Insert right after the last import. If there's a blank line
|
|
500
|
+
// immediately after the imports, overwrite that blank line to
|
|
501
|
+
// avoid introducing an extra empty line above the inserted imports.
|
|
502
|
+
const insertIndex = lastImportIndex === -1 ? 0 : lastImportIndex + 1;
|
|
503
|
+
// Only add imports that don't already exist in the file
|
|
504
|
+
const importLines = imports
|
|
505
|
+
.split("\n")
|
|
506
|
+
.map((l) => l.trim())
|
|
507
|
+
.filter(Boolean);
|
|
508
|
+
const newImportLines = importLines.filter((imp) => !lines.some((ln) => ln.trim() === imp));
|
|
509
|
+
if (newImportLines.length > 0) {
|
|
510
|
+
// Insert imports
|
|
511
|
+
if (insertIndex < lines.length && lines[insertIndex].trim() === "") {
|
|
512
|
+
lines.splice(insertIndex, 1, ...newImportLines);
|
|
481
513
|
}
|
|
482
514
|
else {
|
|
483
|
-
|
|
515
|
+
lines.splice(insertIndex, 0, ...newImportLines);
|
|
516
|
+
}
|
|
517
|
+
// After insertion, ensure exactly one blank line after the import block
|
|
518
|
+
// Find last import line index again
|
|
519
|
+
let lastIdx = -1;
|
|
520
|
+
for (let i = 0; i < lines.length; i++) {
|
|
521
|
+
if (lines[i].trim().startsWith("import"))
|
|
522
|
+
lastIdx = i;
|
|
523
|
+
}
|
|
524
|
+
const nextIdx = lastIdx + 1;
|
|
525
|
+
if (lastIdx !== -1) {
|
|
526
|
+
// Remove multiple blank lines after imports
|
|
527
|
+
let j = nextIdx;
|
|
528
|
+
while (j < lines.length && lines[j].trim() === "") {
|
|
529
|
+
j++;
|
|
530
|
+
}
|
|
531
|
+
// Ensure exactly one blank line after imports unless imports end at EOF
|
|
532
|
+
if (nextIdx < lines.length) {
|
|
533
|
+
lines.splice(nextIdx, j - nextIdx, "");
|
|
534
|
+
}
|
|
484
535
|
}
|
|
485
536
|
}
|
|
486
|
-
lines.splice(insertIndex, 0, imports);
|
|
487
537
|
content = lines.join("\n");
|
|
488
538
|
}
|
|
489
539
|
break;
|
|
490
540
|
case "add-code":
|
|
491
|
-
if (patchOp.code
|
|
541
|
+
if (patchOp.code) {
|
|
492
542
|
const processedCode = this.processTemplate(patchOp.code, context);
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
543
|
+
// Skip insertion if the exact code already exists in the file
|
|
544
|
+
const codeTrimmed = processedCode.trim();
|
|
545
|
+
if (codeTrimmed && content.includes(codeTrimmed)) {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
// Insert after pattern if provided
|
|
549
|
+
if (patchOp.after) {
|
|
550
|
+
const afterPattern = this.processTemplate(patchOp.after, context);
|
|
551
|
+
const index = content.indexOf(afterPattern);
|
|
552
|
+
if (index !== -1) {
|
|
553
|
+
const left = content.slice(0, index + afterPattern.length);
|
|
554
|
+
const right = content.slice(index + afterPattern.length);
|
|
555
|
+
// Normalize code: trim surrounding newlines and ensure single trailing newline
|
|
556
|
+
let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
|
|
557
|
+
// If right already starts with a newline, avoid double-blank by
|
|
558
|
+
// removing trailing newline from codeNormalized so only one newline remains
|
|
559
|
+
const rightStartsWithNewline = right.startsWith("\n");
|
|
560
|
+
if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
|
|
561
|
+
codeNormalized = codeNormalized.replace(/\n+$/, "");
|
|
562
|
+
}
|
|
563
|
+
const leftNeedsNewline = !left.endsWith("\n");
|
|
564
|
+
content = left + (leftNeedsNewline ? "\n" : "") + codeNormalized + right;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Insert before pattern if provided
|
|
568
|
+
if (patchOp.before) {
|
|
569
|
+
const beforePattern = this.processTemplate(patchOp.before, context);
|
|
570
|
+
const index = content.indexOf(beforePattern);
|
|
571
|
+
if (index !== -1) {
|
|
572
|
+
const left = content.slice(0, index);
|
|
573
|
+
const right = content.slice(index);
|
|
574
|
+
// Normalize code: trim surrounding newlines and ensure single trailing newline
|
|
575
|
+
let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
|
|
576
|
+
// If right already starts with a newline, avoid double-blank by
|
|
577
|
+
// removing trailing newline from codeNormalized so only one newline remains
|
|
578
|
+
const rightStartsWithNewline = right.startsWith("\n");
|
|
579
|
+
if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
|
|
580
|
+
codeNormalized = codeNormalized.replace(/\n+$/, "");
|
|
581
|
+
}
|
|
582
|
+
const leftNeedsNewline = !left.endsWith("\n");
|
|
583
|
+
content = left + (leftNeedsNewline ? "\n" : "") + codeNormalized + right;
|
|
584
|
+
}
|
|
500
585
|
}
|
|
501
586
|
}
|
|
502
587
|
break;
|
|
@@ -546,6 +631,8 @@ class AdvancedCodeGenerator {
|
|
|
546
631
|
}
|
|
547
632
|
}
|
|
548
633
|
}
|
|
634
|
+
// Normalize excessive blank lines introduced during patching
|
|
635
|
+
content = content.replace(/\n{3,}/g, "\n\n");
|
|
549
636
|
// Write back the modified content
|
|
550
637
|
await fs.writeFile(filePath, content, "utf-8");
|
|
551
638
|
}
|
|
@@ -620,8 +707,8 @@ class AdvancedCodeGenerator {
|
|
|
620
707
|
if ((type === "framework" && name === selectedModules.framework) ||
|
|
621
708
|
(type === "database" && name === selectedModules.database) ||
|
|
622
709
|
(type === "auth" && name === selectedModules.auth)) {
|
|
623
|
-
|
|
624
|
-
|
|
710
|
+
// Dependencies and devDependencies are now provided via `add-dependency`
|
|
711
|
+
// operations. Keep merging scripts from generator configs for now.
|
|
625
712
|
Object.assign(allScripts, generator.scripts);
|
|
626
713
|
}
|
|
627
714
|
}
|
|
@@ -90,11 +90,24 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
// Collect dependencies/devDependencies from add-dependency operations
|
|
94
|
+
if (generator.operations && Array.isArray(generator.operations)) {
|
|
95
|
+
for (const operation of generator.operations) {
|
|
96
|
+
if (operation.type === "add-dependency") {
|
|
97
|
+
if (operation.dependencies) {
|
|
98
|
+
metadata.dependencies = {
|
|
99
|
+
...metadata.dependencies,
|
|
100
|
+
...operation.dependencies,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (operation.devDependencies) {
|
|
104
|
+
metadata.devDependencies = {
|
|
105
|
+
...metadata.devDependencies,
|
|
106
|
+
...operation.devDependencies,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
98
111
|
}
|
|
99
112
|
if (generator.postInstall && Array.isArray(generator.postInstall)) {
|
|
100
113
|
metadata.postInstall = metadata.postInstall || [];
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
"condition": { "database": "prisma" },
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@auth/prisma-adapter": "^0.5.0"
|
|
37
|
-
}
|
|
38
|
-
"devDependencies": {}
|
|
37
|
+
}
|
|
39
38
|
},
|
|
40
39
|
{
|
|
41
40
|
"type": "add-env",
|
|
@@ -44,11 +43,12 @@
|
|
|
44
43
|
"AUTH_GOOGLE_ID": "",
|
|
45
44
|
"AUTH_GOOGLE_SECRET": ""
|
|
46
45
|
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "add-dependency",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"next-auth": "^5.0.0-beta.30"
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
|
-
]
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"next-auth": "^5.0.0-beta.30"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {},
|
|
53
|
-
"scripts": {}
|
|
53
|
+
]
|
|
54
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { betterAuth } from "better-auth";
|
|
1
|
+
import { betterAuth, env } from "better-auth";
|
|
2
2
|
import { sendEmail } from "./email/email-service";
|
|
3
3
|
import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from "./email/email-templates";
|
|
4
4
|
{{#switch database}}
|
|
@@ -13,6 +13,12 @@ import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
|
13
13
|
{{/switch}}
|
|
14
14
|
|
|
15
15
|
export async function initAuth() {
|
|
16
|
+
{{#if database == 'mongoose'}}
|
|
17
|
+
const mongooseInstance = await mongoose();
|
|
18
|
+
const client = mongooseInstance.connection.getClient();
|
|
19
|
+
const db = client.db();
|
|
20
|
+
{{/if}}
|
|
21
|
+
|
|
16
22
|
return betterAuth({
|
|
17
23
|
{{#switch database}}
|
|
18
24
|
{{#case prisma}}
|
|
@@ -21,12 +27,10 @@ return betterAuth({
|
|
|
21
27
|
}),
|
|
22
28
|
{{/case}}
|
|
23
29
|
{{#case mongoose}}
|
|
24
|
-
const mongooseInstance = await mongoose();
|
|
25
|
-
const client = mongooseInstance.connection.getClient();
|
|
26
|
-
const db = client.db();
|
|
27
30
|
database: mongodbAdapter(db, { client }),
|
|
28
31
|
{{/case}}
|
|
29
32
|
{{/switch}}
|
|
33
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
30
34
|
user: {
|
|
31
35
|
additionalFields: {
|
|
32
36
|
role: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import nodemailer from "nodemailer";
|
|
2
2
|
|
|
3
3
|
// Create email transporter
|
|
4
|
-
const transporter = nodemailer.
|
|
4
|
+
const transporter = nodemailer.createTransport({
|
|
5
5
|
host: process.env.EMAIL_HOST,
|
|
6
6
|
port: parseInt(process.env.EMAIL_PORT || "587"),
|
|
7
7
|
secure: process.env.EMAIL_PORT === "465",
|
|
@@ -27,7 +27,6 @@ export const sendEmail = async ({ to, subject, text, html }: {
|
|
|
27
27
|
html,
|
|
28
28
|
});
|
|
29
29
|
} catch (error) {
|
|
30
|
-
// eslint-disable-next-line no-console
|
|
31
30
|
console.error("Email sending failed:", error);
|
|
32
31
|
throw error;
|
|
33
32
|
}
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"operations": [
|
|
52
52
|
{
|
|
53
53
|
"type": "add-import",
|
|
54
|
-
"imports": ["import { initAuth } from \"
|
|
54
|
+
"imports": ["import { initAuth } from \"../lib/auth\";"]
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"type": "add-code",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"operations": [
|
|
68
68
|
{
|
|
69
69
|
"type": "add-import",
|
|
70
|
-
"imports": ["import { auth } from \"
|
|
70
|
+
"imports": ["import { auth } from \"../lib/auth\";",
|
|
71
71
|
"import { toNodeHandler } from \"better-auth/node\";"]
|
|
72
72
|
},
|
|
73
73
|
{
|
|
@@ -97,12 +97,13 @@
|
|
|
97
97
|
"EMAIL_USER": "",
|
|
98
98
|
"EMAIL_PASS": "",
|
|
99
99
|
"EMAIL_FROM": "noreply@yourapp.com"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "add-dependency",
|
|
104
|
+
"dependencies": {
|
|
105
|
+
"better-auth": "^1.4.12"
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
]
|
|
103
|
-
"dependencies": {
|
|
104
|
-
"better-auth": "^1.4.12"
|
|
105
|
-
},
|
|
106
|
-
"devDependencies": {},
|
|
107
|
-
"scripts": {}
|
|
108
|
+
]
|
|
108
109
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"type": "create-file",
|
|
8
8
|
"source": "lib/mongoose.ts",
|
|
9
|
-
"destination": "
|
|
9
|
+
"destination": "lib/mongoose.ts"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"type": "create-file",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"operations": [
|
|
21
21
|
{
|
|
22
22
|
"type": "add-import",
|
|
23
|
-
"imports": ["import { mongoose } from \"
|
|
23
|
+
"imports": ["import { mongoose } from \"../lib/mongoose\";"]
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "add-code",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
"envVars": {
|
|
35
35
|
"MONGODB_URI": "mongodb://localhost:27017/database_name"
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "add-dependency",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"mongoose": "^8.8.0"
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
|
-
]
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"mongoose": "^8.8.0"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {}
|
|
44
|
+
]
|
|
43
45
|
}
|
|
@@ -91,17 +91,22 @@
|
|
|
91
91
|
"envVars": {
|
|
92
92
|
"DATABASE_URL": "mongodb://localhost:27017/database_name"
|
|
93
93
|
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"type": "add-dependency",
|
|
97
|
+
"dependencies": {
|
|
98
|
+
"dotenv": "^17.2.3"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "add-script",
|
|
103
|
+
"scripts": {
|
|
104
|
+
"db:generate": "npx prisma generate",
|
|
105
|
+
"db:push": "npx prisma db push",
|
|
106
|
+
"db:seed": "tsx prisma/seed.ts",
|
|
107
|
+
"db:migrate": "npx prisma migrate dev",
|
|
108
|
+
"db:studio": "npx prisma studio"
|
|
109
|
+
}
|
|
94
110
|
}
|
|
95
|
-
]
|
|
96
|
-
"dependencies": {
|
|
97
|
-
"dotenv": "^17.2.3"
|
|
98
|
-
},
|
|
99
|
-
"devDependencies": {},
|
|
100
|
-
"scripts": {
|
|
101
|
-
"db:generate": "npx prisma generate",
|
|
102
|
-
"db:push": "npx prisma db push",
|
|
103
|
-
"db:seed": "tsx prisma/seed.ts",
|
|
104
|
-
"db:migrate": "npx prisma migrate dev",
|
|
105
|
-
"db:studio": "npx prisma studio"
|
|
106
|
-
}
|
|
111
|
+
]
|
|
107
112
|
}
|