Skip to content

Commit f68e7a1

Browse files
authored
Merge pull request #43 from mi-examples/pp-2741
pp-2741 Next.js support updates: appId/base path handling, config cleanup, dependency upgrades, and security fixes.
2 parents e5e1473 + 78b1e13 commit f68e7a1

25 files changed

Lines changed: 4929 additions & 5858 deletions

‎package-lock.json‎

Lines changed: 1050 additions & 2011 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"node": ">=22.14"
6767
},
6868
"overrides": {
69-
"chokidar": "^4.0.3"
69+
"chokidar": "^4.0.3",
70+
"minimatch": ">=10.2.1"
7071
},
7172
"peerDependencies": {
7273
"next": ">= 13 < 17"
@@ -81,56 +82,57 @@
8182
"axios": "^1.13.5",
8283
"cac": "^6.7.14",
8384
"chokidar": "^4.0.3",
84-
"deepmerge-ts": "^7.1.3",
85+
"deepmerge-ts": "^7.1.5",
8586
"diff-match-patch": "^1.0.5",
8687
"dir-compare": "^5.0.0",
8788
"ejs": "^4.0.1",
8889
"express": "^5.2.1",
8990
"extract-zip": "^2.0.1",
9091
"file-type": "^19.6.0",
9192
"formdata-node": "^6.0.3",
92-
"http-proxy-middleware": "^3.0.3",
93-
"isbinaryfile": "^5.0.4",
93+
"http-proxy-middleware": "^3.0.5",
94+
"isbinaryfile": "^5.0.7",
9495
"jsdom": "^25.0.1",
9596
"memory-cache": "^0.2.0",
9697
"picocolors": "^1.1.1",
97-
"rollup": "^4.40.0",
98-
"sass": "^1.87.0",
98+
"rollup": "^4.58.0",
99+
"sass": "^1.97.3",
99100
"sharp": "^0.34.5",
100101
"source-map-support": "^0.5.21",
101102
"svgo": "^4.0.0",
102-
"svgtofont": "^6.0.1",
103-
"typescript": "^5.6.0",
103+
"svgtofont": "^6.5.1",
104+
"typescript": "^5.9.3",
104105
"vite": "^7.3.1",
105-
"vite-plugin-image-optimizer": "^1.1.8",
106+
"vite-plugin-image-optimizer": "^1.1.9",
106107
"vite-plugin-zip-pack": "^1.2.4",
107-
"winston": "^3.17.0"
108+
"winston": "^3.19.0"
108109
},
109110
"devDependencies": {
110-
"@playwright/test": "^1.57.0",
111+
"@playwright/test": "^1.58.2",
112+
"next": "^15.5.12",
111113
"@rollup/plugin-terser": "^0.4.4",
112-
"@rollup/plugin-typescript": "^12.1.1",
114+
"@rollup/plugin-typescript": "^12.3.0",
113115
"@rollup/plugin-url": "^8.0.2",
114116
"@semantic-release/changelog": "^6.0.3",
115117
"@semantic-release/git": "^10.0.1",
116118
"@types/diff": "^8.0.0",
117119
"@types/diff-match-patch": "^1.0.36",
118120
"@types/ejs": "^3.1.5",
119-
"@types/express": "^5.0.0",
121+
"@types/express": "^5.0.6",
120122
"@types/jsdom": "^21.1.7",
121123
"@types/memory-cache": "^0.2.6",
122-
"@types/node": "^22.15.0",
123-
"@vitest/coverage-v8": "^3.0.0",
124-
"esbuild": "^0.25.8",
125-
"prettier": "^3.3.3",
126-
"rimraf": "^6.0.1",
127-
"rollup-plugin-dts": "^6.2.1",
124+
"@types/node": "^22.19.11",
125+
"@vitest/coverage-v8": "^3.2.4",
126+
"esbuild": "^0.27.3",
127+
"prettier": "^3.8.1",
128+
"rimraf": "^6.1.3",
129+
"rollup-plugin-dts": "^6.3.0",
128130
"rollup-plugin-scss": "^4.0.1",
129-
"rollup-plugin-visualizer": "^6.0.5",
130-
"semantic-release": "^25.0.2",
131+
"rollup-plugin-visualizer": "6.0.5",
132+
"semantic-release": "^25.0.3",
131133
"tslib": "^2.8.1",
132-
"tsx": "^4.19.2",
133-
"vitest": "^3.0.0",
134+
"tsx": "^4.21.0",
135+
"vitest": "^3.2.4",
134136
"yargs": "^17.7.2"
135137
},
136138
"files": [

‎rollup.config.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ const configs: RollupOptions[] = [
121121
sourcemap: true,
122122
exports: 'auto',
123123
interop: 'compat',
124+
// Fix: avoid _interopNamespaceCompat crash when processing Node.js built-ins
125+
// (path, child_process, etc.) that have inherited prototype properties.
126+
// getOwnPropertyDescriptor returns undefined for inherited props → d.get throws.
127+
externalLiveBindings: false,
124128
generatedCode: {
125129
constBindings: true,
126130
objectShorthand: true,

‎src/cli.ts‎

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { ChangelogGenerator } from './lib/changelog-generator.js';
3636
import { IconFontGenerator } from './lib/icon-font-generator.js';
3737
// Remove the explicit process import since it's globally available
3838
import internalServer from './lib/internal.middleware';
39-
import { safeNextImport, isNextAvailable } from './lib/next-import.js';
39+
import { safeNextImport } from './lib/next-import.js';
4040
import { PP_DEV_CONFIG_NAMES, PP_WATCH_CONFIG_NAMES } from './constants.js';
4141

4242
const cli = cac('pp-dev');
@@ -506,21 +506,25 @@ cli
506506
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
507507
filterDuplicateOptions(options);
508508

509-
let nextApp: any = null;
509+
let nextApp: ReturnType<typeof import('next').default> | null = null;
510510
let httpServer: any = null;
511511
let configWatcher: ConfigWatcher | null = null;
512512
let isRestarting = false;
513513

514-
const projectRoot = root
515-
? path.resolve(process.cwd(), root)
516-
: process.cwd();
517514
const logger = createLogger();
518515

519516
const startNextServer = async () => {
520-
if (isRestarting) return;
517+
if (isRestarting) {
518+
return;
519+
}
520+
521521
isRestarting = true;
522522

523523
try {
524+
// Import Next.js first – with logger available for error reporting
525+
const { next, constants } = await safeNextImport();
526+
const { PHASE_DEVELOPMENT_SERVER } = constants;
527+
524528
// Clean up existing server if any
525529
if (httpServer) {
526530
logger.info(colors.yellow('🛑 Stopping existing Next.js server...'));
@@ -535,32 +539,23 @@ cli
535539
// Clean up existing Next.js app if any
536540
if (nextApp && typeof nextApp.close === 'function') {
537541
await nextApp.close();
542+
538543
nextApp = null;
539544
}
540545

541546
// Clear config cache
542547
const { clearConfigCache } = await import('./config.js');
543548
clearConfigCache();
544549

545-
// Check if Next.js is available before proceeding
546-
if (!(await isNextAvailable())) {
547-
throw new Error(
548-
'Next.js is required but not available. Please install Next.js as a dependency:\n' +
549-
'npm install next@^16\n\n' +
550-
'This package requires Next.js >=13 <17 as a peer dependency.',
551-
);
552-
}
553-
554-
const { next } = await safeNextImport();
555550
const { join, basename } = await import('path');
556551
const { createServer } = await import('http');
557552

558553
const importConfig = await import('next/dist/server/config.js');
559554

560-
const loadConfig =
561-
importConfig.default.default ||
562-
importConfig['module.exports'].default ||
563-
importConfig.default;
555+
const loadConfig: typeof import('next/dist/server/config.js').default =
556+
(importConfig as any).default.default ||
557+
(importConfig as any)['module.exports'].default ||
558+
(importConfig as any).default;
564559

565560
const opts = cleanOptions(options);
566561

@@ -585,10 +580,10 @@ cli
585580
logger.info(projectRoot);
586581

587582
// Get pp-dev config from Next.js app config
588-
const config = await loadConfig('development', projectRoot);
583+
const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, projectRoot);
589584

590585
// Extract pp-dev configuration from Next.js config
591-
let ppDevConfig = config?.experimental?.ppDev || config?.ppDev || {};
586+
let ppDevConfig = config?.ppDev || {};
592587

593588
// If no pp-dev config found in Next.js config, try to load from standalone config file
594589
if (Object.keys(ppDevConfig).length === 0) {
@@ -628,25 +623,36 @@ cli
628623
const {
629624
backendBaseURL = process.env.MI_BACKEND_URL ||
630625
'http://localhost:8080',
631-
portalPageId = parseInt(process.env.MI_PORTAL_PAGE_ID || '1'),
626+
appId: originalAppId,
627+
portalPageId,
632628
templateLess = true,
633629
v7Features = true,
634630
disableSSLValidation = false,
635631
enableProxyCache = true,
636632
proxyCacheTTL = 600000,
637633
personalAccessToken = process.env.MI_ACCESS_TOKEN,
638-
distZip = false,
639-
syncBackupsDir = './backups',
640634
miHudLess = false,
641635
} = ppDevConfig;
642636

637+
const appId: number =
638+
originalAppId ??
639+
portalPageId ??
640+
(process.env.MI_APP_ID
641+
? parseInt(process.env.MI_APP_ID)
642+
: undefined) ??
643+
(process.env.MI_PORTAL_PAGE_ID
644+
? parseInt(process.env.MI_PORTAL_PAGE_ID)
645+
: undefined) ??
646+
1;
647+
643648
// Get template name from config, package.json, or fallback to project directory name
644-
let templateName = ppDevConfig.templateName;
649+
let templateName = null;
645650

646651
if (!templateName) {
647652
try {
648653
const { getPkg } = await import('./config.js');
649654
const pkg = getPkg();
655+
650656
templateName = pkg.name;
651657
} catch (error) {
652658
// Fallback to project directory name
@@ -658,8 +664,15 @@ cli
658664
const pathPagePrefix = '/p'; // templateLess = true - use /p
659665
const pathTemplatePrefix = '/pl'; // templateLess = false && v7Features = true - use /pl
660666

661-
let base = templateLess ? pathPagePrefix : pathTemplatePrefix;
662-
base += `/${templateName}`;
667+
const configBasePath = config?.basePath;
668+
let base = '';
669+
670+
if (configBasePath) {
671+
base = configBasePath;
672+
} else {
673+
base = templateLess ? pathPagePrefix : pathTemplatePrefix;
674+
base += `/${templateName}`;
675+
}
663676

664677
nextApp = next({
665678
dev: true,
@@ -669,18 +682,12 @@ cli
669682
conf: {
670683
...config,
671684
basePath: base,
672-
assetPrefix: base, // Fixed: Make assetPrefix consistent with basePath
685+
assetPrefix: `${templateLess ? pathPagePrefix : '/pt'}/${templateName}`, // Fixed: Make assetPrefix consistent with basePath
673686
},
674687
});
675688

676689
await nextApp.prepare();
677690

678-
// Default to templateLess = true for Next.js development
679-
// const templateLess =
680-
// typeof ppDevConfig.templateLess === "boolean"
681-
// ? ppDevConfig.templateLess
682-
// : true;
683-
684691
if (!base.endsWith('/')) {
685692
base += '/';
686693
}
@@ -691,11 +698,6 @@ cli
691698
);
692699
}
693700

694-
const baseWithoutTrailingSlash = base.substring(
695-
0,
696-
base.lastIndexOf('/'),
697-
);
698-
699701
// Log the configuration
700702
logger.info(colors.green('✅ Next.js app prepared successfully'));
701703
logger.info(
@@ -707,7 +709,7 @@ cli
707709

708710
if (backendBaseURL) {
709711
logger.info(colors.blue(`🌐 Backend URL: ${backendBaseURL}`));
710-
logger.info(colors.blue(`🆔 Portal Page ID: ${portalPageId}`));
712+
logger.info(colors.blue(`🆔 Custom App ID: ${appId}`));
711713
}
712714

713715
// Get the Next.js request handler
@@ -735,8 +737,7 @@ cli
735737
const isInternalNextRoute =
736738
originalPathname.startsWith('/_next/') ||
737739
originalPathname === '/favicon.ico' ||
738-
originalPathname.startsWith('/__nextjs_') ||
739-
originalPathname.startsWith('/api/');
740+
originalPathname.startsWith('/__nextjs_');
740741

741742
if (isInternalNextRoute) {
742743
// For internal routes, only apply essential middlewares (skip proxy, cache, etc.)
@@ -791,17 +792,16 @@ cli
791792
processNextJSRequest();
792793

793794
async function processNextJSRequest() {
794-
// Handle base path requests
795+
// Handle base path requests - pass full path to Next.js so it can apply basePath routing
795796
if (originalPathname.startsWith(base)) {
796-
// Strip the base path for Next.js
797-
const nextPath = originalPathname.substring(base.length);
798-
799-
req.url = nextPath || '/';
800-
parsedUrl = parse(nextPath, true);
797+
// Keep full path - Next.js expects req.url to include basePath for proper routing
798+
parsedUrl = parse(originalUrl, true);
801799
} else if (originalPathname === base.replace(/\/$/, '')) {
802-
// Handle base path without trailing slash
803-
req.url = '/';
804-
parsedUrl = parse('/', true);
800+
// Path without trailing slash - redirect to canonical URL with trailing slash
801+
const redirectUrl = originalUrl.replace(originalPathname, base);
802+
res.writeHead(302, { Location: redirectUrl });
803+
res.end();
804+
return;
805805
} else if (
806806
originalPathname.startsWith('/_next/') ||
807807
originalPathname === '/favicon.ico' ||
@@ -849,8 +849,8 @@ cli
849849
'$1$2',
850850
),
851851
},
852-
portalPageId,
853-
appId: portalPageId,
852+
portalPageId: appId,
853+
appId,
854854
templateLess,
855855
disableSSLValidation,
856856
v7Features,
@@ -931,7 +931,7 @@ cli
931931
'/_next',
932932
'/favicon.ico',
933933
'/__nextjs_',
934-
'/api',
934+
'/installHook.js.map',
935935
],
936936
disableSSLValidation,
937937
miAPI: mi,
@@ -1011,7 +1011,7 @@ cli
10111011
logger.info(
10121012
colors.blue(`🔧 MiAPI initialized for backend: ${backendBaseURL}`),
10131013
);
1014-
logger.info(colors.blue(`🔧 Portal Page ID: ${portalPageId}`));
1014+
logger.info(colors.blue(`🔧 Custom App ID: ${appId}`));
10151015
}
10161016

10171017
httpServer.listen(port, host, () => {

0 commit comments

Comments
 (0)