Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Commits that only reformat code. GitHub skips these in blame automatically;
# locally, opt in once with:
#
# git config blame.ignoreRevsFile .git-blame-ignore-revs

# chore: format the repo with prettier (ENG-2267)
0c8fc5e974d6f9b608286b965d9bb63b01038fe6
7 changes: 7 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@ jobs:
- name: Create .env
run: pnpm dev:setup

# Runs before the linters: formatting takes seconds where linting takes
# minutes, so a formatting slip fails fast instead of after a full lint.
# Otherwise only applied by the lint-staged pre-commit hook, which
# `git commit --no-verify` skips. Run `pnpm format` to fix failures.
- name: Check formatting
run: pnpm format:check

- name: Lint
run: pnpm lint
33 changes: 16 additions & 17 deletions .github/workflows/pr-size-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,41 @@ jobs:
check-pr-size:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Harden the runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- name: Check PR size
id: check-size
run: |
set -euo pipefail

# Fetch the base branch
git fetch origin "${{ github.base_ref }}"

# Get diff stats
diff_output=$(git diff --numstat "origin/${{ github.base_ref }}"...HEAD)

# Count lines, excluding:
# - Test files (*.test.ts, *.spec.tsx, etc.)
# - Locale files (locales/*.json, i18n/*.json)
# - Lock files (pnpm-lock.yaml, package-lock.json, yarn.lock)
# - Generated files (dist/, coverage/, build/, .next/)
# - Storybook stories (*.stories.tsx)

total_additions=0
total_deletions=0
counted_files=0
excluded_files=0

while IFS=$'\t' read -r additions deletions file; do
# Skip if additions or deletions are "-" (binary files)
if [ "$additions" = "-" ] || [ "$deletions" = "-" ]; then
Expand Down Expand Up @@ -83,22 +83,22 @@ jobs:
done <<EOF
${diff_output}
EOF

total_changes=$((total_additions + total_deletions))

echo "counted_files=${counted_files}" >> "${GITHUB_OUTPUT}"
echo "excluded_files=${excluded_files}" >> "${GITHUB_OUTPUT}"
echo "total_additions=${total_additions}" >> "${GITHUB_OUTPUT}"
echo "total_deletions=${total_deletions}" >> "${GITHUB_OUTPUT}"
echo "total_changes=${total_changes}" >> "${GITHUB_OUTPUT}"

# Set flag if PR is too large (> 800 lines)
if [ ${total_changes} -gt 800 ]; then
echo "is_too_large=true" >> "${GITHUB_OUTPUT}"
else
echo "is_too_large=false" >> "${GITHUB_OUTPUT}"
fi

- name: Comment on PR if too large
if: steps.check-size.outputs.is_too_large == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
Expand All @@ -110,7 +110,7 @@ jobs:
const excludedFiles = ${{ steps.check-size.outputs.excluded_files }};
const additions = ${{ steps.check-size.outputs.total_additions }};
const deletions = ${{ steps.check-size.outputs.total_deletions }};

const body = '## 🚨 PR Size Warning\n\n' +
'This PR has approximately **' + totalChanges + ' lines** of changes (' + additions + ' additions, ' + deletions + ' deletions across ' + countedFiles + ' files).\n\n' +
'Large PRs (>800 lines) are significantly harder to review and increase the chance of merge conflicts. Consider splitting this into smaller, self-contained PRs.\n\n' +
Expand All @@ -126,19 +126,19 @@ jobs:
'- **Warning:** 500-800 lines\n' +
'- **Critical:** 800+ lines ⚠️\n\n' +
'If this large PR is unavoidable (e.g., migration, dependency update, major refactor), please explain in the PR description why it couldn\'t be split.';

// Check if we already commented
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('🚨 PR Size Warning')
);

if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
Expand All @@ -156,4 +156,3 @@ jobs:
body: body
});
}

37 changes: 4 additions & 33 deletions .lintstagedrc.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
import path from "node:path";

