From 3f2e827e0de414dff81de238b2e14aefd54fc153 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:22:46 +0200 Subject: [PATCH 01/17] build: disable esbuild minify, fix rollup externalLiveBindings --- rollup.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rollup.config.ts b/rollup.config.ts index affda50..7e1a1eb 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -121,6 +121,10 @@ const configs: RollupOptions[] = [ sourcemap: true, exports: 'auto', interop: 'compat', + // Fix: avoid _interopNamespaceCompat crash when processing Node.js built-ins + // (path, child_process, etc.) that have inherited prototype properties. + // getOwnPropertyDescriptor returns undefined for inherited props → d.get throws. + externalLiveBindings: false, generatedCode: { constBindings: true, objectShorthand: true, From a9b83e0e10c03f70f9980ebc408ee207c004c7e5 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:22:58 +0200 Subject: [PATCH 02/17] build(deps): add next as dev dependency --- package-lock.json | 321 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 322 insertions(+) diff --git a/package-lock.json b/package-lock.json index f3dc43e..430efc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "@types/node": "^22.15.0", "@vitest/coverage-v8": "^3.0.0", "esbuild": "^0.25.8", + "next": "^15.0.0", "prettier": "^3.3.3", "rimraf": "^6.0.1", "rollup-plugin-dts": "^6.2.1", @@ -1363,6 +1364,149 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@next/env": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -2951,6 +3095,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -3921,6 +4075,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -4257,6 +4432,13 @@ "node": ">=8" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -7748,6 +7930,88 @@ "dev": true, "license": "MIT" }, + "node_modules/next": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "15.5.12", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -10779,6 +11043,31 @@ "rc": "cli.js" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, "node_modules/read-package-up": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", @@ -11245,6 +11534,14 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semantic-release": { "version": "25.0.2", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-25.0.2.tgz", @@ -12403,6 +12700,30 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/package.json b/package.json index 661cf91..cf3a3e6 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ }, "devDependencies": { "@playwright/test": "^1.57.0", + "next": "^15.0.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.1", "@rollup/plugin-url": "^8.0.2", From c8c21424fa8c9d8ef99ed235c72176858b080862 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:23:08 +0200 Subject: [PATCH 03/17] refactor: ppDev config from top-level only, update proxy rewritePath --- src/index.ts | 209 +++++++++++++------------------ src/lib/proxy-pass.middleware.ts | 2 +- 2 files changed, 86 insertions(+), 125 deletions(-) diff --git a/src/index.ts b/src/index.ts index 683fd47..ab2308f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,30 @@ -import { InlineConfig, PluginOption } from "vite"; -import type { - NormalizedVitePPDevOptions, -} from "./plugin.js"; -import { normalizeVitePPDevConfig } from "./plugin.js"; -import { clientInjectionPlugin, miTopBarPlugin } from "./plugins/index.js"; -import header from "./banner/header.js"; -import type { NextConfig } from "next"; -import { safeNextImport } from "./lib/next-import.js"; -import { getConfig, getPkg, PPDevConfig } from "./config.js"; - -export type { PPDevConfig, PPWatchConfig } from "./config.js"; - -declare module "vite" { +import { InlineConfig, PluginOption } from 'vite'; +import type { NormalizedVitePPDevOptions } from './plugin.js'; +import { normalizeVitePPDevConfig } from './plugin.js'; +import { clientInjectionPlugin, miTopBarPlugin } from './plugins/index.js'; +import header from './banner/header.js'; +import type { NextConfig } from 'next'; +import { safeNextImport } from './lib/next-import.js'; +import { getConfig, getPkg, PPDevConfig } from './config.js'; + +export type { PPDevConfig, PPWatchConfig } from './config.js'; + +declare module 'vite' { interface UserConfig { ppDevConfig?: NormalizedVitePPDevOptions; } } -declare module "next" { +declare module 'next' { interface NextConfig { - ppDev?: PPDevConfig; - } - - interface ExperimentalConfig { + /** PP-Dev config. Prefer pp-dev.config.js to avoid Next.js validation warnings. */ ppDev?: PPDevConfig; } } -const pathPagePrefix = "/p"; -const pathTemplatePrefix = "/pt"; -const pathTemplateLocalPrefix = "/pl"; +const pathPagePrefix = '/p'; +const pathTemplatePrefix = '/pt'; +const pathTemplateLocalPrefix = '/pl'; export async function getViteConfig() { const pkg = getPkg(); @@ -38,13 +33,13 @@ export async function getViteConfig() { const ppDevConfig = await getConfig(); const normalizedPPDevConfig = normalizeVitePPDevConfig( - Object.assign(ppDevConfig, { templateName }) + Object.assign(ppDevConfig, { templateName }), ); // Lazy import vitePPDev to avoid loading plugin module during Next.js config evaluation - const { default: vitePPDev } = await import("./plugin.js"); + const { default: vitePPDev } = await import('./plugin.js'); - const plugins: InlineConfig["plugins"] = [ + const plugins: InlineConfig['plugins'] = [ vitePPDev(normalizedPPDevConfig), clientInjectionPlugin(), ]; @@ -57,27 +52,27 @@ export async function getViteConfig() { } if (imageOptimizer) { - const { ViteImageOptimizer } = await import("vite-plugin-image-optimizer"); + const { ViteImageOptimizer } = await import('vite-plugin-image-optimizer'); plugins.push( ViteImageOptimizer( - typeof imageOptimizer === "object" ? imageOptimizer : undefined - ) + typeof imageOptimizer === 'object' ? imageOptimizer : undefined, + ), ); } if (distZip) { - const { default: zipPack } = await import("vite-plugin-zip-pack"); + const { default: zipPack } = await import('vite-plugin-zip-pack'); plugins.push({ ...zipPack( - typeof distZip === "object" + typeof distZip === 'object' ? distZip : { outFileName: `${templateName}.zip`, - } + }, ), - enforce: "post", + enforce: 'post', } as PluginOption); } @@ -99,9 +94,9 @@ export async function getViteConfig() { outDir, }, css: { - modules: { localsConvention: "dashes" }, + modules: { localsConvention: 'dashes' }, scss: { - api: "modern", + api: 'modern', }, }, ppDevConfig: normalizedPPDevConfig, @@ -110,34 +105,26 @@ export async function getViteConfig() { } /** - * Gets pp-dev configuration from Next.js config - * - * This function extracts pp-dev configuration from Next.js configuration. - * It checks both `config.ppDev` and `config.experimental.ppDev` locations. - * + * 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(). + * * @param nextConfig - Next.js configuration object * @returns PP-Dev configuration or empty object if not found - * + * * @example * ```ts - * // In next.config.js or next.config.ts - * module.exports = { - * ppDev: { - * backendBaseURL: 'http://localhost:8080', - * portalPageId: 1 - * } - * // OR - * experimental: { - * ppDev: { - * backendBaseURL: 'http://localhost:8080', - * portalPageId: 1 - * } - * } - * } + * // 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 * ``` */ export function getPPDevConfigFromNextConfig(nextConfig: any): PPDevConfig { - return nextConfig?.experimental?.ppDev || nextConfig?.ppDev || {}; + return nextConfig?.ppDev || {}; } // Export the safe import utility for consumers who need it @@ -145,11 +132,11 @@ export { safeNextImport, isNextAvailable, getNextVersion, -} from "./lib/next-import.js"; +} from './lib/next-import.js'; // Export authentication provider for global state management -export { authProvider, AuthProvider } from "./lib/auth.provider.js"; -export type { AuthState } from "./lib/auth.provider.js"; +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 @@ -162,59 +149,25 @@ function createBasePath( templateName: string, templateLess: boolean, isDevelopment: boolean, - v7Features: boolean + v7Features: boolean, ): string { if (isDevelopment) { - return templateLess - ? `${pathPagePrefix}/${templateName}` - : `${pathTemplateLocalPrefix}/${templateName}`; + if (v7Features) { + if (templateLess) { + return `${pathPagePrefix}/${templateName}`; + } else { + return `${pathTemplateLocalPrefix}/${templateName}`; + } + } else { + if (templateLess) { + return `${pathPagePrefix}/${templateName}`; + } else { + return `${pathTemplatePrefix}/${templateName}`; + } + } } - return v7Features - ? `${pathTemplatePrefix}/${templateName}` - : `${pathTemplateLocalPrefix}/${templateName}`; -} - -/** - * Creates runtime configuration for Next.js with PP-Dev specific settings - * @param templateName - Name of the template - * @param devConfig - PP-Dev configuration object - * @returns Runtime configuration object - */ -function createRuntimeConfig(templateName: string, devConfig: PPDevConfig): Partial { - const { - appId: originalAppId, - portalPageId, - backendBaseURL, - templateLess, - v7Features, - ...rest - } = devConfig; - - const appId = originalAppId || portalPageId; - - const normalizedConfig = { - backendBaseURL, - portalPageId: appId, - appId, - templateLess, - v7Features, - ...rest, - }; - - return { - serverRuntimeConfig: { - templateName, - ppDevConfig: normalizedConfig, - }, - publicRuntimeConfig: { - templateName, - ppDevConfig: normalizedConfig, - }, - experimental: { - ppDev: normalizedConfig, - }, - } as Partial; + return `${templateLess ? pathPagePrefix : pathTemplatePrefix}/${templateName}`; } /** @@ -227,7 +180,7 @@ function createRuntimeConfig(templateName: string, devConfig: PPDevConfig): Part function mergeConfigs( baseConfig: NextConfig, nextConfiguration: NextConfig, - additionalConfig?: Partial + additionalConfig?: Partial, ): NextConfig { return Object.assign({}, baseConfig, nextConfiguration, additionalConfig); } @@ -250,13 +203,13 @@ export function withPPDev( | NextConfig | (( phase: string, - nextConfig?: { defaultConfig?: any } + nextConfig?: { defaultConfig?: any }, ) => NextConfig | Promise), - ppDevConfig?: PPDevConfig + ppDevConfig?: PPDevConfig, ) { return async ( phase: string, - nextConfig: { defaultConfig?: any } = {} + nextConfig: { defaultConfig?: any } = {}, ): Promise => { try { const { constants } = await safeNextImport(); @@ -268,21 +221,27 @@ export function withPPDev( // Resolve the Next.js configuration const nextConfiguration = - typeof nextjsConfig === "function" + typeof nextjsConfig === 'function' ? await nextjsConfig(phase, nextConfig) : nextjsConfig; // Get pp-dev config from Next.js config if available // Priority order: file config -> Next.js config -> function parameter config const nextConfigPPDev = getPPDevConfigFromNextConfig(nextConfiguration); + const mergedConfig = Object.assign( + {}, + config, + nextConfigPPDev, + ppDevConfig ?? {}, + ); // Create base configuration with appropriate base path const isDevelopment = phase === PHASE_DEVELOPMENT_SERVER; const basePath = createBasePath( templateName, - config.templateLess ?? false, + mergedConfig.templateLess ?? false, isDevelopment, - config.v7Features ?? false + mergedConfig.v7Features ?? false, ); const baseConfig: NextConfig = { @@ -290,30 +249,32 @@ export function withPPDev( trailingSlash: isDevelopment ? true : undefined, }; - if (isDevelopment) { - // Development server configuration - // Merge configs: file config -> Next.js config -> function parameter config - const devConfig = Object.assign({}, config, nextConfigPPDev, ppDevConfig); - const runtimeConfig = createRuntimeConfig(templateName, devConfig); + if (!mergedConfig.templateLess) { + baseConfig.assetPrefix = `${pathTemplatePrefix}/${templateName}`; + } - return mergeConfigs(baseConfig, nextConfiguration, runtimeConfig); + if (isDevelopment) { + // Merge base config with user's Next.js config. + // PP-Dev config is NOT added to Next.js config (avoids "Unrecognized key" warnings). + // CLI and app get config from getConfig() / pp-dev.config.js instead. + return mergeConfigs(baseConfig, nextConfiguration); } // Production configuration return mergeConfigs(baseConfig, nextConfiguration); } catch (error) { - console.error("Error in withPPDev:", error); - console.warn("Falling back to original Next.js configuration"); + console.error('Error in withPPDev:', error); + console.warn('Falling back to original Next.js configuration'); // Fallback to original config if something goes wrong try { const fallbackConfig = - typeof nextjsConfig === "function" + typeof nextjsConfig === 'function' ? await nextjsConfig(phase, nextConfig) : nextjsConfig; return fallbackConfig; } catch (fallbackError) { - console.error("Error in fallback configuration:", fallbackError); + console.error('Error in fallback configuration:', fallbackError); // Last resort: return empty config return {}; } diff --git a/src/lib/proxy-pass.middleware.ts b/src/lib/proxy-pass.middleware.ts index 9f385ef..8f83dd5 100644 --- a/src/lib/proxy-pass.middleware.ts +++ b/src/lib/proxy-pass.middleware.ts @@ -37,7 +37,7 @@ function streamResponseInterceptor(interceptor?: (data: Buffer, encoding: Buffer } export function initProxy(opts: ProxyOpts) { - const { rewritePath = /^\/(?!pt).*/i, baseURL = '', devServer, disableSSLValidation = false, miAPI } = opts; + const { rewritePath = /^\/(?!p[tl]).*/i, baseURL = '', devServer, disableSSLValidation = false, miAPI } = opts; if (!baseURL) { throw new Error('Base url is required'); From 2c16daea94e11d72a0ce93be0f40ed9f29eb4d6a Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:23:43 +0200 Subject: [PATCH 04/17] feat(cli): appId support, base path handling, API routes passthrough --- src/cli.ts | 91 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 813035d..c8d4aa1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -505,8 +505,9 @@ cli ) .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => { filterDuplicateOptions(options); + const { next } = await safeNextImport(); - let nextApp: any = null; + let nextApp: null | ReturnType = null; let httpServer: any = null; let configWatcher: ConfigWatcher | null = null; let isRestarting = false; @@ -535,6 +536,7 @@ cli // Clean up existing Next.js app if any if (nextApp && typeof nextApp.close === 'function') { await nextApp.close(); + nextApp = null; } @@ -551,16 +553,15 @@ cli ); } - const { next } = await safeNextImport(); const { join, basename } = await import('path'); const { createServer } = await import('http'); const importConfig = await import('next/dist/server/config.js'); - const loadConfig = - importConfig.default.default || - importConfig['module.exports'].default || - importConfig.default; + const loadConfig: typeof import('next/dist/server/config.js').default = + (importConfig as any).default.default || + (importConfig as any)['module.exports'].default || + (importConfig as any).default; const opts = cleanOptions(options); @@ -588,7 +589,7 @@ cli const config = await loadConfig('development', projectRoot); // Extract pp-dev configuration from Next.js config - let ppDevConfig = config?.experimental?.ppDev || config?.ppDev || {}; + let ppDevConfig = config?.ppDev || {}; // If no pp-dev config found in Next.js config, try to load from standalone config file if (Object.keys(ppDevConfig).length === 0) { @@ -628,25 +629,36 @@ cli const { backendBaseURL = process.env.MI_BACKEND_URL || 'http://localhost:8080', - portalPageId = parseInt(process.env.MI_PORTAL_PAGE_ID || '1'), + appId: originalAppId, + portalPageId, templateLess = true, v7Features = true, disableSSLValidation = false, enableProxyCache = true, proxyCacheTTL = 600000, personalAccessToken = process.env.MI_ACCESS_TOKEN, - distZip = false, - syncBackupsDir = './backups', miHudLess = false, } = ppDevConfig; + const appId: number = + originalAppId ?? + portalPageId ?? + (process.env.MI_APP_ID + ? parseInt(process.env.MI_APP_ID) + : undefined) ?? + (process.env.MI_PORTAL_PAGE_ID + ? parseInt(process.env.MI_PORTAL_PAGE_ID) + : undefined) ?? + 1; + // Get template name from config, package.json, or fallback to project directory name - let templateName = ppDevConfig.templateName; + let templateName = null; if (!templateName) { try { const { getPkg } = await import('./config.js'); const pkg = getPkg(); + templateName = pkg.name; } catch (error) { // Fallback to project directory name @@ -658,8 +670,15 @@ cli const pathPagePrefix = '/p'; // templateLess = true - use /p const pathTemplatePrefix = '/pl'; // templateLess = false && v7Features = true - use /pl - let base = templateLess ? pathPagePrefix : pathTemplatePrefix; - base += `/${templateName}`; + const configBasePath = config?.basePath; + let base = ''; + + if (configBasePath) { + base = configBasePath; + } else { + base = templateLess ? pathPagePrefix : pathTemplatePrefix; + base += `/${templateName}`; + } nextApp = next({ dev: true, @@ -669,18 +688,12 @@ cli conf: { ...config, basePath: base, - assetPrefix: base, // Fixed: Make assetPrefix consistent with basePath + assetPrefix: `${templateLess ? pathPagePrefix : '/pt'}/${templateName}`, // Fixed: Make assetPrefix consistent with basePath }, }); await nextApp.prepare(); - // Default to templateLess = true for Next.js development - // const templateLess = - // typeof ppDevConfig.templateLess === "boolean" - // ? ppDevConfig.templateLess - // : true; - if (!base.endsWith('/')) { base += '/'; } @@ -691,11 +704,6 @@ cli ); } - const baseWithoutTrailingSlash = base.substring( - 0, - base.lastIndexOf('/'), - ); - // Log the configuration logger.info(colors.green('✅ Next.js app prepared successfully')); logger.info( @@ -707,7 +715,7 @@ cli if (backendBaseURL) { logger.info(colors.blue(`🌐 Backend URL: ${backendBaseURL}`)); - logger.info(colors.blue(`🆔 Portal Page ID: ${portalPageId}`)); + logger.info(colors.blue(`🆔 Custom App ID: ${appId}`)); } // Get the Next.js request handler @@ -735,8 +743,7 @@ cli const isInternalNextRoute = originalPathname.startsWith('/_next/') || originalPathname === '/favicon.ico' || - originalPathname.startsWith('/__nextjs_') || - originalPathname.startsWith('/api/'); + originalPathname.startsWith('/__nextjs_'); if (isInternalNextRoute) { // For internal routes, only apply essential middlewares (skip proxy, cache, etc.) @@ -791,17 +798,19 @@ cli processNextJSRequest(); async function processNextJSRequest() { - // Handle base path requests + // Handle base path requests - pass full path to Next.js so it can apply basePath routing if (originalPathname.startsWith(base)) { - // Strip the base path for Next.js - const nextPath = originalPathname.substring(base.length); - - req.url = nextPath || '/'; - parsedUrl = parse(nextPath, true); + // Keep full path - Next.js expects req.url to include basePath for proper routing + parsedUrl = parse(originalUrl, true); } else if (originalPathname === base.replace(/\/$/, '')) { - // Handle base path without trailing slash - req.url = '/'; - parsedUrl = parse('/', true); + // Path without trailing slash - redirect to canonical URL with trailing slash + const redirectUrl = originalUrl.replace( + originalPathname, + base + ); + res.writeHead(302, { Location: redirectUrl }); + res.end(); + return; } else if ( originalPathname.startsWith('/_next/') || originalPathname === '/favicon.ico' || @@ -849,8 +858,8 @@ cli '$1$2', ), }, - portalPageId, - appId: portalPageId, + portalPageId: appId, + appId, templateLess, disableSSLValidation, v7Features, @@ -931,7 +940,7 @@ cli '/_next', '/favicon.ico', '/__nextjs_', - '/api', + '/installHook.js.map', ], disableSSLValidation, miAPI: mi, @@ -1011,7 +1020,7 @@ cli logger.info( colors.blue(`🔧 MiAPI initialized for backend: ${backendBaseURL}`), ); - logger.info(colors.blue(`🔧 Portal Page ID: ${portalPageId}`)); + logger.info(colors.blue(`🔧 Custom App ID: ${appId}`)); } httpServer.listen(port, host, () => { From 1017ddabe5e44c9bab5a1de1bb7f3c7eec453f0a Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:23:53 +0200 Subject: [PATCH 05/17] test(nextjs): enable withPPDev, add basePath and assetPrefix --- tests/test-nextjs/next.config.mjs | 8 +++++--- tests/test-nextjs/pp-dev.config.ts | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test-nextjs/next.config.mjs b/tests/test-nextjs/next.config.mjs index b0f0138..946be6b 100644 --- a/tests/test-nextjs/next.config.mjs +++ b/tests/test-nextjs/next.config.mjs @@ -1,4 +1,4 @@ -// import { withPPDev } from '@metricinsights/pp-dev'; +import { withPPDev } from '@metricinsights/pp-dev'; /** @type {import('next').NextConfig} */ // const nextConfig = withPPDev({ @@ -11,7 +11,7 @@ // }, // }); -const nextConfig = { +const nextConfig = withPPDev({ output: 'export', cleanDistDir: true, reactStrictMode: true, @@ -19,6 +19,8 @@ const nextConfig = { images: { unoptimized: true, }, -}; + assetPrefix: '/pt/test-nextjs', + basePath: '/p/test-nextjs', +}); export default nextConfig; diff --git a/tests/test-nextjs/pp-dev.config.ts b/tests/test-nextjs/pp-dev.config.ts index 14c0bcb..6758339 100644 --- a/tests/test-nextjs/pp-dev.config.ts +++ b/tests/test-nextjs/pp-dev.config.ts @@ -4,6 +4,7 @@ const ppDevConfig: PPDevConfig = { backendBaseURL: 'https://stg7x.metricinsights.com', portalPageId: 733, v7Features: true, + templateLess: false, }; export default ppDevConfig; From ba94b6557878229e7b9ea69e1c816d39deb2d356 Mon Sep 17 00:00:00 2001 From: Serhii Shpak Date: Fri, 20 Feb 2026 17:24:02 +0200 Subject: [PATCH 06/17] test(nextjs): add dataset-data API, suppressHydrationWarning --- tests/test-nextjs/package-lock.json | 2 +- tests/test-nextjs/src/api/dataset-data.ts | 5 +++++ tests/test-nextjs/src/api/user.ts | 2 +- tests/test-nextjs/src/pages/_document.tsx | 2 +- tests/test-nextjs/src/pages/index.tsx | 12 +++++++++++- 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 tests/test-nextjs/src/api/dataset-data.ts diff --git a/tests/test-nextjs/package-lock.json b/tests/test-nextjs/package-lock.json index 18d593d..ff57fb0 100644 --- a/tests/test-nextjs/package-lock.json +++ b/tests/test-nextjs/package-lock.json @@ -1794,7 +1794,7 @@ "node_modules/@metricinsights/pp-dev": { "version": "0.13.0-beta.2", "resolved": "file:../../metricinsights-pp-dev-latest.tgz", - "integrity": "sha512-NW37RGwdSB6TkpQGcBM61aLNKFb3iN28mJ52CMXyLbfkTR4tAWYrqDgpTZ2Lu8vcaG5Azmo6VNSiCifbZoc+Kw==", + "integrity": "sha512-r5hprIuQbAAJDYm/4EJm64sx6Q25QucgYMICzgUe0Xom5GMaJW+nGKPiK7I2ZuNyIBp6gQ4AC1g8MjLIcRmFbA==", "license": "ISC", "dependencies": { "axios": "^1.13.5", diff --git a/tests/test-nextjs/src/api/dataset-data.ts b/tests/test-nextjs/src/api/dataset-data.ts new file mode 100644 index 0000000..84ca4e4 --- /dev/null +++ b/tests/test-nextjs/src/api/dataset-data.ts @@ -0,0 +1,5 @@ +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); +} diff --git a/tests/test-nextjs/src/api/user.ts b/tests/test-nextjs/src/api/user.ts index d90eb29..14b93de 100644 --- a/tests/test-nextjs/src/api/user.ts +++ b/tests/test-nextjs/src/api/user.ts @@ -2,4 +2,4 @@ export async function getCurrentUser() { return await fetch('/data/page/index/auth/info', { headers: { accept: 'application/json' } }).then(async (res) => (await res.json()).user, ); -} +} \ No newline at end of file diff --git a/tests/test-nextjs/src/pages/_document.tsx b/tests/test-nextjs/src/pages/_document.tsx index c15484b..c3b8b28 100644 --- a/tests/test-nextjs/src/pages/_document.tsx +++ b/tests/test-nextjs/src/pages/_document.tsx @@ -11,7 +11,7 @@ export default function Document() { return ( - +