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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,18 @@ cli
// If no middlewares, process normally
processNextJSRequest();

/**
* Apply basePath-aware routing for an incoming HTTP request, performing canonical redirects when needed and delegating handling to Next.js.
*
* This function:
* - Preserves the full request URL when it starts with the configured `base` so Next.js can apply basePath routing.
* - Redirects requests that target the base without a trailing slash to the canonical trailing-slash URL (302).
* - Redirects requests for the root path `/` to the configured `base` (302).
* - Allows Next.js internal asset and runtime routes (e.g., `/_next/`, `/favicon.ico`, `/__nextjs_`) to pass through unchanged.
* - Delegates all non-redirect requests to the Next.js request handler, which will complete the response.
*
* Side effects: may send one or more 302 responses and end the HTTP response stream.
*/
async function processNextJSRequest() {
// Handle base path requests - pass full path to Next.js so it can apply basePath routing
if (originalPathname.startsWith(base)) {
Expand Down Expand Up @@ -1670,4 +1682,4 @@ cli
cli.help();
cli.version(VERSION);

cli.parse();
cli.parse();
76 changes: 40 additions & 36 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ const pathPagePrefix = '/p';
const pathTemplatePrefix = '/pt';
const pathTemplateLocalPrefix = '/pl';

/**
* Constructs a Vite InlineConfig configured for the current project using its package name and PP-Dev settings.
*
* The returned config includes:
* - a base path derived from the package name and `templateLess` setting,
* - a dev server listening on port 3000,
* - build options (no minification, assets inline limit, rollup banner, and output directory),
* - CSS options for modules and modern SCSS API,
* - the normalized PP-Dev configuration under `ppDevConfig`,
* - a plugins array that always includes the core PP-Dev plugin and client injection plugin and may include the MI top bar, image optimizer, and zip packaging plugins depending on configuration.
*
* @returns A Vite InlineConfig tailored to the project's PP-Dev configuration and package name.
*/
export async function getViteConfig() {
const pkg = getPkg();

Expand Down Expand Up @@ -105,23 +118,10 @@ export async function getViteConfig() {
}

/**
* Gets pp-dev configuration from Next.js config.
*
* We no longer use experimental.ppDev (triggers Next.js "Unrecognized key" warning).
* Config is read from: (1) top-level ppDev, (2) standalone pp-dev.config.js via getConfig().
* Extracts the PP-Dev configuration from a Next.js configuration object.
*
* @param nextConfig - Next.js configuration object
* @returns PP-Dev configuration or empty object if not found
*
* @example
* ```ts
* // In next.config.js - use withPPDev to avoid validation warnings
* const { withPPDev } = require('@metricinsights/pp-dev');
* module.exports = withPPDev({ ... }, { backendBaseURL: '...' });
*
* // Or use standalone pp-dev.config.js (preferred - no Next.js config pollution)
* module.exports = { ... }; // your next config
* ```
* @param nextConfig - The Next.js config object to read from
* @returns The `ppDev` configuration object found on the top-level of `nextConfig`, or an empty object if none is present
*/
export function getPPDevConfigFromNextConfig(nextConfig: any): PPDevConfig {
return nextConfig?.ppDev || {};
Expand All @@ -139,11 +139,13 @@ export { authProvider, AuthProvider } from './lib/auth.provider.js';
export type { AuthState } from './lib/auth.provider.js';

/**
* Creates the appropriate base path for the template based on configuration and environment
* @param templateName - Name of the template
* @param templateLess - Whether the template is template-less
* @param isDevelopment - Whether running in development mode
* @returns The base path string
* Determine the base path for a template given the template name and environment/feature flags.
*
* @param templateName - Template identifier used in the returned path
* @param templateLess - When true, prefer the page-style path (`/p/{templateName}`) instead of template paths
* @param isDevelopment - When true, compute development-specific local paths
* @param v7Features - When true, apply v7 feature rules for development path selection
* @returns The computed base path (for example `/p/{templateName}`, `/pt/{templateName}`, or `/pl/{templateName}`)
*/
function createBasePath(
templateName: string,
Expand Down Expand Up @@ -171,11 +173,12 @@ function createBasePath(
}

/**
* Merges multiple configuration objects with proper typing and order
* @param baseConfig - Base configuration to start with
* @param nextConfiguration - Next.js configuration to merge
* @param additionalConfig - Additional configuration to merge last
* @returns Merged configuration object
* Merge Next.js configuration objects so later inputs override earlier ones.
*
* @param baseConfig - Base configuration whose values have lowest precedence
* @param nextConfiguration - Next.js configuration whose values override `baseConfig`
* @param additionalConfig - Optional configuration whose values override both previous configs
* @returns The combined NextConfig with precedence: `additionalConfig` > `nextConfiguration` > `baseConfig`
*/
function mergeConfigs(
baseConfig: NextConfig,
Expand All @@ -186,17 +189,18 @@ function mergeConfigs(
}

/**
* Higher-order function that wraps Next.js configuration with PP-Dev specific settings
* Wraps a Next.js configuration (or config factory) to apply PP-Dev-specific basePath and assetPrefix.
*
* This function enhances Next.js configuration by:
* - Adding appropriate base paths for different environments
* - Injecting runtime configuration for PP-Dev
* - Handling development vs production configurations
* - Providing fallback behavior on errors
* The returned function resolves the original Next.js configuration (calling it if it's a factory),
* merges PP-Dev settings (from ppDevConfig, the project's pp-dev config file, or next.config.ppDev) to
* determine template-related options, and returns a NextConfig with an appropriate `basePath` and,
* when applicable, `assetPrefix`. In development the PP-Dev settings are not injected into the Next.js
* config to avoid unrecognized-key warnings; the original Next.js config is returned with the computed
* basePath merged.
*
* @param nextjsConfig - Next.js configuration object or function
* @param ppDevConfig - Optional PP-Dev specific configuration
* @returns Function that returns enhanced Next.js configuration
* @param nextjsConfig - A Next.js config object or a function that receives (phase, nextConfig) and returns a NextConfig
* @param ppDevConfig - Optional PP-Dev configuration that overrides values from the project's pp-dev config and next.config.ppDev
* @returns A function that accepts (phase, nextConfig) and yields a NextConfig with PP-Dev basePath and assetPrefix applied where appropriate
*/
export function withPPDev(
nextjsConfig:
Expand Down Expand Up @@ -280,4 +284,4 @@ export function withPPDev(
}
}
};
}
}
19 changes: 17 additions & 2 deletions src/lib/proxy-pass.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export interface ProxyOpts {
const hostOriginRegExp = /^(https?:\/\/)([^/]+)(\/.*)?$/i;
export const PROXY_HEADER = 'X-PP-Proxy';

// TODO: Implement interceptor for streaming responses
/**
* Creates a streaming response handler that marks the response as proxied, copies headers from the proxy response, and pipes the proxy response body to the client.
*
* @param interceptor - Optional transform function for streamed data chunks. Note: the current implementation accepts this argument but does not apply it to the streamed data.
* @returns An async function suitable as a proxy response handler which sets the `PROXY_HEADER`, copies proxy response headers to the server response, and pipes `proxyRes` into `res`.
*/
function streamResponseInterceptor(interceptor?: (data: Buffer, encoding: BufferEncoding) => Buffer) {
return async <T extends IncomingMessage>(proxyRes: T, req: T, res: ServerResponse<T>) => {
res.setHeader(PROXY_HEADER, 1);
Expand All @@ -36,6 +41,16 @@ function streamResponseInterceptor(interceptor?: (data: Buffer, encoding: Buffer
};
}

/**
* Creates and returns a configured HTTP proxy middleware that rewrites and forwards matching requests to a target base URL.
*
* @param opts - Configuration options for the proxy:
* - `baseURL` (required): the target origin to which matching requests will be proxied.
* - `rewritePath`: path matching rule used to select requests for proxying; defaults to `^\/(?!p[tl]).*` (paths not starting with `/pt` or `/pl`).
* - `proxyIgnore`: list of path prefixes or RegExp values to exclude from proxying.
* - `disableSSLValidation`: when true, TLS certificate validation for the target is disabled.
* - `miAPI`: optional MiAPI instance; if it contains a personalAccessToken it is added as a Bearer Authorization header.
* @returns The configured proxy middleware instance suitable for use with Express/Vite dev server.
export function initProxy(opts: ProxyOpts) {
const { rewritePath = /^\/(?!p[tl]).*/i, baseURL = '', devServer, disableSSLValidation = false, miAPI } = opts;

Expand Down Expand Up @@ -241,4 +256,4 @@ export function initProxy(opts: ProxyOpts) {
});
}

export default initProxy;
export default initProxy;
10 changes: 8 additions & 2 deletions src/plugins/client-injection-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ const PACKAGE_IMPORT = `/${PACKAGE_NAME}/client`;
const CLIENT_PATH = `/${PACKAGE_IMPORT}`;
const PACKAGE_REGEXP = new RegExp(`^\\/?${PACKAGE_NAME}\\/client\\/(.*)$`);

// Memoized function to get template with caching
/**
* Load and compile the client HTML EJS template for the specified base URL, using in-memory caching when enabled.
*
* @param base - Base URL used to resolve injected package asset paths
* @param enableCache - Whether to reuse a previously compiled template from the internal cache
* @returns The compiled asynchronous EJS template function for rendering the client injection HTML
*/
function getTemplate(
base: string,
enableCache: boolean = true,
Expand Down Expand Up @@ -316,4 +322,4 @@ export function clientInjectionPlugin(
console.log(`[pp-dev:client] Performance metrics:`, performanceMetrics);
},
};
}
}
8 changes: 7 additions & 1 deletion tests/test-nextjs-cjs/src/api/dataset-data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/**
* Fetches and returns the `data` field for a dataset by its numeric ID.
*
* @param datasetId - The dataset identifier used as the `dataset` query parameter
* @returns The `data` property extracted from the response JSON
*/
export async function getDatasetData(datasetId: number) {
return await fetch(`/api/dataset_data?dataset=${datasetId}`, {
headers: { accept: 'application/json' },
}).then(async (res) => (await res.json()).data);
}
}
5 changes: 5 additions & 0 deletions tests/test-nextjs-cjs/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Fetches the current authenticated user's information from the server.
*
* @returns The value of the `user` property from the JSON response.
*/
export async function getCurrentUser() {
return await fetch('/data/page/index/auth/info', { headers: { accept: 'application/json' } }).then(async (res) =>
(await res.json()).user,
Expand Down
10 changes: 9 additions & 1 deletion tests/test-nextjs-cjs/src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export const metadata: Metadata = {
description: 'Generated by create next app',
};

/**
* Custom Next.js Document that renders the root HTML structure and injects page-level scripts and runtime markers.
*
* Renders an <Html lang="en"> root with Head and body (suppresses hydration warnings), includes the application mount point,
* injects a client-side script that sets `window.PP_VARIABLES.TEST` to "[Data Classification]", and emits Next.js runtime scripts.
*
* @returns The JSX element used as the server-rendered HTML document for the Next.js application.
*/
export default function Document() {
return (
<Html lang="en">
Expand All @@ -26,4 +34,4 @@ export default function Document() {
</body>
</Html>
);
}
}
11 changes: 10 additions & 1 deletion tests/test-nextjs-cjs/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { useEffect, useState } from 'react';
import { getCurrentUser } from '@/api/user';
import { getDatasetData } from '@/api/dataset-data';

/**
* Render the application's Home page, fetching and displaying current user and dataset information.
*
* The component displays a top banner, a hero area, a status line showing the current user's
* first and last name, and a set of resource cards. On mount it loads the current user and
* dataset (id 1) into local state.
*
* @returns A JSX element representing the Home page
*/
export default function Home() {
const [user, setUser] = useState<{ first_name: string; last_name: string } | null>(null);
const [datasetData, setDatasetData] = useState<any>(null);
Expand Down Expand Up @@ -129,4 +138,4 @@ export default function Home() {
</div>
</main>
);
}
}
8 changes: 7 additions & 1 deletion tests/test-nextjs/src/api/dataset-data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/**
* Fetches dataset-specific data from the `/api/dataset_data` endpoint.
*
* @param datasetId - The numeric dataset identifier sent as the `dataset` query parameter
* @returns The value of the `data` property from the endpoint's JSON response
*/
export async function getDatasetData(datasetId: number) {
return await fetch(`/api/dataset_data?dataset=${datasetId}`, {
headers: { accept: 'application/json' },
}).then(async (res) => (await res.json()).data);
}
}
5 changes: 5 additions & 0 deletions tests/test-nextjs/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Fetches the current authenticated user's information from the server.
*
* @returns The `user` property from the parsed JSON response; may be `undefined` if the response does not include a `user` field.
*/
export async function getCurrentUser() {
return await fetch('/data/page/index/auth/info', { headers: { accept: 'application/json' } }).then(async (res) =>
(await res.json()).user,
Expand Down
7 changes: 6 additions & 1 deletion tests/test-nextjs/src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export const metadata: Metadata = {
description: 'Generated by create next app',
};

/**
* Custom Next.js Document component that renders the HTML skeleton, sets the page language to English, suppresses hydration warnings on the body, and injects a global `window.PP_VARIABLES` script.
*
* @returns A JSX element representing the server-rendered HTML document containing Head, Body (with `suppressHydrationWarning`), the application `Main`, an inline script that defines `window.PP_VARIABLES.TEST`, and Next.js runtime scripts.
*/
export default function Document() {
return (
<Html lang="en">
Expand All @@ -26,4 +31,4 @@ export default function Document() {
</body>
</Html>
);
}
}
9 changes: 8 additions & 1 deletion tests/test-nextjs/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { useEffect, useState } from 'react';
import { getCurrentUser } from '@/api/user';
import { getDatasetData } from '@/api/dataset-data';

/**
* Render the application's home page, display the current user, and present navigation links and branding.
*
* This component fetches the current user and dataset data (on mount) and stores them in local state for display.
*
* @returns The JSX element representing the Home page.
*/
export default function Home() {
const [user, setUser] = useState<{ first_name: string; last_name: string } | null>(null);
const [datasetData, setDatasetData] = useState<any>(null);
Expand Down Expand Up @@ -129,4 +136,4 @@ export default function Home() {
</div>
</main>
);
}
}
Loading