Skip to content

Release: merge develop into main (0.18.1-beta.1 and accumulated work) - #139

Merged
sergak01 merged 14 commits into
mainfrom
develop
Apr 22, 2026
Merged

Release: merge develop into main (0.18.1-beta.1 and accumulated work)#139
sergak01 merged 14 commits into
mainfrom
develop

Conversation

@sergak01

@sergak01 sergak01 commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Release: merge develop into main (0.18.1-beta.1 and accumulated work)

Summary

This merge brings origin/develop into origin/main, promoting the current beta line and all integrated work: core refactors (dist/client services, proxy stack, CLI), tooling (Prettier, Dependabot), build and dependency updates, documentation, and test alignment. The branch includes the release commit chore(release): 0.18.1-beta.1 [skip ci] and 9 preceding feature/maintenance commits. Diff vs main: 105 files, roughly +2.9k / -2.7k lines.


Key changes

  • 🚀 Core runtime: Dist and client services, middleware/proxy stack, plugins (including version plugin), CLI structure, shortcuts, and API/page-template updates; new unavailable-json-api and client asset typings.
  • Build & release: Rollup/esbuild/Vitest/tsconfig, scripts, package.json / lockfile, and semantic-release-style 0.18.1-beta.1 bump on develop.
  • 🧹 Maintenance: Prettier, Dependabot, npm audit patch path; dependency hygiene.
  • 📊 Docs: README, CHANGELOG, CONTRIBUTING, BUILD_IMPROVEMENTS, testing docs.
  • 🧪 Testing: E2E/Playwright, integration middleware specs, unit tests, CommonJS/Next.js fixtures.

Stats

Metric Value
Files changed 105
Insertions ~2,891
Deletions ~2,697

Included commits (origin/main..origin/develop, no merges)

  • 035ee09 — chore(deps): patch npm audit vulnerabilities
  • 4de05ed — fix(cli): harden shortcut cleanup and add dts trace logging
  • 8edd465 — chore(test): refresh commonjs fixture lockfile
  • 42d52b3 — chore(tooling): add Prettier configuration
  • d4780d9 — chore(ci): update Dependabot configuration
  • caf2b98 — chore(build): update dependencies, bundler configs, scripts, and test runners
  • 212c555 — docs: refresh README, changelog, and testing documentation
  • 277ed89 — refactor(core): improve dist and client services, proxy stack, CLI, and plugins
  • b2be1bd — test: align e2e, integration, unit suites, and sandbox fixtures
  • 01e73a7 — chore(release): 0.18.1-beta.1 [skip ci]

Testing / release checklist

  • CI green on develop before merge
  • npm test / project test scripts on a clean checkout
  • Smoke Next.js and Vite sandboxes (tests/test-nextjs*, tests/test-commonjs) if part of release process
  • Confirm npm version and CHANGELOG match expectations for main after merge
  • Post-merge: tag or publish steps per team runbook (if applicable)

Breaking changes / compatibility

  • No explicit breaking-change summary in this description; validate peer dependencies (e.g. Next.js), config, and proxy behavior against production Portal Page consumers before relying on main for downstream installs.

Summary by CodeRabbit

  • New Features

    • Added template sync confirmation modal with user approval flow
    • Added backup analysis and manifest validation for template variables
    • Extended version/manifest metadata with repository URL and branch info
  • Bug Fixes

    • Improved handling for unavailable/non-JSON API responses
    • Fixed keyboard shortcut listener cleanup to avoid accumulating handlers
  • Chores

    • Updated package version to 0.18.1-beta.2
    • Added Prettier configuration and ignore file; adjusted TypeScript build settings

sergak01 and others added 11 commits April 20, 2026 10:49
Update root and test lockfiles to pull non-vulnerable axios, follow-redirects, and next versions so repo-wide audits pass.
Improve stdin shortcut lifecycle cleanup to avoid stale bindings and preserve terminal state across server restarts. Add trace hooks to the dts rollup build to pinpoint where type generation can stall.
Update the CommonJS test fixture lockfile to the latest local pp-dev package and aligned dependency metadata so fixture installs stay reproducible.
PP-3311 pp-dev: dist/client/proxy refactor, tooling, and test alignment
## [0.18.1-beta.1](v0.18.0...v0.18.1-beta.1) (2026-04-21)

### Bug Fixes

* **cli:** harden shortcut cleanup and add dts trace logging ([4de05ed](4de05ed))
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds a browser confirmation modal and bidirectional sync-action flow, introduces ZIP backup analysis and BUILD-MANIFEST fingerprinting, centralizes non-JSON API error handling, extends version/manifest metadata, and applies many formatting/structural edits across build, config, client, service, middleware, and test files.

Changes

