chore: remove webpack config and convert next.config to TypeScript (#26126)

* chore: remove webpack experimental flags from apps/web

- Remove webpackMemoryOptimizations and webpackBuildWorker from experimental config
- Remove unused DefinePlugin for process.env.BUILD_ID (not used anywhere in codebase)
- Remove unused buildId parameter from webpack function signature

These webpack-specific experimental flags were showing up during dev mode
even though Turbopack is used for development. The flags only apply to
production webpack builds and their presence in the config was confusing.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* chore: remove webpack function for full Turbopack build

- Remove entire webpack function (IgnorePlugin, PrismaPlugin, resolve.fallback, sideEffects)
- Remove unused PrismaPlugin import
- Turbopack handles all bundling without webpack configuration

Benchmark shows no performance regression:
- Main (with webpack): 67s compile
- Full webpack removal: 69s compile

Both fail at same pre-existing TypeScript error on main branch.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* chore: convert next.config.js to TypeScript

- Convert CommonJS require() to ES6 imports
- Add proper TypeScript types for config and plugins
- Remove /* eslint-disable */ comment
- Keep all existing functionality intact

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* conditional load axiom

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Volnei Munhoz
2025-12-22 12:23:00 -03:00
committed by GitHub
co-authored by Volnei Munhoz Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 090247a058
commit 76b53eb3d4
9 changed files with 248 additions and 241 deletions
@@ -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})?((?<subdomain>[\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;
-10
View File
@@ -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;
+12
View File
@@ -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;
@@ -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<T>(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<string, string | undefined>;
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<string, string | undefined>;
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: "(?<uid>.*)",
},
@@ -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));
@@ -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`;
+1 -4
View File
@@ -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);
+1 -1
View File
@@ -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",
+2
View File
@@ -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",
+72 -1
View File
@@ -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"