const ESLINT_BIN = "node_modules/.bin/eslint";

// Groups staged files by their owning app/package directory (e.g. "apps/web",
// "packages/database"), each exactly one level under apps/ or packages/.
function groupByPackage(files) {
const groups = new Map();
for (const absFile of files) {
const rel = path.relative(process.cwd(), absFile).split(path.sep).join("/");
const match = /^(apps|packages)\/([^/]+)\//.exec(rel);
if (!match) continue;
const pkgDir = `${match[1]}/${match[2]}`;
if (!groups.has(pkgDir)) groups.set(pkgDir, []);
groups.get(pkgDir).push(path.relative(pkgDir, rel));
}
return groups;
}

// Lints only the staged files themselves (not the whole package) by cd-ing into
// each touched package and invoking its own eslint.config.mjs directly, so a
// commit only pays for the files it touches and pre-existing violations
// elsewhere in the package can't block it.
function lintStagedFiles(files) {
const groups = groupByPackage(files);
return [...groups.entries()].map(([pkgDir, relFiles]) => {
const eslintBin = path.relative(pkgDir, ESLINT_BIN);
const quotedFiles = relFiles.map((f) => `"${f}"`).join(" ");
return `sh -c 'cd "${pkgDir}" && "${eslintBin}" ${quotedFiles}'`;
});
}