Cohort / File(s) Summary
Client UI & Types
src/client/index.ts, src/client/index.html, src/client/assets/css/client.scss, src/client/types/assets.d.ts
Added confirm-modal UI, modal lifecycle helpers/types, confirm overlay styles, and HTML/DOM formatting changes; no exported API surface changes.
Client Build Config
src/client/rollup.config.ts, src/client/tsconfig.json
Embed SCSS content into client CSS source maps, silence Sass deprecation, set inlineSources, and adjust moduleResolution/sourceMap settings.
Client Service & Sync Flow
src/lib/client.service.ts, src/lib/dist.service.ts
Added requestSyncAction/resolver tracking and websocket confirm-response flow; major backup ZIP analysis, VERSION/BUILD-MANIFEST parsing, fingerprint/hash computation, and updated saveBackup behavior.
API & Error Handling
src/api/unavailable-json-api.ts, src/api/page-template.ts, src/api/page.ts, src/api/index.ts
New unavailable-json-api module with error factories and detector; Page and PageTemplate APIs validate JSON structure and throw standardized errors; re-export added.
Middleware & Proxy
src/lib/pp.middleware.ts, src/lib/proxy-pass.middleware.ts, src/lib/proxy-cache.middleware.ts, src/lib/load-pp-data.middleware.ts, src/lib/rewrite-response.middleware.ts
Per-instance HTTP/HTTPS agent changes, isUnavailableJsonApiError handling, socket listener lifecycle hardening, response rewriting/streaming reorganized; mostly error-handling and robustness plus formatting.
Versioning & Build Plugins
src/plugins/version-plugin.ts, src/plugin.ts, src/index.ts, rollup.config.ts
Version plugin extended to emit BUILD-MANIFEST.json, schemaVersion, repository/branch metadata, computeBuildFingerprint, repository normalization and branch resolution; plugin wiring updated and index passes repository URL.
Shortcuts & CLI
src/shortcuts.ts, src/cli.ts
Improved stdin/cleanup binding lifecycle, Ctrl+C re-emits SIGINT, EventEmitter listener-limit adjustment for HTTPS backends, build action error handling restructured.
Middleware Helpers & Misc Libs
src/lib/helpers/*, src/lib/* (auth.provider, login.helper, url.helper, token.helper, server, logger, next-import, changelog-generator)
Numerous formatting and small refactors (single-line signatures, selector tweaks, regex escaping); few behavioral tweaks (login form selector update); most are non-functional.
Build/Tooling & Scripts
rollup.config.ts, esbuild.config.ts, scripts/*.js, scripts/patch-npm-bundled-vulnerabilities.mjs, .prettierrc.cjs, .prettierignore, .github/dependabot.yml
Prettier config added, YAML quoting normalized, rollup external->isExternal predicate, dts monitor plugin, small script adjustments (single quotes, replaceDir brace-expansion call), formatting changes.
Package, Changelog & Docs
package.json, CHANGELOG.md, BUILD_IMPROVEMENTS.md, README.md, CONTRIBUTING.md
Version bump to 0.18.1-beta.2, changelog entry additions/reordering, whitespace/formatting doc updates.
Tests & Fixtures
tests/**, tests/test-commonjs/**, tests/test-nextjs/**, e2e/**, tests/unit/**
Many test formatting changes, a compacted __template_variables.json fixture, updated test config (appId/templateLess), added BUILD-MANIFEST assertions in version-plugin tests; mostly non-functional edits.
TypeScript Configs
tsconfig.json, tsconfig.*.json, vitest*.config.ts, e2e/tsconfig.json, tests/tsconfig.json
Reworked include/exclude, disabled some sourceMap generation, added rootDir, compacted arrays; no semantic compiler behavior regressions expected.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ClientApp
  participant DevServer
  participant DistService
  Browser->>ClientApp: websocket 'template:sync:action-required' (requestId, payload)
  ClientApp->>Browser: open confirmModal() (user prompt)
  alt user approves
    Browser->>ClientApp: modal resolves approved=true
    ClientApp->>DevServer: websocket 'template:sync:action-response' {requestId, approved:true}
    DevServer->>DistService: start sync/build with provided assets
    DistService-->>DevServer: send sync result (success/failure)
    DevServer-->>ClientApp: websocket 'template:sync:response' (status)
  else user cancels
    Browser->>ClientApp: modal resolves approved=false
    ClientApp->>DevServer: websocket 'template:sync:action-response' {requestId, approved:false}
    DevServer-->>ClientApp: websocket 'template:sync:response' {cancelled:true, message}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • michailozdemir
  • Sadilenko
  • MaksymovVolodymyr

"I nibbled code and stitched a seam,
A modal glows in sync-time dream,
Backups hashed with tidy care,
Manifests bloom from build's lair,
A rabbit applauds this clever team!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: a release merge from develop into main for version 0.18.1-beta.1 with accumulated work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
BUILD_IMPROVEMENTS.md (1)

91-104: ⚠️ Potential issue | 🟡 Minor

Remove duplicate "Rollup (Primary)" section.

Lines 91-97 and 98-104 contain identical headings and content. This appears to be an unintended duplication that should be removed to avoid confusion.

🧹 Proposed fix to remove the duplicate
 ### Rollup (Primary)
 
 - Advanced tree-shaking
 - Plugin ecosystem
 - Source map generation
 - Bundle analysis support
-
-### Rollup (Primary)
-
-- Advanced tree-shaking
-- Plugin ecosystem
-- Source map generation
-- Bundle analysis support
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@BUILD_IMPROVEMENTS.md` around lines 91 - 104, Remove the duplicated "Rollup
(Primary)" section by keeping a single heading and its bullet list and deleting
the second identical block; locate the duplicate heading "Rollup (Primary)" and
the repeated bullets under it and remove the redundant copy so the document
contains only one Rollup (Primary) section.
src/lib/helpers/token.helper.ts (1)

26-42: ⚠️ Potential issue | 🟡 Minor

Wrap the switch case in braces to create proper block scope.

Static analysis correctly flags that const lowerMessage at line 27 is declared without block-level scope. While the code functions correctly (each path returns), wrapping the case in braces prevents potential issues if the code is modified later and aligns with best practices.

🔧 Proposed fix to add block scope
   switch (status) {
-    case 412:
+    case 412: {
       const lowerMessage = message.toLowerCase();
 
       if (lowerMessage.includes('session expired') || lowerMessage.includes('session has expired')) {
         return {
           status,
           message,
           code: 'SESSION_EXPIRED',
           userFriendlyMessage: 'Your session has expired',
           suggestions: [
             'Refresh your token in the portal',
             'Re-authenticate with the portal',
             'Check if your token has been revoked',
             'Ensure your token has the correct permissions',
           ],
         };
       }
       return {
         status,
         message,
         code: 'AUTH_FAILED',
         userFriendlyMessage: 'Authentication failed',
         suggestions: [
           'Verify your token is correct',
           'Check token permissions',
           "Ensure the token hasn't expired",
           'Try generating a new token',
         ],
       };
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers/token.helper.ts` around lines 26 - 42, Wrap the case 412
switch branch in its own block to provide proper block-level scoping for the
const lowerMessage: add braces surrounding the branch that starts with case 412
and ends with the return object so that lowerMessage (and any future temp vars)
are scoped to that case; update the case 412 branch near token.helper.ts where
lowerMessage is declared to use this new block scope.
scripts/run-tests.js (1)

119-121: ⚠️ Potential issue | 🟡 Minor

Fix callback signature in error handler.

The error handler passes null as the second argument, but the callback signature expects { exitCode: number }. This is inconsistent with the TypeScript version in e2e/config.spec.ts (line 44), which correctly passes { exitCode: 1 }.

🐛 Proposed fix to match callback contract
    child.on('error', (error) => {
-      callback(error, null);
+      callback(error, { exitCode: 1 });
    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/run-tests.js` around lines 119 - 121, The error handler currently
calls callback(error, null) which violates the callback contract expecting a
second arg shaped like { exitCode: number }; update the handler on the child
process (child.on('error', ...)) to pass an object with an exitCode (e.g., {
exitCode: 1 }) as the second argument so the signature matches the TypeScript
test helper (see e2e/config.spec.ts usage). Ensure only the second argument is
changed and the first remains the error object.
tests/test-nextjs/src/pages/_document.tsx (1)

3-8: ⚠️ Potential issue | 🟡 Minor

Remove the metadata export—it's not supported in Pages Router.

The metadata export (lines 5-8) is exclusive to the App Router and does not function in Pages Router's _document.tsx. For Pages Router, use the next/head component in individual pages to handle metadata instead, or define static site-wide head content directly within this file's <Head> element if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test-nextjs/src/pages/_document.tsx` around lines 3 - 8, Remove the
unsupported Pages Router export: delete the exported metadata constant (export
const metadata: Metadata = {...}) and its import of Metadata from 'next' in
_document.tsx; instead, move any page-specific title/description into individual
pages using next/head or place site-wide head elements inside the existing
<Head> element in the Document component so metadata is handled via the Pages
Router mechanism.
src/lib/helpers/url.helper.ts (1)

18-32: ⚠️ Potential issue | 🟠 Major

Escape dynamic values before building replacement regexes.

originalHost and urlPath are treated as regex source, so values containing metacharacters like ., +, ?, or [ can over-match or throw. Escape the literal inputs before interpolating them.

🛡️ Proposed fix
+const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
 export const urlReplacer = (originalHost: string, destinationHost: string, content: string) => {
-  const urlReplaceRegExp = new RegExp(`(!!)?(https?(:(\\\\)?/(\\\\)?/)${originalHost})`, 'gi');
+  const escapedOriginalHost = escapeRegExp(originalHost);
+  const urlReplaceRegExp = new RegExp(`(!!)?(https?(:(\\\\)?/(\\\\)?/)${escapedOriginalHost})`, 'gi');
 
   return content.replace(urlReplaceRegExp, (substring, ...args) => {
     if (substring.startsWith('!!')) {
       return args[1];
@@
 };
 
 export const urlPathReplacer = (urlPath: string, destinationPath: string, content: string) => {
-  const urlReplaceRegExp = new RegExp(`${urlPath.replace(/\\*\//gi, '\\\\/')}`, 'gi');
-  const unescapedUrlReplaceRegExp = new RegExp(`${urlPath}`, 'gi');
+  const escapedUrlPath = escapeRegExp(urlPath);
+  const urlReplaceRegExp = new RegExp(escapedUrlPath.replace(/\\*\//gi, '\\\\/'), 'gi');
+  const unescapedUrlReplaceRegExp = new RegExp(escapedUrlPath, 'gi');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers/url.helper.ts` around lines 18 - 32, The regexp constructors
in urlReplacer and urlPathReplacer use unescaped user inputs (originalHost and
urlPath), which can cause over-matching or runtime errors; fix by escaping those
inputs before interpolating into new RegExp (create/inline an escapeRegExp
function that replaces special characters like .+*?^${}()|[]\ with escaped
versions) and use the escapedOriginalHost when building urlReplaceRegExp in
urlReplacer and escapedUrlPath when building both urlReplaceRegExp and
unescapedUrlReplaceRegExp in urlPathReplacer so the patterns match the literal
strings.
src/lib/proxy-pass.middleware.ts (1)

32-39: ⚠️ Potential issue | 🟠 Major

Apply the streaming interceptor callback or remove it.

The callback created at Lines 183-185 is never invoked because streamResponseInterceptor ignores its interceptor parameter. Streaming responses are currently piped through without the intended URL rewriting.

Do you want me to generate a safe streaming transform implementation for this path?

Also applies to: 181-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/proxy-pass.middleware.ts` around lines 32 - 39,
streamResponseInterceptor currently ignores its interceptor parameter so proxied
streams are unmodified; implement a streaming Transform that, when interceptor
is provided, reads chunks from proxyRes, converts to Buffer (respecting
encoding), calls interceptor(chunk, encoding) and pushes the interceptor result,
then pipe proxyRes -> transform -> res; if interceptor is undefined, keep the
direct proxyRes.pipe(res) behavior. Update the function
(streamResponseInterceptor) to construct and use a stream.Transform (from
'stream') named e.g. responseTransform, ensure you handle Buffer/Uint8Array and
encoding types correctly and forward errors from proxyRes/transform to res, and
keep the existing res.setHeader/ res.setHeaders logic unchanged.
src/lib/proxy-cache.middleware.ts (1)

221-240: ⚠️ Potential issue | 🟠 Major

Preserve ServerResponse.write/end callback overloads while buffering.

Node.js ServerResponse.write() and end() support overloads where callbacks can be passed as the second argument without encoding (e.g., res.write(chunk, callback) or res.end(chunk, callback)). The current signatures treat the second parameter as encoding only, so passing a callback function causes Buffer.from(data, callbackFn) to fail. Additionally, the write method ignores and never invokes captured callbacks during buffering, breaking code that relies on write completion.

The suggestion to normalize encoding and callback parameters, queue write callbacks, and forward them during final originalEnd call addresses both issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/proxy-cache.middleware.ts` around lines 221 - 240, The buffered
override of (res as any).write incorrectly treats the second argument as
encoding only and drops any callback; update (res as any).write to normalize
parameters by checking if the second arg is a function (treat as callback) or a
string (encoding), default encoding to 'utf8' when needed, capture/queue any
provided callback into a write-callbacks array (e.g., writeCallbacks) alongside
pushing Buffer data into chunks, and ensure those queued callbacks are invoked
when the buffered body is flushed by originalEnd (or originalWrite) so callers
receive their completion callbacks; reference the overridden (res as any).write,
originalWrite, originalEnd, chunks, and the new write-callbacks queue when
making changes.
tests/unit/plugin/version-plugin.spec.ts (1)

222-239: ⚠️ Potential issue | 🟡 Minor

Case-sensitive substring check can miss BUILD-MANIFEST.json in the "disabled" negative test.

files.find((f) => f.includes('VERSION') || f.includes('manifest')) is case-sensitive — 'manifest' won't match 'BUILD-MANIFEST.json'. If the plugin regressed and wrote the build manifest while enabled: false, this test would still pass and miss the regression. Consider matching 'MANIFEST' (or using a case-insensitive regex) so both artifacts are covered.

💡 Proposed fix
-      const versionFile = files.find((f: string) => f.includes('VERSION') || f.includes('manifest'));
+      const versionFile = files.find((f: string) => /VERSION|MANIFEST|manifest/.test(f));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/plugin/version-plugin.spec.ts` around lines 222 - 239, The
negative test uses a case-sensitive substring check that can miss
BUILD-MANIFEST.json; update the assertion in the test that inspects files (the
files.find(...) call) to use a case-insensitive match (e.g., a case-insensitive
RegExp) or explicitly check for 'MANIFEST' in addition to 'manifest' so that any
manifest filename like BUILD-MANIFEST.json or VERSION is detected when
versionPlugin(...) is called with enabled: false and invokeCloseBundle(plugin)
is run.
🟡 Minor comments (5)
CHANGELOG.md-8-10 (1)

8-10: ⚠️ Potential issue | 🟡 Minor

Inconsistent heading hierarchy in changelog.

The changelog mixes # (h1) and ## (h2) for version headers. Line 8 uses # [0.18.0-beta.1] (h1), immediately followed by ### Features (h3) on line 10, which violates markdown heading increment rules. Version headers should consistently use ## (h2) throughout the file, as done for the new 0.18.1-beta.1 entry (line 1) and older entries below.

📝 Proposed fix to standardize heading levels
-# [0.18.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.17.0...v0.18.0-beta.1) (2026-04-06)
+## [0.18.0-beta.1](https://github.com/mi-examples/pp-dev/compare/v0.17.0...v0.18.0-beta.1) (2026-04-06)

 ### Features

Apply this same fix to all other version headers that incorrectly use # instead of ## (lines 14, 24, 40, 46, 64, 76, 82, 93, 103, 109, 115, 121, 127, 134, 144, 154, 160).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 8 - 10, Replace top-level single-hash version
headers that start with "# [" (e.g., "# [0.18.0-beta.1]") with second-level "##
[" headings so all release/version entries use a consistent h2 level; update
every occurrence matching that pattern (the comment lists examples such as the
header on the 0.18.0-beta.1 entry and the other entries that currently use "#"
instead of "##") and leave subheadings like "### Features" unchanged.
rollup.config.ts-67-106 (1)

67-106: ⚠️ Potential issue | 🟡 Minor

Validate DTS monitor interval values before scheduling timers.

Number(process.env...) can produce NaN, 0, or negative values. That can make the timeout fire immediately or the heartbeat spam logs during release builds.

🛡️ Proposed validation
 function typeDefsMonitorPlugin() {
-  const timeoutMs = Number(process.env.PP_DEV_DTS_TIMEOUT_MS ?? 120_000);
-  const heartbeatMs = Number(process.env.PP_DEV_DTS_HEARTBEAT_MS ?? 10_000);
+  const positiveNumberFromEnv = (name: string, fallback: number) => {
+    const value = Number(process.env[name]);
+
+    return Number.isFinite(value) && value > 0 ? value : fallback;
+  };
+
+  const timeoutMs = positiveNumberFromEnv('PP_DEV_DTS_TIMEOUT_MS', 120_000);
+  const heartbeatMs = positiveNumberFromEnv('PP_DEV_DTS_HEARTBEAT_MS', 10_000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rollup.config.ts` around lines 67 - 106, Validate and sanitize timeoutMs and
heartbeatMs in typeDefsMonitorPlugin before scheduling timers: ensure
Number(process.env.PP_DEV_DTS_TIMEOUT_MS) and
Number(process.env.PP_DEV_DTS_HEARTBEAT_MS) are finite positive integers
(fallback to 120_000 and 10_000 respectively if they are NaN, <=0, or Infinity),
then only call setInterval and setTimeout in buildStart when the sanitized
heartbeatMs/timeoutMs are valid; also guard heartbeat.unref and timeout.unref
behind existence checks and clearTimers should still work with potentially unset
timers (heartbeat/timeout possibly null).
src/cli.ts-1300-1302 (1)

1300-1302: ⚠️ Potential issue | 🟡 Minor

Await profiler shutdown in the build finalizer.

stopProfiler can return a promise; without awaiting it, the build command may finish before the CPU profile is written.

🧪 Proposed fix
     } finally {
-      stopProfiler((message) => createLogger(options.logLevel).info(message));
+      await stopProfiler((message) => createLogger(options.logLevel).info(message));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.ts` around lines 1300 - 1302, The finalizer calls stopProfiler(...)
but doesn't await its possible Promise, so ensure you await stopProfiler in the
finally block (i.e., change the call to await stopProfiler((message) =>
createLogger(options.logLevel).info(message))) and make the enclosing
function/handler async or return the resulting promise so the build command
waits for the CPU profile write to complete; update any callers/returns as
needed to propagate the async change.
src/client/index.html-14-18 (1)

14-18: ⚠️ Potential issue | 🟡 Minor

Add rel to _blank links.

These links open a new tab; add rel="noopener noreferrer" to prevent the opened page from accessing window.opener.

Suggested fix
         <a
           href="https://www.npmjs.com/package/{%= PACKAGE_NAME %}/v/{%= VERSION %}"
           target="_blank"
+          rel="noopener noreferrer"
           class="pp-dev-info__link"
         >
         <a
           href="!!{%= backendBaseURL %}/admin/page/edit/id/{%= portalPageId %}"
           target="_blank"
+          rel="noopener noreferrer"
           class="pp-dev-info__link"
           >{%= portalPageId %}</a
         >

Also applies to: 50-55

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/index.html` around lines 14 - 18, The anchor elements using
target="_blank" (the <a> elements with class "pp-dev-info__link" and href like
"https://www.npmjs.com/package/{%= PACKAGE_NAME %}/v/{%= VERSION %}") must
include rel="noopener noreferrer"; update those anchors (both the occurrence
around lines with the npm badge and the other similar anchor at lines ~50-55) to
add rel="noopener noreferrer" to prevent the opened page from accessing
window.opener.
src/client/index.ts-232-287 (1)

232-287: ⚠️ Potential issue | 🟡 Minor

Confirm modal is missing basic accessibility affordances.

The dialog has no ARIA semantics or focus management, which makes the confirm flow hard to use with a keyboard or screen reader:

  • No role="dialog" / aria-modal="true" / aria-labelledby on the overlay or dialog element.
  • Focus is not moved into the modal on open, nor trapped inside it, nor restored to the previously-focused element on close.
  • Tabbing can escape to the underlying page while the backdrop visually blocks it.

This is a dev-tool UI so the impact is limited, but it's cheap to fix — set ARIA attributes on $confirm, call $confirmButton.focus() after append, remember document.activeElement before opening and restore it on close, and add a small Tab-cycling trap between cancel/confirm.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/index.ts` around lines 232 - 287, In confirmModal, add ARIA and
focus management: set role="dialog", aria-modal="true" on $confirm and add
aria-labelledby pointing to an id you assign to $title; before appending, save
document.activeElement to a variable (previousActiveElement) and after appending
call $confirmButton.focus(); implement a simple tab-trap on the modal (keydown
handler that intercepts Tab and cycles between $cancelButton and
$confirmButton); ensure all event listeners are removed and
previousActiveElement.focus() is restored when closing by updating
closeConfirmModalByResult to accept/restore the saved previousActiveElement and
clean up the tab-trap listener.
🧹 Nitpick comments (5)
CONTRIBUTING.md (1)

78-78: Optional: tighten wording for the CI-skip instruction.

Consider a shorter, more direct phrasing to reduce ambiguity (e.g., “To amend a release commit, include [skip ci] in the commit message.”).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` at line 78, Tighten the CI-skip instruction sentence by
replacing the current wording with a shorter, direct phrase; update the line
that currently reads "If you need to make changes to the release commit (like
updating the changelog), you can skip the CI by including `[skip ci]` in your
commit message." to a concise variant such as "To amend a release commit,
include `[skip ci]` in the commit message." so the guidance is clearer and less
ambiguous.
src/lib/helpers/login.helper.ts (3)

377-384: Scope helper control lookups to the injected form.

Because this script is injected into host HTML, global document.getElementById(...) can bind to an unrelated/stale element with the same ID. Query within formContent instead.

Proposed scoped lookup
-const tokenTypeSwitcher = document.getElementById('token-type-switcher');
-const tokenInput = document.getElementById('helper-token') as HTMLInputElement;
-const tokenCaption = document.getElementById('token-caption') as HTMLSpanElement;
-const submitButton = document.getElementById('helper-token-submit') as HTMLButtonElement;
-const errorElement = document.getElementById('error-message') as HTMLDivElement;
+const tokenTypeSwitcher = formContent.querySelector<HTMLElement>('#token-type-switcher');
+const tokenInput = formContent.querySelector<HTMLInputElement>('#helper-token');
+const tokenCaption = formContent.querySelector<HTMLSpanElement>('#token-caption');
+const submitButton = formContent.querySelector<HTMLButtonElement>('#helper-token-submit');
+const errorElement = formContent.querySelector<HTMLDivElement>('#error-message');
 const formWrapper = formContent.querySelector('.helper-login-wrapper') as HTMLElement;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers/login.helper.ts` around lines 377 - 384, The DOM lookups use
global document.getElementById and can pick up unrelated elements; change all
lookups to query within the injected form by replacing
document.getElementById(...) with scoped queries off formContent (e.g.,
formContent.querySelector(...) or formContent.getElementById-equivalent) for the
symbols tokenTypeSwitcher, tokenInput, tokenCaption, submitButton, errorElement
and keep formWrapper found via
formContent.querySelector('.helper-login-wrapper'); ensure the typed casts
(HTMLInputElement/HTMLSpanElement/HTMLButtonElement/HTMLDivElement/HTMLElement)
remain correct and update the null-check that verifies those scoped variables.

398-423: Avoid :has() in runtime selectors — use filtering for broader browser compatibility.

The document.querySelector() call with :has() syntax throws a SyntaxError in browsers without support, which would prevent the fallback logic from executing. While the project targets modern environments (Next.js 15+, Node 22+), the injected helper lacks explicit browser compatibility documentation and would fail silently in older browsers without even reaching the fallback check at line 423.

Replace the selector-level :has() with a broader form selector and JavaScript filtering:

Recommended change
-const formSelector = '#mi-react-root form:not([id]):has(input[name="login"]):has(input[name="password"])';
+const formSelector = '#mi-react-root form:not([id])';
+const findLoginForm = () =>
+  Array.from(document.querySelectorAll<HTMLFormElement>(formSelector)).find(
+    (form) => form.querySelector('input[name="login"]') && form.querySelector('input[name="password"]'),
+  ) ?? null;
 
 // Main logic - use MutationObserver instead of setInterval for better performance
 const observer = new MutationObserver((mutations) => {
   for (const mutation of mutations) {
     if (mutation.type === 'childList') {
-      const loginForm = document.querySelector<HTMLFormElement>(formSelector);
+      const loginForm = findLoginForm();
 
       if (loginForm) {
         observer.disconnect();
         initializeLoginForm(loginForm);
 
@@
 
 // Fallback: check if form already exists
-const existingForm = document.querySelector<HTMLFormElement>(formSelector);
+const existingForm = findLoginForm();

This approach works in all browsers with querySelector support and is consistent with the fallback pattern already in use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers/login.helper.ts` around lines 398 - 423, The current selector
uses :has() which can throw a SyntaxError in some browsers; update by replacing
formSelector with a broader selector like '#mi-react-root form:not([id])' and
then, wherever you call document.querySelector<HTMLFormElement>(formSelector)
(both inside the MutationObserver callback and for existingForm), instead query
for the broad selector and filter the resulting element(s) in JS to ensure they
contain input[name="login"] and input[name="password"] before calling
observer.disconnect() and initializeLoginForm(loginForm); keep the
observer.observe logic unchanged but use the filtered loginForm variable so the
fallback check for existingForm likewise verifies inputs via
form.querySelector('input[name="login"]') and
form.querySelector('input[name="password"]').

338-346: Use element-only traversal for DOM node selection in initializeLoginForm.

firstChild/lastChild can return text or comment nodes, not just elements. The subsequent code calls element-only APIs (innerHTML, setAttribute, appendChild) on these nodes, which would fail at runtime if a non-element node is selected. Use firstElementChild/lastElementChild instead, which only return Element nodes.

Proposed hardening
 function initializeLoginForm(loginForm: HTMLFormElement) {
-  const contentWrapper = loginForm.firstChild as HTMLDivElement;
+  const contentWrapper = loginForm.firstElementChild as HTMLDivElement | null;
 
   if (!contentWrapper) {
     return;
   }
 
   // Create form elements
-  const rowTemplate = (contentWrapper.lastChild as HTMLDivElement)?.cloneNode(true) as HTMLDivElement;
+  const rowTemplate = contentWrapper.lastElementChild?.cloneNode(true) as HTMLDivElement | undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers/login.helper.ts` around lines 338 - 346, The function
initializeLoginForm uses node-only accessors (loginForm.firstChild and
contentWrapper.lastChild) which can return non-Element nodes; change those to
element-only accessors (use loginForm.firstElementChild and (contentWrapper as
Element).lastElementChild) and update the casts to HTMLDivElement to ensure the
variables contentWrapper and rowTemplate are Elements, add null checks after
using firstElementChild/lastElementChild before using element APIs (innerHTML,
setAttribute, appendChild), and adjust any subsequent references that assume
Node to use Element/HTMLDivElement types (e.g., contentWrapper, rowTemplate) so
DOM methods are safe at runtime.
src/index.ts (1)

89-100: Bound the execSync git fallback with a timeout.

execSync('git config --get remote.origin.url', …) has no timeout, so if the git process hangs (e.g., a misconfigured system git, slow NFS, or an unexpected credential helper prompt), getViteConfig() — and therefore dev server/build startup — will block indefinitely. The stdio config already prevents interactive prompts in the common case, but a small safety bound is cheap insurance.

♻️ Proposed change
     const gitRemoteOriginUrl = execSync('git config --get remote.origin.url', {
       encoding: 'utf8',
       stdio: ['ignore', 'pipe', 'ignore'],
+      timeout: 2000,
     }).trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 89 - 100, The execSync call that reads git origin
URL can hang; modify the call (the execSync in getViteConfig / the snippet that
calls execSync('git config --get remote.origin.url', ...)) to include a timeout
option (e.g., timeout: 5000) so the child will be killed after a short bound;
preserve existing options (encoding, stdio) and keep the try/catch behavior so
failures/timeouts are ignored as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/page.ts`:
- Around line 45-50: The code currently returns data.page without validating it;
update the function that checks response data (the block using the variable data
and calling unavailablePageDataError()) to verify that data.page is present and
valid (e.g., typeof data.page === 'object' && data.page !== null or otherwise
meets your page shape) and if not, throw unavailablePageDataError() instead of
returning undefined; replace the final return data.page with a guarded return
after this validation so downstream callers never receive undefined.

In `@src/cli.ts`:
- Around line 1249-1268: The reducer seeds with backups[0] which may be a
directory or non-ZIP; first filter the directory entries to a ZIP-only array
(e.g., create zipBackups = backups.filter(b => b.isFile() &&
b.name.endsWith('.zip'))), check if zipBackups.length is zero and log/return as
you already do, then run the reduce over zipBackups (using zipBackups[0] as the
seed) to compute latestBackup; update references to backups in this block to use
the ZIP-only array and keep backupsDirPath, latestBackup, and the existing
filter/reduce logic names to locate the change.

In `@src/client/index.ts`:
- Around line 228-287: The current closeAllConfirmModals() only removes DOM
nodes and thus leaks document keydown listeners and leaves the Promise returned
by confirmModal() unresolved; update the implementation so that confirmModal()
registers each active modal (overlay, resolve, and onKeyDown) in a Map/Set and
exposes a teardown function that removes the overlay, removes the keydown
listener (onKeyDown), and calls the stored resolve(false) (or resolve(true) for
confirm path) before deleting the entry; then change closeAllConfirmModals() to
iterate active modals and call that teardown for each (ensuring
closeConfirmModalByResult or the new teardown is used for
cancel/escape/overlay-click and the confirm path removes the entry before
resolving true).

In `@src/lib/client.service.ts`:
- Around line 83-94: The promise returned by requestSyncAction can hang forever
if no response arrives; modify requestSyncAction to register a timeout (e.g.,
configurable ms) after creating requestId and setting this.syncActionResolvers
(same map used to resolve on 'template:sync:action-response') that will cleanup
the resolver (this.syncActionResolvers.delete(requestId)) and resolve(false) or
reject after timeout, and ensure the timeout is cleared when the resolver is
invoked normally; also add a cleanup path that removes any outstanding resolvers
when the WS disconnects (hook into server.ws close/error handlers) so stale
entries are removed.

In `@src/lib/dist.service.ts`:
- Around line 575-583: The current early return in DistService (the if using
resolvedAnalysis.allFilesInMeta, resolvedAnalysis.mainHash and lastMainHash) can
skip creating a backup when the manifest hash matches but the on-disk files or
the backupFingerprint changed; modify the condition so we only skip when both
the mainHash is equal AND the backupFingerprint (or whichever computed
fingerprint you use to detect content changes) is unchanged. Concretely, in the
block that checks resolvedAnalysis.allFilesInMeta and resolvedAnalysis.mainHash
and lastMainHash === resolvedAnalysis.mainHash, add a comparison against the
existing backupFingerprint (or compute and compare currentFingerprint) and only
return when fingerprint equality is true; otherwise fall through and create/save
the new backup so the user override prompt is preserved.
- Around line 410-426: The manifest loop currently reads files using
path.join(contentRootDir, relativePath) without validating that relativePath is
safe; ensure you reject unsafe/escaping paths before reading by normalizing and
resolving each path and confirming it is contained within contentRootDir (e.g.,
for entries in normalizedManifestFiles inside the same function in
dist.service.ts that uses pathToPosix and buildManifestChecksum). For each
relativePath, convert to a posix-normalized form, compute const absolutePath =
path.resolve(contentRootDir, relativePath) and if
(!absolutePath.startsWith(path.resolve(contentRootDir) + path.sep)) throw a
descriptive Error (e.g., "VERSION manifest contains unsafe path: ..."); only
then read the file with fs.readFile. Ensure this validation happens before any
file IO and applies to both the manifest parsing loop and the subsequent
file-reading loop.
- Around line 590-604: Currently the code updates this.currentMeta
(lastBackupName, lastBackupHash, lastBackupDate, lastMainHash) and calls
this.syncMeta() before calling fs.writeFile, and uses finally() to log success
even on failure; change the flow so you first await
fs.writeFile(path.resolve(this.backupFolder, filename), backupFile) and only
after that resolves successfully update this.currentMeta (set lastBackupName =
filename, lastBackupHash = resolvedBackupFileHash, lastBackupDate =
backupDate.toISOString(), set or delete lastMainHash based on
resolvedAnalysis.mainHash), then call await this.syncMeta(), and finally call
this.logger.info(`Backup saved to ${filename}`); ensure any error from
fs.writeFile is allowed to propagate (do not update meta or log success on
failure).
- Around line 340-372: The code currently leaves versionManifestPath null when
the BUILD-MANIFEST references a VERSION file that isn't present, allowing
corrupt backups to pass; after resolving candidates (the block that sets
versionManifestPath from resolvedCandidates using this.pathToPosix and
relativePaths), add a check: if buildManifest.compat?.versionFileRequired (or
the manifest flag used by version-plugin, e.g.,
buildManifest.compat.versionFileRequired === true) and versionManifestPath ===
null, throw a descriptive Error stating the required version file (include
buildManifest.versionFile) is missing so the process fails rather than skipping
fingerprint comparison.

In `@src/lib/helpers/server.ts`:
- Around line 36-38: The server.listen override (server.listen) currently
assigns the result of originalListen to the local listener variable but doesn't
return it; update the override so it returns that listener (the Server instance)
— i.e., after listener = originalListen.apply(this, args); add a return of
listener so server.listen conforms to express.Express['listen']'s return type
and preserves chaining for callers expecting the Server from originalListen.
- Around line 55-56: The URL is constructed with brackets around address.address
unconditionally, which breaks for IPv4/hostnames; update the URL creation so it
only brackets IPv6 literals (e.g., use net.isIP(address.address) === 6 or
address.family === 'IPv6' to detect IPv6) and otherwise build the origin as
`http://${address.address}:${address.port}`; adjust the const url assignment
that currently uses `new URL(base || '',
\`http://[${address.address}]:${address.port}\`)` to conditionally include
brackets only for IPv6 addresses.

In `@src/lib/pp.middleware.ts`:
- Around line 246-247: The code treats Page.template_id === null as present
(using typeof page.template_id !== 'undefined'), causing this.#isV710OrHigher
and subsequent non-null assertions to try to use a null template ID; change
those checks to explicitly exclude null (e.g., check page.template_id !==
undefined && page.template_id !== null or use page.template_id != null) wherever
you currently use typeof page.template_id !== 'undefined' (including the
occurrences setting this.#isV710OrHigher and the later blocks that access
template assets), so template_id === null is treated as absent and no null ID is
requested.
- Around line 476-478: Escape regex metacharacters in each page-variable name
before building the RegExp and avoid special `$` replacement interpretation by
using a replacer function: when iterating this.#pageVars (the loop variable v)
compute an escapedName (escape characters like . * + ? ^ $ { } ( ) | [ ] \ /)
then use new RegExp(escapedName, 'g') and call result.replace(theRegex, () =>
v.value) so replacement values are inserted verbatim; add a small helper like
escapeRegExp used by the loop to centralize the escaping.
- Around line 86-111: Remove the process-wide TLS and Axios default mutations:
delete the assignment to process.env.NODE_TLS_REJECT_UNAUTHORIZED and the
assignment to axios.defaults.httpsAgent so certificate validation is only
relaxed for this instance; keep the per-instance httpsAgent creation (the
https.Agent with rejectUnauthorized: false) and pass it into this.#axios =
axios.create(...) as before (retain httpAgent and keepAlive:false). Also verify
no other code paths rely on process-wide NODE_TLS_REJECT_UNAUTHORIZED or
axios.defaults.httpsAgent.

In `@src/lib/proxy-pass.middleware.ts`:
- Around line 127-129: The referer header replacement uses the client-controlled
host directly in a RegExp which can be abused; update the logic in the proxy
middleware where proxyReq.setHeader('referer', ...) is set (look for variables
host, referer, baseURL and the call to proxyReq.setHeader) to first escape regex
metacharacters in host (or, better, parse the referer to get its origin) and
then build a safe anchored pattern (e.g. ^https?://<escapedHost>) so only the
referer origin is replaced with baseURL; ensure you handle non-string referer
and preserve existing behavior when host or referer are absent.
- Around line 159-165: The current logic attaches a new per-request handler with
req.socket.once('close', ...) which accumulates on keep-alive sockets; replace
this by attaching cleanup to the request/response lifecycle (e.g., use
req.on('close', cleanup) or proxyReq.on('close', cleanup) instead of
req.socket.once) or deduplicate at the socket level (e.g., store a
WeakSet/WeakMap keyed by req.socket and only add one shared close handler).
Update the code around req.socket.setMaxListeners(0) and
req.socket.once('close', ...) to remove the per-request socket.once usage and
implement either req.on('close', ...) or proxyReq.on('close', ...) with proper
removal, or add deduplication using a WeakMap keyed by req.socket so duplicate
handlers are not added.

In `@src/plugins/version-plugin.ts`:
- Around line 89-119: normalizeRepositoryUrl currently returns HTTP(S) URLs
unchanged, which can leak userinfo (credentials/tokens) present in CI-provided
repository URLs; update normalizeRepositoryUrl to strip any userinfo from URLs
(e.g., remove "user" or "user:pass@" from "http(s)://...") before returning. In
practice, after computing normalizedUrl (and also for branches where you
construct https://host/path for scp/ssh/git protocols), parse or regex-strip an
optional userinfo segment (username[:password]@) so the final returned string
never contains credentials; update the function normalizeRepositoryUrl to
perform this userinfo removal for all code paths.

In `@src/shortcuts.ts`:
- Around line 98-115: The cleanupClosure must be made idempotent and must
unregister its own close listener from the specific httpServer it was attached
to: capture the http server reference when creating cleanupBinding (e.g. const
boundServer = server.httpServer), attach with boundServer.on('close',
cleanupBinding), and at the start of cleanupBinding return early if
cleanupActiveShortcutBinding !== cleanupBinding; then remove the listener with
boundServer.off('close', cleanupBinding) (or removeListener) and finally clear
cleanupActiveShortcutBinding and revert stdin state; this ensures the old
cleanup won't run later and that each binding unregisters itself from its
original server.

---

Outside diff comments:
In `@BUILD_IMPROVEMENTS.md`:
- Around line 91-104: Remove the duplicated "Rollup (Primary)" section by
keeping a single heading and its bullet list and deleting the second identical
block; locate the duplicate heading "Rollup (Primary)" and the repeated bullets
under it and remove the redundant copy so the document contains only one Rollup
(Primary) section.

In `@scripts/run-tests.js`:
- Around line 119-121: The error handler currently calls callback(error, null)
which violates the callback contract expecting a second arg shaped like {
exitCode: number }; update the handler on the child process (child.on('error',
...)) to pass an object with an exitCode (e.g., { exitCode: 1 }) as the second
argument so the signature matches the TypeScript test helper (see
e2e/config.spec.ts usage). Ensure only the second argument is changed and the
first remains the error object.

In `@src/lib/helpers/token.helper.ts`:
- Around line 26-42: Wrap the case 412 switch branch in its own block to provide
proper block-level scoping for the const lowerMessage: add braces surrounding
the branch that starts with case 412 and ends with the return object so that
lowerMessage (and any future temp vars) are scoped to that case; update the case
412 branch near token.helper.ts where lowerMessage is declared to use this new
block scope.

In `@src/lib/helpers/url.helper.ts`:
- Around line 18-32: The regexp constructors in urlReplacer and urlPathReplacer
use unescaped user inputs (originalHost and urlPath), which can cause
over-matching or runtime errors; fix by escaping those inputs before
interpolating into new RegExp (create/inline an escapeRegExp function that
replaces special characters like .+*?^${}()|[]\ with escaped versions) and use
the escapedOriginalHost when building urlReplaceRegExp in urlReplacer and
escapedUrlPath when building both urlReplaceRegExp and unescapedUrlReplaceRegExp
in urlPathReplacer so the patterns match the literal strings.

In `@src/lib/proxy-cache.middleware.ts`:
- Around line 221-240: The buffered override of (res as any).write incorrectly
treats the second argument as encoding only and drops any callback; update (res
as any).write to normalize parameters by checking if the second arg is a
function (treat as callback) or a string (encoding), default encoding to 'utf8'
when needed, capture/queue any provided callback into a write-callbacks array
(e.g., writeCallbacks) alongside pushing Buffer data into chunks, and ensure
those queued callbacks are invoked when the buffered body is flushed by
originalEnd (or originalWrite) so callers receive their completion callbacks;
reference the overridden (res as any).write, originalWrite, originalEnd, chunks,
and the new write-callbacks queue when making changes.

In `@src/lib/proxy-pass.middleware.ts`:
- Around line 32-39: streamResponseInterceptor currently ignores its interceptor
parameter so proxied streams are unmodified; implement a streaming Transform
that, when interceptor is provided, reads chunks from proxyRes, converts to
Buffer (respecting encoding), calls interceptor(chunk, encoding) and pushes the
interceptor result, then pipe proxyRes -> transform -> res; if interceptor is
undefined, keep the direct proxyRes.pipe(res) behavior. Update the function
(streamResponseInterceptor) to construct and use a stream.Transform (from
'stream') named e.g. responseTransform, ensure you handle Buffer/Uint8Array and
encoding types correctly and forward errors from proxyRes/transform to res, and
keep the existing res.setHeader/ res.setHeaders logic unchanged.

In `@tests/test-nextjs/src/pages/_document.tsx`:
- Around line 3-8: Remove the unsupported Pages Router export: delete the
exported metadata constant (export const metadata: Metadata = {...}) and its
import of Metadata from 'next' in _document.tsx; instead, move any page-specific
title/description into individual pages using next/head or place site-wide head
elements inside the existing <Head> element in the Document component so
metadata is handled via the Pages Router mechanism.

In `@tests/unit/plugin/version-plugin.spec.ts`:
- Around line 222-239: The negative test uses a case-sensitive substring check
that can miss BUILD-MANIFEST.json; update the assertion in the test that
inspects files (the files.find(...) call) to use a case-insensitive match (e.g.,
a case-insensitive RegExp) or explicitly check for 'MANIFEST' in addition to
'manifest' so that any manifest filename like BUILD-MANIFEST.json or VERSION is
detected when versionPlugin(...) is called with enabled: false and
invokeCloseBundle(plugin) is run.

---

Minor comments:
In `@CHANGELOG.md`:
- Around line 8-10: Replace top-level single-hash version headers that start
with "# [" (e.g., "# [0.18.0-beta.1]") with second-level "## [" headings so all
release/version entries use a consistent h2 level; update every occurrence
matching that pattern (the comment lists examples such as the header on the
0.18.0-beta.1 entry and the other entries that currently use "#" instead of
"##") and leave subheadings like "### Features" unchanged.

In `@rollup.config.ts`:
- Around line 67-106: Validate and sanitize timeoutMs and heartbeatMs in
typeDefsMonitorPlugin before scheduling timers: ensure
Number(process.env.PP_DEV_DTS_TIMEOUT_MS) and
Number(process.env.PP_DEV_DTS_HEARTBEAT_MS) are finite positive integers
(fallback to 120_000 and 10_000 respectively if they are NaN, <=0, or Infinity),
then only call setInterval and setTimeout in buildStart when the sanitized
heartbeatMs/timeoutMs are valid; also guard heartbeat.unref and timeout.unref
behind existence checks and clearTimers should still work with potentially unset
timers (heartbeat/timeout possibly null).

In `@src/cli.ts`:
- Around line 1300-1302: The finalizer calls stopProfiler(...) but doesn't await
its possible Promise, so ensure you await stopProfiler in the finally block
(i.e., change the call to await stopProfiler((message) =>
createLogger(options.logLevel).info(message))) and make the enclosing
function/handler async or return the resulting promise so the build command
waits for the CPU profile write to complete; update any callers/returns as
needed to propagate the async change.

In `@src/client/index.html`:
- Around line 14-18: The anchor elements using target="_blank" (the <a> elements
with class "pp-dev-info__link" and href like "https://www.npmjs.com/package/{%=
PACKAGE_NAME %}/v/{%= VERSION %}") must include rel="noopener noreferrer";
update those anchors (both the occurrence around lines with the npm badge and
the other similar anchor at lines ~50-55) to add rel="noopener noreferrer" to
prevent the opened page from accessing window.opener.

In `@src/client/index.ts`:
- Around line 232-287: In confirmModal, add ARIA and focus management: set
role="dialog", aria-modal="true" on $confirm and add aria-labelledby pointing to
an id you assign to $title; before appending, save document.activeElement to a
variable (previousActiveElement) and after appending call
$confirmButton.focus(); implement a simple tab-trap on the modal (keydown
handler that intercepts Tab and cycles between $cancelButton and
$confirmButton); ensure all event listeners are removed and
previousActiveElement.focus() is restored when closing by updating
closeConfirmModalByResult to accept/restore the saved previousActiveElement and
clean up the tab-trap listener.

---

Nitpick comments:
In `@CONTRIBUTING.md`:
- Line 78: Tighten the CI-skip instruction sentence by replacing the current
wording with a shorter, direct phrase; update the line that currently reads "If
you need to make changes to the release commit (like updating the changelog),
you can skip the CI by including `[skip ci]` in your commit message." to a
concise variant such as "To amend a release commit, include `[skip ci]` in the
commit message." so the guidance is clearer and less ambiguous.

In `@src/index.ts`:
- Around line 89-100: The execSync call that reads git origin URL can hang;
modify the call (the execSync in getViteConfig / the snippet that calls
execSync('git config --get remote.origin.url', ...)) to include a timeout option
(e.g., timeout: 5000) so the child will be killed after a short bound; preserve
existing options (encoding, stdio) and keep the try/catch behavior so
failures/timeouts are ignored as before.

In `@src/lib/helpers/login.helper.ts`:
- Around line 377-384: The DOM lookups use global document.getElementById and
can pick up unrelated elements; change all lookups to query within the injected
form by replacing document.getElementById(...) with scoped queries off
formContent (e.g., formContent.querySelector(...) or
formContent.getElementById-equivalent) for the symbols tokenTypeSwitcher,
tokenInput, tokenCaption, submitButton, errorElement and keep formWrapper found
via formContent.querySelector('.helper-login-wrapper'); ensure the typed casts
(HTMLInputElement/HTMLSpanElement/HTMLButtonElement/HTMLDivElement/HTMLElement)
remain correct and update the null-check that verifies those scoped variables.
- Around line 398-423: The current selector uses :has() which can throw a
SyntaxError in some browsers; update by replacing formSelector with a broader
selector like '#mi-react-root form:not([id])' and then, wherever you call
document.querySelector<HTMLFormElement>(formSelector) (both inside the
MutationObserver callback and for existingForm), instead query for the broad
selector and filter the resulting element(s) in JS to ensure they contain
input[name="login"] and input[name="password"] before calling
observer.disconnect() and initializeLoginForm(loginForm); keep the
observer.observe logic unchanged but use the filtered loginForm variable so the
fallback check for existingForm likewise verifies inputs via
form.querySelector('input[name="login"]') and
form.querySelector('input[name="password"]').
- Around line 338-346: The function initializeLoginForm uses node-only accessors
(loginForm.firstChild and contentWrapper.lastChild) which can return non-Element
nodes; change those to element-only accessors (use loginForm.firstElementChild
and (contentWrapper as Element).lastElementChild) and update the casts to
HTMLDivElement to ensure the variables contentWrapper and rowTemplate are
Elements, add null checks after using firstElementChild/lastElementChild before
using element APIs (innerHTML, setAttribute, appendChild), and adjust any
subsequent references that assume Node to use Element/HTMLDivElement types
(e.g., contentWrapper, rowTemplate) so DOM methods are safe at runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 805fed51-0075-41e2-9a6f-a0161fb3418c

📥 Commits

Reviewing files that changed from the base of the PR and between 003238b and 01e73a7.

⛔ Files ignored due to path filters (4)
  • package-lock.json is excluded by !**/package-lock.json
  • tests/test-commonjs/package-lock.json is excluded by !**/package-lock.json
  • tests/test-nextjs-cjs/package-lock.json is excluded by !**/package-lock.json
  • tests/test-nextjs/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (101)
  • .github/dependabot.yml
  • .prettierignore
  • .prettierrc.cjs
  • BUILD_IMPROVEMENTS.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • e2e/config.spec.ts
  • e2e/server.spec.ts
  • e2e/tsconfig.json
  • esbuild.config.ts
  • package.json
  • playwright.config.ts
  • pp-dev.d.ts
  • rollup.config.ts
  • scripts/build-optimizer.js
  • scripts/build-parallel.js
  • scripts/patch-npm-bundled-vulnerabilities.mjs
  • scripts/postbuild.js
  • scripts/run-tests.js
  • scripts/startup-optimizer.js
  • src/api/assets.ts
  • src/api/index.ts
  • src/api/page-template.ts
  • src/api/page.ts
  • src/api/unavailable-json-api.ts
  • src/cli.ts
  • src/client/assets/css/client.scss
  • src/client/index.html
  • src/client/index.ts
  • src/client/rollup.config.ts
  • src/client/tsconfig.json
  • src/client/types/assets.d.ts
  • src/config.ts
  • src/constants.ts
  • src/index.ts
  • src/lib/auth.provider.ts
  • src/lib/changelog-generator.ts
  • src/lib/client.service.ts
  • src/lib/dist.service.ts
  • src/lib/helpers/content-encoding.helper.ts
  • src/lib/helpers/login.helper.ts
  • src/lib/helpers/server.ts
  • src/lib/helpers/token.helper.ts
  • src/lib/helpers/url.helper.ts
  • src/lib/internal.middleware.ts
  • src/lib/load-pp-data.middleware.ts
  • src/lib/logger.ts
  • src/lib/next-import.ts
  • src/lib/pp.middleware.ts
  • src/lib/proxy-cache.middleware.ts
  • src/lib/proxy-pass.middleware.ts
  • src/lib/rewrite-response.middleware.ts
  • src/plugin.ts
  • src/plugins/client-injection-plugin.ts
  • src/plugins/mi-topbar-plugin.ts
  • src/plugins/version-plugin.ts
  • src/shortcuts.ts
  • tests/README.md
  • tests/TESTING.md
  • tests/integration/middleware/pp-redirect.spec.ts
  • tests/integration/middleware/proxy-cache.spec.ts
  • tests/integration/middleware/rewrite-response.spec.ts
  • tests/setup.ts
  • tests/test-commonjs/index.html
  • tests/test-commonjs/pp-dev.config.ts
  • tests/test-commonjs/public/__template_variables.json
  • tests/test-commonjs/src/api/dataset-data.ts
  • tests/test-commonjs/src/api/entity.ts
  • tests/test-commonjs/src/api/user.ts
  • tests/test-commonjs/src/constants.ts
  • tests/test-commonjs/src/main.ts
  • tests/test-nextjs-cjs/src/api/entity.ts
  • tests/test-nextjs-cjs/src/api/user.ts
  • tests/test-nextjs-cjs/src/constants.ts
  • tests/test-nextjs-cjs/src/pages/index.tsx
  • tests/test-nextjs-cjs/src/styles/global.scss
  • tests/test-nextjs-cjs/src/styles/tailwind.css
  • tests/test-nextjs-cjs/tsconfig.json
  • tests/test-nextjs/custom-server.ts
  • tests/test-nextjs/src/api/entity.ts
  • tests/test-nextjs/src/api/user.ts
  • tests/test-nextjs/src/constants.ts
  • tests/test-nextjs/src/pages/_document.tsx
  • tests/test-nextjs/src/pages/index.tsx
  • tests/test-nextjs/src/styles/global.scss
  • tests/test-nextjs/src/styles/tailwind.css
  • tests/test-nextjs/tsconfig.json
  • tests/tsconfig.json
  • tests/unit/config/config.loader.spec.ts
  • tests/unit/helpers/formatting.spec.ts
  • tests/unit/lib/content-encoding.helper.spec.ts
  • tests/unit/lib/token.helper.spec.ts
  • tests/unit/lib/url.helper.spec.ts
  • tests/unit/plugin/plugin.normalize.spec.ts
  • tests/unit/plugin/version-plugin.spec.ts
  • tsconfig.cjs.json
  • tsconfig.esm.json
  • tsconfig.json
  • vitest.config.ts
  • vitest.integration.config.ts
