@aime-aatrick/create-fullstack-template 1.0.6 → 1.0.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.
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
5
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile, copyFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import degit from "degit";
|
|
@@ -58,10 +59,167 @@ const askPackageManager = async (rl) => {
|
|
|
58
59
|
return "pnpm";
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
const readIfExists = async (path) => {
|
|
63
|
+
if (!existsSync(path)) return null;
|
|
64
|
+
return readFile(path, "utf8");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const shouldIgnoreRelativePath = (relativePath) => {
|
|
68
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
69
|
+
if (
|
|
70
|
+
normalized === ".env" ||
|
|
71
|
+
normalized === ".env.local" ||
|
|
72
|
+
normalized === ".env.example" ||
|
|
73
|
+
normalized === "backend/.env" ||
|
|
74
|
+
normalized === "backend/.env.example" ||
|
|
75
|
+
normalized === "frontend/.env.local" ||
|
|
76
|
+
normalized === "frontend/.env.example"
|
|
77
|
+
) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ignoredPathParts = [".git", "node_modules", ".next", "dist", "coverage", ".turbo"];
|
|
82
|
+
return ignoredPathParts.some(
|
|
83
|
+
(part) => normalized === part || normalized.startsWith(`${part}/`) || normalized.includes(`/${part}/`),
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const syncTemplateTree = async (sourceDir, destinationDir, options) => {
|
|
88
|
+
const { force } = options;
|
|
89
|
+
const stats = {
|
|
90
|
+
copied: 0,
|
|
91
|
+
updated: 0,
|
|
92
|
+
skipped: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const walk = async (sourcePath, destinationPath, relativePath = "") => {
|
|
96
|
+
if (shouldIgnoreRelativePath(relativePath)) return;
|
|
97
|
+
|
|
98
|
+
const sourceStat = await stat(sourcePath);
|
|
99
|
+
if (sourceStat.isDirectory()) {
|
|
100
|
+
if (!existsSync(destinationPath)) {
|
|
101
|
+
await mkdir(destinationPath, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
const entries = await readdir(sourcePath, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const childSource = join(sourcePath, entry.name);
|
|
106
|
+
const childDestination = join(destinationPath, entry.name);
|
|
107
|
+
const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
108
|
+
await walk(childSource, childDestination, childRelative);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!existsSync(destinationPath)) {
|
|
114
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
115
|
+
await copyFile(sourcePath, destinationPath);
|
|
116
|
+
stats.copied += 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sourceContent = await readFile(sourcePath);
|
|
121
|
+
const destinationContent = await readFile(destinationPath);
|
|
122
|
+
if (sourceContent.equals(destinationContent)) return;
|
|
123
|
+
|
|
124
|
+
if (!force) {
|
|
125
|
+
stats.skipped += 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await writeFile(destinationPath, sourceContent);
|
|
130
|
+
stats.updated += 1;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await walk(sourceDir, destinationDir);
|
|
134
|
+
return stats;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const runUpdate = async (args) => {
|
|
138
|
+
const force = args.includes("--force");
|
|
139
|
+
const targetArg = args.find((value) => !value.startsWith("--")) || ".";
|
|
140
|
+
const targetDir = resolve(process.cwd(), targetArg);
|
|
141
|
+
|
|
142
|
+
if (!existsSync(targetDir)) {
|
|
143
|
+
throw new Error(`Target folder does not exist: ${targetDir}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const backendExists = existsSync(join(targetDir, "backend"));
|
|
147
|
+
const frontendExists = existsSync(join(targetDir, "frontend"));
|
|
148
|
+
if (!backendExists && !frontendExists) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
'Target does not look like a generated project. Expected "backend" and/or "frontend" folders.',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "fullstack-template-update-"));
|
|
155
|
+
const templateDir = join(tempRoot, "template");
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
console.log(`\nFetching latest template from ${REPO} ...`);
|
|
159
|
+
const emitter = degit(REPO, { cache: false, force: true, verbose: true });
|
|
160
|
+
await emitter.clone(templateDir);
|
|
161
|
+
|
|
162
|
+
await rm(join(templateDir, "create-fullstack-template"), { recursive: true, force: true });
|
|
163
|
+
await rm(join(templateDir, ".github"), { recursive: true, force: true });
|
|
164
|
+
|
|
165
|
+
const total = { copied: 0, updated: 0, skipped: 0 };
|
|
166
|
+
|
|
167
|
+
if (backendExists && existsSync(join(templateDir, "backend"))) {
|
|
168
|
+
console.log("Updating backend files...");
|
|
169
|
+
const backendStats = await syncTemplateTree(
|
|
170
|
+
join(templateDir, "backend"),
|
|
171
|
+
join(targetDir, "backend"),
|
|
172
|
+
{ force },
|
|
173
|
+
);
|
|
174
|
+
total.copied += backendStats.copied;
|
|
175
|
+
total.updated += backendStats.updated;
|
|
176
|
+
total.skipped += backendStats.skipped;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (frontendExists && existsSync(join(templateDir, "frontend"))) {
|
|
180
|
+
console.log("Updating frontend files...");
|
|
181
|
+
const frontendStats = await syncTemplateTree(
|
|
182
|
+
join(templateDir, "frontend"),
|
|
183
|
+
join(targetDir, "frontend"),
|
|
184
|
+
{ force },
|
|
185
|
+
);
|
|
186
|
+
total.copied += frontendStats.copied;
|
|
187
|
+
total.updated += frontendStats.updated;
|
|
188
|
+
total.skipped += frontendStats.skipped;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const templateReadme = await readIfExists(join(templateDir, "README.md"));
|
|
192
|
+
if (templateReadme && (force || !existsSync(join(targetDir, "README.md")))) {
|
|
193
|
+
const readmeExists = existsSync(join(targetDir, "README.md"));
|
|
194
|
+
await writeFile(join(targetDir, "README.md"), templateReadme, "utf8");
|
|
195
|
+
if (readmeExists) {
|
|
196
|
+
total.updated += 1;
|
|
197
|
+
} else {
|
|
198
|
+
total.copied += 1;
|
|
199
|
+
}
|
|
200
|
+
} else if (templateReadme) {
|
|
201
|
+
total.skipped += 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log("\nUpdate complete.");
|
|
205
|
+
console.log(`- Copied new files: ${total.copied}`);
|
|
206
|
+
console.log(`- Updated files: ${total.updated}`);
|
|
207
|
+
console.log(`- Skipped conflicts (use --force to overwrite): ${total.skipped}`);
|
|
208
|
+
} finally {
|
|
209
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
61
213
|
const main = async () => {
|
|
62
214
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
63
215
|
|
|
64
216
|
try {
|
|
217
|
+
const command = process.argv[2];
|
|
218
|
+
if (command === "update") {
|
|
219
|
+
await runUpdate(process.argv.slice(3));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
65
223
|
const argName = process.argv[2];
|
|
66
224
|
const projectName =
|
|
67
225
|
argName || (await rl.question("Project name (folder): ")).trim();
|
|
@@ -193,13 +351,22 @@ const main = async () => {
|
|
|
193
351
|
if (hasFrontend) {
|
|
194
352
|
let frontendEnv = await readEnvTemplate(
|
|
195
353
|
frontendEnvExample,
|
|
196
|
-
[
|
|
354
|
+
[
|
|
355
|
+
"NEXT_PUBLIC_API_URL=http://localhost:3000",
|
|
356
|
+
"NEXT_PUBLIC_DESIGN_MODE=false",
|
|
357
|
+
"",
|
|
358
|
+
].join("\n"),
|
|
197
359
|
);
|
|
198
360
|
frontendEnv = setEnvValue(
|
|
199
361
|
frontendEnv,
|
|
200
362
|
"NEXT_PUBLIC_API_URL",
|
|
201
363
|
`http://localhost:${backendPort}`,
|
|
202
364
|
);
|
|
365
|
+
frontendEnv = setEnvValue(
|
|
366
|
+
frontendEnv,
|
|
367
|
+
"NEXT_PUBLIC_DESIGN_MODE",
|
|
368
|
+
mode === "frontend" ? "true" : "false",
|
|
369
|
+
);
|
|
203
370
|
await writeFile(frontendEnvPath, frontendEnv, "utf8");
|
|
204
371
|
}
|
|
205
372
|
|