diff --git a/apps/web/getNextjsOrgRewriteConfig.js b/apps/web/getNextjsOrgRewriteConfig.ts similarity index 60% rename from apps/web/getNextjsOrgRewriteConfig.js rename to apps/web/getNextjsOrgRewriteConfig.ts index 188a35514c..06618dbc0b 100644 --- a/apps/web/getNextjsOrgRewriteConfig.js +++ b/apps/web/getNextjsOrgRewriteConfig.ts @@ -1,5 +1,6 @@ const isSingleOrgModeEnabled = !!process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG; const orgSlugCaptureGroupName = "orgSlug"; + /** * Returns the leftmost subdomain from a given URL. * It needs the URL domain to have atleast two dots. @@ -7,35 +8,44 @@ const orgSlugCaptureGroupName = "orgSlug"; * app.company.cal.com -> app * app.company.com -> app */ -const getLeftMostSubdomain = (url) => { - if (!url.startsWith("http:") && !url.startsWith("https:")) { +const getLeftMostSubdomain = (url: string): string | null => { + let normalizedUrl = url; + if (!normalizedUrl.startsWith("http:") && !normalizedUrl.startsWith("https:")) { // Make it a valid URL. Maybe we can simply return null and opt-out from orgs support till the use a URL scheme. - url = `https://${url}`; + normalizedUrl = `https://${normalizedUrl}`; } - const _url = new URL(url); - const regex = new RegExp(/^([a-z]+\:\/{2})?((?[\w-.]+)\.[\w-]+\.\w+)$/); - //console.log(_url.hostname, _url.hostname.match(regex)); - return _url.hostname.match(regex)?.groups?.subdomain || null; + const _url = new URL(normalizedUrl); + // Using positional capture instead of named capture group for ES2017 compatibility + // Group 1: protocol (optional), Group 2: full domain, Group 3: subdomain + const regex = /^([a-z]+:\/{2})?(([\w-.]+)\.[\w-]+\.\w+)$/; + return _url.hostname.match(regex)?.[3] ?? null; }; -const getRegExpNotMatchingLeftMostSubdomain = (url) => { +const getRegExpNotMatchingLeftMostSubdomain = (url: string): string => { const leftMostSubdomain = getLeftMostSubdomain(url); const subdomain = leftMostSubdomain ? `(?!${leftMostSubdomain})[^.]+` : "[^.]+"; return subdomain; }; +export interface NextJsOrgRewriteConfig { + orgSlug: string; + orgHostPath: string; + disableRootPathRewrite: boolean; + disableRootEmbedPathRewrite: boolean; +} + // For app.cal.com, it will match all domains that are not starting with "app". Technically we would want to match domains like acme.cal.com, dunder.cal.com and not app.cal.com -const getRegExpThatMatchesAllOrgDomains = (exports.getRegExpThatMatchesAllOrgDomains = ({ webAppUrl }) => { +export const getRegExpThatMatchesAllOrgDomains = ({ webAppUrl }: { webAppUrl: string }): string => { if (isSingleOrgModeEnabled) { console.log("Single-Org-Mode enabled - Consider all domains to be org domains"); // It works in combination with next.config.js where in this case we use orgSlug=NEXT_PUBLIC_SINGLE_ORG_SLUG return `.*`; } const subdomainRegExp = getRegExpNotMatchingLeftMostSubdomain(webAppUrl); - return `^(?<${orgSlugCaptureGroupName}>${subdomainRegExp})\\.(?!vercel\.app).*`; -}); + return `^(?<${orgSlugCaptureGroupName}>${subdomainRegExp})\\.(?!vercel\\.app).*`; +}; -const nextJsOrgRewriteConfig = { +export const nextJsOrgRewriteConfig: NextJsOrgRewriteConfig = { // :orgSlug is special value which would get matching group from the regex in orgHostPath orgSlug: process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG || `:${orgSlugCaptureGroupName}`, orgHostPath: getRegExpThatMatchesAllOrgDomains({ @@ -43,6 +53,6 @@ const nextJsOrgRewriteConfig = { }), // We disable root path rewrite because we want to serve dashboard on root path disableRootPathRewrite: isSingleOrgModeEnabled, + // We disable root embed path rewrite in single org mode as well + disableRootEmbedPathRewrite: isSingleOrgModeEnabled, }; - -exports.nextJsOrgRewriteConfig = nextJsOrgRewriteConfig; diff --git a/apps/web/next-i18next.config.js b/apps/web/next-i18next.config.js deleted file mode 100644 index 0d55e2dabb..0000000000 --- a/apps/web/next-i18next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const path = require("path"); -const i18nConfig = require("@calcom/config/next-i18next.config"); - -/** @type {import("next-i18next").UserConfig} */ -const config = { - ...i18nConfig, - localePath: path.resolve("./public/static/locales"), -}; - -module.exports = config; diff --git a/apps/web/next-i18next.config.ts b/apps/web/next-i18next.config.ts new file mode 100644 index 0000000000..54e5cc5195 --- /dev/null +++ b/apps/web/next-i18next.config.ts @@ -0,0 +1,12 @@ +import path from "path"; + +import i18nConfig from "@calcom/config/next-i18next.config"; + +import type { UserConfig } from "next-i18next"; + +const config: UserConfig = { + ...i18nConfig, + localePath: path.resolve("./public/static/locales"), +}; + +export default config; diff --git a/apps/web/next.config.js b/apps/web/next.config.ts similarity index 67% rename from apps/web/next.config.js rename to apps/web/next.config.ts index 8cfd6c7e45..a4b1f80bd8 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.ts @@ -1,40 +1,75 @@ -/* eslint-disable */ -require("dotenv").config({ path: "../../.env" }); -const englishTranslation = require("./public/static/locales/en/common.json"); -const { withAxiom } = require("next-axiom"); -const { withBotId } = require("botid/next/config"); -const { version } = require("./package.json"); -const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin"); -const { - i18n: { locales }, -} = require("./next-i18next.config"); -const { +import { withBotId } from "botid/next/config"; +import { config as dotenvConfig } from "dotenv"; +import type { NextConfig } from "next"; +import type { RouteHas } from "next/dist/lib/load-custom-routes"; +import { withAxiom } from "next-axiom"; + +import i18nConfig from "./next-i18next.config"; +import packageJson from "./package.json"; +import { nextJsOrgRewriteConfig, orgUserRoutePath, orgUserTypeRoutePath, orgUserTypeEmbedRoutePath, -} = require("./pagesAndRewritePaths"); +} from "./pagesAndRewritePaths"; + +dotenvConfig({ path: "../../.env" }); + +const { version } = packageJson; +const { + i18n: { locales }, +} = i18nConfig; + +type NextConfigPlugin = (config: NextConfig) => NextConfig; + +// Type guard to filter out null/undefined values with proper type narrowing +function isNotNull(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} + +function adjustEnvVariables(): void { + // Type-safe way to modify process.env (which is typed as readonly in environment.d.ts) + const envMutable = process.env as Record; + if (process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG) { + if (process.env.RESERVED_SUBDOMAINS) { + console.warn( + `⚠️ WARNING: RESERVED_SUBDOMAINS is ignored when SINGLE_ORG_SLUG is set. Single org mode doesn't need to use reserved subdomain validation.` + ); + delete envMutable.RESERVED_SUBDOMAINS; + } + + if (!process.env.ORGANIZATIONS_ENABLED) { + console.log("Auto-enabling ORGANIZATIONS_ENABLED because SINGLE_ORG_SLUG is set"); + envMutable.ORGANIZATIONS_ENABLED = "1"; + } + } +} adjustEnvVariables(); if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET"); if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY"); + const isOrganizationsEnabled = process.env.ORGANIZATIONS_ENABLED === "1" || process.env.ORGANIZATIONS_ENABLED === "true"; -// To be able to use the version in the app without having to import package.json -process.env.NEXT_PUBLIC_CALCOM_VERSION = version; -// So we can test deploy previews preview +// Type-safe way to assign to process.env (which is typed as readonly in environment.d.ts) +const env = process.env as Record; + +env.NEXT_PUBLIC_CALCOM_VERSION = version; + if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) { - process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`; + env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`; } -// Check for configuration of NEXTAUTH_URL before overriding + if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) { - process.env.NEXTAUTH_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/auth`; + env.NEXTAUTH_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/auth`; } + if (!process.env.NEXT_PUBLIC_WEBSITE_URL) { - process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL; + env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL; } + if ( process.env.CSP_POLICY === "strict" && (process.env.CALCOM_ENV === "production" || process.env.NODE_ENV === "production") @@ -54,21 +89,21 @@ if (!process.env.EMAIL_FROM) { if (!process.env.NEXTAUTH_URL) throw new Error("Please set NEXTAUTH_URL"); -const getHttpsUrl = (url) => { +function getHttpsUrl(url: string | undefined): string | undefined { if (!url) return url; if (url.startsWith("http://")) { return url.replace("http://", "https://"); } return url; -}; - -if (process.argv.includes("--experimental-https")) { - process.env.NEXT_PUBLIC_WEBAPP_URL = getHttpsUrl(process.env.NEXT_PUBLIC_WEBAPP_URL); - process.env.NEXTAUTH_URL = getHttpsUrl(process.env.NEXTAUTH_URL); - process.env.NEXT_PUBLIC_EMBED_LIB_URL = getHttpsUrl(process.env.NEXT_PUBLIC_EMBED_LIB_URL); } -const validJson = (jsonString) => { +if (process.argv.includes("--experimental-https")) { + env.NEXT_PUBLIC_WEBAPP_URL = getHttpsUrl(process.env.NEXT_PUBLIC_WEBAPP_URL); + env.NEXTAUTH_URL = getHttpsUrl(process.env.NEXTAUTH_URL); + env.NEXT_PUBLIC_EMBED_LIB_URL = getHttpsUrl(process.env.NEXT_PUBLIC_EMBED_LIB_URL); +} + +function validJson(jsonString: string): object | false { try { const o = JSON.parse(jsonString); if (o && typeof o === "object") { @@ -78,7 +113,7 @@ const validJson = (jsonString) => { console.error(e); } return false; -}; +} if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CREDENTIALS)) { console.warn( @@ -88,29 +123,10 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED ); } -const informAboutDuplicateTranslations = () => { - const valueMap = {}; +const plugins: NextConfigPlugin[] = []; - for (const key in englishTranslation) { - const value = englishTranslation[key]; - - if (valueMap[value]) { - console.warn( - "\x1b[33mDuplicate value found in common.json keys:", - "\x1b[0m ", - key, - "and", - valueMap[value] - ); - } else { - valueMap[value] = key; - } - } -}; - -const plugins = []; if (process.env.ANALYZE === "true") { - // only load dependency if env `ANALYZE` was set + // eslint-disable-next-line @typescript-eslint/no-require-imports const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: true, }); @@ -123,7 +139,18 @@ if (process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER === "1") { plugins.push(withBotId); } -const orgDomainMatcherConfig = { +interface OrgDomainMatcher { + has: RouteHas[]; + source: string; +} + +const orgDomainMatcherConfig: { + root: OrgDomainMatcher | null; + rootEmbed: OrgDomainMatcher | null; + user: OrgDomainMatcher; + userType: OrgDomainMatcher; + userTypeEmbed: OrgDomainMatcher; +} = { root: nextJsOrgRewriteConfig.disableRootPathRewrite ? null : { @@ -179,10 +206,8 @@ const orgDomainMatcherConfig = { }, }; -/** @type {import("next").NextConfig} */ -const nextConfig = (phase) => { +const nextConfig = (phase: string): NextConfig => { if (isOrganizationsEnabled) { - // We want to log the phase here because it is important that the rewrite is added during the build phase(phase=phase-production-build) console.log( `[Phase: ${phase}] Adding rewrite config for organizations - orgHostPath: ${nextJsOrgRewriteConfig.orgHostPath}, orgSlug: ${nextJsOrgRewriteConfig.orgSlug}, disableRootPathRewrite: ${nextJsOrgRewriteConfig.disableRootPathRewrite}` ); @@ -191,23 +216,21 @@ const nextConfig = (phase) => { `[Phase: ${phase}] Skipping rewrite config for organizations because ORGANIZATIONS_ENABLED is not set` ); } + return { output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined, serverExternalPackages: [ "deasync", - "http-cookie-agent", // Dependencies of @ewsjs/xhr + "http-cookie-agent", "rest-facade", - "superagent-proxy", // Dependencies of @tryvital/vital-node - "superagent", // Dependencies of akismet - "formidable", // Dependencies of akismet + "superagent-proxy", + "superagent", + "formidable", "@boxyhq/saml-jackson", - "jose", // Dependency of @boxyhq/saml-jackson + "jose", ], experimental: { - // externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage optimizePackageImports: ["@calcom/ui"], - webpackMemoryOptimizations: true, - webpackBuildWorker: true, }, productionBrowserSourceMaps: true, transpilePackages: [ @@ -235,55 +258,10 @@ const nextConfig = (phase) => { unoptimized: true, }, turbopack: {}, - webpack: (config, { webpack, buildId, isServer, dev }) => { - if (!dev) { - if (config.cache) { - config.cache = Object.freeze({ - type: "memory", - }); - } - } - - if (isServer) { - // Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612 - config.plugins.push( - new webpack.IgnorePlugin({ - resourceRegExp: - /(^@google-cloud\/spanner|^@mongodb-js\/zstd|^@sap\/hana-client\/extension\/Stream$|^@sap\/hana-client|^@sap\/hana-client$|^aws-crt|^aws4$|^better-sqlite3$|^bson-ext$|^cardinal$|^cloudflare:sockets$|^hdb-pool$|^ioredis$|^kerberos$|^mongodb-client-encryption$|^mysql$|^oracledb$|^pg-native$|^pg-query-stream$|^react-native-sqlite-storage$|^snappy\/package\.json$|^snappy$|^sql.js$|^sqlite3$|^typeorm-aurora-data-api-driver$)/, - }) - ); - - config.plugins = [...config.plugins, new PrismaPlugin()]; - - config.externals.push("formidable"); - } - - config.plugins.push(new webpack.DefinePlugin({ "process.env.BUILD_ID": JSON.stringify(buildId) })); - - config.resolve.fallback = { - ...config.resolve.fallback, // if you miss it, all the other options in fallback, specified - // by next.js will be dropped. Doesn't make much sense, but how it is - fs: false, - // ignore module resolve errors caused by the server component bundler - "pg-native": false, - }; - - /** - * TODO: Find more possible barrels for this project. - * @see https://github.com/vercel/next.js/issues/12557#issuecomment-1196931845 - **/ - config.module.rules.push({ - test: [/lib\/.*.tsx?/i], - sideEffects: false, - }); - - return config; - }, async rewrites() { const { orgSlug } = nextJsOrgRewriteConfig; const beforeFiles = [ { - // This should be the first item in `beforeFiles` to take precedence over other rewrites source: `/(${locales.join("|")})/:path*`, destination: "/:path*", }, @@ -307,7 +285,7 @@ const nextConfig = (phase) => { source: "/success/:path*", has: [ { - type: "query", + type: "query" as const, key: "uid", value: "(?.*)", }, @@ -319,10 +297,6 @@ const nextConfig = (phase) => { destination: "/booking/:path*", }, { - /** - * Needed due to the introduction of dotted usernames - * @see https://github.com/calcom/cal.com/pull/11706 - */ source: "/embed.js", destination: "/embed/embed.js", }, @@ -330,7 +304,6 @@ const nextConfig = (phase) => { source: "/login", destination: "/auth/login", }, - // These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles ...(isOrganizationsEnabled ? [ orgDomainMatcherConfig.root @@ -359,9 +332,9 @@ const nextConfig = (phase) => { }, ] : []), - ].filter(Boolean); + ].filter(isNotNull); - let afterFiles = [ + const afterFiles = [ { source: "/org/:slug", destination: "/team/:slug", @@ -378,24 +351,14 @@ const nextConfig = (phase) => { source: "/icons/sprite.svg", destination: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/icons/sprite.svg`, }, - // for @dub/analytics, @see: https://d.to/reverse-proxy { source: "/_proxy/dub/track/:path", destination: "https://api.dub.co/track/:path", }, - - // When updating this also update pagesAndRewritePaths.js - ...[ - { - source: "/:user/avatar.png", - destination: "/api/user/avatar?username=:user", - }, - ], - - /* TODO: have these files being served from another deployment or CDN { - source: "/embed/embed.js", - destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?, - }, */ + { + source: "/:user/avatar.png", + destination: "/api/user/avatar?username=:user", + }, ]; if (process.env.NEXT_PUBLIC_API_V2_URL) { @@ -412,8 +375,6 @@ const nextConfig = (phase) => { }, async headers() { const { orgSlug } = nextJsOrgRewriteConfig; - // This header can be set safely as it ensures the browser will load the resources even when COEP is set. - // But this header must be set only on those resources that are safe to be loaded in a cross-origin context e.g. all embeddable pages's resources const CORP_CROSS_ORIGIN_HEADER = { key: "Cross-Origin-Resource-Policy", value: "cross-origin", @@ -462,47 +423,42 @@ const nextConfig = (phase) => { }, { source: "/:path*/embed", - // COEP require-corp header is set conditionally when flag.coep is set to true headers: [CORP_CROSS_ORIGIN_HEADER], }, { source: "/:path*", has: [ { - type: "host", + type: "host" as const, value: "cal.com", }, ], headers: [ - // make sure to pass full referer URL for booking pages { key: "Referrer-Policy", value: "no-referrer-when-downgrade", }, ], }, - // These resources loads through embed as well, so they need to have CORP_CROSS_ORIGIN_HEADER - ...[ - { - source: "/api/avatar/:path*", - headers: [CORP_CROSS_ORIGIN_HEADER], - }, - { - source: "/avatar.svg", - headers: [CORP_CROSS_ORIGIN_HEADER], - }, - { - source: "/icons/sprite.svg(\\?v=[0-9a-zA-Z\\-\\.]+)?", - headers: [ - CORP_CROSS_ORIGIN_HEADER, - ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - ], + { + source: "/api/avatar/:path*", + headers: [CORP_CROSS_ORIGIN_HEADER], + }, + { + source: "/avatar.svg", + headers: [CORP_CROSS_ORIGIN_HEADER], + }, + { + source: "/icons/sprite.svg(\\?v=[0-9a-zA-Z\\-\\.]+)?", + headers: [ + CORP_CROSS_ORIGIN_HEADER, + ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, ...(isOrganizationsEnabled ? [ orgDomainMatcherConfig.root @@ -545,7 +501,7 @@ const nextConfig = (phase) => { }, ] : []), - ].filter(Boolean); + ].filter(isNotNull); }, async redirects() { const redirects = [ @@ -594,7 +550,6 @@ const nextConfig = (phase) => { destination: "/settings/admin/flags", permanent: true, }, - /* V2 testers get redirected to the new settings */ { source: "/settings/profile", destination: "/settings/my-account/profile", @@ -615,15 +570,13 @@ const nextConfig = (phase) => { destination: "/video/:path*", permanent: false, }, - /* Attempt to mitigate DDoS attack */ { source: "/api/auth/:path*", has: [ { - type: "query", + type: "query" as const, key: "callbackUrl", - // prettier-ignore - value: "^(?!https?:\/\/).*$", + value: "^(?!https?://).*$", }, ], destination: "/404", @@ -638,7 +591,7 @@ const nextConfig = (phase) => { source: "/support", missing: [ { - type: "header", + type: "header" as const, key: "host", value: nextJsOrgRewriteConfig.orgHostPath, }, @@ -671,17 +624,14 @@ const nextConfig = (phase) => { destination: "/settings/admin/apps/calendar", permanent: true, }, - // OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL ...(process.env.NODE_ENV === "development" && - // Safer to enable the redirect only when the user is opting to test out organizations isOrganizationsEnabled && - // Prevent infinite redirect by checking that we aren't already on localhost process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000" ? [ { has: [ { - type: "header", + type: "header" as const, key: "host", value: "localhost:3000", }, @@ -719,22 +669,4 @@ const nextConfig = (phase) => { }; }; -function adjustEnvVariables() { - if (process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG) { - if (process.env.RESERVED_SUBDOMAINS) { - // It is better to ignore it completely so that accidentally if the org slug is itself in Reserved Subdomain that doesn't cause the booking pages to start giving 404s - console.warn( - `⚠️ WARNING: RESERVED_SUBDOMAINS is ignored when SINGLE_ORG_SLUG is set. Single org mode doesn't need to use reserved subdomain validation.` - ); - delete process.env.RESERVED_SUBDOMAINS; - } - - if (!process.env.ORGANIZATIONS_ENABLED) { - // This is basically a consent to add rewrites related to organizations. So, if single org slug mode is there, we have the consent already. - console.log("Auto-enabling ORGANIZATIONS_ENABLED because SINGLE_ORG_SLUG is set"); - process.env.ORGANIZATIONS_ENABLED = "1"; - } - } -} - -module.exports = (phase) => plugins.reduce((acc, next) => next(acc), nextConfig(phase)); +export default (phase: string): NextConfig => plugins.reduce((acc, plugin) => plugin(acc), nextConfig(phase)); diff --git a/apps/web/pagesAndRewritePaths.js b/apps/web/pagesAndRewritePaths.ts similarity index 75% rename from apps/web/pagesAndRewritePaths.js rename to apps/web/pagesAndRewritePaths.ts index b92901b0e9..8a8d8ae431 100644 --- a/apps/web/pagesAndRewritePaths.js +++ b/apps/web/pagesAndRewritePaths.ts @@ -1,16 +1,14 @@ -/* eslint-env node */ -/* eslint-disable @typescript-eslint/no-require-imports, no-undef */ -const glob = require("glob"); -const { nextJsOrgRewriteConfig } = require("./getNextjsOrgRewriteConfig"); +import { sync as globSync } from "glob"; + +import { nextJsOrgRewriteConfig } from "./getNextjsOrgRewriteConfig"; // Top-level route names that are explicitly allowed for org rewrite (whitelist) - -const topLevelRouteNamesWhitelistedForRewrite = (exports.topLevelRouteNamesWhitelistedForRewrite = [ +export const topLevelRouteNamesWhitelistedForRewrite: string[] = [ // We don't allow all dashboard route names to be used as slug because people are probably accustomed to access links like acme.cal.com/workflows, acme.cal.com/event-types etc. // So, we carefully allow, what is absolutely needed. // Allowed to be a team/user slug in organization because onboarding is a common team name "onboarding", -]); +]; /** * Extracts top-level route names from all pages/app files and excludes them from org rewrite. @@ -19,14 +17,12 @@ const topLevelRouteNamesWhitelistedForRewrite = (exports.topLevelRouteNamesWhite * These top-level route names are excluded from rewrites in beforeFiles in next.config.js * to prevent conflicts with organization slug rewrites. */ -/* eslint-disable no-undef */ -let topLevelRoutesExcludedFromOrgRewrite = (exports.topLevelRoutesExcludedFromOrgRewrite = glob - .sync( - "{pages,app,app/(booking-page-wrapper),app/(use-page-wrapper),app/(use-page-wrapper)/(main-nav)}/**/[^_]*.{tsx,js,ts}", - { - cwd: __dirname, - } - ) +export const topLevelRoutesExcludedFromOrgRewrite: string[] = globSync( + "{pages,app,app/(booking-page-wrapper),app/(use-page-wrapper),app/(use-page-wrapper)/(main-nav)}/**/[^_]*.{tsx,js,ts}", + { + cwd: __dirname, + } +) .map((filename) => filename // Remove the directory prefix (pages/, app/ and route group folders.) @@ -58,7 +54,7 @@ let topLevelRoutesExcludedFromOrgRewrite = (exports.topLevelRoutesExcludedFromOr ) .filter((page) => { return !topLevelRouteNamesWhitelistedForRewrite.includes(page); - })); + }); // .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does) // It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages @@ -66,14 +62,13 @@ let topLevelRoutesExcludedFromOrgRewrite = (exports.topLevelRoutesExcludedFromOr // ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page. // [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work. // book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked -/* eslint-disable no-undef */ -exports.nextJsOrgRewriteConfig = nextJsOrgRewriteConfig; +export { nextJsOrgRewriteConfig }; /** * Returns a regex that matches all existing routes, virtual routes (like /forms, /router, /success etc) and nextjs special paths (_next, public) - * @param {string} suffix - The suffix to append to each route in the regex + * @param suffix - The suffix to append to each route in the regex */ -function getRegExpMatchingAllReservedRoutes(suffix) { +function getRegExpMatchingAllReservedRoutes(suffix: string): string { // Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns // Make sure to keep it upto date as more nonExistingRouteRewrites are added. // "app" is reserved for the Cal.com Companion landing page served by Framer at cal.com/app. @@ -89,7 +84,7 @@ function getRegExpMatchingAllReservedRoutes(suffix) { // We should infact scan through all files in public and exclude them instead. const nextJsSpecialPaths = ["_next", "public"]; - let allTopLevelRoutesExcludedFromOrgRewrite = topLevelRoutesExcludedFromOrgRewrite + const allTopLevelRoutesExcludedFromOrgRewrite = topLevelRoutesExcludedFromOrgRewrite .concat(otherNonExistingRoutePrefixes) .concat(nextJsSpecialPaths) .concat(staticAssets); @@ -97,12 +92,10 @@ function getRegExpMatchingAllReservedRoutes(suffix) { } // To handle /something -exports.orgUserRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/?$")})[a-zA-Z0-9\-_]+)`; +export const orgUserRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/?$")})[a-zA-Z0-9\\-_]+)`; // To handle /something/somethingelse -exports.orgUserTypeRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes( - "/" -)})[^/]+)/:type((?!avatar\.png)[^/]+)`; +export const orgUserTypeRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/")})[^/]+)/:type((?!avatar\\.png)[^/]+)`; // To handle /something/somethingelse/embed -exports.orgUserTypeEmbedRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/")})[^/]+)/:type/embed`; +export const orgUserTypeEmbedRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/")})[^/]+)/:type/embed`; diff --git a/apps/web/test/lib/next-config.test.ts b/apps/web/test/lib/next-config.test.ts index bae6bcb60c..b4ebd5b269 100644 --- a/apps/web/test/lib/next-config.test.ts +++ b/apps/web/test/lib/next-config.test.ts @@ -12,10 +12,7 @@ beforeAll(async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore process.env.NEXT_PUBLIC_WEBAPP_URL = "http://example.com"; - const { - orgUserRoutePath, - orgUserTypeRoutePath, - } = require("../../pagesAndRewritePaths"); + const { orgUserRoutePath, orgUserTypeRoutePath } = await import("../../pagesAndRewritePaths"); orgUserTypeRouteMatch = match(orgUserTypeRoutePath); diff --git a/package.json b/package.json index dad50678c0..0c4afb55fb 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "prismock": "1.35.3", "resize-observer-polyfill": "^1.5.1", "tsc-absolute": "^1.0.0", - "turbo": "^2.5.5", + "turbo": "2.7.1", "typescript": "5.9.0-beta", "vitest": "^2.1.9", "vitest-fetch-mock": "^0.3.0", diff --git a/turbo.json b/turbo.json index d9359f528f..758593794c 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,8 @@ "ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED", "ATOMS_E2E_OAUTH_CLIENT_SECRET", "ATOMS_E2E_ORG_ID", + "AXIOM_TOKEN", + "AXIOM_DATASET", "BASECAMP3_CLIENT_ID", "BASECAMP3_CLIENT_SECRET", "BASECAMP3_USER_AGENT", diff --git a/yarn.lock b/yarn.lock index 8039028ba7..61f463c05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20222,7 +20222,7 @@ __metadata: prismock: "npm:1.35.3" resize-observer-polyfill: "npm:^1.5.1" tsc-absolute: "npm:^1.0.0" - turbo: "npm:^2.5.5" + turbo: "npm:2.7.1" typescript: "npm:5.9.0-beta" vitest: "npm:^2.1.9" vitest-fetch-mock: "npm:^0.3.0" @@ -41736,6 +41736,13 @@ __metadata: languageName: node linkType: hard +"turbo-darwin-64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-darwin-64@npm:2.7.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "turbo-darwin-arm64@npm:2.5.5": version: 2.5.5 resolution: "turbo-darwin-arm64@npm:2.5.5" @@ -41743,6 +41750,13 @@ __metadata: languageName: node linkType: hard +"turbo-darwin-arm64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-darwin-arm64@npm:2.7.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "turbo-linux-64@npm:2.5.5": version: 2.5.5 resolution: "turbo-linux-64@npm:2.5.5" @@ -41750,6 +41764,13 @@ __metadata: languageName: node linkType: hard +"turbo-linux-64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-linux-64@npm:2.7.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "turbo-linux-arm64@npm:2.5.5": version: 2.5.5 resolution: "turbo-linux-arm64@npm:2.5.5" @@ -41757,6 +41778,13 @@ __metadata: languageName: node linkType: hard +"turbo-linux-arm64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-linux-arm64@npm:2.7.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "turbo-windows-64@npm:2.5.5": version: 2.5.5 resolution: "turbo-windows-64@npm:2.5.5" @@ -41764,6 +41792,13 @@ __metadata: languageName: node linkType: hard +"turbo-windows-64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-windows-64@npm:2.7.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "turbo-windows-arm64@npm:2.5.5": version: 2.5.5 resolution: "turbo-windows-arm64@npm:2.5.5" @@ -41771,6 +41806,42 @@ __metadata: languageName: node linkType: hard +"turbo-windows-arm64@npm:2.7.1": + version: 2.7.1 + resolution: "turbo-windows-arm64@npm:2.7.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"turbo@npm:2.7.1": + version: 2.7.1 + resolution: "turbo@npm:2.7.1" + dependencies: + turbo-darwin-64: "npm:2.7.1" + turbo-darwin-arm64: "npm:2.7.1" + turbo-linux-64: "npm:2.7.1" + turbo-linux-arm64: "npm:2.7.1" + turbo-windows-64: "npm:2.7.1" + turbo-windows-arm64: "npm:2.7.1" + dependenciesMeta: + turbo-darwin-64: + optional: true + turbo-darwin-arm64: + optional: true + turbo-linux-64: + optional: true + turbo-linux-arm64: + optional: true + turbo-windows-64: + optional: true + turbo-windows-arm64: + optional: true + bin: + turbo: bin/turbo + checksum: 10/0497f55a9d26499914d871b5393354ce6b84d946ce61970564c5e1ce05346980e5d96164cffee975e78d80fc50f040ddc9f2ef608ba0bfd1c645de2e225ee7d7 + languageName: node + linkType: hard + "turbo@npm:^2.5.5": version: 2.5.5 resolution: "turbo@npm:2.5.5"