💤 Files with no reviewable changes (1)
  • src/api/assets.ts

Comment thread src/api/page.ts
Comment thread src/cli.ts Outdated
Comment thread src/client/index.ts
Comment thread src/lib/client.service.ts
Comment thread src/lib/dist.service.ts
Comment thread src/lib/pp.middleware.ts
Comment thread src/lib/proxy-pass.middleware.ts
Comment thread src/lib/proxy-pass.middleware.ts
Comment thread src/plugins/version-plugin.ts
Comment thread src/shortcuts.ts Outdated
Serhii Shpak added 2 commits April 22, 2026 10:36
Prevent unresolved sync actions and stale listeners while enforcing required VERSION manifest presence and stripping sensitive URL/replacement edge cases.
PP-3311 Fix modal/sync lifecycle and safety checks
## [0.18.1-beta.2](v0.18.1-beta.1...v0.18.1-beta.2) (2026-04-22)

### Bug Fixes

* harden sync prompt lifecycle and metadata safety ([3aaaeb6](3aaaeb6))

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
src/lib/pp.middleware.ts (2)

250-252: ⚠️ Potential issue | 🟠 Major

null template IDs still treated as present.

typeof page.template_id !== 'undefined' returns true for null, which flips #isV710OrHigher and then drives pageTemplateApi.get(pageInfo.template_id!, ...) calls (lines 539, 559) with a null ID. Same pattern at line 426. Use page.template_id != null to treat null as absent.

