@aime-aatrick/create-fullstack-template 1.0.5 → 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();
|
|
@@ -115,6 +273,10 @@ const main = async () => {
|
|
|
115
273
|
const emitter = degit(REPO, { cache: false, force: true, verbose: true });
|
|
116
274
|
await emitter.clone(targetDir);
|
|
117
275
|
|
|
276
|
+
// Remove template-maintenance folders that should not ship in generated apps.
|
|
277
|
+
await rm(join(targetDir, "create-fullstack-template"), { recursive: true, force: true });
|
|
278
|
+
await rm(join(targetDir, ".github"), { recursive: true, force: true });
|
|
279
|
+
|
|
118
280
|
if (mode === "backend") {
|
|
119
281
|
await rm(join(targetDir, "frontend"), { recursive: true, force: true });
|
|
120
282
|
} else if (mode === "frontend") {
|
|
@@ -189,13 +351,22 @@ const main = async () => {
|
|
|
189
351
|
if (hasFrontend) {
|
|
190
352
|
let frontendEnv = await readEnvTemplate(
|
|
191
353
|
frontendEnvExample,
|
|
192
|
-
[
|
|
354
|
+
[
|
|
355
|
+
"NEXT_PUBLIC_API_URL=http://localhost:3000",
|
|
356
|
+
"NEXT_PUBLIC_DESIGN_MODE=false",
|
|
357
|
+
"",
|
|
358
|
+
].join("\n"),
|
|
193
359
|
);
|
|
194
360
|
frontendEnv = setEnvValue(
|
|
195
361
|
frontendEnv,
|
|
196
362
|
"NEXT_PUBLIC_API_URL",
|
|
197
363
|
`http://localhost:${backendPort}`,
|
|
198
364
|
);
|
|
365
|
+
frontendEnv = setEnvValue(
|
|
366
|
+
frontendEnv,
|
|
367
|
+
"NEXT_PUBLIC_DESIGN_MODE",
|
|
368
|
+
mode === "frontend" ? "true" : "false",
|
|
369
|
+
);
|
|
199
370
|
await writeFile(frontendEnvPath, frontendEnv, "utf8");
|
|
200
371
|
}
|
|
201
372
|
|