feat: turbostarter boilerplate
Production-ready Next.js boilerplate with: - Runtime env validation (fail-fast on missing vars) - Feature-gated config (S3, Stripe, email, OAuth) - Docker + Coolify deployment pipeline - PostgreSQL + pgvector, MinIO S3, Better Auth - TypeScript strict mode (no ignoreBuildErrors) - i18n (en/es), AI modules, billing, monitoring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
3
tooling/scripts/src/dev.mjs
Normal file
3
tooling/scripts/src/dev.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import "./version.mjs";
|
||||
import "./license.mjs";
|
||||
import "./requirements.mjs";
|
||||
86
tooling/scripts/src/license.mjs
Normal file
86
tooling/scripts/src/license.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
function validateVisibility() {
|
||||
let remoteUrl;
|
||||
|
||||
try {
|
||||
remoteUrl = execSync("git config --get remote.origin.url")
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (error) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!remoteUrl.includes("github.com")) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let ownerRepo;
|
||||
|
||||
if (remoteUrl.startsWith("https://github.com/")) {
|
||||
ownerRepo = remoteUrl.slice("https://github.com/".length);
|
||||
} else if (remoteUrl.startsWith("git@github.com:")) {
|
||||
ownerRepo = remoteUrl.slice("git@github.com:".length);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ownerRepo = ownerRepo.replace(/\.git$/, "");
|
||||
|
||||
return fetch(`https://api.github.com/repos/${ownerRepo}`)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
return res.json();
|
||||
} else if (res.status === 404) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`GitHub API request failed with status code: ${res.status}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
if (data && !data.private) {
|
||||
console.error(
|
||||
"The repository has been LEAKED on GitHub. Please delete the repository. A Takedown Request will automatically be requested in the coming hours.",
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function isOnline() {
|
||||
const { lookup } = await import("dns");
|
||||
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
lookup("google.com", (err) => {
|
||||
if (err && err.code === "ENOTFOUND") {
|
||||
reject(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}).catch(() => false);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isUserOnline = await isOnline();
|
||||
|
||||
if (!isUserOnline) {
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
await validateVisibility();
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
58
tooling/scripts/src/migrations.mjs
Normal file
58
tooling/scripts/src/migrations.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export function checkPendingMigrations() {
|
||||
try {
|
||||
console.info("\x1b[34m%s\x1b[0m", "Checking for pending migrations...");
|
||||
|
||||
const output = execSync("pnpm --filter @turbostarter/db db:status", {
|
||||
encoding: "utf-8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
const lines = output.split("\n");
|
||||
const pendingStart = lines.findIndex(
|
||||
(line) => line.trim() === "Pending migrations:",
|
||||
);
|
||||
|
||||
let pendingMigrations = [];
|
||||
if (pendingStart !== -1) {
|
||||
for (let i = pendingStart + 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line === "") continue;
|
||||
if (line === "(none)") break;
|
||||
if (line.startsWith("- ")) {
|
||||
pendingMigrations.push(line.slice(2).trim());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"⚠️ Could not determine pending migrations. Please check manually.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingMigrations.length > 0) {
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"⚠️ There are pending migrations that need to be applied:",
|
||||
);
|
||||
pendingMigrations.forEach((migration) => console.log(` - ${migration}`));
|
||||
console.log(
|
||||
"\nSome functionality may not work as expected until these migrations are applied.",
|
||||
);
|
||||
console.log(
|
||||
'\nAfter testing the migrations in your local environment and ideally in a staging environment, please run "pnpm --filter @turbostarter/db db:migrate" to apply them to your database. If you have any questions, please open a support ticket.',
|
||||
);
|
||||
} else {
|
||||
console.log("\x1b[32m%s\x1b[0m", "✅ All migrations are up to date.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"❌ Could not connect to the database. Please ensure your connection string is up to date and your database instance is running.\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
61
tooling/scripts/src/requirements.mjs
Normal file
61
tooling/scripts/src/requirements.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
// check requirements to run TurboStarter
|
||||
void checkRequirements();
|
||||
|
||||
function checkRequirements() {
|
||||
validateNodeInstalled();
|
||||
validatePnpmInstalled();
|
||||
validatePathNotOneDrive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that pnpm is installed.
|
||||
* If pnpm is not installed, it exits the script with an error message.
|
||||
*/
|
||||
function validatePnpmInstalled() {
|
||||
const currentPnpmVersion = execSync("pnpm --version").toString().trim();
|
||||
|
||||
if (!currentPnpmVersion) {
|
||||
console.error(
|
||||
`\x1b[31m%s\x1b[0m`,
|
||||
`You are running TurboStarter from a directory that does not have pnpm installed. Please install pnpm and run "pnpm install" in your project directory.`,
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that Node is installed.
|
||||
* If Node is not installed, it exits the script with an error message.
|
||||
*/
|
||||
function validateNodeInstalled() {
|
||||
const currentNodeVersion = process.versions.node;
|
||||
|
||||
if (!currentNodeVersion) {
|
||||
console.error(
|
||||
`\x1b[31m%s\x1b[0m`,
|
||||
`You are running TurboStarter from a device that does not have Node installed. Please install Node (https://nodejs.org/en/download/) and try again.`,
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current working directory is not OneDrive.
|
||||
* If the current working directory is OneDrive, it exits the script with an error message.
|
||||
*/
|
||||
function validatePathNotOneDrive() {
|
||||
const path = process.cwd();
|
||||
|
||||
if (path.includes("OneDrive")) {
|
||||
console.error(
|
||||
`\x1b[31m%s\x1b[0m`,
|
||||
`You are running TurboStarter from OneDrive. Please move your project to a local folder.`,
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
93
tooling/scripts/src/version.mjs
Normal file
93
tooling/scripts/src/version.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
import { checkPendingMigrations } from "./migrations.mjs";
|
||||
|
||||
function runGitCommand(command) {
|
||||
try {
|
||||
return execSync(command, { encoding: "utf8", stdio: "pipe" }).trim();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkTurboStarterVersion() {
|
||||
const fetchResult = runGitCommand("git fetch upstream");
|
||||
|
||||
if (fetchResult === null) {
|
||||
console.info(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"⚠️ You have not setup 'upstream'. Please set up the upstream remote so you can update your TurboStarter version.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const behindCount = runGitCommand("git rev-list --count HEAD..upstream/main");
|
||||
|
||||
if (behindCount === null) {
|
||||
console.warn(
|
||||
"Failed to get commit count. Ensure you're on a branch that tracks upstream/main.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const count = parseInt(behindCount, 10);
|
||||
const { severity } = getSeveriyLevel(count);
|
||||
|
||||
if (severity === "critical") {
|
||||
console.log(
|
||||
"\x1b[31m%s\x1b[0m",
|
||||
"❌ Your TurboStarter version is outdated. Please update to the latest version.",
|
||||
);
|
||||
} else if (severity === "warning") {
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"⚠️ Your TurboStarter version is outdated! Best to update to the latest version.",
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"\x1b[32m%s\x1b[0m",
|
||||
"✅ Your TurboStarter version is up to date!",
|
||||
);
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
logInstructions(count);
|
||||
}
|
||||
}
|
||||
|
||||
function logInstructions(count) {
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
`You are ${count} commit(s) behind the latest version.`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
"\x1b[33m%s\x1b[0m",
|
||||
"Please consider updating to the latest version for bug fixes and optimizations that your version does not have.",
|
||||
);
|
||||
|
||||
console.log("\x1b[36m%s\x1b[0m", "To update, run: git pull upstream main");
|
||||
}
|
||||
|
||||
function getSeveriyLevel(count) {
|
||||
if (count > 5) {
|
||||
return {
|
||||
severity: "critical",
|
||||
};
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
return {
|
||||
severity: "warning",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
severity: "success",
|
||||
};
|
||||
}
|
||||
|
||||
checkTurboStarterVersion();
|
||||
checkPendingMigrations();
|
||||
Reference in New Issue
Block a user