🐛 Proposed fix
-      if (typeof page.template_id !== 'undefined') {
+      if (page.template_id != null) {
         this.#isV710OrHigher = true;
       }

Apply the same change at line 426, and optionally add && pageInfo.template_id != null to the #isV710OrHigher branches at 538 and 558.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/pp.middleware.ts` around lines 250 - 252, The check treating template
IDs as present uses typeof page.template_id !== 'undefined', which is true for
null; update the presence checks to use page.template_id != null (and the
equivalent pageInfo.template_id != null) so null is considered absent, update
the assignment to this.#isV710OrHigher to use that null-safe check (occurences
that set `#isV710OrHigher` and the earlier check around the same logic), and also
add explicit pageInfo.template_id != null guards before calling
pageTemplateApi.get(...) to avoid passing null into those calls.

86-111: ⚠️ Potential issue | 🔴 Critical

Process-wide TLS disable + axios.defaults mutation still present.

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' (line 87) and axios.defaults.httpsAgent = httpsAgent (line 110) were flagged in the prior review and remain in the code. These mutate global state: once MiAPI is constructed with disableSSLValidation, every HTTPS request in the process — including unrelated libraries that use bare axios or plain https — bypasses certificate validation for the lifetime of the process. The per-instance httpsAgent with rejectUnauthorized: false already handles TLS relaxation for this axios client; the globals are unnecessary.

