mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-28 17:40:47 -05:00
Compare commits
12 Commits
4.2.0-rc.2
...
chore/basi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb8c35440d | ||
|
|
57a5b40717 | ||
|
|
ef601a5437 | ||
|
|
c65ee80066 | ||
|
|
ed70c5fb73 | ||
|
|
ed26427302 | ||
|
|
554809742b | ||
|
|
28adfb905c | ||
|
|
05c455ed62 | ||
|
|
f7687bc0ea | ||
|
|
af34391309 | ||
|
|
70978fbbdf |
@@ -13,7 +13,11 @@ function getAbsolutePath(value: string): any {
|
||||
}
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
stories: [
|
||||
"../src/**/*.mdx",
|
||||
"../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)",
|
||||
"../../../packages/ui/src/**/stories.@(js|jsx|mjs|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-onboarding"),
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Preview } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import { I18nProvider } from "../../web/lingodotdev/client";
|
||||
import { I18nProvider as WebI18nProvider } from "../../web/lingodotdev/client";
|
||||
import "../../web/modules/ui/globals.css";
|
||||
|
||||
// Create a Storybook-specific Lingodot Dev decorator
|
||||
// Create a Storybook-specific Lingodot Dev decorator for web components
|
||||
const withLingodotDev = (Story: any) => {
|
||||
return React.createElement(
|
||||
I18nProvider,
|
||||
WebI18nProvider,
|
||||
{
|
||||
language: "en-US",
|
||||
defaultLanguage: "en-US",
|
||||
@@ -14,7 +14,6 @@ const withLingodotDev = (Story: any) => {
|
||||
React.createElement(Story)
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "../web"),
|
||||
"@ui": path.resolve(__dirname, "../../packages/ui/src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,7 +26,15 @@ import {
|
||||
import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsComboBox";
|
||||
|
||||
export type QuestionFilterOptions = {
|
||||
type: TSurveyQuestionTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas";
|
||||
type:
|
||||
| TSurveyQuestionTypeEnum
|
||||
| "Attributes"
|
||||
| "Tags"
|
||||
| "Languages"
|
||||
| "Quotas"
|
||||
| "Hidden Fields"
|
||||
| "Meta"
|
||||
| OptionsType.OTHERS;
|
||||
filterOptions: (string | TI18nString)[];
|
||||
filterComboBoxOptions: (string | TI18nString)[];
|
||||
id: string;
|
||||
|
||||
@@ -76,9 +76,9 @@ export const generateQuestionAndFilterOptions = (
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
} => {
|
||||
let questionOptions: QuestionOptions[] = [];
|
||||
let questionFilterOptions: any = [];
|
||||
let questionFilterOptions: QuestionFilterOptions[] = [];
|
||||
|
||||
let questionsOptions: any = [];
|
||||
let questionsOptions: QuestionOption[] = [];
|
||||
|
||||
survey.questions.forEach((q) => {
|
||||
if (Object.keys(conditionOptions).includes(q.type)) {
|
||||
|
||||
@@ -14,6 +14,7 @@ declare global {
|
||||
renderSurveyModal: (props: SurveyContainerProps) => void;
|
||||
renderSurvey: (props: SurveyContainerProps) => void;
|
||||
onFilePick: (files: { name: string; type: string; base64: string }[]) => void;
|
||||
setNonce: (nonce: string | undefined) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +98,17 @@ The `packages/` directory contains shared libraries and utilities:
|
||||
|
||||
* Migration management
|
||||
|
||||
### packages/ui/
|
||||
|
||||
* React-authored survey UI components
|
||||
* Survey rendering logic and UI components
|
||||
* Used in web app, Storybook, and compiled to Preact for embeds
|
||||
|
||||
### packages/surveys/
|
||||
|
||||
* Survey-specific functionality
|
||||
|
||||
* Survey rendering logic and UI components
|
||||
|
||||
* Survey state management
|
||||
* Preact-compiled survey embed bundle
|
||||
* Lightweight widget for embedding surveys into customer websites
|
||||
* Compiled from ui package using preact/compat
|
||||
|
||||
## Module Organization
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ This document shows how you can use Formbricks to manage survey definitions and
|
||||
## Core components
|
||||
|
||||
1. **Formbricks Backend:** Use the Formbricks app or Management API to create surveys (questions, flows, locales, validations).
|
||||
2. **Your UI Survey Package:** Renders your custom UI, collects the data and sends to Formbricks backend using Formbricks API. For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/surveys). With an active Enterprise license you can even fork our surveys package, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
|
||||
2. **Your UI Survey Package:** Renders your custom UI, collects the data and sends to Formbricks backend using Formbricks API. For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/ui). With an active Enterprise license you can even fork our survey packages, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
|
||||
3. **Webhook Integration:** Using in-built Webhook integration forward the data to your Analysis tool or Data warehouse.
|
||||
4. **Your Analysis Tool / Data Warehouse:** Receive all the data from Formbricks integration and process it for analysis.
|
||||
|
||||
@@ -136,7 +136,7 @@ Body:
|
||||
|
||||
Your frontend receives the survey JSON and renders it using your own UI components.
|
||||
|
||||
For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/surveys). With an active Enterprise license you can even fork our surveys package, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
|
||||
For inspiration, you can start looking [here](https://github.com/formbricks/formbricks/tree/main/packages/ui). With an active Enterprise license you can even fork our survey packages, make changes and keep them private to your organization (freed from AGPL obligation to also release your changes under AGPL)
|
||||
|
||||
* Question rendering based on type (openText, multipleChoiceSingle, rating, etc.)
|
||||
* Skip logic and conditional branching
|
||||
|
||||
@@ -76,6 +76,19 @@ const registerRouteChange = async (): Promise<void> => {
|
||||
await queue.add(checkPageUrl, CommandType.GeneralAction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the CSP nonce for inline styles
|
||||
* @param nonce - The CSP nonce value (without 'nonce-' prefix), or undefined to clear
|
||||
*/
|
||||
const setNonce = (nonce: string | undefined): void => {
|
||||
// Store nonce on window for access when surveys package loads
|
||||
globalThis.window.__formbricksNonce = nonce;
|
||||
|
||||
// Set nonce in surveys package if it's already loaded
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime check for surveys package availability
|
||||
globalThis.window.formbricksSurveys?.setNonce?.(nonce);
|
||||
};
|
||||
|
||||
const formbricks = {
|
||||
/** @deprecated Use setup() instead. This method will be removed in a future version */
|
||||
init: (initConfig: TLegacyConfigInput) => setup(initConfig as unknown as TConfigInput),
|
||||
@@ -88,6 +101,7 @@ const formbricks = {
|
||||
track,
|
||||
logout,
|
||||
registerRouteChange,
|
||||
setNonce,
|
||||
};
|
||||
|
||||
type TFormbricks = typeof formbricks;
|
||||
|
||||
@@ -201,19 +201,24 @@ export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(CONTAINER_ID)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof globalThis.window.formbricksSurveys> => {
|
||||
const config = Config.getInstance();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- We need to check if the formbricksSurveys object exists
|
||||
if (window.formbricksSurveys) {
|
||||
resolve(window.formbricksSurveys);
|
||||
if (globalThis.window.formbricksSurveys) {
|
||||
resolve(globalThis.window.formbricksSurveys);
|
||||
} else {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${config.get().appUrl}/js/surveys.umd.cjs`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
resolve(window.formbricksSurveys);
|
||||
// Apply stored nonce if it was set before surveys package loaded
|
||||
const storedNonce = globalThis.window.__formbricksNonce;
|
||||
if (storedNonce) {
|
||||
globalThis.window.formbricksSurveys.setNonce(storedNonce);
|
||||
}
|
||||
resolve(globalThis.window.formbricksSurveys);
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/legacy-react.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
extends: ["@formbricks/eslint-config/legacy-react.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
2
packages/surveys/.gitignore
vendored
2
packages/surveys/.gitignore
vendored
@@ -21,4 +21,4 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
@@ -1,6 +1,6 @@
|
||||
## Overview
|
||||
|
||||
The `@formbricks/surveys` package provides a complete survey rendering system built with Preact/React. It features automated translation management through Lingo.dev.
|
||||
The `@formbricks/surveys` package provides a complete survey rendering system built with Preact. It features automated translation management through Lingo.dev and is compiled from React components in `@formbricks/ui`.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/embed-snippet": "1.3.3",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@formkit/auto-animate": "0.8.2",
|
||||
"i18next": "25.5.2",
|
||||
"i18next-icu": "2.4.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { isValidHTML } from "@/lib/html-utils";
|
||||
import { isValidHTML, stripInlineStyles } from "@/lib/html-utils";
|
||||
|
||||
interface HeadlineProps {
|
||||
headline: string;
|
||||
@@ -12,8 +12,16 @@ interface HeadlineProps {
|
||||
|
||||
export function Headline({ headline, questionId, required = true, alignTextCenter = false }: HeadlineProps) {
|
||||
const { t } = useTranslation();
|
||||
const isHeadlineHtml = isValidHTML(headline);
|
||||
const safeHtml = isHeadlineHtml && headline ? DOMPurify.sanitize(headline, { ADD_ATTR: ["target"] }) : "";
|
||||
// Strip inline styles BEFORE parsing to avoid CSP violations
|
||||
const strippedHeadline = stripInlineStyles(headline);
|
||||
const isHeadlineHtml = isValidHTML(strippedHeadline);
|
||||
const safeHtml =
|
||||
isHeadlineHtml && strippedHeadline
|
||||
? DOMPurify.sanitize(strippedHeadline, {
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
||||
})
|
||||
: "";
|
||||
|
||||
return (
|
||||
<label htmlFor={questionId} className="fb-text-heading fb-mb-[3px] fb-flex fb-flex-col">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { isValidHTML } from "@/lib/html-utils";
|
||||
import { isValidHTML, stripInlineStyles } from "@/lib/html-utils";
|
||||
|
||||
interface SubheaderProps {
|
||||
subheader?: string;
|
||||
@@ -8,8 +8,16 @@ interface SubheaderProps {
|
||||
}
|
||||
|
||||
export function Subheader({ subheader, questionId }: SubheaderProps) {
|
||||
const isHtml = subheader ? isValidHTML(subheader) : false;
|
||||
const safeHtml = isHtml && subheader ? DOMPurify.sanitize(subheader, { ADD_ATTR: ["target"] }) : "";
|
||||
// Strip inline styles BEFORE parsing to avoid CSP violations
|
||||
const strippedSubheader = subheader ? stripInlineStyles(subheader) : "";
|
||||
const isHtml = strippedSubheader ? isValidHTML(strippedSubheader) : false;
|
||||
const safeHtml =
|
||||
isHtml && strippedSubheader
|
||||
? DOMPurify.sanitize(strippedSubheader, {
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
||||
})
|
||||
: "";
|
||||
|
||||
if (!subheader) return null;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback } from "react";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyAddressQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from "preact/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyCalQuestion, type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { CalEmbed } from "@/components/general/cal-embed";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyConsentQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyContactInfoQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyCTAQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -3,7 +3,7 @@ import DatePicker, { DatePickerProps } from "react-date-picker";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -4,13 +4,13 @@ import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import type { TSurveyFileUploadQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { BackButton } from "../buttons/back-button";
|
||||
import { FileInput } from "../general/file-input";
|
||||
import { Subheader } from "../general/subheader";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
TSurveyMatrixQuestionChoice,
|
||||
TSurveyQuestionId,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyNPSQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { ZEmail, ZUrl } from "@formbricks/types/common";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
TSurveyQuestionId,
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyQuestionId, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { BackButton } from "@formbricks/ui";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RenderSurvey } from "@/components/general/render-survey";
|
||||
import { I18nProvider } from "@/components/i18n/provider";
|
||||
import { FILE_PICK_EVENT } from "@/lib/constants";
|
||||
import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import { addCustomThemeToDom, addStylesToDom } from "@/lib/styles";
|
||||
import { addCustomThemeToDom, addStylesToDom, setStyleNonce } from "@/lib/styles";
|
||||
|
||||
export const renderSurveyInline = (props: SurveyContainerProps) => {
|
||||
const inlineProps: SurveyContainerProps = {
|
||||
@@ -70,15 +70,17 @@ export const renderSurveyModal = renderSurvey;
|
||||
|
||||
export const onFilePick = (files: { name: string; type: string; base64: string }[]) => {
|
||||
const fileUploadEvent = new CustomEvent(FILE_PICK_EVENT, { detail: files });
|
||||
window.dispatchEvent(fileUploadEvent);
|
||||
globalThis.dispatchEvent(fileUploadEvent);
|
||||
};
|
||||
|
||||
// Initialize the global formbricksSurveys object if it doesn't exist
|
||||
if (typeof window !== "undefined") {
|
||||
window.formbricksSurveys = {
|
||||
if (globalThis.window !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type definition is in @formbricks/types package
|
||||
(globalThis.window as any).formbricksSurveys = {
|
||||
renderSurveyInline,
|
||||
renderSurveyModal,
|
||||
renderSurvey,
|
||||
onFilePick,
|
||||
};
|
||||
setNonce: setStyleNonce,
|
||||
} as typeof globalThis.window.formbricksSurveys;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isValidHTML } from "./html-utils";
|
||||
import { isValidHTML, stripInlineStyles } from "./html-utils";
|
||||
|
||||
describe("html-utils", () => {
|
||||
describe("stripInlineStyles", () => {
|
||||
test("should remove inline styles with double quotes", () => {
|
||||
const input = '<div style="color: red;">Test</div>';
|
||||
const expected = "<div>Test</div>";
|
||||
expect(stripInlineStyles(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove inline styles with single quotes", () => {
|
||||
const input = "<div style='color: red;'>Test</div>";
|
||||
const expected = "<div>Test</div>";
|
||||
expect(stripInlineStyles(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove multiple inline styles", () => {
|
||||
const input = '<div style="color: red;"><span style="font-size: 14px;">Test</span></div>';
|
||||
const expected = "<div><span>Test</span></div>";
|
||||
expect(stripInlineStyles(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle complex inline styles", () => {
|
||||
const input = '<p style="margin: 10px; padding: 5px; background-color: blue;">Content</p>';
|
||||
const expected = "<p>Content</p>";
|
||||
expect(stripInlineStyles(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should not affect other attributes", () => {
|
||||
const input = '<div class="test" id="myDiv" style="color: red;">Test</div>';
|
||||
const expected = '<div class="test" id="myDiv">Test</div>';
|
||||
expect(stripInlineStyles(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should return unchanged string if no inline styles", () => {
|
||||
const input = '<div class="test">Test</div>';
|
||||
expect(stripInlineStyles(input)).toBe(input);
|
||||
});
|
||||
|
||||
test("should handle empty string", () => {
|
||||
expect(stripInlineStyles("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidHTML", () => {
|
||||
test("should return false for empty string", () => {
|
||||
expect(isValidHTML("")).toBe(false);
|
||||
@@ -22,5 +63,9 @@ describe("html-utils", () => {
|
||||
test("should return true for complex HTML", () => {
|
||||
expect(isValidHTML('<div class="test"><p>Test</p></div>')).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle HTML with inline styles (they should be stripped)", () => {
|
||||
expect(isValidHTML('<p style="color: red;">Test</p>')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
/**
|
||||
* Strip inline style attributes from HTML string to avoid CSP violations
|
||||
* @param html - The HTML string to process
|
||||
* @returns HTML string with all style attributes removed
|
||||
* @note This is a security measure to prevent CSP violations during HTML parsing
|
||||
*/
|
||||
export const stripInlineStyles = (html: string): string => {
|
||||
// Remove style="..." or style='...' attributes
|
||||
// Use separate patterns for each quote type to avoid ReDoS vulnerability
|
||||
// The pattern [^"]* and [^']* are safe as they don't cause backtracking
|
||||
return html.replace(/\s+style\s*=\s*["'][^"']*["']/gi, ""); //NOSONAR
|
||||
};
|
||||
|
||||
/**
|
||||
* Lightweight HTML detection for browser environments
|
||||
* Uses native DOMParser (built-in, 0 KB bundle size)
|
||||
* @param str - The input string to test
|
||||
* @returns true if the string contains valid HTML elements, false otherwise
|
||||
* @note Returns false in non-browser environments (SSR, Node.js) where window is undefined
|
||||
* @note Strips inline styles before parsing to avoid CSP violations
|
||||
*/
|
||||
export const isValidHTML = (str: string): boolean => {
|
||||
// This should ideally never happen because the surveys package should be used in an environment where DOM is available
|
||||
@@ -12,7 +26,10 @@ export const isValidHTML = (str: string): boolean => {
|
||||
if (!str) return false;
|
||||
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(str, "text/html");
|
||||
// Strip inline style attributes to avoid CSP violations during parsing
|
||||
const strippedStr = stripInlineStyles(str);
|
||||
|
||||
const doc = new DOMParser().parseFromString(strippedStr, "text/html");
|
||||
const errorNode = doc.querySelector("parsererror");
|
||||
if (errorNode) return false;
|
||||
return Array.from(doc.body.childNodes).some((node) => node.nodeType === 1);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { addCustomThemeToDom, addStylesToDom } from "./styles";
|
||||
import { addCustomThemeToDom, addStylesToDom, getStyleNonce, setStyleNonce } from "./styles";
|
||||
|
||||
// Mock CSS module imports
|
||||
vi.mock("@/styles/global.css?inline", () => ({ default: ".global {}" }));
|
||||
@@ -40,11 +40,85 @@ const getBaseProjectStyling = (overrides: Partial<TProjectStyling> = {}): TProje
|
||||
};
|
||||
};
|
||||
|
||||
describe("setStyleNonce and getStyleNonce", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the DOM and nonce before each test
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
setStyleNonce(undefined);
|
||||
});
|
||||
|
||||
test("should set and get the nonce value", () => {
|
||||
const nonce = "test-nonce-123";
|
||||
setStyleNonce(nonce);
|
||||
expect(getStyleNonce()).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should allow clearing the nonce with undefined", () => {
|
||||
setStyleNonce("initial-nonce");
|
||||
expect(getStyleNonce()).toBe("initial-nonce");
|
||||
setStyleNonce(undefined);
|
||||
expect(getStyleNonce()).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should update existing formbricks__css element with nonce", () => {
|
||||
// Create an existing style element
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css";
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
const nonce = "test-nonce-456";
|
||||
setStyleNonce(nonce);
|
||||
|
||||
expect(existingElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should update existing formbricks__css__custom element with nonce", () => {
|
||||
// Create an existing custom style element
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css__custom";
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
const nonce = "test-nonce-789";
|
||||
setStyleNonce(nonce);
|
||||
|
||||
expect(existingElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should not update nonce on existing elements when nonce is undefined", () => {
|
||||
// Create existing style elements
|
||||
const mainElement = document.createElement("style");
|
||||
mainElement.id = "formbricks__css";
|
||||
mainElement.setAttribute("nonce", "existing-nonce");
|
||||
document.head.appendChild(mainElement);
|
||||
|
||||
const customElement = document.createElement("style");
|
||||
customElement.id = "formbricks__css__custom";
|
||||
customElement.setAttribute("nonce", "existing-nonce");
|
||||
document.head.appendChild(customElement);
|
||||
|
||||
setStyleNonce(undefined);
|
||||
|
||||
// Elements should retain their existing nonce (or be cleared if implementation removes it)
|
||||
// The current implementation doesn't remove nonce when undefined, so we check it's not changed
|
||||
expect(mainElement.getAttribute("nonce")).toBe("existing-nonce");
|
||||
expect(customElement.getAttribute("nonce")).toBe("existing-nonce");
|
||||
});
|
||||
|
||||
test("should handle setting nonce when elements don't exist", () => {
|
||||
const nonce = "test-nonce-no-elements";
|
||||
setStyleNonce(nonce);
|
||||
expect(getStyleNonce()).toBe(nonce);
|
||||
// Should not throw and should store the nonce for future use
|
||||
});
|
||||
});
|
||||
|
||||
describe("addStylesToDom", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the DOM before each test
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
setStyleNonce(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -52,6 +126,7 @@ describe("addStylesToDom", () => {
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
}
|
||||
setStyleNonce(undefined);
|
||||
});
|
||||
|
||||
test("should add a style element to the head with combined CSS", () => {
|
||||
@@ -78,12 +153,68 @@ describe("addStylesToDom", () => {
|
||||
expect(secondStyleElement).toBe(firstStyleElement);
|
||||
expect(secondStyleElement?.innerHTML).toBe(initialInnerHTML);
|
||||
});
|
||||
|
||||
test("should apply nonce to new style element when nonce is set", () => {
|
||||
const nonce = "test-nonce-styles";
|
||||
setStyleNonce(nonce);
|
||||
addStylesToDom();
|
||||
|
||||
const styleElement = document.getElementById("formbricks__css") as HTMLStyleElement;
|
||||
expect(styleElement).not.toBeNull();
|
||||
expect(styleElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should not apply nonce when nonce is not set", () => {
|
||||
addStylesToDom();
|
||||
const styleElement = document.getElementById("formbricks__css") as HTMLStyleElement;
|
||||
expect(styleElement).not.toBeNull();
|
||||
expect(styleElement.getAttribute("nonce")).toBeNull();
|
||||
});
|
||||
|
||||
test("should update nonce on existing style element if nonce is set after creation", () => {
|
||||
addStylesToDom(); // Create element without nonce
|
||||
const styleElement = document.getElementById("formbricks__css") as HTMLStyleElement;
|
||||
expect(styleElement.getAttribute("nonce")).toBeNull();
|
||||
|
||||
const nonce = "test-nonce-update";
|
||||
setStyleNonce(nonce);
|
||||
addStylesToDom(); // Call again to trigger update logic
|
||||
|
||||
expect(styleElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should not overwrite existing nonce when updating via addStylesToDom", () => {
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css";
|
||||
existingElement.setAttribute("nonce", "existing-nonce");
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
// Don't call setStyleNonce - just verify addStylesToDom doesn't overwrite
|
||||
addStylesToDom(); // Should not overwrite since nonce already exists
|
||||
|
||||
// The update logic in addStylesToDom only sets nonce if it doesn't exist
|
||||
expect(existingElement.getAttribute("nonce")).toBe("existing-nonce");
|
||||
});
|
||||
|
||||
test("should overwrite existing nonce when setStyleNonce is called directly", () => {
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css";
|
||||
existingElement.setAttribute("nonce", "existing-nonce");
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
const newNonce = "new-nonce";
|
||||
setStyleNonce(newNonce); // setStyleNonce always updates existing elements
|
||||
|
||||
// setStyleNonce directly updates the nonce attribute
|
||||
expect(existingElement.getAttribute("nonce")).toBe(newNonce);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addCustomThemeToDom", () => {
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
setStyleNonce(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -91,6 +222,7 @@ describe("addCustomThemeToDom", () => {
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
}
|
||||
setStyleNonce(undefined);
|
||||
});
|
||||
|
||||
const getCssVariables = (styleElement: HTMLStyleElement | null): Record<string, string> => {
|
||||
@@ -271,6 +403,66 @@ describe("addCustomThemeToDom", () => {
|
||||
expect(variables["--fb-survey-background-color"]).toBeUndefined();
|
||||
expect(variables["--fb-input-background-color"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should apply nonce to new custom theme style element when nonce is set", () => {
|
||||
const nonce = "test-nonce-custom";
|
||||
setStyleNonce(nonce);
|
||||
const styling = getBaseProjectStyling({ brandColor: { light: "#FF0000" } });
|
||||
addCustomThemeToDom({ styling });
|
||||
|
||||
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
|
||||
expect(styleElement).not.toBeNull();
|
||||
expect(styleElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should not apply nonce when nonce is not set", () => {
|
||||
const styling = getBaseProjectStyling({ brandColor: { light: "#FF0000" } });
|
||||
addCustomThemeToDom({ styling });
|
||||
|
||||
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
|
||||
expect(styleElement).not.toBeNull();
|
||||
expect(styleElement.getAttribute("nonce")).toBeNull();
|
||||
});
|
||||
|
||||
test("should update nonce on existing custom style element if nonce is set after creation", () => {
|
||||
const styling = getBaseProjectStyling({ brandColor: { light: "#FF0000" } });
|
||||
addCustomThemeToDom({ styling }); // Create element without nonce
|
||||
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
|
||||
expect(styleElement.getAttribute("nonce")).toBeNull();
|
||||
|
||||
const nonce = "test-nonce-custom-update";
|
||||
setStyleNonce(nonce);
|
||||
addCustomThemeToDom({ styling }); // Call again to trigger update logic
|
||||
|
||||
expect(styleElement.getAttribute("nonce")).toBe(nonce);
|
||||
});
|
||||
|
||||
test("should not overwrite existing nonce when updating custom theme via addCustomThemeToDom", () => {
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css__custom";
|
||||
existingElement.setAttribute("nonce", "existing-custom-nonce");
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
// Don't call setStyleNonce - just verify addCustomThemeToDom doesn't overwrite
|
||||
const styling = getBaseProjectStyling({ brandColor: { light: "#FF0000" } });
|
||||
addCustomThemeToDom({ styling }); // Should not overwrite since nonce already exists
|
||||
|
||||
// The update logic in addCustomThemeToDom only sets nonce if it doesn't exist
|
||||
expect(existingElement.getAttribute("nonce")).toBe("existing-custom-nonce");
|
||||
});
|
||||
|
||||
test("should overwrite existing nonce when setStyleNonce is called directly on custom theme", () => {
|
||||
const existingElement = document.createElement("style");
|
||||
existingElement.id = "formbricks__css__custom";
|
||||
existingElement.setAttribute("nonce", "existing-custom-nonce");
|
||||
document.head.appendChild(existingElement);
|
||||
|
||||
const newNonce = "new-custom-nonce";
|
||||
setStyleNonce(newNonce); // setStyleNonce directly updates the nonce attribute
|
||||
|
||||
// setStyleNonce directly updates the nonce attribute
|
||||
expect(existingElement.getAttribute("nonce")).toBe(newNonce);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBaseProjectStyling_Helper", () => {
|
||||
|
||||
@@ -8,24 +8,74 @@ import preflight from "@/styles/preflight.css?inline";
|
||||
import editorCss from "../../../../apps/web/modules/ui/components/editor/styles-editor-frontend.css?inline";
|
||||
import datePickerCustomCss from "../styles/date-picker.css?inline";
|
||||
|
||||
// Store the nonce globally for style elements
|
||||
let styleNonce: string | undefined;
|
||||
|
||||
/**
|
||||
* Set the CSP nonce to be applied to all style elements
|
||||
* @param nonce - The CSP nonce value (without 'nonce-' prefix)
|
||||
*/
|
||||
export const setStyleNonce = (nonce: string | undefined): void => {
|
||||
styleNonce = nonce;
|
||||
|
||||
// Update existing style elements if they exist
|
||||
const existingStyleElement = document.getElementById("formbricks__css");
|
||||
if (existingStyleElement && nonce) {
|
||||
existingStyleElement.setAttribute("nonce", nonce);
|
||||
}
|
||||
|
||||
const existingCustomStyleElement = document.getElementById("formbricks__css__custom");
|
||||
if (existingCustomStyleElement && nonce) {
|
||||
existingCustomStyleElement.setAttribute("nonce", nonce);
|
||||
}
|
||||
};
|
||||
|
||||
export const getStyleNonce = (): string | undefined => {
|
||||
return styleNonce;
|
||||
};
|
||||
|
||||
export const addStylesToDom = () => {
|
||||
if (document.getElementById("formbricks__css") === null) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.id = "formbricks__css";
|
||||
|
||||
// Apply nonce if available
|
||||
if (styleNonce) {
|
||||
styleElement.setAttribute("nonce", styleNonce);
|
||||
}
|
||||
|
||||
styleElement.innerHTML =
|
||||
preflight + global + editorCss + datePickerCss + calendarCss + datePickerCustomCss;
|
||||
document.head.appendChild(styleElement);
|
||||
} else {
|
||||
// If style element already exists, update its nonce if needed
|
||||
const existingStyleElement = document.getElementById("formbricks__css");
|
||||
if (existingStyleElement && styleNonce && !existingStyleElement.getAttribute("nonce")) {
|
||||
existingStyleElement.setAttribute("nonce", styleNonce);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TSurveyStyling }): void => {
|
||||
// Check if the style element already exists
|
||||
let styleElement = document.getElementById("formbricks__css__custom");
|
||||
let styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement | null;
|
||||
|
||||
// If the style element doesn't exist, create it and append to the head
|
||||
if (!styleElement) {
|
||||
// If the style element exists, update nonce if needed
|
||||
if (styleElement) {
|
||||
// Update nonce if it wasn't set before
|
||||
if (styleNonce && !styleElement.getAttribute("nonce")) {
|
||||
styleElement.setAttribute("nonce", styleNonce);
|
||||
}
|
||||
} else {
|
||||
// Create it and append to the head
|
||||
styleElement = document.createElement("style");
|
||||
styleElement.id = "formbricks__css__custom";
|
||||
|
||||
// Apply nonce if available
|
||||
if (styleNonce) {
|
||||
styleElement.setAttribute("nonce", styleNonce);
|
||||
}
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
|
||||
1
packages/surveys/src/vite-env.d.ts
vendored
1
packages/surveys/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -7,10 +7,12 @@
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@formbricks/ui": ["../ui/src/index.ts"],
|
||||
"@ui/*": ["../ui/src/*"]
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"extends": "@formbricks/config-typescript/js-library.json",
|
||||
"include": ["src", "../types/surveys.d.ts"]
|
||||
"include": ["src", "../types/surveys.d.ts", "vite-env.d.ts"]
|
||||
}
|
||||
|
||||
6
packages/surveys/vite-env.d.ts
vendored
Normal file
6
packages/surveys/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.css?inline" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import preact from "@preact/preset-vite";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import type { Plugin } from "vite";
|
||||
import { loadEnv } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
@@ -10,6 +11,40 @@ import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
/**
|
||||
* Vite plugin that intercepts ui package's cn utility import
|
||||
* and replaces it with our prefixed version
|
||||
*/
|
||||
function addFbPrefixPlugin(): Plugin {
|
||||
const uiUtilsPath = resolve(__dirname, "../ui/src/lib/utils.ts");
|
||||
const uiSrcPath = resolve(__dirname, "../ui/src");
|
||||
|
||||
return {
|
||||
name: "add-fb-prefix-to-ui",
|
||||
enforce: "pre",
|
||||
resolveId(id, importer) {
|
||||
if (!importer) return null;
|
||||
|
||||
// Normalize paths for comparison
|
||||
const normalizedImporter = importer.replace(/\\/g, "/");
|
||||
const normalizedUiSrc = uiSrcPath.replace(/\\/g, "/");
|
||||
|
||||
// Check if the importer is from ui package
|
||||
const isFromUi =
|
||||
normalizedImporter.includes("ui/src") ||
|
||||
normalizedImporter.includes("ui\\src") ||
|
||||
normalizedImporter.startsWith(normalizedUiSrc) ||
|
||||
normalizedImporter.includes("/ui/") ||
|
||||
normalizedImporter.includes("\\ui\\");
|
||||
|
||||
// Plugin functionality removed - cn-with-prefix.ts file was deleted
|
||||
// If prefix functionality is needed, it should be reimplemented
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const config = ({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
|
||||
@@ -54,10 +89,21 @@ const config = ({ mode }) => {
|
||||
},
|
||||
plugins: [
|
||||
preact(),
|
||||
addFbPrefixPlugin(),
|
||||
dts({ rollupTypes: true }),
|
||||
tsconfigPaths(),
|
||||
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
// Alias React to Preact for ui package components
|
||||
react: "preact/compat",
|
||||
"react-dom": "preact/compat",
|
||||
"react/jsx-runtime": "preact/jsx-runtime",
|
||||
// Allow importing from ui package source files
|
||||
"@formbricks/ui/src": resolve(__dirname, "../../ui/src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
11
packages/types/.gitignore
vendored
Normal file
11
packages/types/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.turbo
|
||||
coverage
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# TypeScript declaration files (generated build artifacts)
|
||||
**/*.d.ts
|
||||
**/*.d.ts.map
|
||||
|
||||
2
packages/types/surveys.d.ts
vendored
2
packages/types/surveys.d.ts
vendored
@@ -7,6 +7,8 @@ declare global {
|
||||
renderSurveyModal: (props: SurveyContainerProps) => void;
|
||||
renderSurvey: (props: SurveyContainerProps) => void;
|
||||
onFilePick: (files: { name: string; type: string; base64: string }[]) => void;
|
||||
setNonce: (nonce: string | undefined) => void;
|
||||
};
|
||||
__formbricksNonce?: string;
|
||||
}
|
||||
}
|
||||
|
||||
11
packages/ui/.gitignore
vendored
Normal file
11
packages/ui/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.turbo
|
||||
coverage
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# TypeScript declaration files (generated build artifacts)
|
||||
**/*.d.ts
|
||||
**/*.d.ts.map
|
||||
|
||||
83
packages/ui/README.md
Normal file
83
packages/ui/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
## Overview
|
||||
|
||||
The `@formbricks/ui` package provides React-authored survey UI components. These components are written using standard React APIs (hooks, JSX, etc.) for maximum familiarity and ecosystem compatibility.
|
||||
|
||||
## Purpose
|
||||
|
||||
This package serves as the source of truth for survey UI components that are used across:
|
||||
|
||||
- **Storybook** (React) - Component documentation and visual testing
|
||||
- **Next.js web app** (React) - Main application UI
|
||||
- **Embed bundle** (Preact) - Compiled via `@formbricks/surveys` using `preact/compat`
|
||||
|
||||
## Architecture
|
||||
|
||||
### React-First Development
|
||||
|
||||
All components are authored using standard React patterns:
|
||||
- React hooks (`useState`, `useEffect`, etc.)
|
||||
- JSX syntax
|
||||
- React Context API
|
||||
- Standard React component patterns
|
||||
|
||||
### Build Strategy
|
||||
|
||||
- **ui**: React components, treated as a normal React library
|
||||
- **surveys**: Build step aliases `react` → `preact/compat`, producing a small Preact-powered widget
|
||||
- **Web app + Storybook**: Continue using real React with no changes
|
||||
|
||||
## Features
|
||||
|
||||
- **React Developer Experience**: Familiar React patterns for all contributors
|
||||
- **Type Safety**: Full TypeScript support
|
||||
- **Testing**: Comprehensive test coverage with Vitest
|
||||
- **Single Component Codebase**: Same UI code works everywhere
|
||||
|
||||
## File Structure
|
||||
|
||||
```text
|
||||
packages/ui/
|
||||
├── src/
|
||||
│ ├── components/ # React survey components
|
||||
│ │ ├── buttons/ # Survey navigation buttons
|
||||
│ │ ├── general/ # Core survey components
|
||||
│ │ ├── i18n/ # i18n provider component
|
||||
│ │ ├── icons/ # Icon components
|
||||
│ │ ├── questions/ # Question type components
|
||||
│ │ └── wrappers/ # Layout wrappers
|
||||
│ ├── lib/ # Utilities and helpers
|
||||
│ ├── styles/ # CSS styles
|
||||
│ └── types/ # TypeScript types
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Scripts
|
||||
|
||||
- `pnpm dev` - Start development build with watch mode
|
||||
- `pnpm build` - Build for production
|
||||
- `pnpm test` - Run tests
|
||||
- `pnpm test:coverage` - Run tests with coverage
|
||||
- `pnpm lint` - Lint and fix code
|
||||
|
||||
## Usage
|
||||
|
||||
### In React Applications
|
||||
|
||||
```tsx
|
||||
import { SurveyComponent } from "@formbricks/ui";
|
||||
|
||||
function App() {
|
||||
return <SurveyComponent {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
### In Storybook
|
||||
|
||||
Components from this package are automatically available in Storybook for visual testing and documentation.
|
||||
|
||||
### In Embed Bundle
|
||||
|
||||
The `@formbricks/surveys` package imports from this package and compiles it to Preact for lightweight embeds.
|
||||
|
||||
63
packages/ui/package.json
Normal file
63
packages/ui/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@formbricks/ui",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Formbricks UI components - React-authored survey UI components for use in web app, Storybook, and embed bundle.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/formbricks/formbricks"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"source": "src/index.ts",
|
||||
"main": "dist/index.umd.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.umd.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch --mode dev",
|
||||
"build": "tsc && vite build",
|
||||
"build:dev": "tsc && vite build --mode dev",
|
||||
"go": "vite build --watch --mode dev",
|
||||
"lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
|
||||
"preview": "vite preview",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@formbricks/i18n-utils": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@types/react": "19.1.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "10.4.21",
|
||||
"postcss": "8.5.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"terser": "5.39.1",
|
||||
"vite": "6.4.1",
|
||||
"vite-plugin-dts": "4.5.3",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
23
packages/ui/src/components/common/back-button.tsx
Normal file
23
packages/ui/src/components/common/back-button.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
interface BackButtonProps {
|
||||
onClick: () => void;
|
||||
backButtonLabel?: string;
|
||||
tabIndex?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) {
|
||||
return (
|
||||
<button
|
||||
dir="auto"
|
||||
tabIndex={tabIndex}
|
||||
type="button"
|
||||
className={cn(
|
||||
"hover:bg-input-bg text-heading focus:ring-focus rounded-custom mb-1 flex items-center px-3 py-3 text-base font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
)}
|
||||
onClick={onClick}>
|
||||
{backButtonLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
100
packages/ui/src/components/common/stories.tsx
Normal file
100
packages/ui/src/components/common/stories.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { BackButton } from "./back-button";
|
||||
|
||||
const meta: Meta<typeof BackButton> = {
|
||||
title: "Survey Core/Common/BackButton",
|
||||
component: BackButton,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha" },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **BackButton** component is used in surveys to allow users to navigate to the previous question. It supports internationalization and custom labels.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
onClick: {
|
||||
action: "clicked",
|
||||
description: "Click handler function",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "() => void" },
|
||||
},
|
||||
},
|
||||
backButtonLabel: {
|
||||
control: "text",
|
||||
description:
|
||||
"Custom label for the back button. If not provided, uses the translated 'common.back' key.",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
tabIndex: {
|
||||
control: "number",
|
||||
description: "Tab index for keyboard navigation",
|
||||
table: {
|
||||
category: "Accessibility",
|
||||
type: { summary: "number" },
|
||||
defaultValue: { summary: "2" },
|
||||
},
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
args: { onClick: fn() },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof BackButton>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onClick: fn(),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Default back button using the translated 'common.back' text.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomLabel: Story = {
|
||||
args: {
|
||||
onClick: fn(),
|
||||
backButtonLabel: "Go Back",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Back button with a custom label instead of the translated text.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
args: {
|
||||
onClick: fn(),
|
||||
className: "bg-blue-500 hover:bg-blue-600 text-white",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Back button with custom styling applied via className prop.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
13
packages/ui/src/index.ts
Normal file
13
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// @formbricks/ui
|
||||
// React-authored survey UI components
|
||||
//
|
||||
// This package exports React components that can be used in:
|
||||
// - Storybook (React)
|
||||
// - Next.js web app (React)
|
||||
// - Embed bundle (compiled to Preact via @formbricks/surveys)
|
||||
|
||||
// Common components
|
||||
export { BackButton } from "./components/common/back-button";
|
||||
|
||||
// Utilities
|
||||
export { cn } from "./lib/utils";
|
||||
7
packages/ui/src/lib/utils.ts
Normal file
7
packages/ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Utility function to combine class names
|
||||
* Filters out falsy values and joins them with spaces
|
||||
*/
|
||||
export const cn = (...classes: (string | undefined | null | false)[]): string => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
15
packages/ui/tsconfig.json
Normal file
15
packages/ui/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"baseUrl": ".",
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"extends": "@formbricks/config-typescript/react-library.json",
|
||||
"include": ["src", "../types/surveys.d.ts"]
|
||||
}
|
||||
63
packages/ui/vite.config.mts
Normal file
63
packages/ui/vite.config.mts
Normal file
@@ -0,0 +1,63 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { loadEnv } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const config = ({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
|
||||
return defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
environmentMatchGlobs: [
|
||||
["**/*.test.tsx", "jsdom"],
|
||||
["**/lib/**/*.test.ts", "jsdom"],
|
||||
],
|
||||
setupFiles: ["./vitestSetup.ts"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
env: env,
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
reportsDirectory: "./coverage",
|
||||
include: ["src/lib/**/*.ts"],
|
||||
exclude: ["**/*.tsx"],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(mode),
|
||||
},
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
minify: "terser",
|
||||
rollupOptions: {
|
||||
// Externalize node-html-parser to keep bundle size small (~53KB)
|
||||
// It's pulled in via @formbricks/types but not used in browser runtime
|
||||
external: ["node-html-parser"],
|
||||
output: {
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "formbricksSurveyCore",
|
||||
formats: ["es", "umd"],
|
||||
fileName: "index",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
dts({ rollupTypes: true }),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
11
packages/ui/vitestSetup.ts
Normal file
11
packages/ui/vitestSetup.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as matchers from "@testing-library/jest-dom/matchers";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, expect } from "vitest";
|
||||
|
||||
// Extend Vitest's expect with jest-dom matchers
|
||||
expect.extend(matchers);
|
||||
|
||||
// Cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
58
pnpm-lock.yaml
generated
58
pnpm-lock.yaml
generated
@@ -803,6 +803,9 @@ importers:
|
||||
'@calcom/embed-snippet':
|
||||
specifier: 1.3.3
|
||||
version: 1.3.3
|
||||
'@formbricks/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
'@formkit/auto-animate':
|
||||
specifier: 0.8.2
|
||||
version: 0.8.2
|
||||
@@ -893,6 +896,61 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../database
|
||||
|
||||
packages/ui:
|
||||
dependencies:
|
||||
react:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
devDependencies:
|
||||
'@formbricks/config-typescript':
|
||||
specifier: workspace:*
|
||||
version: link:../config-typescript
|
||||
'@formbricks/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../config-eslint
|
||||
'@formbricks/i18n-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../i18n-utils
|
||||
'@formbricks/types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
'@testing-library/react':
|
||||
specifier: 16.3.0
|
||||
version: 16.3.0(@testing-library/dom@8.20.1)(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@types/react':
|
||||
specifier: 19.1.4
|
||||
version: 19.1.4
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.4
|
||||
version: 4.4.1(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
|
||||
autoprefixer:
|
||||
specifier: 10.4.21
|
||||
version: 10.4.21(postcss@8.5.3)
|
||||
postcss:
|
||||
specifier: 8.5.3
|
||||
version: 8.5.3
|
||||
tailwindcss:
|
||||
specifier: 3.4.17
|
||||
version: 3.4.17(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3))
|
||||
terser:
|
||||
specifier: 5.39.1
|
||||
version: 5.39.1
|
||||
vite:
|
||||
specifier: 6.4.1
|
||||
version: 6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1)
|
||||
vite-plugin-dts:
|
||||
specifier: 4.5.3
|
||||
version: 4.5.3(@types/node@22.15.18)(rollup@4.52.5)(typescript@5.8.3)(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
|
||||
vite-tsconfig-paths:
|
||||
specifier: 5.1.4
|
||||
version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))
|
||||
vitest:
|
||||
specifier: 3.1.3
|
||||
version: 3.1.3(@types/node@22.15.18)(jiti@2.4.2)(jsdom@26.1.0)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1)
|
||||
|
||||
packages/vite-plugins:
|
||||
devDependencies:
|
||||
'@formbricks/config-typescript':
|
||||
|
||||
@@ -2,16 +2,16 @@ sonar.projectKey=formbricks_formbricks
|
||||
sonar.organization=formbricks
|
||||
|
||||
# Sources
|
||||
sonar.sources=apps/web,packages/surveys,packages/js-core,packages/cache,packages/storage
|
||||
sonar.sources=apps/web,packages/surveys,packages/ui,packages/js-core,packages/cache,packages/storage
|
||||
sonar.exclusions=**/node_modules/**,**/.next/**,**/dist/**,**/build/**,**/*.test.*,**/*.spec.*,**/__mocks__/**
|
||||
|
||||
# Tests
|
||||
sonar.tests=apps/web,packages/surveys,packages/js-core,packages/cache,packages/storage
|
||||
sonar.tests=apps/web,packages/surveys,packages/ui,packages/js-core,packages/cache,packages/storage
|
||||
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts
|
||||
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/js-core/coverage/lcov.info,packages/cache/coverage/lcov.info,packages/storage/coverage/lcov.info
|
||||
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/ui/coverage/lcov.info,packages/js-core/coverage/lcov.info,packages/cache/coverage/lcov.info,packages/storage/coverage/lcov.info
|
||||
|
||||
# TypeScript configuration
|
||||
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/js-core/tsconfig.json,packages/cache/tsconfig.json,packages/storage/tsconfig.json
|
||||
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/ui/tsconfig.json,packages/js-core/tsconfig.json,packages/cache/tsconfig.json,packages/storage/tsconfig.json
|
||||
|
||||
# SCM
|
||||
sonar.scm.provider=git
|
||||
|
||||
13
turbo.json
13
turbo.json
@@ -87,6 +87,19 @@
|
||||
"dependsOn": ["@formbricks/surveys#build"],
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/ui#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"@formbricks/ui#build:dev": {
|
||||
"dependsOn": ["^build:dev", "@formbricks/i18n-utils#build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"@formbricks/ui#go": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@formbricks/ui#build"],
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/web#go": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@formbricks/database#db:setup"],
|
||||
|
||||
Reference in New Issue
Block a user