export default {
"(apps|packages)/**/*.{js,ts,jsx,tsx,mjs}": ["prettier --write", lintStagedFiles],
// scripts/lint-staged-eslint.mjs receives the staged paths from lint-staged as
// argv, so nothing is interpolated into a shell command. It lints each touched
// package from that package's own directory.
"(apps|packages)/**/*.{js,ts,jsx,tsx,mjs}": ["prettier --write", "node scripts/lint-staged-eslint.mjs"],
"*.json": ["prettier --write"],
"packages/database/schema/**/*.prisma": ["prisma format"],
};
31 changes: 31 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Prettier already skips everything in .gitignore (that is a default ignore
# path in Prettier 3), so this file only needs to list *tracked* files that
# `prettier --write .` must not touch.

# Generated artifacts: the tool that writes them would immediately undo any
# formatting, which would turn `pnpm format:check` red on an unrelated PR.
pnpm-lock.yaml
docs/api-v3-reference/openapi.yml
docs/api-v3-reference/.redocly.lint-ignore.yaml
charts/**/README.md

# Helm templates are Go templates that only look like YAML — Prettier's YAML
# parser cannot parse them.
charts/**/templates/

# Prettier's MDX printer treats Markdown inside JSX children as a single text
# block and re-wraps it, so lists and numbered steps inside Mintlify
# components (<Step>, <Accordion>, <Tab>, …) collapse into one paragraph.
# Formatting the docs would silently change what they render.
*.mdx

# Intentional empty list items are the fill-in placeholders of this form;
# Prettier folds them into the label above and changes the template.
.github/pull_request_template.md

# Partly owned by a generator: .agents/install.sh rewrites the `robots:start`
# block, and Prettier wants a blank line after that marker where the installer
# emits none. Ignoring is per-file, not per-region, so the whole file is out —
# otherwise every regeneration of the block turns format:check red on whichever
# unrelated PR happens to carry it.
AGENTS.md
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Formbricks runs as a pnpm/turbo monorepo. `apps/web` is the Next.js product surf
- `pnpm dev` — run all app and worker dev servers in parallel via Turborepo.
- `pnpm build` — generate production builds for every package and app.
- `pnpm lint` — apply the shared ESLint rules across the workspace.
- `pnpm format` / `pnpm format:check` — apply or verify Prettier across the workspace; `format:check` is what CI runs, so run `pnpm format` before pushing if you committed with `--no-verify`.
- `pnpm test` / `pnpm test:coverage` — execute Vitest suites with optional coverage.
- `pnpm test:e2e` — launch the Playwright browser regression suite.
- `pnpm db:migrate:dev` — apply Prisma migrations against the dev database.
Expand All @@ -31,6 +32,7 @@ The `@formbricks/surveys` package is pre-compiled (Vite → UMD + ESM) and the b
## Coding Style & Naming Conventions

TypeScript, React, and Prisma are the primary languages. Use the shared ESLint presets (`@formbricks/eslint-config`) and Prettier preset (110-char width, semicolons, double quotes, sorted import groups). Two-space indentation is standard; prefer `PascalCase` for React components and folders under `modules/`, `camelCase` for functions/variables, and `SCREAMING_SNAKE_CASE` only for constants. When adding mocks, place them inside `__mocks__` so import ordering stays stable.
Import order is set by `@trivago/prettier-plugin-sort-imports` and verified in CI by `pnpm format:check`, so it is not a matter of taste: `__mocks__` imports come first (they carry `vi.mock` calls), then `server-only`, then third-party packages, then `@formbricks/*`, `~/*`, `@/*`, and relative imports. Do not ask for or apply a different order in review — it will fail the check.
We are using SonarQube to identify code smells and security hotspots.
Always mark React component props as `Readonly<>` (e.g., `({ children }: Readonly<MyProps>)`).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const WidgetStatusIndicator = ({ workspace }: WidgetStatusIndicatorProps)
<currentStatus.icon />
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const SettingsTitle = ({ title }: { title: string }) => {
return <h2 className="my-4 text-2xl font-medium leading-6 text-slate-800">{title}</h2>;
return <h2 className="my-4 text-2xl leading-6 font-medium text-slate-800">{title}</h2>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("workspace.integrations.google_sheets.link_new_sheet")}
</Button>
</div>
Expand Down Expand Up @@ -51,7 +51,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("workspace.integrations.notion.link_database")}
</Button>
</div>
Expand Down Expand Up @@ -48,7 +48,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const ElementSummaryHeader = ({
const elementType = getElementTypes(t).find((type) => type.id === elementSummary.element.type);

return (
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
<div className="space-y-2 px-4 pt-6 pb-5 md:px-6">
<div className={"align-center flex justify-between gap-4"}>
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
{formatTextWithSlashes(
Expand Down
24 changes: 7 additions & 17 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Metadata } from "next";
import React from "react";
import { NoScriptWarning } from "@/app/components/NoScriptWarning";
import { SentryProvider } from "@/app/sentry/SentryProvider";
import {
DEFAULT_LOCALE,
IS_PRODUCTION,
SENTRY_DSN,
SENTRY_ENVIRONMENT,
SENTRY_RELEASE,
} from "@/lib/constants";
import { DEFAULT_LOCALE } from "@/lib/constants";
import { SentryClientConfigScript } from "@/lib/sentry/SentryClientConfigScript";
import { I18nProvider } from "@/lingodotdev/client";
import { getLocale } from "@/lingodotdev/language";
import "../modules/ui/globals.css";
Expand All @@ -27,16 +21,12 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<html lang={locale} translate="no">
<body className="flex h-dvh flex-col transition-all ease-in-out">
{/* First in the document so instrumentation-client.ts can start Sentry as early as possible. */}
<SentryClientConfigScript />
<NoScriptWarning locale={locale} />
<SentryProvider
sentryDsn={SENTRY_DSN}
sentryRelease={SENTRY_RELEASE}
sentryEnvironment={SENTRY_ENVIRONMENT}
isEnabled={IS_PRODUCTION}>
<I18nProvider language={locale} defaultLanguage={DEFAULT_LOCALE}>
{children}
</I18nProvider>
</SentryProvider>
<I18nProvider language={locale} defaultLanguage={DEFAULT_LOCALE}>
{children}
</I18nProvider>
</body>
</html>
);
Expand Down
69 changes: 0 additions & 69 deletions apps/web/app/sentry/SentryProvider.tsx

This file was deleted.

Loading
Loading