🛡️ Proposed fix
-      if (disableSSLValidation) {
-        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
-      }
-
       const httpsAgent = new https.Agent({
         keepAlive: false,
         ...(disableSSLValidation ? { rejectUnauthorized: false } : {}),
       });

       this.#axios = axios.create({
         baseURL,
         headers,
         timeout: 30000,
         maxRedirects: 5,
         httpAgent: new http.Agent({ keepAlive: false }),
         httpsAgent,
       });
-
-      if (disableSSLValidation) {
-        axios.defaults.httpsAgent = httpsAgent;
-      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/pp.middleware.ts` around lines 86 - 111, Remove the process-wide TLS
and axios-default mutations: delete the assignment to
process.env.NODE_TLS_REJECT_UNAUTHORIZED and the assignment to
axios.defaults.httpsAgent so TLS relaxation is not applied globally; keep the
per-instance httpsAgent creation (with rejectUnauthorized:false when
disableSSLValidation is true) and pass that agent into this.#axios so only the
MiAPI instance uses the relaxed TLS behavior (refer to disableSSLValidation,
httpsAgent, and this.#axios in the constructor).
src/lib/dist.service.ts (2)

581-610: ⚠️ Potential issue | 🟠 Major

Save-skip and metadata persistence ordering: two unresolved concerns from the previous review.

  1. Skip condition (lines 581-589): still triggers when mainHash matches, regardless of versionManifestHashMismatches.length or buildManifestMismatch. If on-disk files diverge from VERSION (the exact case the new prompt flow exists to handle), the new server backup that produced the override prompt is discarded and never written to backups/, so users can't inspect what the server sent. Gate the skip on a clean analysis.

  2. Metadata order (lines 596-610): this.currentMeta is updated and syncMeta() is awaited before fs.writeFile, and the .finally() logs "Backup saved" even on write failure. A disk/permission error leaves sync-service.meta.json pointing at a backup file that doesn't exist. Persist metadata only after the write resolves.

🛡️ Proposed combined fix
-      if (resolvedAnalysis.allFilesInMeta && resolvedAnalysis.mainHash && lastMainHash === resolvedAnalysis.mainHash) {
+      if (
+        resolvedAnalysis.allFilesInMeta &&
+        resolvedAnalysis.mainHash &&
+        lastMainHash === resolvedAnalysis.mainHash &&
+        resolvedAnalysis.versionManifestHashMismatches.length === 0 &&
+        !resolvedAnalysis.buildManifestMismatch
+      ) {
@@
-    this.currentMeta!.lastBackupName = filename;
-    this.currentMeta!.lastBackupHash = resolvedBackupFileHash;
-    this.currentMeta!.lastBackupDate = backupDate.toISOString();
-
-    if (resolvedAnalysis.mainHash) {
-      this.currentMeta!.lastMainHash = resolvedAnalysis.mainHash;
-    } else {
-      delete this.currentMeta!.lastMainHash;
-    }
-
-    await this.syncMeta();
-
-    return await fs.writeFile(path.resolve(this.backupFolder, filename), backupFile).finally(() => {
-      this.logger.info(`Backup saved to ${filename}`);
-    });
+    await fs.writeFile(path.resolve(this.backupFolder, filename), backupFile);
+
+    this.currentMeta!.lastBackupName = filename;
+    this.currentMeta!.lastBackupHash = resolvedBackupFileHash;
+    this.currentMeta!.lastBackupDate = backupDate.toISOString();
+
+    if (resolvedAnalysis.mainHash) {
+      this.currentMeta!.lastMainHash = resolvedAnalysis.mainHash;
+    } else {
+      delete this.currentMeta!.lastMainHash;
+    }
+
+    await this.syncMeta();
+    this.logger.info(`Backup saved to ${filename}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/dist.service.ts` around lines 581 - 610, The skip logic in the
resolvedAnalysis check currently only compares mainHash and can discard
server-produced backups even when versionManifestHashMismatches or
buildManifestMismatch indicate on-disk divergence; update the early-return
condition in the block that references resolvedAnalysis (e.g., check
resolvedAnalysis.versionManifestHashMismatches.length === 0 &&
!resolvedAnalysis.buildManifestMismatch in addition to mainHash equality) so the
skip only occurs for a clean analysis. Also change the save flow so
fs.writeFile(path.resolve(this.backupFolder, filename), backupFile) is awaited
first and only on successful resolution update this.currentMeta
(lastBackupName/lastBackupHash/lastBackupDate/lastMainHash), call await
this.syncMeta(), and only then call logger.info(`Backup saved to ${filename}`);
ensure the .finally() no longer unconditionally logs success and that errors
from fs.writeFile propagate or are handled before metadata is persisted.

416-454: ⚠️ Potential issue | 🟠 Major

Path traversal via VERSION manifest entries still unguarded.

relativePath keys here come from the untrusted backup's VERSION.files map, not from the extracted file list. A crafted manifest with an entry like ../../etc/passwd will cause path.join(contentRootDir, relativePath) at line 431 to read outside the extracted directory. Validate that each normalized path is relative and stays within contentRootDir before recording it or reading the file.

🛡️ Proposed fix
         for (const [relativePath, expectedHash] of Object.entries(manifest.files)) {
           if (typeof expectedHash !== 'string' || !expectedHash.trim()) {
             throw new Error(`VERSION manifest contains invalid hash for file: ${relativePath}`);
           }

-          normalizedManifestFiles[this.pathToPosix(relativePath)] = expectedHash;
+          const normalizedRelativePath = this.pathToPosix(relativePath);
+
+          if (
+            !normalizedRelativePath ||
+            path.isAbsolute(relativePath) ||
+            path.win32.isAbsolute(relativePath) ||
+            normalizedRelativePath.split('/').includes('..')
+          ) {
+            throw new Error(`VERSION manifest contains unsafe file path: ${relativePath}`);
+          }
+
+          normalizedManifestFiles[normalizedRelativePath] = expectedHash;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/dist.service.ts` around lines 416 - 454, The manifest-relativePath
entries are untrusted and can escape contentRootDir; update the loop that
processes manifest.files/normalizedManifestFiles (symbols: manifest.files,
normalizedManifestFiles, pathToPosix, buildManifestChecksum,
isTemplateVariablesBasename) to validate each normalized path before saving or
reading: compute resolved = path.resolve(contentRootDir, normalizedPath) and
compare against contentRootRootResolved = path.resolve(contentRootDir) (use a
trailing path.sep when comparing) to ensure resolved
startsWith(contentRootRootResolved + path.sep) (and reject absolute paths or any
path that would navigate above the root); if validation fails, throw or skip the
entry so you never call fs.readFile on an escaped path. Also ensure pathToPosix
is applied consistently when checking/recording entries.
🧹 Nitpick comments (3)
src/lib/pp.middleware.ts (1)

155-157: Minor: escaping / is unnecessary.

/ has no special meaning in a JS regex pattern (only as the literal delimiter in regex literals, not inside new RegExp(...)); including it in the character class is harmless but slightly misleading. This matches the helper in cli.ts (line 115) and dist.service.ts (line 250) which correctly omit /.

♻️ Proposed change
   `#escapeRegExp`(value: string): string {
-    return value.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&');
+    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/pp.middleware.ts` around lines 155 - 157, The character class in the
private method `#escapeRegExp` currently includes an unnecessary '/' character;
update the regex in `#escapeRegExp`(value: string): string to remove the '/' from
the character class (so it matches other special regex chars but not '/'),
keeping the same replace logic and return value to maintain behavior consistent
with cli.ts and dist.service.ts helpers.
src/lib/client.service.ts (1)

268-395: Mismatch prompting ordering is intricate — worth a brief test.

The interplay between onlyTemplateVariablesMismatch, mixedTemplateAndOthers, pathsForVersionModal, and the BUILD-MANIFEST-mismatch-only prompt (gated on versionManifestHashMismatches.length === 0) is correct but non-obvious. Because user-facing behavior differs across four cases (only-template-vars, mixed, other-only, BUILD-MANIFEST-only), this is a good candidate for a unit test on onTemplateSync that exercises each BackupAnalysis shape and asserts the expected requestSyncAction/template:sync:response sequence.

Want me to draft a test matrix for these four mismatch scenarios?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/client.service.ts` around lines 268 - 395, Add unit tests for
onTemplateSync that cover the four user-facing scenarios:
onlyTemplateVariablesMismatch, mixedTemplateAndOthers, other-only
(pathsForVersionModal non-empty), and BUILD-MANIFEST-only
(backupAnalysis.buildManifestMismatch true with
versionManifestHashMismatches.length === 0). For each test, construct a
BackupAnalysis shape exercising the specific booleans/arrays (use
backupAnalysis.versionManifestHashMismatches, backupAnalysis.unknownFiles,
backupAnalysis.templateVariables, and backupAnalysis.buildManifestMismatch),
stub requestSyncAction to capture prompts and server.ws.send to capture
responses, then assert the exact sequence of calls: whether
promptReplaceTemplateVariables (indirectly via requestSyncAction) runs
before/after version prompts, whether requestSyncAction was invoked with the
VERSION list (pathsForVersionModal), and whether BUILD-MANIFEST prompt occurs
only when expected; reuse promptReplaceTemplateVariables,
onlyTemplateVariablesMismatch, mixedTemplateAndOthers, pathsForVersionModal, and
buildManifestMismatch as identifiers to target behavior.
src/cli.ts (1)

842-848: Use per-emitter setMaxListeners(20) on the httpsAgent instead of bumping the process-wide default.

The httpsAgent that triggers the listener warnings is created inside MiAPI's constructor (src/lib/pp.middleware.ts). Rather than bumping EventEmitter.defaultMaxListeners globally in cli.ts, apply the limit directly to the agent instance:

const httpsAgent = new https.Agent({
  keepAlive: false,
  ...(disableSSLValidation ? { rejectUnauthorized: false } : {}),
});
httpsAgent.setMaxListeners(20);

This approach is more targeted, clearer in intent, and avoids masking potential listener leaks elsewhere in the application. Remove the process-wide bump from cli.ts:846 once the per-emitter approach is in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.ts` around lines 842 - 848, The code currently raises the
process-wide EventEmitter.defaultMaxListeners in cli.ts
(EventEmitter.defaultMaxListeners) which is too broad; instead update the https
agent created in MiAPI's constructor (the httpsAgent inside the MiAPI
constructor in src/lib/pp.middleware.ts) to call httpsAgent.setMaxListeners(20)
after instantiation (and include the disableSSLValidation option as needed),
then remove the global EventEmitter.defaultMaxListeners bump in cli.ts so only
the specific httpsAgent emitter has its listener limit increased.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/dist.service.ts`:
- Around line 581-610: The skip logic in the resolvedAnalysis check currently
only compares mainHash and can discard server-produced backups even when
versionManifestHashMismatches or buildManifestMismatch indicate on-disk
divergence; update the early-return condition in the block that references
resolvedAnalysis (e.g., check
resolvedAnalysis.versionManifestHashMismatches.length === 0 &&
!resolvedAnalysis.buildManifestMismatch in addition to mainHash equality) so the
skip only occurs for a clean analysis. Also change the save flow so
fs.writeFile(path.resolve(this.backupFolder, filename), backupFile) is awaited
first and only on successful resolution update this.currentMeta
(lastBackupName/lastBackupHash/lastBackupDate/lastMainHash), call await
this.syncMeta(), and only then call logger.info(`Backup saved to ${filename}`);
ensure the .finally() no longer unconditionally logs success and that errors
from fs.writeFile propagate or are handled before metadata is persisted.
- Around line 416-454: The manifest-relativePath entries are untrusted and can
escape contentRootDir; update the loop that processes
manifest.files/normalizedManifestFiles (symbols: manifest.files,
normalizedManifestFiles, pathToPosix, buildManifestChecksum,
isTemplateVariablesBasename) to validate each normalized path before saving or
reading: compute resolved = path.resolve(contentRootDir, normalizedPath) and
compare against contentRootRootResolved = path.resolve(contentRootDir) (use a
trailing path.sep when comparing) to ensure resolved
startsWith(contentRootRootResolved + path.sep) (and reject absolute paths or any
path that would navigate above the root); if validation fails, throw or skip the
entry so you never call fs.readFile on an escaped path. Also ensure pathToPosix
is applied consistently when checking/recording entries.

In `@src/lib/pp.middleware.ts`:
- Around line 250-252: The check treating template IDs as present uses typeof
page.template_id !== 'undefined', which is true for null; update the presence
checks to use page.template_id != null (and the equivalent pageInfo.template_id
!= null) so null is considered absent, update the assignment to
this.#isV710OrHigher to use that null-safe check (occurences that set
`#isV710OrHigher` and the earlier check around the same logic), and also add
explicit pageInfo.template_id != null guards before calling
pageTemplateApi.get(...) to avoid passing null into those calls.
- Around line 86-111: Remove the process-wide TLS and axios-default mutations:
delete the assignment to process.env.NODE_TLS_REJECT_UNAUTHORIZED and the
assignment to axios.defaults.httpsAgent so TLS relaxation is not applied
globally; keep the per-instance httpsAgent creation (with
rejectUnauthorized:false when disableSSLValidation is true) and pass that agent
into this.#axios so only the MiAPI instance uses the relaxed TLS behavior (refer
to disableSSLValidation, httpsAgent, and this.#axios in the constructor).

---

Nitpick comments:
In `@src/cli.ts`:
- Around line 842-848: The code currently raises the process-wide
EventEmitter.defaultMaxListeners in cli.ts (EventEmitter.defaultMaxListeners)
which is too broad; instead update the https agent created in MiAPI's
constructor (the httpsAgent inside the MiAPI constructor in
src/lib/pp.middleware.ts) to call httpsAgent.setMaxListeners(20) after
instantiation (and include the disableSSLValidation option as needed), then
remove the global EventEmitter.defaultMaxListeners bump in cli.ts so only the
specific httpsAgent emitter has its listener limit increased.

In `@src/lib/client.service.ts`:
- Around line 268-395: Add unit tests for onTemplateSync that cover the four
user-facing scenarios: onlyTemplateVariablesMismatch, mixedTemplateAndOthers,
other-only (pathsForVersionModal non-empty), and BUILD-MANIFEST-only
(backupAnalysis.buildManifestMismatch true with
versionManifestHashMismatches.length === 0). For each test, construct a
BackupAnalysis shape exercising the specific booleans/arrays (use
backupAnalysis.versionManifestHashMismatches, backupAnalysis.unknownFiles,
backupAnalysis.templateVariables, and backupAnalysis.buildManifestMismatch),
stub requestSyncAction to capture prompts and server.ws.send to capture
responses, then assert the exact sequence of calls: whether
promptReplaceTemplateVariables (indirectly via requestSyncAction) runs
before/after version prompts, whether requestSyncAction was invoked with the
VERSION list (pathsForVersionModal), and whether BUILD-MANIFEST prompt occurs
only when expected; reuse promptReplaceTemplateVariables,
onlyTemplateVariablesMismatch, mixedTemplateAndOthers, pathsForVersionModal, and
buildManifestMismatch as identifiers to target behavior.

In `@src/lib/pp.middleware.ts`:
- Around line 155-157: The character class in the private method `#escapeRegExp`
currently includes an unnecessary '/' character; update the regex in
`#escapeRegExp`(value: string): string to remove the '/' from the character class
(so it matches other special regex chars but not '/'), keeping the same replace
logic and return value to maintain behavior consistent with cli.ts and
dist.service.ts helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af960c61-d286-4f53-a4e7-6ffb4559a62a

📥 Commits

Reviewing files that changed from the base of the PR and between 01e73a7 and 50c3359.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • tests/test-nextjs/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • CHANGELOG.md
  • package.json
  • src/cli.ts
  • src/client/index.ts
  • src/lib/client.service.ts
  • src/lib/dist.service.ts
  • src/lib/pp.middleware.ts
  • src/plugins/version-plugin.ts
  • src/shortcuts.ts
✅ Files skipped from review due to trivial changes (1)
  • package.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants