Conversation
…ve type definitions
…Compressor and FFI integration.
…Compressor and FFI integration.
…ort, fixing deadlock issues and improving compression handling
…i-platform compressor libraries.
…nifier, add methods for custom compressor path and force native compression
…ion in script files
…on options in JS compressor
…pdate test method names for clarity and consistency
…cking - Removed the executedScripts tracking from RuntimeManager to simplify script execution logic. - Updated script execution methods to directly append and remove script elements without checking for duplicates. - Cleaned up related comments and adjusted method calls in AppManager to reflect the removal of executed scripts cache. - Enhanced logging for script execution to aid in debugging and monitoring.
- Eliminated console.log statements related to script execution, caching, and loading in the RuntimeManager class across multiple script files. - This change aims to clean up the code and reduce unnecessary logging during script operations.
- Eliminated console logging related to script execution and DOM updates in the RuntimeManager and navigateHistory files. - This change aims to clean up the code and reduce unnecessary logging during script operations and history navigation.
- Removed inline script execution from `runScriptsForElement`. - Updated `runPhpSpaScripts` to handle script attributes more efficiently. - Improved cache handling for script execution, ensuring attributes are preserved. - Simplified the logic for creating and executing new script elements.
…style-preload behavior - add RuntimeConfig with preserveUpdatedHtmlState and waitForStyles - persist current target HTML into history before navigation when enabled - expose phpspa.config(...) for runtime client configuration - document both options in CHANGELOG v2.0.9
- Added support for 'popstate' event in RuntimeManager with associated payload structure. - Implemented `off` and `resetEvents` methods in RuntimeManager to manage event listeners effectively. - Updated AppManager to include `off` and `resetEvents` methods for better event handling. - Modified navigateHistory function to utilize the new popstate event structure and prevent default behavior. - Refactored EventObject and EventPayload types to accommodate new event payloads and improve type safety. - Updated TypeScript definitions to reflect changes in event handling and payload structures.
…der method in HttpRequest
Updated image source URL in README for PhpSPA.
There was a problem hiding this comment.
Pull request overview
This PR updates PhpSPA to better support PHP 8.4 and expands both server-side and client-side runtime capabilities (native compression options, asset URL routing changes, and richer client runtime configuration/events).
Changes:
- Adds native compressor configuration options (custom library path, force native mode, optional esbuild JS minification) and introduces binary response compression negotiation (zstd/br/gzip).
- Updates client runtime to add configurable navigation/style-loading behavior and new
popstatelifecycle with listener management. - Revises asset URL structure (nested folders) and updates related tests/types/docs/build pipeline outputs accordingly.
Reviewed changes
Copilot reviewed 34 out of 46 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/Core/Utils/HtmlCompressorTest.php | Updates unit test to use new binary compression API. |
| tests/Unit/Core/Helper/AssetLinkManagerTest.php | Adjusts regex expectations for new nested asset URL structure. |
| tests/AssetLinkTest.php | Updates integration-style regex checks for nested asset URLs. |
| src/script/types/utils/preloadStylesFromContent.d.ts.map | Regenerated TS declaration map after runtime/style loader changes. |
| src/script/types/types/RuntimeInterfaces.d.ts.map | Regenerated TS declaration map after runtime event/config typing changes. |
| src/script/types/types/RuntimeInterfaces.d.ts | Adds typed event payload map, runtime config, and popstate payload typing. |
| src/script/types/helpers/navigateHistrory.d.ts.map | Regenerated declaration map after popstate handling changes. |
| src/script/types/core/RuntimeManager.d.ts.map | Regenerated declaration map after runtime API typing changes. |
| src/script/types/core/RuntimeManager.d.ts | Updates runtime API typings (config, typed emit/off/reset, etc.). |
| src/script/types/core/AppManager.d.ts.map | Regenerated declaration map after AppManager API changes. |
| src/script/types/core/AppManager.d.ts | Updates AppManager typings for config + typed event on/off/reset. |
| src/script/phpspa.mjs | Updates distributed client runtime (config, popstate event, style handling, script execution changes). |
| src/script/phpspa.min.js | Updates minified distributed runtime bundle to match new runtime behavior. |
| src/script/phpspa.js | Updates UMD distributed runtime bundle to match new runtime behavior. |
| src/script/phpspa.cjs | Updates CJS distributed runtime bundle to match new runtime behavior. |
| src/script/FFIBridge.cpp | Adds new FFI export to compress with scope/debug support (esbuild-enabled path). |
| src/compression/functions/minifyJS.cpp | Introduces esbuild-based JS minification path (temp files + command execution) with fallback. |
| src/compression/HtmlCompressor.h | Extends JS minifier API to accept scope/debug output. |
| package.json | Bumps npm package version. |
| lib/utils/preloadStylesFromContent.ts | Changes stylesheet preloading to regex-based extraction + stabilized URL resolution. |
| lib/types/RuntimeInterfaces.ts | Adds typed event payload map + runtime config types. |
| lib/helpers/navigateHistrory.ts | Adds cancelable popstate event and adjusts DOM restore path. |
| lib/core/RuntimeManager.ts | Adds runtime config/events API updates and changes script execution strategy. |
| lib/core/AppManager.ts | Adds runtime config forwarding, DOM-state snapshot option, and event off/reset methods. |
| examples/index.php | Updates example to use new native compression configuration API and script naming. |
| examples/components/Counter.php | Updates component script registration to include a name/id. |
| docs/performance/html-compression.md | Documents new native compressor/esbuild configuration features. |
| composer.json | Changes local dev server port for composer start. |
| badge/loc.svg | Updates LOC badge value. |
| app/interfaces/ApplicationContract.php | Adds new public API methods for compressor configuration. |
| app/core/Utils/Timer.php | Adds timing utility used for compression timing/logging. |
| app/core/Utils/HtmlCompressor.php | Adds scope/esbuild options and replaces gzip-only with negotiated binary compression. |
| app/core/Router/PrefixRouter.php | Formatting/tidy updates and minor return expression simplification. |
| app/core/Impl/RealImpl/AppImpl.php | Wires new compressor configuration + scope-aware asset compression; adds compression timing logs. |
| app/core/Http/HttpRequest.php | Refactors header retrieval + signature; formatting updates. |
| app/core/Helper/AssetLinkManager.php | Changes asset URLs to use nested folders and updates request parsing accordingly. |
| app/core/Compression/NativeCompressor.php | Extends native compressor invocation for scope/esbuild + debug output handling. |
| app/client/Http/Request.php | Updates request interface header() signature + docs; formatting changes. |
| app/client/Compression/Compressor.php | Adds typed class constants for PHP 8.4+. |
| app/client/App.php | Updates phpdoc @method surface to include new ApplicationContract APIs. |
| README.md | Updates hero image URL. |
| CHANGELOG.md | Adds development changelog section describing new compression/runtime features. |
| .vscode/settings.json | Updates editor defaults (font size, formatOnSave). |
| .github/workflows/build-compressor.yml | Adds WSL build artifact and adjusts packaging/release attachment steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static runScriptsForElement(element: HTMLElement): void { | ||
| this.runInlineScripts(element) | ||
| this.runPhpSpaScripts(element) | ||
| } |
There was a problem hiding this comment.
runScriptsForElement() no longer calls runInlineScripts(), but runInlineScripts() remains in the class. This makes inline-script handling behavior harder to reason about and leaves dead code behind. Either remove runInlineScripts() or call it from runScriptsForElement() (and keep responsibilities between inline scripts vs PhpSPA-managed scripts clearly separated).
| private static runPhpSpaScripts(container: HTMLElement) { | ||
| const scripts = container.querySelectorAll("phpspa-script, script") as NodeListOf<HTMLScriptElement> | ||
| const nonce = document.head.getAttribute('x-phpspa') | ||
|
|
||
| scripts.forEach(async (script: HTMLScriptElement): Promise<void> => { | ||
| const scriptUrl = script.getAttribute('src') | ||
| const typeAttr = (script.getAttribute('type') ?? '').trim().toLowerCase() | ||
| const isModule = typeAttr === 'module' | ||
| const isExecutable = | ||
| typeAttr === '' || | ||
| typeAttr === 'text/javascript' || | ||
| typeAttr === 'application/javascript' || | ||
| typeAttr === 'application/ecmascript' || | ||
| typeAttr === 'text/ecmascript' || | ||
| isModule | ||
|
|
||
|
|
||
| if (!isExecutable) { | ||
| const newScript = document.createElement("script") | ||
|
|
||
| newScript.nonce = nonce ?? undefined; | ||
| for (const attribute of Array.from(script.attributes)) { | ||
| newScript.setAttribute(attribute.name, attribute.value) | ||
| } | ||
| newScript.textContent = script.textContent | ||
|
|
||
| // --- Copy all attributes except the data-type identifier --- | ||
| if (!newScript.getAttribute('nonce')) newScript.nonce = nonce ?? undefined | ||
|
|
||
| // --- Execute and immediately remove from DOM --- | ||
| return document.head.appendChild(newScript).remove() | ||
| } | ||
|
|
||
| if (!scriptUrl) { | ||
| // --- Create new script element --- | ||
| const newScript = document.createElement("script") | ||
|
|
||
| // --- Copy all attributes --- | ||
| for (const attribute of Array.from(script.attributes)) { | ||
| newScript.setAttribute(attribute.name, attribute.value) | ||
| } | ||
|
|
||
| if (!newScript.getAttribute('nonce')) newScript.nonce = nonce ?? undefined | ||
|
|
||
| newScript.textContent = script.textContent | ||
|
|
||
| // --- Execute and immediately remove from DOM --- | ||
| document.head.appendChild(newScript).remove() | ||
| return document.head.appendChild(newScript).remove() | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // --- Check cache first, then execute the cached content else download the script --- | ||
| if (this.ScriptsCachedContent[scriptUrl]) { | ||
| const newScript = document.createElement("script") | ||
|
|
||
| private static runPhpSpaScripts(container: HTMLElement) { | ||
| const scripts = container.querySelectorAll("phpspa-script, script[data-type=\"phpspa/script\"]") as NodeListOf<HTMLScriptElement> | ||
| const nonce = document.head.getAttribute('x-phpspa') | ||
| newScript.textContent = this.ScriptsCachedContent[scriptUrl] | ||
|
|
||
| scripts.forEach(async (script: HTMLScriptElement): Promise<void> => { | ||
| const scriptUrl = script.getAttribute('src') ?? '' | ||
| const scriptType = script.getAttribute('type') ?? '' | ||
|
|
||
| // --- Skip if this script has already been executed --- | ||
| if (!this.executedScripts.has(scriptUrl)) { | ||
| this.executedScripts.add(scriptUrl); | ||
|
|
||
| // --- Check cache first --- | ||
| if (this.ScriptsCachedContent[scriptUrl]) { | ||
| const newScript = document.createElement("script"); | ||
| newScript.textContent = `(()=>{\n${this.ScriptsCachedContent[scriptUrl]}\n})()`; | ||
| newScript.nonce = nonce ?? undefined; | ||
| newScript.type = scriptType; | ||
|
|
||
| // --- Execute and immediately remove from DOM --- | ||
| document.head.appendChild(newScript).remove(); | ||
| return; | ||
| for (const attribute of Array.from(script.attributes)) { | ||
| if (attribute.name == 'src') continue | ||
| newScript.setAttribute(attribute.name, attribute.value) | ||
| } | ||
|
|
||
| const response = await fetch(scriptUrl, { | ||
| headers: { | ||
| "X-Requested-With": "PHPSPA_REQUEST_SCRIPT", | ||
| } | ||
| }) | ||
| if (!newScript.getAttribute('nonce')) newScript.nonce = nonce ?? undefined | ||
|
|
||
| if (response.ok) { | ||
| const scriptContent = await response.text() | ||
| // --- Execute and immediately remove from DOM --- | ||
| return document.head.appendChild(newScript).remove() | ||
| } | ||
|
|
||
| // --- Create new script element --- | ||
| const newScript = document.createElement("script") | ||
| newScript.textContent = `(()=>{\n${scriptContent}\n})()`; | ||
| newScript.nonce = nonce ?? undefined; | ||
| newScript.type = scriptType; | ||
| const response = await fetch(scriptUrl, { | ||
| headers: { | ||
| "X-Requested-With": "PHPSPA_REQUEST_SCRIPT", | ||
| } | ||
| }) | ||
|
|
||
| if (response.ok) { | ||
| const scriptContent = await response.text() | ||
|
|
There was a problem hiding this comment.
runPhpSpaScripts() now handles all <script> tags (including ones with src) by fetching the URL and executing it as an inline script (copying attributes except src). This changes browser semantics for external/module scripts (e.g., import.meta.url, module resolution/base URL, CSP/SRI behavior, and caching), and can break scripts that expect to be loaded via src. Consider limiting this fetch+inline path to phpspa-script / script[data-type="phpspa/script"], and for regular <script src> prefer injecting a new <script src> element (optionally with dedupe) instead of inlining fetched content.
No description provided.