Revert "Unify workspace dropdowns; Update layouts (#458)"

This reverts commit 4bc8448a1d.
This commit is contained in:
gbuomprisco
2026-03-11 14:47:47 +08:00
parent 4bc8448a1d
commit 4912e402a3
530 changed files with 11182 additions and 14382 deletions

View File

@@ -2,14 +2,18 @@ import type { PlopTypes } from '@turbo/gen';
import { createCloudflareGenerator } from './templates/cloudflare/generator';
import { createDockerGenerator } from './templates/docker/generator';
import { createEnvironmentVariablesGenerator } from './templates/env/generator';
import { createKeystaticAdminGenerator } from './templates/keystatic/generator';
import { createPackageGenerator } from './templates/package/generator';
import { createSetupGenerator } from './templates/setup/generator';
import { createEnvironmentVariablesValidatorGenerator } from './templates/validate-env/generator';
// List of generators to be registered
const generators = [
createPackageGenerator,
createKeystaticAdminGenerator,
createEnvironmentVariablesGenerator,
createEnvironmentVariablesValidatorGenerator,
createSetupGenerator,
createCloudflareGenerator,
createDockerGenerator,

View File

@@ -65,7 +65,7 @@ ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=90s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/api/healthcheck || exit 1
CMD curl -f http://localhost:3000/healthcheck || exit 1
# Start the server
CMD ["node", "apps/web/server.js"]

View File

@@ -0,0 +1,340 @@
import type { PlopTypes } from '@turbo/gen';
import { writeFileSync } from 'node:fs';
import { generator } from '../../utils';
const DOCS_URL =
'https://makerkit.dev/docs/next-supabase-turbo/environment-variables';
export function createEnvironmentVariablesGenerator(
plop: PlopTypes.NodePlopAPI,
) {
const allVariables = generator.loadAllEnvironmentVariables('apps/web');
if (allVariables) {
console.log(
`Loaded ${Object.values(allVariables).length} default environment variables in your env files. We use these as defaults.`,
);
}
return plop.setGenerator('env', {
description: 'Generate the environment variables to be used in the app',
actions: [
async (answers) => {
let env = '';
for (const [key, value] of Object.entries(
(
answers as {
values: Record<string, string>;
}
).values,
)) {
env += `${key}=${value}\n`;
}
writeFileSync('turbo/generators/templates/env/.env.local', env);
return 'Environment variables generated at /turbo/generators/templates/env/.env.local.\nPlease double check and use this file in your hosting provider to set the environment variables. \nNever commit this file, it contains secrets!';
},
],
prompts: [
{
type: 'input',
name: 'values.NEXT_PUBLIC_SITE_URL',
message: `What is the site URL of you website? (Ex. https://makerkit.dev). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_URL')}\n`,
default: allVariables.NEXT_PUBLIC_SITE_URL,
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_PRODUCT_NAME',
message: `What is the name of your product? (Ex. MakerKit). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_PRODUCT_NAME')}\n`,
default: allVariables.NEXT_PUBLIC_PRODUCT_NAME,
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_SITE_TITLE',
message: `What is the title of your website? (Ex. MakerKit - The best way to make things). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_TITLE')}\n`,
default: allVariables.NEXT_PUBLIC_SITE_TITLE,
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_SITE_DESCRIPTION',
message: `What is the description of your website? (Ex. MakerKit is the best way to make things and stuff). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_DESCRIPTION')}\n`,
default: allVariables.NEXT_PUBLIC_SITE_DESCRIPTION,
},
{
type: 'list',
name: 'values.NEXT_PUBLIC_DEFAULT_THEME_MODE',
message: `What is the default theme mode of your website? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_DEFAULT_THEME_MODE')}\n`,
choices: ['light', 'dark', 'system'],
default: allVariables.NEXT_PUBLIC_DEFAULT_THEME_MODE ?? 'light',
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_DEFAULT_LOCALE',
message: `What is the default locale of your website? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_DEFAULT_LOCALE')}\n`,
default: allVariables.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en',
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_AUTH_PASSWORD',
message: `Do you want to use email/password authentication? If not - we will hide the password login from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_AUTH_PASSWORD')}\n`,
default: getBoolean(allVariables.NEXT_PUBLIC_AUTH_PASSWORD, true),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_AUTH_MAGIC_LINK',
message: `Do you want to use magic link authentication? If not - we will hide the magic link login from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_AUTH_MAGIC_LINK')}\n`,
default: getBoolean(allVariables.NEXT_PUBLIC_AUTH_MAGIC_LINK, false),
},
{
type: 'input',
name: 'values.CONTACT_EMAIL',
message: `What is the contact email you want to receive emails to? \nFor more info: ${getUrlToDocs('CONTACT_EMAIL')}\n`,
default: allVariables.CONTACT_EMAIL,
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
message: `Do you want to enable the theme toggle? If not - we will hide the theme toggle from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_THEME_TOGGLE')}\n`,
default: getBoolean(allVariables.NEXT_PUBLIC_ENABLE_THEME_TOGGLE, true),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
message: `Do you want to enable personal account deletion? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
message: `Do you want to enable personal account billing? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
message: `Do you want to enable team accounts? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
message: `Do you want to enable team account deletion? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
message: `Do you want to enable team account billing? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
message: `Do you want to enable team account creation? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION,
true,
),
},
{
type: 'confirm',
name: 'values.NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
message: `Do you want to enable notifications? If not - we will hide the notifications bell from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_NOTIFICATIONS')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_NOTIFICATIONS,
true,
),
},
{
when: (answers) => answers.values.NEXT_PUBLIC_ENABLE_NOTIFICATIONS,
type: 'confirm',
name: 'values.NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
message: `Do you want to enable realtime notifications? If yes, we will enable the realtime notifications from Supabase. If not - updated will be fetched lazily.\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_REALTIME_NOTIFICATIONS')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_REALTIME_NOTIFICATIONS,
false,
),
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
message: `Do you want to enable the version updater popup? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_VERSION_UPDATER')}\n`,
default: getBoolean(
allVariables.NEXT_PUBLIC_ENABLE_VERSION_UPDATER,
false,
),
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_SUPABASE_URL',
message: `What is the Supabase URL? (Ex. https://yourapp.supabase.co).\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SUPABASE_URL')}\n`,
default: allVariables.NEXT_PUBLIC_SUPABASE_URL,
},
{
type: 'input',
name: 'values.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY',
message: `What is the Supabase public key?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SUPABASE_PUBLIC_KEY')}\n`,
},
{
type: 'input',
name: 'values.SUPABASE_SECRET_KEY',
message: `What is the Supabase secret key?\nFor more info: ${getUrlToDocs('SUPABASE_SECRET_KEY')}\n`,
},
{
type: 'list',
name: 'values.NEXT_PUBLIC_BILLING_PROVIDER',
message: `What is the billing provider you want to use?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_BILLING_PROVIDER')}\n`,
choices: ['stripe', 'lemon-squeezy'],
default: allVariables.NEXT_PUBLIC_BILLING_PROVIDER ?? 'stripe',
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe',
type: 'input',
name: 'values.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
message: `What is the Stripe publishable key?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY')}\n`,
default: allVariables.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe',
type: 'input',
name: 'values.STRIPE_SECRET_KEY',
message: `What is the Stripe secret key? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_BILLING_PROVIDER')}\n`,
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe',
type: 'input',
name: 'values.STRIPE_WEBHOOK_SECRET',
message: `What is the Stripe webhook secret? \nFor more info: ${getUrlToDocs('STRIPE_WEBHOOK_SECRET')}\n`,
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy',
type: 'input',
name: 'values.LEMON_SQUEEZY_SECRET_KEY',
message: `What is the Lemon Squeezy secret key? \nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_SECRET_KEY')}\n`,
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy',
type: 'input',
name: 'values.LEMON_SQUEEZY_STORE_ID',
message: `What is the Lemon Squeezy store ID? \nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_STORE_ID')}\n`,
default: allVariables.LEMON_SQUEEZY_STORE_ID,
},
{
when: (answers) =>
answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy',
type: 'input',
name: 'values.LEMON_SQUEEZY_SIGNING_SECRET',
message: `What is the Lemon Squeezy signing secret?\nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_SIGNING_SECRET')}\n`,
},
{
type: 'input',
name: 'values.SUPABASE_DB_WEBHOOK_SECRET',
message: `What is the DB webhook secret?\nFor more info: ${getUrlToDocs('SUPABASE_DB_WEBHOOK_SECRET')}\n`,
},
{
type: 'list',
name: 'values.CMS_CLIENT',
message: `What is the CMS client you want to use?\nFor more info: ${getUrlToDocs('CMS_CLIENT')}\n`,
choices: ['keystatic', 'wordpress'],
default: allVariables.CMS_CLIENT ?? 'keystatic',
},
{
type: 'list',
name: 'values.MAILER_PROVIDER',
message: `What is the mailer provider you want to use?\nFor more info: ${getUrlToDocs('MAILER_PROVIDER')}\n`,
choices: ['nodemailer', 'resend'],
default: allVariables.MAILER_PROVIDER ?? 'nodemailer',
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'resend',
type: 'input',
name: 'values.RESEND_API_KEY',
message: `What is the Resend API key?\nFor more info: ${getUrlToDocs('RESEND_API_KEY')}\n`,
},
{
type: 'input',
name: 'values.EMAIL_SENDER',
message: `What is the email sender? (ex. info@makerkit.dev).\nFor more info: ${getUrlToDocs('EMAIL_SENDER')}\n`,
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer',
type: 'input',
name: 'values.EMAIL_HOST',
message: `What is the email host?\nFor more info: ${getUrlToDocs('EMAIL_HOST')}\n`,
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer',
type: 'input',
name: 'values.EMAIL_PORT',
message: `What is the email port?\nFor more info: ${getUrlToDocs('EMAIL_PORT')}\n`,
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer',
type: 'input',
name: 'values.EMAIL_USER',
message: `What is the email username? (check your email provider).\nFor more info: ${getUrlToDocs('EMAIL_USER')}\n`,
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer',
type: 'input',
name: 'values.EMAIL_PASSWORD',
message: `What is the email password? (check your email provider).\nFor more info: ${getUrlToDocs('EMAIL_PASSWORD')}\n`,
},
{
when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer',
type: 'confirm',
name: 'values.EMAIL_TLS',
message: `Do you want to enable TLS for your emails?\nFor more info: ${getUrlToDocs('EMAIL_TLS')}\n`,
default: getBoolean(allVariables.EMAIL_TLS, true),
},
{
type: 'confirm',
name: 'captcha',
message: `Do you want to enable Cloudflare Captcha protection for the Auth endpoints?`,
},
{
when: (answers) => answers.captcha,
type: 'input',
name: 'values.NEXT_PUBLIC_CAPTCHA_SITE_KEY',
message: `What is the Cloudflare Captcha site key? NB: this is the PUBLIC key!\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_CAPTCHA_SITE_KEY')}\n`,
},
{
when: (answers) => answers.captcha,
type: 'input',
name: 'values.CAPTCHA_SECRET_TOKEN',
message: `What is the Cloudflare Captcha secret key? NB: this is the PRIVATE key!\nFor more info: ${getUrlToDocs('CAPTCHA_SECRET_TOKEN')}\n`,
},
],
});
}
function getBoolean(value: string | undefined, defaultValue: boolean) {
return value === 'true' ? true : defaultValue;
}
function getUrlToDocs(envVar: string) {
return `${DOCS_URL}#${envVar.toLowerCase()}`;
}

View File

@@ -0,0 +1,192 @@
import type { PlopTypes } from '@turbo/gen';
// quick hack to avoid installing zod globally
import { z } from '../../../../apps/web/node_modules/zod';
import { generator } from '../../utils';
const BooleanStringEnum = z.enum(['true', 'false']);
const Schema: Record<string, z.ZodType> = {
NEXT_PUBLIC_SITE_URL: z
.string({
description: `This is the URL of your website. It should start with https:// like https://makerkit.dev.`,
})
.url({
message:
'NEXT_PUBLIC_SITE_URL must be a valid URL. Please use HTTPS for production sites, otherwise it will fail.',
})
.refine(
(url) => {
return url.startsWith('https://');
},
{
message: 'NEXT_PUBLIC_SITE_URL must start with https://',
path: ['NEXT_PUBLIC_SITE_URL'],
},
),
NEXT_PUBLIC_PRODUCT_NAME: z
.string({
message: 'Product name must be a string',
description: `This is the name of your product. It should be a short name like MakerKit.`,
})
.min(1),
NEXT_PUBLIC_SITE_DESCRIPTION: z.string({
message: 'Site description must be a string',
description: `This is the description of your website. It should be a short sentence or two.`,
}),
NEXT_PUBLIC_DEFAULT_THEME_MODE: z.enum(['light', 'dark', 'system'], {
message: 'Default theme mode must be light, dark or system',
description: `This is the default theme mode for your website. It should be light, dark or system.`,
}),
NEXT_PUBLIC_DEFAULT_LOCALE: z.string({
message: 'Default locale must be a string',
description: `This is the default locale for your website. It should be a two-letter code like en or fr.`,
}),
CONTACT_EMAIL: z
.string({
message: 'Contact email must be a valid email',
description: `This is the email address that will receive contact form submissions.`,
})
.email(),
NEXT_PUBLIC_ENABLE_THEME_TOGGLE: BooleanStringEnum,
NEXT_PUBLIC_AUTH_PASSWORD: BooleanStringEnum,
NEXT_PUBLIC_AUTH_MAGIC_LINK: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: BooleanStringEnum,
NEXT_PUBLIC_REALTIME_NOTIFICATIONS: BooleanStringEnum,
NEXT_PUBLIC_ENABLE_NOTIFICATIONS: BooleanStringEnum,
NEXT_PUBLIC_SUPABASE_URL: z
.string({
description: `This is the URL to your hosted Supabase instance.`,
})
.url({
message: 'Supabase URL must be a valid URL',
}),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string({
message: 'Supabase anon key must be a string',
description: `This is the key provided by Supabase. It is a public key used client-side.`,
}),
SUPABASE_SERVICE_ROLE_KEY: z.string({
message: 'Supabase service role key must be a string',
description: `This is the key provided by Supabase. It is a private key used server-side.`,
}),
NEXT_PUBLIC_BILLING_PROVIDER: z.enum(['stripe', 'lemon-squeezy'], {
message: 'Billing provider must be stripe or lemon-squeezy',
description: `This is the billing provider you want to use. It should be stripe or lemon-squeezy.`,
}),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z
.string({
message: 'Stripe publishable key must be a string',
description: `This is the publishable key from your Stripe dashboard. It should start with pk_`,
})
.refine(
(value) => {
return value.startsWith('pk_');
},
{
message: 'Stripe publishable key must start with pk_',
path: ['NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY'],
},
),
STRIPE_SECRET_KEY: z
.string({
message: 'Stripe secret key must be a string',
description: `This is the secret key from your Stripe dashboard. It should start with sk_`,
})
.refine(
(value) => {
return value.startsWith('sk_');
},
{
message: 'Stripe secret key must start with sk_',
path: ['STRIPE_SECRET_KEY'],
},
),
STRIPE_WEBHOOK_SECRET: z
.string({
message: 'Stripe webhook secret must be a string',
description: `This is the signing secret you copy after creating a webhook in your Stripe dashboard.`,
})
.min(1)
.refine(
(value) => {
return value.startsWith('whsec_');
},
{
message: 'Stripe webhook secret must start with whsec_',
path: ['STRIPE_WEBHOOK_SECRET'],
},
),
LEMON_SQUEEZY_SECRET_KEY: z
.string({
message: 'Lemon Squeezy API key must be a string',
description: `This is the API key from your Lemon Squeezy account`,
})
.min(1),
LEMON_SQUEEZY_STORE_ID: z
.string({
message: 'Lemon Squeezy store ID must be a string',
description: `This is the store ID of your Lemon Squeezy account`,
})
.min(1),
LEMON_SQUEEZY_SIGNING_SECRET: z
.string({
message: 'Lemon Squeezy signing secret must be a string',
description: `This is a shared secret that you must set in your Lemon Squeezy account when you create an API Key`,
})
.min(1),
MAILER_PROVIDER: z.enum(['nodemailer', 'resend'], {
message: 'Mailer provider must be nodemailer or resend',
description: `This is the mailer provider you want to use for sending emails. nodemailer is a generic SMTP mailer, resend is a service.`,
}),
};
export function createEnvironmentVariablesValidatorGenerator(
plop: PlopTypes.NodePlopAPI,
) {
return plop.setGenerator('validate-env', {
description: 'Validate the environment variables to be used in the app',
actions: [
async (answers) => {
if (!('path' in answers) || !answers.path) {
throw new Error('URL is required');
}
const env = generator.loadEnvironmentVariables(answers.path as string);
for (const key of Object.keys(env)) {
const property = Schema[key];
const value = env[key];
if (property) {
// parse with Zod
const { error } = property.safeParse(value);
if (error) {
throw new Error(
`Encountered a validation error for key ${key}:${value} \n\n${JSON.stringify(error, null, 2)}`,
);
} else {
console.log(`Key ${key} is valid!`);
}
}
}
return 'Environment variables are valid!';
},
],
prompts: [
{
type: 'input',
name: 'path',
message:
'Where is the path to the environment variables file? Leave empty to use the generated turbo/generators/templates/env/.env.local',
default: 'turbo/generators/templates/env/.env.local',
},
],
});
}