Enable jqhtml data caching with automatic ES6 class registration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-07 18:24:12 +00:00
parent c4a338fe7c
commit 12e676f317
54 changed files with 3187 additions and 1007 deletions

View File

@@ -1831,6 +1831,13 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
$files['js'][] = $manifest_file;
}
// Generate JQHTML cache class registration (must come after manifest)
// Registers all ES6 classes with jqhtml for proper data cache serialization
$jqhtml_registration_file = $this->_create_jqhtml_class_registration($files['js'] ?? []);
if ($jqhtml_registration_file) {
$files['js'][] = $jqhtml_registration_file;
}
// Generate route definitions for JavaScript
$route_file = $this->_create_javascript_routes();
if ($route_file) {
@@ -2455,6 +2462,91 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
return $this->_write_temp_file($js_code, 'js');
}
/**
* Create JQHTML cache class registration code
*
* Registers all ES6 classes with jqhtml for proper serialization during data caching.
* In data cache mode, jqhtml enforces hot/cold cache parity - any class instance stored
* in this.data that is NOT registered will be converted to a plain object (properties
* preserved but prototype methods lost).
*
* This code runs immediately after Manifest._define() so classes are available.
*
* @param array $js_files Array of JavaScript file paths in the bundle
* @return string|null Path to temp file containing registration code, or null if no classes
*/
protected function _create_jqhtml_class_registration(array $js_files): ?string
{
$manifest_files = Manifest::get_all();
$class_names = [];
// Collect all class names from JS files (same logic as _create_javascript_manifest)
foreach ($js_files as $file) {
// Skip temp files except auto-generated model classes
if (str_contains($file, 'storage/rsx-tmp/')) {
if (str_contains($file, 'bundle_generated_models_')) {
$content = file_get_contents($file);
if (preg_match_all('/class\s+([A-Za-z_][A-Za-z0-9_]*)\s+extends/', $content, $matches)) {
foreach ($matches[1] as $class_name) {
$class_names[] = $class_name;
}
}
}
continue;
}
// Handle stub files
if (str_contains($file, 'storage/rsx-build/js-stubs/') || str_contains($file, 'storage/rsx-build/js-model-stubs/')) {
$stub_content = file_get_contents($file);
$stub_metadata = $this->_extract_stub_class_info($stub_content);
if (!empty($stub_metadata['class'])) {
$class_names[] = $stub_metadata['class'];
}
continue;
}
// Get class name from manifest
$relative = str_replace(base_path() . '/', '', $file);
$file_data = $manifest_files[$relative] ?? null;
if ($file_data && !empty($file_data['class'])) {
$class_name = $file_data['class'];
// Skip 'object' as it's not a real class
if (strtolower($class_name) !== 'object') {
$class_names[] = $class_name;
}
}
}
// If no classes found, return null
if (empty($class_names)) {
return null;
}
// Remove duplicates and sort for deterministic output
$class_names = array_unique($class_names);
sort($class_names);
// Generate JavaScript code for jqhtml class registration
$js_code = "// JQHTML Cache Class Registration - Generated by BundleCompiler\n";
$js_code .= "// Registers all ES6 classes for proper serialization in jqhtml data caching.\n";
$js_code .= "// Without registration, class instances in this.data become plain objects on cache restore.\n";
$js_code .= "(function() {\n";
$js_code .= " if (typeof jqhtml === 'undefined' || typeof jqhtml.register_cache_class !== 'function') {\n";
$js_code .= " return; // jqhtml not available or doesn't support cache class registration\n";
$js_code .= " }\n";
foreach ($class_names as $class_name) {
// Check that class exists before registering (safety check)
$js_code .= " if (typeof {$class_name} !== 'undefined') jqhtml.register_cache_class({$class_name});\n";
}
$js_code .= "})();\n";
// Write to temporary file
return $this->_write_temp_file($js_code, 'js');
}
/**
* Create JavaScript runner for automatic class initialization

View File

@@ -95,12 +95,14 @@ class Jqhtml_Integration {
// scope_key() changes when: app code changes, user logs out, site changes.
// This automatically invalidates cached component renders.
//
// TEMPORARILY DISABLED: jqhtml caching is temporarily disabled for a client
// demo while we figure out why it's not serializing Rsx_Js_Model classes
// correctly. We likely want to add a serialize and unserialize callback to
// jqhtml to help make the integration between jqhtml and rspade tighter.
// Data Cache Mode (jqhtml 2.x):
// - Enforces hot/cold cache parity - fresh data and cached data behave identically
// - Any ES6 class instance stored in this.data must be registered with jqhtml
// - Class registration is done by BundleCompiler via register_cache_class()
// - Without registration, class instances become plain objects on cache restore
// (properties preserved but prototype methods lost)
// ─────────────────────────────────────────────────────────────────────
// jqhtml.set_cache_key(Rsx.scope_key());
jqhtml.set_cache_key(Rsx.scope_key(), 'data');
window.jqhtml.debug.verbose = false;
}

48
node_modules/.package-lock.json generated vendored
View File

@@ -2211,9 +2211,9 @@
}
},
"node_modules/@jqhtml/core": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.11.tgz",
"integrity": "sha512-HxkBSauw+bHIzU0jnLTuE2EWArthVxpGVV7zhG4q/zurGh+tjog1Jst2UbquHSoqB8Yr32FHZ2wAVm1FVZFKVg==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.12.tgz",
"integrity": "sha512-IaYxg5N/ikNvS560hr71y+fmcd3LprMlFMZdtYnQjOA2vN3ccCISstXkDG7wErvicMJt2mD6H46FvXJOoAhefg==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2237,9 +2237,9 @@
}
},
"node_modules/@jqhtml/parser": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.11.tgz",
"integrity": "sha512-vXyB1+KjkwDlv8vKbe1b0HZkIAM4oQMnx1pMk/xAfEtGP+WZyqXAvuiaboV20vyZUG/JhveZ9pdHvB8dZNlW/A==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.12.tgz",
"integrity": "sha512-/srJqbPVw6XK9hhtuqGh+BhwBeuvCk/UtFGYcaum/AEuJq/6vNLBbz9h4IWUmfQb7MoW9I24d/lgMcnNBE3QwQ==",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
@@ -2277,9 +2277,9 @@
}
},
"node_modules/@jqhtml/vscode-extension": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.11.tgz",
"integrity": "sha512-VHg5Tu1hPtU6LPhp/GGQ2sXCI67bMtrUynUgQp/V5KZnL0PRbY1C0usgF+71yzNoC2YvZY1ahKjrwznq+m0WCA==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.12.tgz",
"integrity": "sha512-obVJOhLKB9KRP732gsQyge8KHew0mt4aRWOZMM0/XryK0fXS6cck1+l+QKdU5XyCnVwivDHF3LdEAzwvEn93nA==",
"license": "MIT",
"engines": {
"vscode": "^1.74.0"
@@ -3997,9 +3997,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz",
"integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz",
"integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -5662,9 +5662,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.263",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz",
"integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==",
"version": "1.5.266",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
"license": "ISC"
},
"node_modules/elliptic": {
@@ -9044,9 +9044,9 @@
}
},
"node_modules/nwsapi": {
"version": "2.2.22",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz",
"integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/object-inspect": {
@@ -11772,9 +11772,9 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"version": "5.3.15",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz",
"integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
@@ -12281,9 +12281,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz",
"integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
"funding": [
{
"type": "opencollective",

View File

@@ -44,6 +44,12 @@ export declare class Jqhtml_Component {
private _reload_debounced?;
private next_reload_force_refresh;
private __lifecycle_authorized;
private _cache_key;
private _cached_html;
private _used_cached_html;
private _should_cache_html_after_ready;
private _is_dynamic;
private _on_render_complete;
constructor(element?: any, args?: Record<string, any>);
/**
* Protect lifecycle methods from manual invocation
@@ -148,6 +154,18 @@ export declare class Jqhtml_Component {
* @private
*/
private _wait_for_children_ready;
/**
* Wait for all child components to complete on_render (post-on_load)
* Used by HTML cache mode to ensure DOM is fully rendered before taking snapshot
*
* HTML CACHE ARCHITECTURE:
* - Parent waits for all children to complete their on_render after on_load
* - This ensures the HTML snapshot captures fully rendered DOM
* - Static parents (is_dynamic=false) don't block - they immediately let children proceed
*
* @private
*/
private _wait_for_children_on_render;
/**
* Reload component - re-fetch data and re-render (debounced)
*

View File

@@ -1 +1 @@
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,YAAY,CAAC,EAAE;YACb,GAAG,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;YACjF,UAAU,EAAE,MAAM,IAAI,CAAC;SACxB,CAAC;KACH;CACF;AAED,qBAAa,gBAAgB;IAE3B,MAAM,CAAC,kBAAkB,UAAQ;IACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC;IAGtB,CAAC,EAAE,GAAG,CAAC;IACP,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAK;IAGzB,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,oBAAoB,CAAwE;IACpG,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,oBAAoB,CAAoC;IAChE,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,uBAAuB,CAAoC;IACnE,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAsB;IAChD,OAAO,CAAC,yBAAyB,CAAwB;IACzD,OAAO,CAAC,sBAAsB,CAAkB;gBAEpC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;IA8IzD;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAmClC;;;;;;OAMG;YACW,eAAe;IAO7B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;;OAGG;IACH;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,MAAM;IA6QzC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IA+CtC;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IAItC;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAiG7B;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwR5B;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB7B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3C;;;;OAIG;YACW,wBAAwB;IAsCtC;;;;;;;;OAQG;IACG,MAAM,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBpD;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyK9B;;;;OAIG;IACH;;;;OAIG;IACH,KAAK,IAAI,IAAI;IA+Cb;;;OAGG;IACH,IAAI,IAAI,IAAI;IAkBZ,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACjC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IACxB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAC/B,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,MAAM;IAEnB;;;;OAIG;IACH;;;OAGG;IACH,gBAAgB,IAAI,OAAO;IAiB3B;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;;OAMG;IACH,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAsB7E;;;OAGG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAiBjC;;;OAGG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAK3C;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG;IAgB3B;;;;;;;;;;;;;;;OAeG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAgB9C;;;OAGG;IACH,YAAY,IAAI,gBAAgB,GAAG,IAAI;IAIvC;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAa1C;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAoBlD;;OAEG;IACH,MAAM,CAAC,mBAAmB,IAAI,MAAM,EAAE;IA0CtC,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,yBAAyB;IAuHjC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,gBAAgB;IAcxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,0BAA0B;CAqEnC"}
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,YAAY,CAAC,EAAE;YACb,GAAG,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;YACjF,UAAU,EAAE,MAAM,IAAI,CAAC;SACxB,CAAC;KACH;CACF;AAED,qBAAa,gBAAgB;IAE3B,MAAM,CAAC,kBAAkB,UAAQ;IACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC;IAGtB,CAAC,EAAE,GAAG,CAAC;IACP,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAK;IAGzB,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,oBAAoB,CAAwE;IACpG,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,oBAAoB,CAAoC;IAChE,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,uBAAuB,CAAoC;IACnE,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAsB;IAChD,OAAO,CAAC,yBAAyB,CAAwB;IACzD,OAAO,CAAC,sBAAsB,CAAkB;IAGhD,OAAO,CAAC,UAAU,CAAuB;IAGzC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,8BAA8B,CAAkB;IACxD,OAAO,CAAC,WAAW,CAAkB;IAGrC,OAAO,CAAC,mBAAmB,CAAkB;gBAEjC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;IA8IzD;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAmClC;;;;;;OAMG;YACW,eAAe;IAO7B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;;OAGG;IACH;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,MAAM;IA0UzC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IA+CtC;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IAItC;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAiI7B;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgU5B;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAwD7B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3C;;;;OAIG;YACW,wBAAwB;IAqCtC;;;;;;;;;;OAUG;YACW,4BAA4B;IAqC1C;;;;;;;;OAQG;IACG,MAAM,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBpD;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmO9B;;;;OAIG;IACH;;;;OAIG;IACH,KAAK,IAAI,IAAI;IA+Cb;;;OAGG;IACH,IAAI,IAAI,IAAI;IAkBZ,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACjC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IACxB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAC/B,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,MAAM;IAEnB;;;;OAIG;IACH;;;OAGG;IACH,gBAAgB,IAAI,OAAO;IA6B3B;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;;OAMG;IACH,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAsB7E;;;OAGG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAiBjC;;;OAGG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAK3C;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG;IAgB3B;;;;;;;;;;;;;;;OAeG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAgB9C;;;OAGG;IACH,YAAY,IAAI,gBAAgB,GAAG,IAAI;IAIvC;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAa1C;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAoBlD;;OAEG;IACH,MAAM,CAAC,mBAAmB,IAAI,MAAM,EAAE;IA0CtC,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,yBAAyB;IAuHjC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,gBAAgB;IAcxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,0BAA0B;CAqEnC"}

View File

@@ -1126,6 +1126,15 @@ class Jqhtml_Component {
this.__data_frozen = false; // Track if this.data is currently frozen
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
this.__lifecycle_authorized = false; // Flag for lifecycle method protection
// Cache mode properties
this._cache_key = null; // Cache key for caching
// 'html' mode caching
this._cached_html = null; // Cached HTML to inject on first render
this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
// on_render synchronization (HTML cache mode)
this._on_render_complete = false; // True after on_render() has been called post-on_load
this._cid = this._generate_cid();
this._lifecycle_manager = LifecycleManager.get_instance();
// Create or wrap element
@@ -1356,6 +1365,40 @@ class Jqhtml_Component {
`The framework will automatically re-render if this.data changes during on_load().`);
}
this._log_lifecycle('render', 'start');
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
if (this._cached_html !== null) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.length });
}
// Inject cached HTML directly
this.$[0].innerHTML = this._cached_html;
// Mark that we used cached HTML (forces re-render after on_load)
this._used_cached_html = true;
// Clear cached HTML so next render uses template
this._cached_html = null;
// Mark first render complete
this._did_first_render = true;
this._log_lifecycle('render', 'complete (cached HTML)');
// NEW ARCHITECTURE: Call on_render() even after cache inject
// This ensures consistent behavior - on_render() always runs after DOM update
// Note: this.data has defaults from on_create(), not fresh data yet
const cacheRenderResult = this._call_lifecycle_sync('on_render');
if (cacheRenderResult && typeof cacheRenderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
}
// Emit lifecycle event
this.trigger('render');
// Store args/data snapshots for later comparison
try {
this._args_on_last_render = JSON.parse(JSON.stringify(this.args));
}
catch (error) {
this._args_on_last_render = null;
}
this._data_on_last_render = JSON.stringify(this.data);
return current_render_id;
}
// Determine child-finding strategy: If component is off-DOM, children can't register
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
// If attached to DOM, children register normally and we can use the fast _dom_children path.
@@ -1523,6 +1566,12 @@ class Jqhtml_Component {
this._log_lifecycle('render', 'complete');
// Call on_render() with authorization (sync) immediately after render completes
// This ensures event handlers are always re-attached after DOM updates
//
// NEW ARCHITECTURE: on_render() can now access this.data normally
// The HTML cache mode now properly synchronizes - on_render() runs after both:
// 1. Cache inject (with on_create() defaults)
// 2. Second render (with fresh data from on_load())
// Since on_render() always runs with appropriate data, no proxy restriction needed
const renderResult = this._call_lifecycle_sync('on_render');
if (renderResult && typeof renderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
@@ -1543,6 +1592,12 @@ class Jqhtml_Component {
}
// Store data snapshot for refresh() comparison
this._data_on_last_render = JSON.stringify(this.data);
// HTML CACHE MODE: Mark on_render complete after second render (post-on_load)
// This signals to parent components that this component's DOM is fully rendered
// with fresh data and ready for HTML snapshot
if (this._ready_state >= 2) {
this._on_render_complete = true;
}
// Return the render ID so callers can check if this render is still current
return current_render_id;
}
@@ -1616,8 +1671,8 @@ class Jqhtml_Component {
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
await result; // Still wait for it to avoid breaking existing code
}
// CACHE READ - Hydrate this.data from cache BEFORE first render
// This happens after on_create() but before render, allowing instant first render with cached data
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
// This happens after on_create() but before render, allowing instant first render
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
@@ -1648,21 +1703,48 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
}
return; // Skip cache check
}
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null) {
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) hydrated from cache in create()`, { cache_key, data: this.data });
}
// Don't return - continue to snapshot this.data for on_load restoration
}
else {
// Store cache key for later use
this._cache_key = cache_key;
// Get cache mode
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) cache miss in create()`, { cache_key });
console.log(`[Cache ${cache_mode}] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML to inject on first render
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key: html_cache_key });
}
}
}
else {
// Data cache mode (default) - check for cached data to hydrate this.data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object') {
// Hydrate this.data with cached data
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key });
}
}
}
}
// Snapshot this.data after on_create() completes
@@ -1852,8 +1934,7 @@ class Jqhtml_Component {
// Always clear loading flag and complete coordination
this.__loading = false;
complete_coordination();
// Freeze this.data after on_load() completes
this.__data_frozen = true;
// Note: this.data stays unfrozen until after normalization below
}
// Validate that on_load() only modified this.data
let argsAfterLoad = null;
@@ -1882,12 +1963,43 @@ class Jqhtml_Component {
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
if (data_after_load !== data_before_load && data_after_load !== '{}') {
Jqhtml_Local_Storage.set(cache_key, this.data);
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
// so developers catch missing class registrations during development rather than
// only after a page reload when data comes from cache.
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (cache_mode === 'data') {
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load()`, { cache_key, data: this.data });
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
}
}
// Freeze this.data after normalization completes
this.__data_frozen = true;
// CACHE WRITE - If data changed during on_load(), write to cache based on mode
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
// Track if component is "dynamic" (this.data changed during on_load)
// Used by HTML cache mode for synchronization - static parents don't block children
this._is_dynamic = data_changed && data_after_load !== '{}';
if (this._is_dynamic) {
if (this._cache_key) {
if (cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready`, { cache_key: this._cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(this._cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load()`, { cache_key: this._cache_key, data: this.data });
}
}
}
}
this._ready_state = 2;
@@ -1905,6 +2017,34 @@ class Jqhtml_Component {
if (this._stopped || this._ready_state >= 4)
return;
this._log_lifecycle('ready', 'start');
// HTML CACHE MODE - New synchronization architecture:
// 1. Wait for all children to complete on_render (post-on_load)
// 2. Take HTML snapshot BEFORE waiting for children ready
// 3. This ensures we capture the DOM after on_render but before on_ready manipulations
if (this._should_cache_html_after_ready && this._cache_key) {
// Wait for all children to complete their on_render
await this._wait_for_children_on_render();
// Only cache if this component is dynamic (data changed during on_load)
// Static parents don't cache - they just let children proceed
if (this._is_dynamic) {
this._should_cache_html_after_ready = false;
// Cache the rendered HTML (async import to avoid circular dependency)
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const html = this.$.html();
const html_cache_key = `${this._cache_key}::html`;
Jqhtml_Local_Storage.set(html_cache_key, html);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
}
}
else {
// Static component - just clear the flag, don't cache
this._should_cache_html_after_ready = false;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) is static (no data change) - skipping HTML cache`);
}
}
}
// Wait for all children to reach ready state (bottom-up execution)
await this._wait_for_children_ready();
await this._call_lifecycle('on_ready');
@@ -1985,6 +2125,47 @@ class Jqhtml_Component {
// Wait for all children to be ready
await Promise.all(ready_promises);
}
/**
* Wait for all child components to complete on_render (post-on_load)
* Used by HTML cache mode to ensure DOM is fully rendered before taking snapshot
*
* HTML CACHE ARCHITECTURE:
* - Parent waits for all children to complete their on_render after on_load
* - This ensures the HTML snapshot captures fully rendered DOM
* - Static parents (is_dynamic=false) don't block - they immediately let children proceed
*
* @private
*/
async _wait_for_children_on_render() {
const children = this._get_dom_children();
if (children.length === 0) {
return; // No children, nothing to wait for
}
// Create promises for each child that hasn't completed on_render yet
const render_promises = [];
for (const child of children) {
// If child already completed on_render post-on_load, skip
if (child._on_render_complete) {
continue;
}
// Create promise that resolves when child completes on_render
const render_promise = new Promise((resolve) => {
// Poll for completion (simple approach - could use events for more efficiency)
const check = () => {
if (child._on_render_complete || child._stopped) {
resolve();
}
else {
setTimeout(check, 10);
}
};
check();
});
render_promises.push(render_promise);
}
// Wait for all children to complete on_render
await Promise.all(render_promises);
}
/**
* Reload component - re-fetch data and re-render (debounced)
*
@@ -2097,19 +2278,38 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
// Check for cached data/html when args changed
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
this._cache_key = cache_key;
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] reload() - Component ${this._cid} (${this.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
}
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
this.render();
rendered_from_cache = true;
}
}
else {
// Data cache mode (default) - check for cached data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
}
// Hydrate this.data with cached data
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
this.render();
rendered_from_cache = true;
}
// Unfreeze to set cached data, then re-freeze
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
await this.render();
rendered_from_cache = true;
}
}
}
@@ -2129,12 +2329,25 @@ class Jqhtml_Component {
// Freeze this.data after on_load() completes
this.__data_frozen = true;
}
// Import for cache operations
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// (Same as in _load() - ensures hot/cold cache parity)
const reload_cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (reload_cache_mode === 'data') {
this.__data_frozen = false;
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
this.__data_frozen = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load() in reload()`, { data: this.data });
}
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
@@ -2152,11 +2365,22 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
// Write to cache based on mode
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load() in reload()`, { cache_key, data: this.data });
this._cache_key = cache_key;
if (reload_cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready in reload()`, { cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load() in reload()`, { cache_key, data: this.data });
}
}
}
}
@@ -2282,6 +2506,15 @@ class Jqhtml_Component {
* @private
*/
_should_rerender() {
// HTML CACHE MODE - If we used cached HTML, always re-render to get live components
if (this._used_cached_html) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
}
// Clear the flag
this._used_cached_html = false;
return true;
}
// Compare current data state with data state before initial render
const currentDataState = JSON.stringify(this.data);
const dataChanged = this._data_before_render !== currentDataState;
@@ -3509,6 +3742,13 @@ if (typeof window !== 'undefined' && window.jQuery) {
* - **Quota management**: Auto-clears storage when full and retries operation
* - **Scope validation**: Clears storage when cache key changes
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
*
* Class-Aware Serialization:
* ES6 class instances can be serialized and deserialized if registered via
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
* and restored to proper instances on retrieval. Nested class instances in objects
* and arrays are handled recursively.
*
* Scoping Strategy:
* Storage is scoped by a user-provided cache key (typically a session identifier,
@@ -3530,13 +3770,20 @@ if (typeof window !== 'undefined' && window.jQuery) {
* once. This ensures the application continues functioning even when storage is full.
*
* Usage:
* // Register classes that need to be cached (call once at startup)
* jqhtml.register_cache_class(Contact_Model);
* jqhtml.register_cache_class(User_Profile);
*
* // Must set cache key first (typically done once on page load)
* Jqhtml_Local_Storage.set_cache_key('user_123');
*
* // Then use storage normally
* Jqhtml_Local_Storage.set('cached_component', {html: '...', timestamp: Date.now()});
* // Then use storage normally - ES6 classes serialize automatically
* Jqhtml_Local_Storage.set('cached_component', {
* contact: new Contact_Model(),
* timestamp: Date.now()
* });
* const cached = Jqhtml_Local_Storage.get('cached_component');
* Jqhtml_Local_Storage.remove('cached_component');
* // cached.contact instanceof Contact_Model === true
*
* IMPORTANT - Volatile Storage:
* Storage can be cleared at any time due to:
@@ -3552,14 +3799,290 @@ if (typeof window !== 'undefined' && window.jQuery) {
*
* @internal This class is not exposed in the public API
*/
// ============================================================================
// Class Registry for Serialization
// ============================================================================
// Registry mapping class names to constructors
const class_registry = Object.create(null);
// Markers for serialized class instances (unique to avoid collisions with user data)
const CLASS_MARKER = '__jqhtml_class__';
const PROPS_MARKER = '__jqhtml_props__';
/**
* Register a class for cache serialization/deserialization.
* Must be called before attempting to cache instances of this class.
*
* @param klass - The class constructor to register
* @throws Error if klass is not a named function/class
*/
function register_cache_class(klass) {
if (typeof klass !== 'function' || !klass.name) {
throw new Error('[JQHTML Cache] register_cache_class requires a named class constructor');
}
class_registry[klass.name] = klass;
}
// ============================================================================
// Serialization (Object → String)
// ============================================================================
/**
* Serialize a value to JSON string, handling ES6 class instances recursively.
* Returns null if serialization fails for any reason.
*
* @param value - The value to serialize
* @param verbose - Whether to log warnings
* @returns Serialized JSON string, or null if serialization failed
*/
function serialize_value(value, verbose) {
try {
const seen = new WeakSet();
const processed = process_for_serialization(value, verbose, seen);
if (processed === undefined) {
// Serialization failed - undefined signals failure
return null;
}
return JSON.stringify(processed);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Serialization failed:', e);
}
return null;
}
}
/**
* Recursively process a value for serialization.
* Returns undefined if serialization should fail (unserializable value encountered).
*
* @param value - The value to process
* @param verbose - Whether to log warnings
* @param seen - WeakSet to detect circular references
* @returns Processed value ready for JSON.stringify, or undefined if failed
*/
function process_for_serialization(value, verbose, seen) {
// Handle null and undefined
if (value === null) {
return null;
}
if (value === undefined) {
return undefined; // JSON.stringify will omit this property
}
// Handle primitives
if (typeof value !== 'object') {
// string, number, boolean are fine
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
// bigint, symbol, function cannot be serialized
if (verbose) {
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
}
// Return undefined to omit this value (not fail entire serialization)
return undefined;
}
// Handle circular references
if (seen.has(value)) {
if (verbose) {
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
}
return undefined; // Fail entire serialization
}
seen.add(value);
// Handle arrays
if (Array.isArray(value)) {
const result = [];
for (let i = 0; i < value.length; i++) {
const item = value[i];
const processed = process_for_serialization(item, verbose, seen);
// For arrays, we keep undefined as null to preserve indices
result.push(processed === undefined ? null : processed);
}
return result;
}
// Handle Date objects specially
if (value instanceof Date) {
return {
[CLASS_MARKER]: 'Date',
[PROPS_MARKER]: value.toISOString()
};
}
// Handle Map objects
if (value instanceof Map) {
const entries = [];
for (const [k, v] of value) {
const processedKey = process_for_serialization(k, verbose, seen);
const processedValue = process_for_serialization(v, verbose, seen);
entries.push([processedKey, processedValue]);
}
return {
[CLASS_MARKER]: 'Map',
[PROPS_MARKER]: entries
};
}
// Handle Set objects
if (value instanceof Set) {
const items = [];
for (const item of value) {
items.push(process_for_serialization(item, verbose, seen));
}
return {
[CLASS_MARKER]: 'Set',
[PROPS_MARKER]: items
};
}
// Get the constructor
const ctor = value.constructor;
// Handle registered class instances
if (ctor && ctor.name && class_registry[ctor.name]) {
const props = {};
// Extract own enumerable properties (bypass any toJSON method)
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (failed serialization)
if (processed !== undefined || propValue === undefined) {
props[key] = processed;
}
}
return {
[CLASS_MARKER]: ctor.name,
[PROPS_MARKER]: props
};
}
// Handle plain objects (constructor is Object or no constructor)
if (ctor === Object || ctor === undefined || ctor === null) {
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// Unregistered class instance - convert to plain object for hot/cold parity
// This ensures developers catch missing class registrations during development
// (methods will be lost, only properties preserved)
if (verbose) {
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
}
// Convert to plain object - properties are preserved, prototype methods are lost
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// ============================================================================
// Deserialization (String → Object)
// ============================================================================
/**
* Deserialize a JSON string, restoring ES6 class instances.
* Returns null if deserialization fails.
*
* @param str - The JSON string to deserialize
* @param verbose - Whether to log warnings
* @returns Deserialized value with class instances restored, or null if failed
*/
function deserialize_value(str, verbose) {
try {
const parsed = JSON.parse(str);
return process_for_deserialization(parsed, verbose);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Deserialization failed:', e);
}
return null;
}
}
/**
* Recursively process a parsed value, restoring class instances.
*
* @param value - The parsed value to process
* @param verbose - Whether to log warnings
* @returns Processed value with class instances restored
*/
function process_for_deserialization(value, verbose) {
// Handle primitives and null
if (value === null || value === undefined || typeof value !== 'object') {
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return value.map(item => process_for_deserialization(item, verbose));
}
// Check for class marker
if (value[CLASS_MARKER] !== undefined && value[PROPS_MARKER] !== undefined) {
const class_name = value[CLASS_MARKER];
const props = value[PROPS_MARKER];
// Handle built-in types
if (class_name === 'Date') {
return new Date(props);
}
if (class_name === 'Map') {
const map = new Map();
for (const [k, v] of props) {
map.set(process_for_deserialization(k, verbose), process_for_deserialization(v, verbose));
}
return map;
}
if (class_name === 'Set') {
const set = new Set();
for (const item of props) {
set.add(process_for_deserialization(item, verbose));
}
return set;
}
// Look up registered class
const klass = class_registry[class_name];
if (!klass) {
if (verbose) {
console.warn(`[JQHTML Cache] Cannot restore class "${class_name}" - not registered. ` +
`Data will be returned as plain object. ` +
`Call jqhtml.register_cache_class(${class_name}) to restore class instances.`);
}
// Return as plain object rather than failing completely
return process_for_deserialization(props, verbose);
}
// Create instance without calling constructor and assign properties
try {
const instance = Object.create(klass.prototype);
const processed_props = process_for_deserialization(props, verbose);
Object.assign(instance, processed_props);
return instance;
}
catch (e) {
if (verbose) {
console.warn(`[JQHTML Cache] Failed to restore class "${class_name}":`, e);
}
// Return null to signal cache should be treated as miss
return null;
}
}
// Plain object - process recursively
const result = {};
for (const key of Object.keys(value)) {
result[key] = process_for_deserialization(value[key], verbose);
}
return result;
}
class Jqhtml_Local_Storage {
/**
* Set the cache key and initialize storage
* Must be called before any get/set operations
* @param {string} cache_key - Unique identifier for this cache scope
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
*/
static set_cache_key(cache_key) {
static set_cache_key(cache_key, cache_mode = 'data') {
this._cache_key = cache_key;
this._cache_mode = cache_mode;
this._init();
}
/**
@@ -3569,6 +4092,13 @@ class Jqhtml_Local_Storage {
static has_cache_key() {
return this._cache_key !== null;
}
/**
* Get the current cache mode
* @returns {CacheMode} Current cache mode ('data' or 'html')
*/
static get_cache_mode() {
return this._cache_mode;
}
/**
* Initialize storage system and validate scope
* Called automatically after cache key is set
@@ -3603,6 +4133,13 @@ class Jqhtml_Local_Storage {
return false;
}
}
/**
* Check if verbose mode is enabled
* @private
*/
static _is_verbose() {
return window.jqhtml?.debug?.verbose === true;
}
/**
* Validate storage scope and clear JQHTML keys if cache key changed
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
@@ -3681,34 +4218,90 @@ class Jqhtml_Local_Storage {
return this._storage_available === true && this._cache_key !== null && this._initialized;
}
/**
* Set item in localStorage
* Set item in localStorage with class-aware serialization.
*
* If serialization fails (e.g., unregistered class instances, circular refs),
* the existing cache entry is removed and nothing is stored.
*
* @param {string} key - Storage key
* @param {*} value - Value to store (will be JSON serialized)
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
*/
static set(key, value) {
if (!this._is_ready()) {
return;
}
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
// Serialize with class-aware processing
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed - remove any existing cache entry
if (verbose) {
console.warn(`[JQHTML Cache] Failed to serialize value for key "${key}". ` +
`Removing existing cache entry if present.`);
}
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
// Check size before attempting to store (1MB limit)
const serialized = JSON.stringify(value);
const size_bytes = new Blob([serialized]).size;
const size_mb = size_bytes / (1024 * 1024);
if (size_mb > 1) {
console.warn(`[JQHTML Local Storage] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
if (verbose) {
console.warn(`[JQHTML Cache] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
}
// Remove existing cache entry since we can't update it
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
this._set_item(key, value, serialized);
this._set_item(key, serialized);
}
/**
* Get item from localStorage
* Get item from localStorage with class-aware deserialization.
*
* If deserialization fails, returns null (as if no cache exists).
*
* @param {string} key - Storage key
* @returns {*|null} Parsed value or null if not found/unavailable
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
*/
static get(key) {
if (!this._is_ready()) {
return null;
}
return this._get_item(key);
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
const result = deserialize_value(serialized, verbose);
if (result === null) {
// Deserialization failed - treat as cache miss
if (verbose) {
console.warn(`[JQHTML Cache] Failed to deserialize value for key "${key}". ` +
`Treating as cache miss.`);
}
return null;
}
return result;
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Failed to get item:', e);
}
return null;
}
}
/**
* Remove item from localStorage
@@ -3720,14 +4313,49 @@ class Jqhtml_Local_Storage {
}
this._remove_item(key);
}
/**
* Perform a serialize/deserialize round-trip on a value.
*
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
* (restored from cache). Unregistered class instances will be converted to plain
* objects, exactly as they would be if restored from cache.
*
* Use this to normalize data after on_load() so developers catch missing class
* registrations immediately rather than only after a page reload.
*
* @param {any} value - The value to normalize
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
*/
static normalize_for_cache(value) {
const verbose = this._is_verbose();
// Serialize to JSON string
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed completely - return original
// (This happens with circular references or other fatal issues)
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Serialization failed, returning original value');
}
return value;
}
// Deserialize back to object
const deserialized = deserialize_value(serialized, verbose);
if (deserialized === null) {
// Deserialization failed - return original
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Deserialization failed, returning original value');
}
return value;
}
return deserialized;
}
/**
* Internal set implementation with scope validation and quota handling
* @param {string} key
* @param {*} value - Original value (not used, kept for clarity)
* @param {string} serialized - Pre-serialized JSON string
* @private
*/
static _set_item(key, value, serialized) {
static _set_item(key, serialized) {
// Validate scope before every write
this._validate_scope();
const scoped_key = this._build_key(key);
@@ -3753,26 +4381,6 @@ class Jqhtml_Local_Storage {
}
}
}
/**
* Internal get implementation
* @param {string} key
* @returns {*|null}
* @private
*/
static _get_item(key) {
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
return JSON.parse(serialized);
}
catch (e) {
console.error('[JQHTML Local Storage] Failed to get item:', e);
return null;
}
}
/**
* Internal remove implementation
* @param {string} key
@@ -3789,12 +4397,14 @@ class Jqhtml_Local_Storage {
}
}
Jqhtml_Local_Storage._cache_key = null;
Jqhtml_Local_Storage._cache_mode = 'data';
Jqhtml_Local_Storage._storage_available = null;
Jqhtml_Local_Storage._initialized = false;
var localStorage$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
Jqhtml_Local_Storage: Jqhtml_Local_Storage
Jqhtml_Local_Storage: Jqhtml_Local_Storage,
register_cache_class: register_cache_class
});
/**
@@ -4057,7 +4667,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.11';
const version = '2.3.12';
// Default export with all functionality
const jqhtml = {
// Core
@@ -4141,9 +4751,17 @@ const jqhtml = {
return version;
},
// Cache key setter - enables component caching via local storage
set_cache_key(cache_key) {
Jqhtml_Local_Storage.set_cache_key(cache_key);
// cache_mode: 'data' (default, recommended) or 'html'
set_cache_key(cache_key, cache_mode = 'data') {
Jqhtml_Local_Storage.set_cache_key(cache_key, cache_mode);
},
// Get current cache mode
get_cache_mode() {
return Jqhtml_Local_Storage.get_cache_mode();
},
// Register a class for cache serialization/deserialization
// Required for ES6 class instances to be stored and restored from localStorage
register_cache_class,
// Boot - hydrate server-rendered component placeholders
boot
};
@@ -4192,6 +4810,7 @@ exports.logInstruction = logInstruction;
exports.logLifecycle = logLifecycle;
exports.process_instructions = process_instructions;
exports.register = register;
exports.register_cache_class = register_cache_class;
exports.register_component = register_component;
exports.register_template = register_template;
exports.render_template = render_template;

File diff suppressed because one or more lines are too long

View File

@@ -24,8 +24,9 @@ import { process_instructions, extract_slots } from './instruction-processor.js'
import { render_template, escape_html } from './template-renderer.js';
import { boot } from './boot.js';
import './jquery-plugin.js';
import { Jqhtml_Local_Storage } from './local-storage.js';
export { Jqhtml_Local_Storage };
import { Jqhtml_Local_Storage, register_cache_class } from './local-storage.js';
export { Jqhtml_Local_Storage, register_cache_class };
export type { CacheMode } from './local-storage.js';
import { Load_Coordinator } from './load-coordinator.js';
export { Load_Coordinator };
export declare const version = "__VERSION__";
@@ -83,7 +84,9 @@ declare const jqhtml: {
installGlobals(): void;
_version(): any;
version(): string;
set_cache_key(cache_key: string): void;
set_cache_key(cache_key: string, cache_mode?: "data" | "html"): void;
get_cache_mode(): import("./local-storage.js").CacheMode;
register_cache_class: typeof register_cache_class;
boot: typeof boot;
};
export default jqhtml;

View File

@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

View File

@@ -1122,6 +1122,15 @@ class Jqhtml_Component {
this.__data_frozen = false; // Track if this.data is currently frozen
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
this.__lifecycle_authorized = false; // Flag for lifecycle method protection
// Cache mode properties
this._cache_key = null; // Cache key for caching
// 'html' mode caching
this._cached_html = null; // Cached HTML to inject on first render
this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
// on_render synchronization (HTML cache mode)
this._on_render_complete = false; // True after on_render() has been called post-on_load
this._cid = this._generate_cid();
this._lifecycle_manager = LifecycleManager.get_instance();
// Create or wrap element
@@ -1352,6 +1361,40 @@ class Jqhtml_Component {
`The framework will automatically re-render if this.data changes during on_load().`);
}
this._log_lifecycle('render', 'start');
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
if (this._cached_html !== null) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.length });
}
// Inject cached HTML directly
this.$[0].innerHTML = this._cached_html;
// Mark that we used cached HTML (forces re-render after on_load)
this._used_cached_html = true;
// Clear cached HTML so next render uses template
this._cached_html = null;
// Mark first render complete
this._did_first_render = true;
this._log_lifecycle('render', 'complete (cached HTML)');
// NEW ARCHITECTURE: Call on_render() even after cache inject
// This ensures consistent behavior - on_render() always runs after DOM update
// Note: this.data has defaults from on_create(), not fresh data yet
const cacheRenderResult = this._call_lifecycle_sync('on_render');
if (cacheRenderResult && typeof cacheRenderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
}
// Emit lifecycle event
this.trigger('render');
// Store args/data snapshots for later comparison
try {
this._args_on_last_render = JSON.parse(JSON.stringify(this.args));
}
catch (error) {
this._args_on_last_render = null;
}
this._data_on_last_render = JSON.stringify(this.data);
return current_render_id;
}
// Determine child-finding strategy: If component is off-DOM, children can't register
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
// If attached to DOM, children register normally and we can use the fast _dom_children path.
@@ -1519,6 +1562,12 @@ class Jqhtml_Component {
this._log_lifecycle('render', 'complete');
// Call on_render() with authorization (sync) immediately after render completes
// This ensures event handlers are always re-attached after DOM updates
//
// NEW ARCHITECTURE: on_render() can now access this.data normally
// The HTML cache mode now properly synchronizes - on_render() runs after both:
// 1. Cache inject (with on_create() defaults)
// 2. Second render (with fresh data from on_load())
// Since on_render() always runs with appropriate data, no proxy restriction needed
const renderResult = this._call_lifecycle_sync('on_render');
if (renderResult && typeof renderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
@@ -1539,6 +1588,12 @@ class Jqhtml_Component {
}
// Store data snapshot for refresh() comparison
this._data_on_last_render = JSON.stringify(this.data);
// HTML CACHE MODE: Mark on_render complete after second render (post-on_load)
// This signals to parent components that this component's DOM is fully rendered
// with fresh data and ready for HTML snapshot
if (this._ready_state >= 2) {
this._on_render_complete = true;
}
// Return the render ID so callers can check if this render is still current
return current_render_id;
}
@@ -1612,8 +1667,8 @@ class Jqhtml_Component {
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
await result; // Still wait for it to avoid breaking existing code
}
// CACHE READ - Hydrate this.data from cache BEFORE first render
// This happens after on_create() but before render, allowing instant first render with cached data
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
// This happens after on_create() but before render, allowing instant first render
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
@@ -1644,21 +1699,48 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
}
return; // Skip cache check
}
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null) {
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) hydrated from cache in create()`, { cache_key, data: this.data });
}
// Don't return - continue to snapshot this.data for on_load restoration
}
else {
// Store cache key for later use
this._cache_key = cache_key;
// Get cache mode
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) cache miss in create()`, { cache_key });
console.log(`[Cache ${cache_mode}] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML to inject on first render
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key: html_cache_key });
}
}
}
else {
// Data cache mode (default) - check for cached data to hydrate this.data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object') {
// Hydrate this.data with cached data
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key });
}
}
}
}
// Snapshot this.data after on_create() completes
@@ -1848,8 +1930,7 @@ class Jqhtml_Component {
// Always clear loading flag and complete coordination
this.__loading = false;
complete_coordination();
// Freeze this.data after on_load() completes
this.__data_frozen = true;
// Note: this.data stays unfrozen until after normalization below
}
// Validate that on_load() only modified this.data
let argsAfterLoad = null;
@@ -1878,12 +1959,43 @@ class Jqhtml_Component {
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
if (data_after_load !== data_before_load && data_after_load !== '{}') {
Jqhtml_Local_Storage.set(cache_key, this.data);
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
// so developers catch missing class registrations during development rather than
// only after a page reload when data comes from cache.
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (cache_mode === 'data') {
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load()`, { cache_key, data: this.data });
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
}
}
// Freeze this.data after normalization completes
this.__data_frozen = true;
// CACHE WRITE - If data changed during on_load(), write to cache based on mode
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
// Track if component is "dynamic" (this.data changed during on_load)
// Used by HTML cache mode for synchronization - static parents don't block children
this._is_dynamic = data_changed && data_after_load !== '{}';
if (this._is_dynamic) {
if (this._cache_key) {
if (cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready`, { cache_key: this._cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(this._cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load()`, { cache_key: this._cache_key, data: this.data });
}
}
}
}
this._ready_state = 2;
@@ -1901,6 +2013,34 @@ class Jqhtml_Component {
if (this._stopped || this._ready_state >= 4)
return;
this._log_lifecycle('ready', 'start');
// HTML CACHE MODE - New synchronization architecture:
// 1. Wait for all children to complete on_render (post-on_load)
// 2. Take HTML snapshot BEFORE waiting for children ready
// 3. This ensures we capture the DOM after on_render but before on_ready manipulations
if (this._should_cache_html_after_ready && this._cache_key) {
// Wait for all children to complete their on_render
await this._wait_for_children_on_render();
// Only cache if this component is dynamic (data changed during on_load)
// Static parents don't cache - they just let children proceed
if (this._is_dynamic) {
this._should_cache_html_after_ready = false;
// Cache the rendered HTML (async import to avoid circular dependency)
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const html = this.$.html();
const html_cache_key = `${this._cache_key}::html`;
Jqhtml_Local_Storage.set(html_cache_key, html);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
}
}
else {
// Static component - just clear the flag, don't cache
this._should_cache_html_after_ready = false;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) is static (no data change) - skipping HTML cache`);
}
}
}
// Wait for all children to reach ready state (bottom-up execution)
await this._wait_for_children_ready();
await this._call_lifecycle('on_ready');
@@ -1981,6 +2121,47 @@ class Jqhtml_Component {
// Wait for all children to be ready
await Promise.all(ready_promises);
}
/**
* Wait for all child components to complete on_render (post-on_load)
* Used by HTML cache mode to ensure DOM is fully rendered before taking snapshot
*
* HTML CACHE ARCHITECTURE:
* - Parent waits for all children to complete their on_render after on_load
* - This ensures the HTML snapshot captures fully rendered DOM
* - Static parents (is_dynamic=false) don't block - they immediately let children proceed
*
* @private
*/
async _wait_for_children_on_render() {
const children = this._get_dom_children();
if (children.length === 0) {
return; // No children, nothing to wait for
}
// Create promises for each child that hasn't completed on_render yet
const render_promises = [];
for (const child of children) {
// If child already completed on_render post-on_load, skip
if (child._on_render_complete) {
continue;
}
// Create promise that resolves when child completes on_render
const render_promise = new Promise((resolve) => {
// Poll for completion (simple approach - could use events for more efficiency)
const check = () => {
if (child._on_render_complete || child._stopped) {
resolve();
}
else {
setTimeout(check, 10);
}
};
check();
});
render_promises.push(render_promise);
}
// Wait for all children to complete on_render
await Promise.all(render_promises);
}
/**
* Reload component - re-fetch data and re-render (debounced)
*
@@ -2093,19 +2274,38 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
// Check for cached data/html when args changed
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
this._cache_key = cache_key;
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] reload() - Component ${this._cid} (${this.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
}
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
this.render();
rendered_from_cache = true;
}
}
else {
// Data cache mode (default) - check for cached data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
}
// Hydrate this.data with cached data
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
this.render();
rendered_from_cache = true;
}
// Unfreeze to set cached data, then re-freeze
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
await this.render();
rendered_from_cache = true;
}
}
}
@@ -2125,12 +2325,25 @@ class Jqhtml_Component {
// Freeze this.data after on_load() completes
this.__data_frozen = true;
}
// Import for cache operations
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// (Same as in _load() - ensures hot/cold cache parity)
const reload_cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (reload_cache_mode === 'data') {
this.__data_frozen = false;
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
this.__data_frozen = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load() in reload()`, { data: this.data });
}
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
@@ -2148,11 +2361,22 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
// Write to cache based on mode
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load() in reload()`, { cache_key, data: this.data });
this._cache_key = cache_key;
if (reload_cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready in reload()`, { cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load() in reload()`, { cache_key, data: this.data });
}
}
}
}
@@ -2278,6 +2502,15 @@ class Jqhtml_Component {
* @private
*/
_should_rerender() {
// HTML CACHE MODE - If we used cached HTML, always re-render to get live components
if (this._used_cached_html) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
}
// Clear the flag
this._used_cached_html = false;
return true;
}
// Compare current data state with data state before initial render
const currentDataState = JSON.stringify(this.data);
const dataChanged = this._data_before_render !== currentDataState;
@@ -3505,6 +3738,13 @@ if (typeof window !== 'undefined' && window.jQuery) {
* - **Quota management**: Auto-clears storage when full and retries operation
* - **Scope validation**: Clears storage when cache key changes
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
*
* Class-Aware Serialization:
* ES6 class instances can be serialized and deserialized if registered via
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
* and restored to proper instances on retrieval. Nested class instances in objects
* and arrays are handled recursively.
*
* Scoping Strategy:
* Storage is scoped by a user-provided cache key (typically a session identifier,
@@ -3526,13 +3766,20 @@ if (typeof window !== 'undefined' && window.jQuery) {
* once. This ensures the application continues functioning even when storage is full.
*
* Usage:
* // Register classes that need to be cached (call once at startup)
* jqhtml.register_cache_class(Contact_Model);
* jqhtml.register_cache_class(User_Profile);
*
* // Must set cache key first (typically done once on page load)
* Jqhtml_Local_Storage.set_cache_key('user_123');
*
* // Then use storage normally
* Jqhtml_Local_Storage.set('cached_component', {html: '...', timestamp: Date.now()});
* // Then use storage normally - ES6 classes serialize automatically
* Jqhtml_Local_Storage.set('cached_component', {
* contact: new Contact_Model(),
* timestamp: Date.now()
* });
* const cached = Jqhtml_Local_Storage.get('cached_component');
* Jqhtml_Local_Storage.remove('cached_component');
* // cached.contact instanceof Contact_Model === true
*
* IMPORTANT - Volatile Storage:
* Storage can be cleared at any time due to:
@@ -3548,14 +3795,290 @@ if (typeof window !== 'undefined' && window.jQuery) {
*
* @internal This class is not exposed in the public API
*/
// ============================================================================
// Class Registry for Serialization
// ============================================================================
// Registry mapping class names to constructors
const class_registry = Object.create(null);
// Markers for serialized class instances (unique to avoid collisions with user data)
const CLASS_MARKER = '__jqhtml_class__';
const PROPS_MARKER = '__jqhtml_props__';
/**
* Register a class for cache serialization/deserialization.
* Must be called before attempting to cache instances of this class.
*
* @param klass - The class constructor to register
* @throws Error if klass is not a named function/class
*/
function register_cache_class(klass) {
if (typeof klass !== 'function' || !klass.name) {
throw new Error('[JQHTML Cache] register_cache_class requires a named class constructor');
}
class_registry[klass.name] = klass;
}
// ============================================================================
// Serialization (Object → String)
// ============================================================================
/**
* Serialize a value to JSON string, handling ES6 class instances recursively.
* Returns null if serialization fails for any reason.
*
* @param value - The value to serialize
* @param verbose - Whether to log warnings
* @returns Serialized JSON string, or null if serialization failed
*/
function serialize_value(value, verbose) {
try {
const seen = new WeakSet();
const processed = process_for_serialization(value, verbose, seen);
if (processed === undefined) {
// Serialization failed - undefined signals failure
return null;
}
return JSON.stringify(processed);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Serialization failed:', e);
}
return null;
}
}
/**
* Recursively process a value for serialization.
* Returns undefined if serialization should fail (unserializable value encountered).
*
* @param value - The value to process
* @param verbose - Whether to log warnings
* @param seen - WeakSet to detect circular references
* @returns Processed value ready for JSON.stringify, or undefined if failed
*/
function process_for_serialization(value, verbose, seen) {
// Handle null and undefined
if (value === null) {
return null;
}
if (value === undefined) {
return undefined; // JSON.stringify will omit this property
}
// Handle primitives
if (typeof value !== 'object') {
// string, number, boolean are fine
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
// bigint, symbol, function cannot be serialized
if (verbose) {
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
}
// Return undefined to omit this value (not fail entire serialization)
return undefined;
}
// Handle circular references
if (seen.has(value)) {
if (verbose) {
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
}
return undefined; // Fail entire serialization
}
seen.add(value);
// Handle arrays
if (Array.isArray(value)) {
const result = [];
for (let i = 0; i < value.length; i++) {
const item = value[i];
const processed = process_for_serialization(item, verbose, seen);
// For arrays, we keep undefined as null to preserve indices
result.push(processed === undefined ? null : processed);
}
return result;
}
// Handle Date objects specially
if (value instanceof Date) {
return {
[CLASS_MARKER]: 'Date',
[PROPS_MARKER]: value.toISOString()
};
}
// Handle Map objects
if (value instanceof Map) {
const entries = [];
for (const [k, v] of value) {
const processedKey = process_for_serialization(k, verbose, seen);
const processedValue = process_for_serialization(v, verbose, seen);
entries.push([processedKey, processedValue]);
}
return {
[CLASS_MARKER]: 'Map',
[PROPS_MARKER]: entries
};
}
// Handle Set objects
if (value instanceof Set) {
const items = [];
for (const item of value) {
items.push(process_for_serialization(item, verbose, seen));
}
return {
[CLASS_MARKER]: 'Set',
[PROPS_MARKER]: items
};
}
// Get the constructor
const ctor = value.constructor;
// Handle registered class instances
if (ctor && ctor.name && class_registry[ctor.name]) {
const props = {};
// Extract own enumerable properties (bypass any toJSON method)
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (failed serialization)
if (processed !== undefined || propValue === undefined) {
props[key] = processed;
}
}
return {
[CLASS_MARKER]: ctor.name,
[PROPS_MARKER]: props
};
}
// Handle plain objects (constructor is Object or no constructor)
if (ctor === Object || ctor === undefined || ctor === null) {
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// Unregistered class instance - convert to plain object for hot/cold parity
// This ensures developers catch missing class registrations during development
// (methods will be lost, only properties preserved)
if (verbose) {
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
}
// Convert to plain object - properties are preserved, prototype methods are lost
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// ============================================================================
// Deserialization (String → Object)
// ============================================================================
/**
* Deserialize a JSON string, restoring ES6 class instances.
* Returns null if deserialization fails.
*
* @param str - The JSON string to deserialize
* @param verbose - Whether to log warnings
* @returns Deserialized value with class instances restored, or null if failed
*/
function deserialize_value(str, verbose) {
try {
const parsed = JSON.parse(str);
return process_for_deserialization(parsed, verbose);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Deserialization failed:', e);
}
return null;
}
}
/**
* Recursively process a parsed value, restoring class instances.
*
* @param value - The parsed value to process
* @param verbose - Whether to log warnings
* @returns Processed value with class instances restored
*/
function process_for_deserialization(value, verbose) {
// Handle primitives and null
if (value === null || value === undefined || typeof value !== 'object') {
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return value.map(item => process_for_deserialization(item, verbose));
}
// Check for class marker
if (value[CLASS_MARKER] !== undefined && value[PROPS_MARKER] !== undefined) {
const class_name = value[CLASS_MARKER];
const props = value[PROPS_MARKER];
// Handle built-in types
if (class_name === 'Date') {
return new Date(props);
}
if (class_name === 'Map') {
const map = new Map();
for (const [k, v] of props) {
map.set(process_for_deserialization(k, verbose), process_for_deserialization(v, verbose));
}
return map;
}
if (class_name === 'Set') {
const set = new Set();
for (const item of props) {
set.add(process_for_deserialization(item, verbose));
}
return set;
}
// Look up registered class
const klass = class_registry[class_name];
if (!klass) {
if (verbose) {
console.warn(`[JQHTML Cache] Cannot restore class "${class_name}" - not registered. ` +
`Data will be returned as plain object. ` +
`Call jqhtml.register_cache_class(${class_name}) to restore class instances.`);
}
// Return as plain object rather than failing completely
return process_for_deserialization(props, verbose);
}
// Create instance without calling constructor and assign properties
try {
const instance = Object.create(klass.prototype);
const processed_props = process_for_deserialization(props, verbose);
Object.assign(instance, processed_props);
return instance;
}
catch (e) {
if (verbose) {
console.warn(`[JQHTML Cache] Failed to restore class "${class_name}":`, e);
}
// Return null to signal cache should be treated as miss
return null;
}
}
// Plain object - process recursively
const result = {};
for (const key of Object.keys(value)) {
result[key] = process_for_deserialization(value[key], verbose);
}
return result;
}
class Jqhtml_Local_Storage {
/**
* Set the cache key and initialize storage
* Must be called before any get/set operations
* @param {string} cache_key - Unique identifier for this cache scope
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
*/
static set_cache_key(cache_key) {
static set_cache_key(cache_key, cache_mode = 'data') {
this._cache_key = cache_key;
this._cache_mode = cache_mode;
this._init();
}
/**
@@ -3565,6 +4088,13 @@ class Jqhtml_Local_Storage {
static has_cache_key() {
return this._cache_key !== null;
}
/**
* Get the current cache mode
* @returns {CacheMode} Current cache mode ('data' or 'html')
*/
static get_cache_mode() {
return this._cache_mode;
}
/**
* Initialize storage system and validate scope
* Called automatically after cache key is set
@@ -3599,6 +4129,13 @@ class Jqhtml_Local_Storage {
return false;
}
}
/**
* Check if verbose mode is enabled
* @private
*/
static _is_verbose() {
return window.jqhtml?.debug?.verbose === true;
}
/**
* Validate storage scope and clear JQHTML keys if cache key changed
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
@@ -3677,34 +4214,90 @@ class Jqhtml_Local_Storage {
return this._storage_available === true && this._cache_key !== null && this._initialized;
}
/**
* Set item in localStorage
* Set item in localStorage with class-aware serialization.
*
* If serialization fails (e.g., unregistered class instances, circular refs),
* the existing cache entry is removed and nothing is stored.
*
* @param {string} key - Storage key
* @param {*} value - Value to store (will be JSON serialized)
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
*/
static set(key, value) {
if (!this._is_ready()) {
return;
}
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
// Serialize with class-aware processing
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed - remove any existing cache entry
if (verbose) {
console.warn(`[JQHTML Cache] Failed to serialize value for key "${key}". ` +
`Removing existing cache entry if present.`);
}
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
// Check size before attempting to store (1MB limit)
const serialized = JSON.stringify(value);
const size_bytes = new Blob([serialized]).size;
const size_mb = size_bytes / (1024 * 1024);
if (size_mb > 1) {
console.warn(`[JQHTML Local Storage] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
if (verbose) {
console.warn(`[JQHTML Cache] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
}
// Remove existing cache entry since we can't update it
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
this._set_item(key, value, serialized);
this._set_item(key, serialized);
}
/**
* Get item from localStorage
* Get item from localStorage with class-aware deserialization.
*
* If deserialization fails, returns null (as if no cache exists).
*
* @param {string} key - Storage key
* @returns {*|null} Parsed value or null if not found/unavailable
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
*/
static get(key) {
if (!this._is_ready()) {
return null;
}
return this._get_item(key);
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
const result = deserialize_value(serialized, verbose);
if (result === null) {
// Deserialization failed - treat as cache miss
if (verbose) {
console.warn(`[JQHTML Cache] Failed to deserialize value for key "${key}". ` +
`Treating as cache miss.`);
}
return null;
}
return result;
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Failed to get item:', e);
}
return null;
}
}
/**
* Remove item from localStorage
@@ -3716,14 +4309,49 @@ class Jqhtml_Local_Storage {
}
this._remove_item(key);
}
/**
* Perform a serialize/deserialize round-trip on a value.
*
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
* (restored from cache). Unregistered class instances will be converted to plain
* objects, exactly as they would be if restored from cache.
*
* Use this to normalize data after on_load() so developers catch missing class
* registrations immediately rather than only after a page reload.
*
* @param {any} value - The value to normalize
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
*/
static normalize_for_cache(value) {
const verbose = this._is_verbose();
// Serialize to JSON string
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed completely - return original
// (This happens with circular references or other fatal issues)
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Serialization failed, returning original value');
}
return value;
}
// Deserialize back to object
const deserialized = deserialize_value(serialized, verbose);
if (deserialized === null) {
// Deserialization failed - return original
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Deserialization failed, returning original value');
}
return value;
}
return deserialized;
}
/**
* Internal set implementation with scope validation and quota handling
* @param {string} key
* @param {*} value - Original value (not used, kept for clarity)
* @param {string} serialized - Pre-serialized JSON string
* @private
*/
static _set_item(key, value, serialized) {
static _set_item(key, serialized) {
// Validate scope before every write
this._validate_scope();
const scoped_key = this._build_key(key);
@@ -3749,26 +4377,6 @@ class Jqhtml_Local_Storage {
}
}
}
/**
* Internal get implementation
* @param {string} key
* @returns {*|null}
* @private
*/
static _get_item(key) {
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
return JSON.parse(serialized);
}
catch (e) {
console.error('[JQHTML Local Storage] Failed to get item:', e);
return null;
}
}
/**
* Internal remove implementation
* @param {string} key
@@ -3785,12 +4393,14 @@ class Jqhtml_Local_Storage {
}
}
Jqhtml_Local_Storage._cache_key = null;
Jqhtml_Local_Storage._cache_mode = 'data';
Jqhtml_Local_Storage._storage_available = null;
Jqhtml_Local_Storage._initialized = false;
var localStorage$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
Jqhtml_Local_Storage: Jqhtml_Local_Storage
Jqhtml_Local_Storage: Jqhtml_Local_Storage,
register_cache_class: register_cache_class
});
/**
@@ -4053,7 +4663,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.11';
const version = '2.3.12';
// Default export with all functionality
const jqhtml = {
// Core
@@ -4137,9 +4747,17 @@ const jqhtml = {
return version;
},
// Cache key setter - enables component caching via local storage
set_cache_key(cache_key) {
Jqhtml_Local_Storage.set_cache_key(cache_key);
// cache_mode: 'data' (default, recommended) or 'html'
set_cache_key(cache_key, cache_mode = 'data') {
Jqhtml_Local_Storage.set_cache_key(cache_key, cache_mode);
},
// Get current cache mode
get_cache_mode() {
return Jqhtml_Local_Storage.get_cache_mode();
},
// Register a class for cache serialization/deserialization
// Required for ES6 class instances to be stored and restored from localStorage
register_cache_class,
// Boot - hydrate server-rendered component placeholders
boot
};
@@ -4159,5 +4777,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
}
}
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_component, register_template, render_template, version };
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, version };
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/**
* JQHTML Core v2.3.11
* JQHTML Core v2.3.12
* (c) 2025 JQHTML Team
* Released under the MIT License
*/
@@ -1127,6 +1127,15 @@ class Jqhtml_Component {
this.__data_frozen = false; // Track if this.data is currently frozen
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
this.__lifecycle_authorized = false; // Flag for lifecycle method protection
// Cache mode properties
this._cache_key = null; // Cache key for caching
// 'html' mode caching
this._cached_html = null; // Cached HTML to inject on first render
this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
// on_render synchronization (HTML cache mode)
this._on_render_complete = false; // True after on_render() has been called post-on_load
this._cid = this._generate_cid();
this._lifecycle_manager = LifecycleManager.get_instance();
// Create or wrap element
@@ -1357,6 +1366,40 @@ class Jqhtml_Component {
`The framework will automatically re-render if this.data changes during on_load().`);
}
this._log_lifecycle('render', 'start');
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
if (this._cached_html !== null) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.length });
}
// Inject cached HTML directly
this.$[0].innerHTML = this._cached_html;
// Mark that we used cached HTML (forces re-render after on_load)
this._used_cached_html = true;
// Clear cached HTML so next render uses template
this._cached_html = null;
// Mark first render complete
this._did_first_render = true;
this._log_lifecycle('render', 'complete (cached HTML)');
// NEW ARCHITECTURE: Call on_render() even after cache inject
// This ensures consistent behavior - on_render() always runs after DOM update
// Note: this.data has defaults from on_create(), not fresh data yet
const cacheRenderResult = this._call_lifecycle_sync('on_render');
if (cacheRenderResult && typeof cacheRenderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
}
// Emit lifecycle event
this.trigger('render');
// Store args/data snapshots for later comparison
try {
this._args_on_last_render = JSON.parse(JSON.stringify(this.args));
}
catch (error) {
this._args_on_last_render = null;
}
this._data_on_last_render = JSON.stringify(this.data);
return current_render_id;
}
// Determine child-finding strategy: If component is off-DOM, children can't register
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
// If attached to DOM, children register normally and we can use the fast _dom_children path.
@@ -1524,6 +1567,12 @@ class Jqhtml_Component {
this._log_lifecycle('render', 'complete');
// Call on_render() with authorization (sync) immediately after render completes
// This ensures event handlers are always re-attached after DOM updates
//
// NEW ARCHITECTURE: on_render() can now access this.data normally
// The HTML cache mode now properly synchronizes - on_render() runs after both:
// 1. Cache inject (with on_create() defaults)
// 2. Second render (with fresh data from on_load())
// Since on_render() always runs with appropriate data, no proxy restriction needed
const renderResult = this._call_lifecycle_sync('on_render');
if (renderResult && typeof renderResult.then === 'function') {
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
@@ -1544,6 +1593,12 @@ class Jqhtml_Component {
}
// Store data snapshot for refresh() comparison
this._data_on_last_render = JSON.stringify(this.data);
// HTML CACHE MODE: Mark on_render complete after second render (post-on_load)
// This signals to parent components that this component's DOM is fully rendered
// with fresh data and ready for HTML snapshot
if (this._ready_state >= 2) {
this._on_render_complete = true;
}
// Return the render ID so callers can check if this render is still current
return current_render_id;
}
@@ -1617,8 +1672,8 @@ class Jqhtml_Component {
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
await result; // Still wait for it to avoid breaking existing code
}
// CACHE READ - Hydrate this.data from cache BEFORE first render
// This happens after on_create() but before render, allowing instant first render with cached data
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
// This happens after on_create() but before render, allowing instant first render
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
@@ -1649,21 +1704,48 @@ class Jqhtml_Component {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
}
return; // Skip cache check
}
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null) {
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) hydrated from cache in create()`, { cache_key, data: this.data });
}
// Don't return - continue to snapshot this.data for on_load restoration
}
else {
// Store cache key for later use
this._cache_key = cache_key;
// Get cache mode
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) cache miss in create()`, { cache_key });
console.log(`[Cache ${cache_mode}] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, cache_mode, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
}
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML to inject on first render
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key: html_cache_key });
}
}
}
else {
// Data cache mode (default) - check for cached data to hydrate this.data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object') {
// Hydrate this.data with cached data
this.data = cached_data;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) hydrated from cache`, { cache_key, data: cached_data });
}
}
else {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cache miss`, { cache_key });
}
}
}
}
// Snapshot this.data after on_create() completes
@@ -1853,8 +1935,7 @@ class Jqhtml_Component {
// Always clear loading flag and complete coordination
this.__loading = false;
complete_coordination();
// Freeze this.data after on_load() completes
this.__data_frozen = true;
// Note: this.data stays unfrozen until after normalization below
}
// Validate that on_load() only modified this.data
let argsAfterLoad = null;
@@ -1883,12 +1964,43 @@ class Jqhtml_Component {
` ❌ this.${newProperties[0]} = value;\n` +
` ✅ this.data.${newProperties[0]} = value;`);
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
if (data_after_load !== data_before_load && data_after_load !== '{}') {
Jqhtml_Local_Storage.set(cache_key, this.data);
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
// (restored from cache). Unregistered class instances become plain objects immediately,
// so developers catch missing class registrations during development rather than
// only after a page reload when data comes from cache.
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (cache_mode === 'data') {
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load()`, { cache_key, data: this.data });
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
}
}
// Freeze this.data after normalization completes
this.__data_frozen = true;
// CACHE WRITE - If data changed during on_load(), write to cache based on mode
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
// Track if component is "dynamic" (this.data changed during on_load)
// Used by HTML cache mode for synchronization - static parents don't block children
this._is_dynamic = data_changed && data_after_load !== '{}';
if (this._is_dynamic) {
if (this._cache_key) {
if (cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready`, { cache_key: this._cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(this._cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load()`, { cache_key: this._cache_key, data: this.data });
}
}
}
}
this._ready_state = 2;
@@ -1906,6 +2018,34 @@ class Jqhtml_Component {
if (this._stopped || this._ready_state >= 4)
return;
this._log_lifecycle('ready', 'start');
// HTML CACHE MODE - New synchronization architecture:
// 1. Wait for all children to complete on_render (post-on_load)
// 2. Take HTML snapshot BEFORE waiting for children ready
// 3. This ensures we capture the DOM after on_render but before on_ready manipulations
if (this._should_cache_html_after_ready && this._cache_key) {
// Wait for all children to complete their on_render
await this._wait_for_children_on_render();
// Only cache if this component is dynamic (data changed during on_load)
// Static parents don't cache - they just let children proceed
if (this._is_dynamic) {
this._should_cache_html_after_ready = false;
// Cache the rendered HTML (async import to avoid circular dependency)
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const html = this.$.html();
const html_cache_key = `${this._cache_key}::html`;
Jqhtml_Local_Storage.set(html_cache_key, html);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
}
}
else {
// Static component - just clear the flag, don't cache
this._should_cache_html_after_ready = false;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) is static (no data change) - skipping HTML cache`);
}
}
}
// Wait for all children to reach ready state (bottom-up execution)
await this._wait_for_children_ready();
await this._call_lifecycle('on_ready');
@@ -1986,6 +2126,47 @@ class Jqhtml_Component {
// Wait for all children to be ready
await Promise.all(ready_promises);
}
/**
* Wait for all child components to complete on_render (post-on_load)
* Used by HTML cache mode to ensure DOM is fully rendered before taking snapshot
*
* HTML CACHE ARCHITECTURE:
* - Parent waits for all children to complete their on_render after on_load
* - This ensures the HTML snapshot captures fully rendered DOM
* - Static parents (is_dynamic=false) don't block - they immediately let children proceed
*
* @private
*/
async _wait_for_children_on_render() {
const children = this._get_dom_children();
if (children.length === 0) {
return; // No children, nothing to wait for
}
// Create promises for each child that hasn't completed on_render yet
const render_promises = [];
for (const child of children) {
// If child already completed on_render post-on_load, skip
if (child._on_render_complete) {
continue;
}
// Create promise that resolves when child completes on_render
const render_promise = new Promise((resolve) => {
// Poll for completion (simple approach - could use events for more efficiency)
const check = () => {
if (child._on_render_complete || child._stopped) {
resolve();
}
else {
setTimeout(check, 10);
}
};
check();
});
render_promises.push(render_promise);
}
// Wait for all children to complete on_render
await Promise.all(render_promises);
}
/**
* Reload component - re-fetch data and re-render (debounced)
*
@@ -2098,19 +2279,38 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
// Check for cached data/html when args changed
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
this._cache_key = cache_key;
if (cache_mode === 'html') {
// HTML cache mode - check for cached HTML
const html_cache_key = `${cache_key}::html`;
const cached_html = Jqhtml_Local_Storage.get(html_cache_key);
if (cached_html !== null && typeof cached_html === 'string') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] reload() - Component ${this._cid} (${this.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
}
// Store cached HTML for injection in _render()
this._cached_html = cached_html;
this.render();
rendered_from_cache = true;
}
}
else {
// Data cache mode (default) - check for cached data
const cached_data = Jqhtml_Local_Storage.get(cache_key);
if (cached_data !== null && typeof cached_data === 'object' && JSON.stringify(cached_data) !== '{}') {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
}
// Hydrate this.data with cached data
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
this.render();
rendered_from_cache = true;
}
// Unfreeze to set cached data, then re-freeze
this.__data_frozen = false;
this.data = cached_data;
this.__data_frozen = true;
await this.render();
rendered_from_cache = true;
}
}
}
@@ -2130,12 +2330,25 @@ class Jqhtml_Component {
// Freeze this.data after on_load() completes
this.__data_frozen = true;
}
// Import for cache operations
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
// (Same as in _load() - ensures hot/cold cache parity)
const reload_cache_mode = Jqhtml_Local_Storage.get_cache_mode();
if (reload_cache_mode === 'data') {
this.__data_frozen = false;
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
this.data = normalized;
this.__data_frozen = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load() in reload()`, { data: this.data });
}
}
// Check if data changed during on_load() - if so, update cache (but not if empty)
const data_after_load = JSON.stringify(this.data);
const data_changed = data_after_load !== data_before_load;
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
@@ -2153,11 +2366,22 @@ class Jqhtml_Component {
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
// Write to cache based on mode
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load() in reload()`, { cache_key, data: this.data });
this._cache_key = cache_key;
if (reload_cache_mode === 'html') {
// HTML cache mode - flag to cache HTML after children ready in _ready()
this._should_cache_html_after_ready = true;
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) will cache HTML after ready in reload()`, { cache_key });
}
}
else {
// Data cache mode (default) - write this.data to cache
Jqhtml_Local_Storage.set(cache_key, this.data);
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) cached data after on_load() in reload()`, { cache_key, data: this.data });
}
}
}
}
@@ -2283,6 +2507,15 @@ class Jqhtml_Component {
* @private
*/
_should_rerender() {
// HTML CACHE MODE - If we used cached HTML, always re-render to get live components
if (this._used_cached_html) {
if (window.jqhtml?.debug?.verbose) {
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
}
// Clear the flag
this._used_cached_html = false;
return true;
}
// Compare current data state with data state before initial render
const currentDataState = JSON.stringify(this.data);
const dataChanged = this._data_before_render !== currentDataState;
@@ -3510,6 +3743,13 @@ if (typeof window !== 'undefined' && window.jQuery) {
* - **Quota management**: Auto-clears storage when full and retries operation
* - **Scope validation**: Clears storage when cache key changes
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
*
* Class-Aware Serialization:
* ES6 class instances can be serialized and deserialized if registered via
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
* and restored to proper instances on retrieval. Nested class instances in objects
* and arrays are handled recursively.
*
* Scoping Strategy:
* Storage is scoped by a user-provided cache key (typically a session identifier,
@@ -3531,13 +3771,20 @@ if (typeof window !== 'undefined' && window.jQuery) {
* once. This ensures the application continues functioning even when storage is full.
*
* Usage:
* // Register classes that need to be cached (call once at startup)
* jqhtml.register_cache_class(Contact_Model);
* jqhtml.register_cache_class(User_Profile);
*
* // Must set cache key first (typically done once on page load)
* Jqhtml_Local_Storage.set_cache_key('user_123');
*
* // Then use storage normally
* Jqhtml_Local_Storage.set('cached_component', {html: '...', timestamp: Date.now()});
* // Then use storage normally - ES6 classes serialize automatically
* Jqhtml_Local_Storage.set('cached_component', {
* contact: new Contact_Model(),
* timestamp: Date.now()
* });
* const cached = Jqhtml_Local_Storage.get('cached_component');
* Jqhtml_Local_Storage.remove('cached_component');
* // cached.contact instanceof Contact_Model === true
*
* IMPORTANT - Volatile Storage:
* Storage can be cleared at any time due to:
@@ -3553,14 +3800,290 @@ if (typeof window !== 'undefined' && window.jQuery) {
*
* @internal This class is not exposed in the public API
*/
// ============================================================================
// Class Registry for Serialization
// ============================================================================
// Registry mapping class names to constructors
const class_registry = Object.create(null);
// Markers for serialized class instances (unique to avoid collisions with user data)
const CLASS_MARKER = '__jqhtml_class__';
const PROPS_MARKER = '__jqhtml_props__';
/**
* Register a class for cache serialization/deserialization.
* Must be called before attempting to cache instances of this class.
*
* @param klass - The class constructor to register
* @throws Error if klass is not a named function/class
*/
function register_cache_class(klass) {
if (typeof klass !== 'function' || !klass.name) {
throw new Error('[JQHTML Cache] register_cache_class requires a named class constructor');
}
class_registry[klass.name] = klass;
}
// ============================================================================
// Serialization (Object → String)
// ============================================================================
/**
* Serialize a value to JSON string, handling ES6 class instances recursively.
* Returns null if serialization fails for any reason.
*
* @param value - The value to serialize
* @param verbose - Whether to log warnings
* @returns Serialized JSON string, or null if serialization failed
*/
function serialize_value(value, verbose) {
try {
const seen = new WeakSet();
const processed = process_for_serialization(value, verbose, seen);
if (processed === undefined) {
// Serialization failed - undefined signals failure
return null;
}
return JSON.stringify(processed);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Serialization failed:', e);
}
return null;
}
}
/**
* Recursively process a value for serialization.
* Returns undefined if serialization should fail (unserializable value encountered).
*
* @param value - The value to process
* @param verbose - Whether to log warnings
* @param seen - WeakSet to detect circular references
* @returns Processed value ready for JSON.stringify, or undefined if failed
*/
function process_for_serialization(value, verbose, seen) {
// Handle null and undefined
if (value === null) {
return null;
}
if (value === undefined) {
return undefined; // JSON.stringify will omit this property
}
// Handle primitives
if (typeof value !== 'object') {
// string, number, boolean are fine
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
// bigint, symbol, function cannot be serialized
if (verbose) {
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
}
// Return undefined to omit this value (not fail entire serialization)
return undefined;
}
// Handle circular references
if (seen.has(value)) {
if (verbose) {
console.warn('[JQHTML Cache] Circular reference detected - cannot serialize');
}
return undefined; // Fail entire serialization
}
seen.add(value);
// Handle arrays
if (Array.isArray(value)) {
const result = [];
for (let i = 0; i < value.length; i++) {
const item = value[i];
const processed = process_for_serialization(item, verbose, seen);
// For arrays, we keep undefined as null to preserve indices
result.push(processed === undefined ? null : processed);
}
return result;
}
// Handle Date objects specially
if (value instanceof Date) {
return {
[CLASS_MARKER]: 'Date',
[PROPS_MARKER]: value.toISOString()
};
}
// Handle Map objects
if (value instanceof Map) {
const entries = [];
for (const [k, v] of value) {
const processedKey = process_for_serialization(k, verbose, seen);
const processedValue = process_for_serialization(v, verbose, seen);
entries.push([processedKey, processedValue]);
}
return {
[CLASS_MARKER]: 'Map',
[PROPS_MARKER]: entries
};
}
// Handle Set objects
if (value instanceof Set) {
const items = [];
for (const item of value) {
items.push(process_for_serialization(item, verbose, seen));
}
return {
[CLASS_MARKER]: 'Set',
[PROPS_MARKER]: items
};
}
// Get the constructor
const ctor = value.constructor;
// Handle registered class instances
if (ctor && ctor.name && class_registry[ctor.name]) {
const props = {};
// Extract own enumerable properties (bypass any toJSON method)
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (failed serialization)
if (processed !== undefined || propValue === undefined) {
props[key] = processed;
}
}
return {
[CLASS_MARKER]: ctor.name,
[PROPS_MARKER]: props
};
}
// Handle plain objects (constructor is Object or no constructor)
if (ctor === Object || ctor === undefined || ctor === null) {
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// Unregistered class instance - convert to plain object for hot/cold parity
// This ensures developers catch missing class registrations during development
// (methods will be lost, only properties preserved)
if (verbose) {
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
}
// Convert to plain object - properties are preserved, prototype methods are lost
const result = {};
for (const key of Object.keys(value)) {
const propValue = value[key];
const processed = process_for_serialization(propValue, verbose, seen);
// Only include if not undefined (unless original was undefined)
if (processed !== undefined || propValue === undefined) {
result[key] = processed;
}
}
return result;
}
// ============================================================================
// Deserialization (String → Object)
// ============================================================================
/**
* Deserialize a JSON string, restoring ES6 class instances.
* Returns null if deserialization fails.
*
* @param str - The JSON string to deserialize
* @param verbose - Whether to log warnings
* @returns Deserialized value with class instances restored, or null if failed
*/
function deserialize_value(str, verbose) {
try {
const parsed = JSON.parse(str);
return process_for_deserialization(parsed, verbose);
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Deserialization failed:', e);
}
return null;
}
}
/**
* Recursively process a parsed value, restoring class instances.
*
* @param value - The parsed value to process
* @param verbose - Whether to log warnings
* @returns Processed value with class instances restored
*/
function process_for_deserialization(value, verbose) {
// Handle primitives and null
if (value === null || value === undefined || typeof value !== 'object') {
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return value.map(item => process_for_deserialization(item, verbose));
}
// Check for class marker
if (value[CLASS_MARKER] !== undefined && value[PROPS_MARKER] !== undefined) {
const class_name = value[CLASS_MARKER];
const props = value[PROPS_MARKER];
// Handle built-in types
if (class_name === 'Date') {
return new Date(props);
}
if (class_name === 'Map') {
const map = new Map();
for (const [k, v] of props) {
map.set(process_for_deserialization(k, verbose), process_for_deserialization(v, verbose));
}
return map;
}
if (class_name === 'Set') {
const set = new Set();
for (const item of props) {
set.add(process_for_deserialization(item, verbose));
}
return set;
}
// Look up registered class
const klass = class_registry[class_name];
if (!klass) {
if (verbose) {
console.warn(`[JQHTML Cache] Cannot restore class "${class_name}" - not registered. ` +
`Data will be returned as plain object. ` +
`Call jqhtml.register_cache_class(${class_name}) to restore class instances.`);
}
// Return as plain object rather than failing completely
return process_for_deserialization(props, verbose);
}
// Create instance without calling constructor and assign properties
try {
const instance = Object.create(klass.prototype);
const processed_props = process_for_deserialization(props, verbose);
Object.assign(instance, processed_props);
return instance;
}
catch (e) {
if (verbose) {
console.warn(`[JQHTML Cache] Failed to restore class "${class_name}":`, e);
}
// Return null to signal cache should be treated as miss
return null;
}
}
// Plain object - process recursively
const result = {};
for (const key of Object.keys(value)) {
result[key] = process_for_deserialization(value[key], verbose);
}
return result;
}
class Jqhtml_Local_Storage {
/**
* Set the cache key and initialize storage
* Must be called before any get/set operations
* @param {string} cache_key - Unique identifier for this cache scope
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
*/
static set_cache_key(cache_key) {
static set_cache_key(cache_key, cache_mode = 'data') {
this._cache_key = cache_key;
this._cache_mode = cache_mode;
this._init();
}
/**
@@ -3570,6 +4093,13 @@ class Jqhtml_Local_Storage {
static has_cache_key() {
return this._cache_key !== null;
}
/**
* Get the current cache mode
* @returns {CacheMode} Current cache mode ('data' or 'html')
*/
static get_cache_mode() {
return this._cache_mode;
}
/**
* Initialize storage system and validate scope
* Called automatically after cache key is set
@@ -3604,6 +4134,13 @@ class Jqhtml_Local_Storage {
return false;
}
}
/**
* Check if verbose mode is enabled
* @private
*/
static _is_verbose() {
return window.jqhtml?.debug?.verbose === true;
}
/**
* Validate storage scope and clear JQHTML keys if cache key changed
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
@@ -3682,34 +4219,90 @@ class Jqhtml_Local_Storage {
return this._storage_available === true && this._cache_key !== null && this._initialized;
}
/**
* Set item in localStorage
* Set item in localStorage with class-aware serialization.
*
* If serialization fails (e.g., unregistered class instances, circular refs),
* the existing cache entry is removed and nothing is stored.
*
* @param {string} key - Storage key
* @param {*} value - Value to store (will be JSON serialized)
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
*/
static set(key, value) {
if (!this._is_ready()) {
return;
}
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
// Serialize with class-aware processing
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed - remove any existing cache entry
if (verbose) {
console.warn(`[JQHTML Cache] Failed to serialize value for key "${key}". ` +
`Removing existing cache entry if present.`);
}
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
// Check size before attempting to store (1MB limit)
const serialized = JSON.stringify(value);
const size_bytes = new Blob([serialized]).size;
const size_mb = size_bytes / (1024 * 1024);
if (size_mb > 1) {
console.warn(`[JQHTML Local Storage] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
if (verbose) {
console.warn(`[JQHTML Cache] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
}
// Remove existing cache entry since we can't update it
try {
localStorage.removeItem(scoped_key);
}
catch (e) {
// Ignore removal errors
}
return;
}
this._set_item(key, value, serialized);
this._set_item(key, serialized);
}
/**
* Get item from localStorage
* Get item from localStorage with class-aware deserialization.
*
* If deserialization fails, returns null (as if no cache exists).
*
* @param {string} key - Storage key
* @returns {*|null} Parsed value or null if not found/unavailable
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
*/
static get(key) {
if (!this._is_ready()) {
return null;
}
return this._get_item(key);
const verbose = this._is_verbose();
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
const result = deserialize_value(serialized, verbose);
if (result === null) {
// Deserialization failed - treat as cache miss
if (verbose) {
console.warn(`[JQHTML Cache] Failed to deserialize value for key "${key}". ` +
`Treating as cache miss.`);
}
return null;
}
return result;
}
catch (e) {
if (verbose) {
console.warn('[JQHTML Cache] Failed to get item:', e);
}
return null;
}
}
/**
* Remove item from localStorage
@@ -3721,14 +4314,49 @@ class Jqhtml_Local_Storage {
}
this._remove_item(key);
}
/**
* Perform a serialize/deserialize round-trip on a value.
*
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
* (restored from cache). Unregistered class instances will be converted to plain
* objects, exactly as they would be if restored from cache.
*
* Use this to normalize data after on_load() so developers catch missing class
* registrations immediately rather than only after a page reload.
*
* @param {any} value - The value to normalize
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
*/
static normalize_for_cache(value) {
const verbose = this._is_verbose();
// Serialize to JSON string
const serialized = serialize_value(value, verbose);
if (serialized === null) {
// Serialization failed completely - return original
// (This happens with circular references or other fatal issues)
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Serialization failed, returning original value');
}
return value;
}
// Deserialize back to object
const deserialized = deserialize_value(serialized, verbose);
if (deserialized === null) {
// Deserialization failed - return original
if (verbose) {
console.warn('[JQHTML Cache] normalize_for_cache: Deserialization failed, returning original value');
}
return value;
}
return deserialized;
}
/**
* Internal set implementation with scope validation and quota handling
* @param {string} key
* @param {*} value - Original value (not used, kept for clarity)
* @param {string} serialized - Pre-serialized JSON string
* @private
*/
static _set_item(key, value, serialized) {
static _set_item(key, serialized) {
// Validate scope before every write
this._validate_scope();
const scoped_key = this._build_key(key);
@@ -3754,26 +4382,6 @@ class Jqhtml_Local_Storage {
}
}
}
/**
* Internal get implementation
* @param {string} key
* @returns {*|null}
* @private
*/
static _get_item(key) {
const scoped_key = this._build_key(key);
try {
const serialized = localStorage.getItem(scoped_key);
if (serialized === null) {
return null;
}
return JSON.parse(serialized);
}
catch (e) {
console.error('[JQHTML Local Storage] Failed to get item:', e);
return null;
}
}
/**
* Internal remove implementation
* @param {string} key
@@ -3790,12 +4398,14 @@ class Jqhtml_Local_Storage {
}
}
Jqhtml_Local_Storage._cache_key = null;
Jqhtml_Local_Storage._cache_mode = 'data';
Jqhtml_Local_Storage._storage_available = null;
Jqhtml_Local_Storage._initialized = false;
var localStorage$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
Jqhtml_Local_Storage: Jqhtml_Local_Storage
Jqhtml_Local_Storage: Jqhtml_Local_Storage,
register_cache_class: register_cache_class
});
/**
@@ -4058,7 +4668,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.11';
const version = '2.3.12';
// Default export with all functionality
const jqhtml = {
// Core
@@ -4142,9 +4752,17 @@ const jqhtml = {
return version;
},
// Cache key setter - enables component caching via local storage
set_cache_key(cache_key) {
Jqhtml_Local_Storage.set_cache_key(cache_key);
// cache_mode: 'data' (default, recommended) or 'html'
set_cache_key(cache_key, cache_mode = 'data') {
Jqhtml_Local_Storage.set_cache_key(cache_key, cache_mode);
},
// Get current cache mode
get_cache_mode() {
return Jqhtml_Local_Storage.get_cache_mode();
},
// Register a class for cache serialization/deserialization
// Required for ES6 class instances to be stored and restored from localStorage
register_cache_class,
// Boot - hydrate server-rendered component placeholders
boot
};
@@ -4164,5 +4782,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
}
}
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_component, register_template, render_template, version };
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, version };
//# sourceMappingURL=jqhtml-core.esm.js.map

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,13 @@
* - **Quota management**: Auto-clears storage when full and retries operation
* - **Scope validation**: Clears storage when cache key changes
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
* - **Class-aware serialization**: ES6 class instances serialize and restore properly
*
* Class-Aware Serialization:
* ES6 class instances can be serialized and deserialized if registered via
* register_cache_class(). Classes are wrapped as {__jqhtml_class__: "Name", __jqhtml_props__: {...}}
* and restored to proper instances on retrieval. Nested class instances in objects
* and arrays are handled recursively.
*
* Scoping Strategy:
* Storage is scoped by a user-provided cache key (typically a session identifier,
@@ -31,13 +38,20 @@
* once. This ensures the application continues functioning even when storage is full.
*
* Usage:
* // Register classes that need to be cached (call once at startup)
* jqhtml.register_cache_class(Contact_Model);
* jqhtml.register_cache_class(User_Profile);
*
* // Must set cache key first (typically done once on page load)
* Jqhtml_Local_Storage.set_cache_key('user_123');
*
* // Then use storage normally
* Jqhtml_Local_Storage.set('cached_component', {html: '...', timestamp: Date.now()});
* // Then use storage normally - ES6 classes serialize automatically
* Jqhtml_Local_Storage.set('cached_component', {
* contact: new Contact_Model(),
* timestamp: Date.now()
* });
* const cached = Jqhtml_Local_Storage.get('cached_component');
* Jqhtml_Local_Storage.remove('cached_component');
* // cached.contact instanceof Contact_Model === true
*
* IMPORTANT - Volatile Storage:
* Storage can be cleared at any time due to:
@@ -53,21 +67,53 @@
*
* @internal This class is not exposed in the public API
*/
/**
* Register a class for cache serialization/deserialization.
* Must be called before attempting to cache instances of this class.
*
* @param klass - The class constructor to register
* @throws Error if klass is not a named function/class
*/
export declare function register_cache_class(klass: new (...args: any[]) => any): void;
/**
* Check if a class is registered for caching
*/
export declare function is_cache_class_registered(class_name: string): boolean;
/**
* Cache mode determines how component data is cached and restored.
*
* - 'data': (Recommended) Caches this.data with class-aware serialization.
* Requires ES6 classes to be registered via register_cache_class().
* On cache hit, this.data is hydrated before first render.
*
* - 'html': Caches rendered DOM HTML after children are ready.
* Does not require class registration. On cache hit, HTML is injected
* directly. Note: this.data is NOT populated during on_render() with
* cached HTML - use on_ready() for any DOM manipulation that depends on data.
*/
export type CacheMode = 'data' | 'html';
export declare class Jqhtml_Local_Storage {
private static _cache_key;
private static _cache_mode;
private static _storage_available;
private static _initialized;
/**
* Set the cache key and initialize storage
* Must be called before any get/set operations
* @param {string} cache_key - Unique identifier for this cache scope
* @param {CacheMode} cache_mode - Cache strategy: 'data' (default, recommended) or 'html'
*/
static set_cache_key(cache_key: string): void;
static set_cache_key(cache_key: string, cache_mode?: CacheMode): void;
/**
* Check if cache key has been set
* @returns {boolean} True if cache key is configured
*/
static has_cache_key(): boolean;
/**
* Get the current cache mode
* @returns {CacheMode} Current cache mode ('data' or 'html')
*/
static get_cache_mode(): CacheMode;
/**
* Initialize storage system and validate scope
* Called automatically after cache key is set
@@ -80,6 +126,11 @@ export declare class Jqhtml_Local_Storage {
* @private
*/
private static _is_storage_available;
/**
* Check if verbose mode is enabled
* @private
*/
private static _is_verbose;
/**
* Validate storage scope and clear JQHTML keys if cache key changed
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
@@ -106,15 +157,22 @@ export declare class Jqhtml_Local_Storage {
*/
private static _is_ready;
/**
* Set item in localStorage
* Set item in localStorage with class-aware serialization.
*
* If serialization fails (e.g., unregistered class instances, circular refs),
* the existing cache entry is removed and nothing is stored.
*
* @param {string} key - Storage key
* @param {*} value - Value to store (will be JSON serialized)
* @param {*} value - Value to store (primitives, objects, arrays, or registered class instances)
*/
static set(key: string, value: any): void;
/**
* Get item from localStorage
* Get item from localStorage with class-aware deserialization.
*
* If deserialization fails, returns null (as if no cache exists).
*
* @param {string} key - Storage key
* @returns {*|null} Parsed value or null if not found/unavailable
* @returns {*|null} Deserialized value with class instances restored, or null if not found/failed
*/
static get(key: string): any | null;
/**
@@ -122,21 +180,27 @@ export declare class Jqhtml_Local_Storage {
* @param {string} key - Storage key
*/
static remove(key: string): void;
/**
* Perform a serialize/deserialize round-trip on a value.
*
* This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
* (restored from cache). Unregistered class instances will be converted to plain
* objects, exactly as they would be if restored from cache.
*
* Use this to normalize data after on_load() so developers catch missing class
* registrations immediately rather than only after a page reload.
*
* @param {any} value - The value to normalize
* @returns {any} The value after serialize/deserialize round-trip, or original if serialization fails
*/
static normalize_for_cache(value: any): any;
/**
* Internal set implementation with scope validation and quota handling
* @param {string} key
* @param {*} value - Original value (not used, kept for clarity)
* @param {string} serialized - Pre-serialized JSON string
* @private
*/
private static _set_item;
/**
* Internal get implementation
* @param {string} key
* @returns {*|null}
* @private
*/
private static _get_item;
/**
* Internal remove implementation
* @param {string} key

View File

@@ -1 +1 @@
{"version":3,"file":"local-storage.d.ts","sourceRoot":"","sources":["../src/local-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,qBAAa,oBAAoB;IAC7B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAuB;IAChD,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwB;IACzD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAkB;IAE7C;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAK7C;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,OAAO;IAI/B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,KAAK;IAepB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAYpC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA2BjC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAIxB;;;;OAIG;IACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAqBzC;;;;OAIG;IACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAQnC;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IA4BxB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAexB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;CAS9B"}
{"version":3,"file":"local-storage.d.ts","sourceRoot":"","sources":["../src/local-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAaH;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAErE;AAySD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,qBAAa,oBAAoB;IAC7B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAuB;IAChD,OAAO,CAAC,MAAM,CAAC,WAAW,CAAqB;IAC/C,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwB;IACzD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAkB;IAE7C;;;;;OAKG;IACH,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,SAAkB,GAAG,IAAI;IAM7E;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,OAAO;IAI/B;;;OAGG;IACH,MAAM,CAAC,cAAc,IAAI,SAAS;IAIlC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,KAAK;IAepB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAYpC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAI1B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA2BjC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAIxB;;;;;;;;OAQG;IACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAkDzC;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAoCnC;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;IAiC3C;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IA4BxB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;CAS9B"}

View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/core",
"version": "2.3.11",
"version": "2.3.12",
"description": "Core runtime library for JQHTML",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1348,7 +1348,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) {
code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.3.11',\n`; // Version will be replaced during build
code += ` _jqhtml_version: '2.3.12',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;

View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/parser",
"version": "2.3.11",
"version": "2.3.12",
"description": "JQHTML template parser - converts templates to JavaScript",
"type": "module",
"main": "dist/index.js",

View File

@@ -1 +1 @@
2.3.11
2.3.12

View File

@@ -2,7 +2,7 @@
"name": "@jqhtml/vscode-extension",
"displayName": "JQHTML",
"description": "Syntax highlighting and language support for JQHTML template files",
"version": "2.3.11",
"version": "2.3.12",
"publisher": "jqhtml",
"license": "MIT",
"publishConfig": {

View File

@@ -31,7 +31,7 @@ If you are only using this module to generate minimum browser versions for Basel
However, if you are targeting Newly available, using the [`getAllVersions()`](#get-data-for-all-browser-versions) function or heavily relying on the data for downstream browsers, you should update this module more frequently. If you target a feature cut off date within the last two months and your installed version of `baseline-browser-mapping` has data that is more than 2 months old, you will receive a console warning advising you to update to the latest version when you call `getCompatibleVersions()` or `getAllVersions()`.
If you want to suppress these warnings you can use the `suppressWarnings: true` option in the configuration object passed to `getCompatibleVersions()` or `getAllVersions()`. Alternatively, you can use the `BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA=true` environment variable when running your build process. This module also respects the `BROWSERSLIST_IGNORE_OLD_DATA=true` environment variable. Environment variables can also be provided in a `.env` file from Node 20 onwards.
If you want to suppress these warnings you can use the `suppressWarnings: true` option in the configuration object passed to `getCompatibleVersions()` or `getAllVersions()`. Alternatively, you can use the `BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA=true` environment variable when running your build process. This module also respects the `BROWSERSLIST_IGNORE_OLD_DATA=true` environment variable. Environment variables can also be provided in a `.env` file from Node 20 onwards; however, this module does not load .env files automatically to avoid conflicts with other libraries with different requirements. You will need to use `process.loadEnvFile()` or a library like `dotenv` to load .env files before `baseline-browser-mapping` is called.
If you want to ensure [reproducible builds](https://www.wikiwand.com/en/articles/Reproducible_builds), we strongly recommend using the `widelyAvailableOnDate` option to fix the Widely available date on a per build basis to ensure dependent tools provide the same output and you do not produce data staleness warnings. If you are using [`browserslist`](https://github.com/browserslist/browserslist) to target Baseline Widely available, consider automatically updating your `browserslist` configuration in `package.json` or `.browserslistrc` to `baseline widely available on {YYYY-MM-DD}` as part of your build process to ensure the same or sufficiently similar list of minimum browsers is reproduced for historical builds.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"name": "baseline-browser-mapping",
"main": "./dist/index.cjs",
"version": "2.9.0",
"version": "2.9.4",
"description": "A library for obtaining browser versions with their maximum supported Baseline feature set and Widely Available status.",
"exports": {
".": {
@@ -30,9 +30,9 @@
"fix-cli-permissions": "output=$(npx baseline-browser-mapping 2>&1); path=$(printf '%s\n' \"$output\" | sed -n 's/^.*: \\(.*\\): Permission denied$/\\1/p; t; s/^\\(.*\\): Permission denied$/\\1/p'); if [ -n \"$path\" ]; then echo \"Permission denied for: $path\"; echo \"Removing $path ...\"; rm -rf \"$path\"; else echo \"$output\"; fi",
"test:format": "npx prettier --check .",
"test:lint": "npx eslint .",
"test:browserslist": "mkdir test-browserslist && cd test-browserslist && npm init -y && npm i ../../baseline-browser-mapping browserslist &&jq '. += {\"browserslist\":[\"baseline widely available with downstream\"]}' package.json >p && mv p package.json && npx browserslist && cd ../ && rm -rf test-browserslist",
"test:jasmine": "npx jasmine",
"test": "npm run build && npm run fix-cli-permissions && rm -rf test-browserslist && npm run test:format && npm run test:lint && npx jasmine && npm run test:browserslist",
"test:jasmine-browser": "npx jasmine-browser-runner runSpecs --config ./spec/support/jasmine-browser.js",
"test": "npm run build && npm run fix-cli-permissions && npm run test:format && npm run test:lint && npm run test:jasmine && npm run test:jasmine-browser",
"build": "rm -rf dist; npx prettier . --write; rollup -c; rm -rf ./dist/scripts/expose-data.d.ts ./dist/cli.d.ts",
"refresh-downstream": "npx tsx scripts/refresh-downstream.ts",
"refresh-static": "npx tsx scripts/refresh-static.ts",
@@ -42,19 +42,20 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@mdn/browser-compat-data": "^7.1.23",
"@mdn/browser-compat-data": "^7.1.24",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.3",
"@types/node": "^22.15.17",
"eslint-plugin-new-with-error": "^5.0.0",
"jasmine": "^5.8.0",
"jasmine-browser-runner": "^3.0.0",
"jasmine-spec-reporter": "^7.0.0",
"prettier": "^3.5.3",
"rollup": "^4.44.0",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.35.0",
"web-features": "^3.9.3"
"web-features": "^3.10.0"
},
"repository": {
"type": "git",

0
node_modules/electron-to-chromium/LICENSE generated vendored Executable file → Normal file
View File

0
node_modules/electron-to-chromium/README.md generated vendored Executable file → Normal file
View File

0
node_modules/electron-to-chromium/chromium-versions.js generated vendored Executable file → Normal file
View File

0
node_modules/electron-to-chromium/chromium-versions.json generated vendored Executable file → Normal file
View File

9
node_modules/electron-to-chromium/full-chromium-versions.js generated vendored Executable file → Normal file
View File

@@ -2604,7 +2604,11 @@ module.exports = {
"39.2.3"
],
"142.0.7444.177": [
"39.2.4"
"39.2.4",
"39.2.5"
],
"142.0.7444.226": [
"39.2.6"
],
"143.0.7499.0": [
"40.0.0-alpha.2"
@@ -2619,6 +2623,7 @@ module.exports = {
"40.0.0-alpha.8"
],
"144.0.7527.0": [
"40.0.0-beta.1"
"40.0.0-beta.1",
"40.0.0-beta.2"
]
};

2
node_modules/electron-to-chromium/full-chromium-versions.json generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

5
node_modules/electron-to-chromium/full-versions.js generated vendored Executable file → Normal file
View File

@@ -1670,11 +1670,14 @@ module.exports = {
"39.2.2": "142.0.7444.162",
"39.2.3": "142.0.7444.175",
"39.2.4": "142.0.7444.177",
"39.2.5": "142.0.7444.177",
"39.2.6": "142.0.7444.226",
"40.0.0-alpha.2": "143.0.7499.0",
"40.0.0-alpha.4": "144.0.7506.0",
"40.0.0-alpha.5": "144.0.7526.0",
"40.0.0-alpha.6": "144.0.7526.0",
"40.0.0-alpha.7": "144.0.7526.0",
"40.0.0-alpha.8": "144.0.7526.0",
"40.0.0-beta.1": "144.0.7527.0"
"40.0.0-beta.1": "144.0.7527.0",
"40.0.0-beta.2": "144.0.7527.0"
};

2
node_modules/electron-to-chromium/full-versions.json generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/electron-to-chromium/index.js generated vendored Executable file → Normal file
View File

2
node_modules/electron-to-chromium/package.json generated vendored Executable file → Normal file
View File

@@ -1,6 +1,6 @@
{
"name": "electron-to-chromium",
"version": "1.5.263",
"version": "1.5.266",
"description": "Provides a list of electron-to-chromium version mappings",
"main": "index.js",
"files": [

0
node_modules/electron-to-chromium/versions.js generated vendored Executable file → Normal file
View File

0
node_modules/electron-to-chromium/versions.json generated vendored Executable file → Normal file
View File

2
node_modules/nwsapi/package.json generated vendored
View File

@@ -1,6 +1,6 @@
{
"name": "nwsapi",
"version": "2.2.22",
"version": "2.2.23",
"description": "Fast CSS Selectors API Engine",
"homepage": "https://javascript.nwbox.com/nwsapi/",
"main": "./src/nwsapi",

0
node_modules/nwsapi/src/modules/nwsapi-jquery.js generated vendored Executable file → Normal file
View File

0
node_modules/nwsapi/src/modules/nwsapi-traversal.js generated vendored Executable file → Normal file
View File

8
node_modules/nwsapi/src/nwsapi.js generated vendored
View File

@@ -5,9 +5,9 @@
* nwsapi.js - Fast CSS Selectors API Engine
*
* Author: Diego Perini <diego.perini at gmail com>
* Version: 2.2.22
* Version: 2.2.23
* Created: 20070722
* Release: 20250901
* Release: 20251205
*
* License:
* https://javascript.nwbox.com/nwsapi/MIT-LICENSE
@@ -30,7 +30,7 @@
})(this, function Factory(global, Export) {
var version = 'nwsapi-2.2.22',
var version = 'nwsapi-2.2.23',
doc = global.document,
root = doc.documentElement,
@@ -1590,7 +1590,7 @@
}
// normalize input string
parsed = selectors.
parsed = unescape(selectors).
replace(/\x00|\\$/g, '\ufffd').
replace(REX.CombineWSP, '\x20').
replace(REX.PseudosWSP, '$1').

0
node_modules/terser-webpack-plugin/LICENSE generated vendored Executable file → Normal file
View File

138
node_modules/terser-webpack-plugin/README.md generated vendored Executable file → Normal file
View File

@@ -17,7 +17,9 @@ This plugin uses [terser](https://github.com/terser/terser) to minify/minimize y
## Getting Started
Webpack v5 comes with the latest `terser-webpack-plugin` out of the box. If you are using Webpack v5 or above and wish to customize the options, you will still need to install `terser-webpack-plugin`. Using Webpack v4, you have to install `terser-webpack-plugin` v4.
Webpack v5 comes with the latest `terser-webpack-plugin` out of the box.
If you are using Webpack v5 or above and wish to customize the options, you will still need to install `terser-webpack-plugin`.
Using Webpack v4, you have to install `terser-webpack-plugin` v4.
To begin, you'll need to install `terser-webpack-plugin`:
@@ -37,7 +39,7 @@ or
pnpm add -D terser-webpack-plugin
```
Then add the plugin to your `webpack` config. For example:
Then add the plugin to your `webpack` configuration. For example:
**webpack.config.js**
@@ -52,7 +54,7 @@ module.exports = {
};
```
And run `webpack` via your preferred method.
Finally, run `webpack` using the method you normally use (e.g., via CLI or an npm script).
## Note about source maps
@@ -61,7 +63,7 @@ And run `webpack` via your preferred method.
Why?
- `eval` wraps modules in `eval("string")` and the minimizer does not handle strings.
- `cheap` has not column information and minimizer generate only a single line, which leave only a single mapping.
- `cheap` has no column information and the minimizer generates only a single line, which leaves only a single mapping.
Using supported `devtool` values enable source map generation.
@@ -80,7 +82,7 @@ Using supported `devtool` values enable source map generation.
Type:
```ts
type test = string | RegExp | Array<string | RegExp>;
type test = string | RegExp | (string | RegExp)[];
```
Default: `/\.m?js(\?.*)?$/i`
@@ -107,7 +109,7 @@ module.exports = {
Type:
```ts
type include = string | RegExp | Array<string | RegExp>;
type include = string | RegExp | (string | RegExp)[];
```
Default: `undefined`
@@ -134,7 +136,7 @@ module.exports = {
Type:
```ts
type exclude = string | RegExp | Array<string | RegExp>;
type exclude = string | RegExp | (string | RegExp)[];
```
Default: `undefined`
@@ -167,6 +169,7 @@ type parallel = boolean | number;
Default: `true`
Use multi-process parallel running to improve the build speed.
Default number of concurrent runs: `os.cpus().length - 1` or `os.availableParallelism() - 1` (if this function is supported).
> **Note**
@@ -175,7 +178,7 @@ Default number of concurrent runs: `os.cpus().length - 1` or `os.availableParall
> **Warning**
>
> If you use **Circle CI** or any other environment that doesn't provide real available count of CPUs then you need to setup explicitly number of CPUs to avoid `Error: Call retries were exceeded` (see [#143](https://github.com/webpack-contrib/terser-webpack-plugin/issues/143), [#202](https://github.com/webpack-contrib/terser-webpack-plugin/issues/202)).
> If you use **Circle CI** or any other environment that doesn't provide the real available count of CPUs then you need to explicitly set up the number of CPUs to avoid `Error: Call retries were exceeded` (see [#143](https://github.com/webpack/terser-webpack-plugin/issues/143), [#202](https://github.com/webpack/terser-webpack-plugin/issues/202)).
#### `boolean`
@@ -221,9 +224,7 @@ Type:
```ts
type minify = (
input: {
[file: string]: string;
},
input: Record<string, string>,
sourceMap: import("@jridgewell/trace-mapping").SourceMapInput | undefined,
minifyOptions: {
module?: boolean | undefined;
@@ -242,7 +243,7 @@ type minify = (
pos: number;
line: number;
col: number;
}
},
) => boolean)
| {
condition?:
@@ -258,7 +259,7 @@ type minify = (
pos: number;
line: number;
col: number;
}
},
) => boolean)
| undefined;
filename?: string | ((fileData: any) => string) | undefined;
@@ -268,7 +269,7 @@ type minify = (
| ((commentsFile: string) => string)
| undefined;
}
| undefined
| undefined,
) => Promise<{
code: string;
map?: import("@jridgewell/trace-mapping").SourceMapInput | undefined;
@@ -280,7 +281,7 @@ type minify = (
Default: `TerserPlugin.terserMinify`
Allows you to override default minify function.
Allows you to override the default minify function.
By default plugin uses [terser](https://github.com/terser/terser) package.
Useful for using and testing unpublished versions or forks.
@@ -313,7 +314,6 @@ minify.getMinimizerVersion = () => {
let packageJson;
try {
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
packageJson = require("uglify-module/package.json");
} catch (error) {
// Ignore
@@ -342,7 +342,7 @@ module.exports = {
Type:
```ts
type terserOptions = {
interface terserOptions {
compress?: boolean | CompressOptions;
ecma?: ECMA;
enclose?: boolean | string;
@@ -359,7 +359,7 @@ type terserOptions = {
safari10?: boolean;
sourceMap?: boolean | SourceMapOptions;
toplevel?: boolean;
};
}
```
Default: [default](https://github.com/terser/terser#minify-options)
@@ -413,7 +413,7 @@ type extractComments =
pos: number;
line: number;
col: number;
}
},
) => boolean)
| {
condition?:
@@ -429,7 +429,7 @@ type extractComments =
pos: number;
line: number;
col: number;
}
},
) => boolean)
| undefined;
filename?: string | ((fileData: any) => string) | undefined;
@@ -444,9 +444,12 @@ type extractComments =
Default: `true`
Whether comments shall be extracted to a separate file, (see [details](https://github.com/webpack/webpack/commit/71933e979e51c533b432658d5e37917f9e71595a)).
By default extract only comments using `/^\**!|@preserve|@license|@cc_on/i` regexp condition and remove remaining comments.
By default, extract only comments using `/^\**!|@preserve|@license|@cc_on/i` RegExp condition and remove remaining comments.
If the original file is named `foo.js`, then the comments will be stored to `foo.js.LICENSE.txt`.
The `terserOptions.format.comments` option specifies whether the comment will be preserved, i.e. it is possible to preserve some comments (e.g. annotations) while extracting others or even preserving comments that have been extracted.
The `terserOptions.format.comments` option specifies whether the comment will be preserved - i.e., it is possible to preserve some comments (e.g. annotations) while extracting others, or even preserve comments that have already been extracted.
#### `boolean`
@@ -469,7 +472,7 @@ module.exports = {
#### `string`
Extract `all` or `some` (use `/^\**!|@preserve|@license|@cc_on/i` RegExp) comments.
Extract `all` or `some` (use the `/^\**!|@preserve|@license|@cc_on/i` RegExp) comments.
**webpack.config.js**
@@ -488,7 +491,7 @@ module.exports = {
#### `RegExp`
All comments that match the given expression will be extracted to the separate file.
All comments that match the given expression will be extracted to a separate file.
**webpack.config.js**
@@ -507,7 +510,7 @@ module.exports = {
#### `function`
All comments that match the given expression will be extracted to the separate file.
All comments that match the given expression will be extracted to a separate file.
**webpack.config.js**
@@ -532,7 +535,7 @@ module.exports = {
#### `object`
Allow to customize condition for extract comments, specify extracted file name and banner.
Allows you to customize condition for extracting comments, and specify the extracted file name and banner.
**webpack.config.js**
@@ -544,13 +547,11 @@ module.exports = {
new TerserPlugin({
extractComments: {
condition: /^\**!|@preserve|@license|@cc_on/i,
filename: (fileData) => {
filename: (fileData) =>
// The "fileData" argument contains object with "filename", "basename", "query" and "hash"
return `${fileData.filename}.LICENSE.txt${fileData.query}`;
},
banner: (licenseFile) => {
return `License information can be found in ${licenseFile}`;
},
`${fileData.filename}.LICENSE.txt${fileData.query}`,
banner: (licenseFile) =>
`License information can be found in ${licenseFile}`,
},
}),
],
@@ -576,12 +577,12 @@ type condition =
pos: number;
line: number;
col: number;
}
},
) => boolean)
| undefined;
```
Condition what comments you need extract.
The condition that determines which comments should be extracted.
**webpack.config.js**
@@ -593,13 +594,11 @@ module.exports = {
new TerserPlugin({
extractComments: {
condition: "some",
filename: (fileData) => {
filename: (fileData) =>
// The "fileData" argument contains object with "filename", "basename", "query" and "hash"
return `${fileData.filename}.LICENSE.txt${fileData.query}`;
},
banner: (licenseFile) => {
return `License information can be found in ${licenseFile}`;
},
`${fileData.filename}.LICENSE.txt${fileData.query}`,
banner: (licenseFile) =>
`License information can be found in ${licenseFile}`,
},
}),
],
@@ -620,11 +619,12 @@ Default: `[file].LICENSE.txt[query]`
Available placeholders: `[file]`, `[query]` and `[filebase]` (`[base]` for webpack 5).
The file where the extracted comments will be stored.
Default is to append the suffix `.LICENSE.txt` to the original filename.
> **Warning**
>
> We highly recommend using the `txt` extension. Using `js`/`cjs`/`mjs` extensions may conflict with existing assets which leads to broken code.
> We highly recommend using the `.txt` extension. Using `.js`/`.cjs`/`.mjs` extensions may conflict with existing assets, which leads to broken code.
**webpack.config.js**
@@ -637,9 +637,8 @@ module.exports = {
extractComments: {
condition: /^\**!|@preserve|@license|@cc_on/i,
filename: "extracted-comments.js",
banner: (licenseFile) => {
return `License information can be found in ${licenseFile}`;
},
banner: (licenseFile) =>
`License information can be found in ${licenseFile}`,
},
}),
],
@@ -657,9 +656,11 @@ type banner = string | boolean | ((commentsFile: string) => string) | undefined;
Default: `/*! For license information please see ${commentsFile} */`
The banner text that points to the extracted file and will be added on top of the original file.
Can be `false` (no banner), a `String`, or a `Function<(string) -> String>` that will be called with the filename where extracted comments have been stored.
Will be wrapped into comment.
The banner text that points to the extracted file and will be added at the top of the original file.
It can be `false` (no banner), a `String`, or a `function<(string) -> String>` that will be called with the filename where the extracted comments have been stored.
The banner will be wrapped in a comment.
**webpack.config.js**
@@ -671,13 +672,11 @@ module.exports = {
new TerserPlugin({
extractComments: {
condition: true,
filename: (fileData) => {
filename: (fileData) =>
// The "fileData" argument contains object with "filename", "basename", "query" and "hash"
return `${fileData.filename}.LICENSE.txt${fileData.query}`;
},
banner: (commentsFile) => {
return `My custom banner about license information ${commentsFile}`;
},
`${fileData.filename}.LICENSE.txt${fileData.query}`,
banner: (commentsFile) =>
`My custom banner about license information ${commentsFile}`,
},
}),
],
@@ -713,7 +712,7 @@ module.exports = {
### Remove Comments
If you avoid building with comments, use this config:
If you want to build without comments, use this config:
**webpack.config.js**
@@ -759,11 +758,11 @@ module.exports = {
### [`swc`](https://github.com/swc-project/swc)
[`swc`](https://github.com/swc-project/swc) is a super-fast compiler written in rust; producing widely-supported javascript from modern standards and typescript.
[`swc`](https://github.com/swc-project/swc) is a super-fast compiler written in `Rust`, producing widely supported JavaScript from modern standards and TypeScript.
> **Warning**
>
> the `extractComments` option is not supported and all comments will be removed by default, it will be fixed in future
> The `extractComments` option is not supported, and all comments will be removed by default. This will be fixed in future
**webpack.config.js**
@@ -789,7 +788,7 @@ module.exports = {
> **Warning**
>
> the `extractComments` option is not supported and all legal comments (i.e. copyright, licenses and etc) will be preserved
> The `extractComments` option is not supported, and all legal comments (i.e. copyright, licenses and etc) will be preserved.
**webpack.config.js**
@@ -818,7 +817,7 @@ module.exports = {
### Custom Minify Function
Override default minify function - use `uglify-js` for minification.
Override the default minify function - use `uglify-js` for minification.
**webpack.config.js**
@@ -850,7 +849,7 @@ module.exports = {
### Typescript
With default terser minify function:
With default Terser minify function:
```ts
module.exports = {
@@ -870,10 +869,10 @@ module.exports = {
With built-in minify functions:
```ts
import type { JsMinifyOptions as SwcOptions } from "@swc/core";
import type { MinifyOptions as UglifyJSOptions } from "uglify-js";
import type { TransformOptions as EsbuildOptions } from "esbuild";
import type { MinifyOptions as TerserOptions } from "terser";
import { type JsMinifyOptions as SwcOptions } from "@swc/core";
import { type TransformOptions as EsbuildOptions } from "esbuild";
import { type MinifyOptions as TerserOptions } from "terser";
import { type MinifyOptions as UglifyJSOptions } from "uglify-js";
module.exports = {
optimization: {
@@ -912,9 +911,10 @@ module.exports = {
## Contributing
Please take a moment to read our contributing guidelines if you haven't yet done so.
We welcome all contributions!
If you're new here, please take a moment to review our contributing guidelines before submitting issues or pull requests.
[CONTRIBUTING](./.github/CONTRIBUTING.md)
[CONTRIBUTING](https://github.com/webpack/terser-webpack-plugin?tab=contributing-ov-file#contributing)
## License
@@ -924,10 +924,10 @@ Please take a moment to read our contributing guidelines if you haven't yet done
[npm-url]: https://npmjs.com/package/terser-webpack-plugin
[node]: https://img.shields.io/node/v/terser-webpack-plugin.svg
[node-url]: https://nodejs.org
[tests]: https://github.com/webpack-contrib/terser-webpack-plugin/workflows/terser-webpack-plugin/badge.svg
[tests-url]: https://github.com/webpack-contrib/terser-webpack-plugin/actions
[cover]: https://codecov.io/gh/webpack-contrib/terser-webpack-plugin/branch/master/graph/badge.svg
[cover-url]: https://codecov.io/gh/webpack-contrib/terser-webpack-plugin
[tests]: https://github.com/webpack/terser-webpack-plugin/workflows/terser-webpack-plugin/badge.svg
[tests-url]: https://github.com/webpack/terser-webpack-plugin/actions
[cover]: https://codecov.io/gh/webpack/terser-webpack-plugin/branch/main/graph/badge.svg
[cover-url]: https://codecov.io/gh/webpack/terser-webpack-plugin
[discussion]: https://img.shields.io/github/discussions/webpack/webpack
[discussion-url]: https://github.com/webpack/webpack/discussions
[size]: https://packagephobia.now.sh/badge?p=terser-webpack-plugin

333
node_modules/terser-webpack-plugin/dist/index.js generated vendored Executable file → Normal file
View File

@@ -1,46 +1,49 @@
"use strict";
const path = require("path");
const os = require("os");
const path = require("path");
const {
validate
} = require("schema-utils");
const {
throttleAll,
memoize,
terserMinify,
uglifyJsMinify,
swcMinify,
esbuildMinify
} = require("./utils");
const schema = require("./options.json");
const {
minify
} = require("./minify");
const schema = require("./options.json");
const {
esbuildMinify,
memoize,
swcMinify,
terserMinify,
throttleAll,
uglifyJsMinify
} = require("./utils");
/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack").WebpackError} WebpackError */
/** @typedef {import("webpack").Configuration} Configuration */
/** @typedef {import("webpack").Asset} Asset */
/** @typedef {import("webpack").AssetInfo} AssetInfo */
/** @typedef {import("jest-worker").Worker} JestWorker */
/** @typedef {import("@jridgewell/trace-mapping").SourceMapInput} SourceMapInput */
/** @typedef {import("@jridgewell/trace-mapping").EncodedSourceMap & { sources: string[], sourcesContent?: string[], file: string }} RawSourceMap */
/** @typedef {import("@jridgewell/trace-mapping").TraceMap} TraceMap */
/** @typedef {RegExp | string} Rule */
/** @typedef {Rule[] | Rule} Rules */
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @callback ExtractCommentsFunction
* @param {any} astNode
* @param {{ value: string, type: 'comment1' | 'comment2' | 'comment3' | 'comment4', pos: number, line: number, col: number }} comment
* @returns {boolean}
* @param {any} astNode ast Node
* @param {{ value: string, type: 'comment1' | 'comment2' | 'comment3' | 'comment4', pos: number, line: number, col: number }} comment comment node
* @returns {boolean} true when need to extract comment, otherwise false
*/
/**
* @typedef {boolean | 'all' | 'some' | RegExp | ExtractCommentsFunction} ExtractCommentsCondition
*/
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @typedef {string | ((fileData: any) => string)} ExtractCommentsFilename
*/
@@ -50,10 +53,10 @@ const {
*/
/**
* @typedef {Object} ExtractCommentsObject
* @property {ExtractCommentsCondition} [condition]
* @property {ExtractCommentsFilename} [filename]
* @property {ExtractCommentsBanner} [banner]
* @typedef {object} ExtractCommentsObject
* @property {ExtractCommentsCondition=} condition condition which comments need to be expected
* @property {ExtractCommentsFilename=} filename filename for extracted comments
* @property {ExtractCommentsBanner=} banner banner in filename for extracted comments
*/
/**
@@ -61,18 +64,27 @@ const {
*/
/**
* @typedef {Object} MinimizedResult
* @property {string} code
* @property {SourceMapInput} [map]
* @property {Array<Error | string>} [errors]
* @property {Array<Error | string>} [warnings]
* @property {Array<string>} [extractedComments]
* @typedef {object} ErrorObject
* @property {string} message message
* @property {number=} line line number
* @property {number=} column column number
* @property {string=} stack error stack trace
*/
/**
* @typedef {object} MinimizedResult
* @property {string=} code code
* @property {RawSourceMap=} map source map
* @property {Array<Error | string>=} errors errors
* @property {Array<Error | string>=} warnings warnings
* @property {Array<string>=} extractedComments extracted comments
*/
/**
* @typedef {{ [file: string]: string }} Input
*/
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @typedef {{ [key: string]: any }} CustomOptions
*/
@@ -84,9 +96,9 @@ const {
/**
* @template T
* @typedef {Object} PredefinedOptions
* @property {T extends { module?: infer P } ? P : boolean | string} [module]
* @property {T extends { ecma?: infer P } ? P : number | string} [ecma]
* @typedef {object} PredefinedOptions
* @property {T extends { module?: infer P } ? P : boolean | string=} module true when code is a EC module, otherwise false
* @property {T extends { ecma?: infer P } ? P : number | string=} ecma ecma version
*/
/**
@@ -98,16 +110,16 @@ const {
* @template T
* @callback BasicMinimizerImplementation
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {RawSourceMap | undefined} sourceMap
* @param {MinimizerOptions<T>} minifyOptions
* @param {ExtractCommentsOptions | undefined} extractComments
* @returns {Promise<MinimizedResult>}
* @returns {Promise<MinimizedResult> | MinimizedResult}
*/
/**
* @typedef {object} MinimizeFunctionHelpers
* @property {() => string | undefined} [getMinimizerVersion]
* @property {() => boolean | undefined} [supportsWorkerThreads]
* @property {() => string | undefined=} getMinimizerVersion function that returns version of minimizer
* @property {() => boolean | undefined=} supportsWorkerThreads true when minimizer support worker threads, otherwise false
*/
/**
@@ -117,17 +129,17 @@ const {
/**
* @template T
* @typedef {Object} InternalOptions
* @property {string} name
* @property {string} input
* @property {SourceMapInput | undefined} inputSourceMap
* @property {ExtractCommentsOptions | undefined} extractComments
* @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> }} minimizer
* @typedef {object} InternalOptions
* @property {string} name name
* @property {string} input input
* @property {RawSourceMap | undefined} inputSourceMap input source map
* @property {ExtractCommentsOptions | undefined} extractComments extract comments option
* @property {{ implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> }} minimizer minimizer
*/
/**
* @template T
* @typedef {JestWorker & { transform: (options: string) => MinimizedResult, minify: (options: InternalOptions<T>) => MinimizedResult }} MinimizerWorker
* @typedef {JestWorker & { transform: (options: string) => Promise<MinimizedResult>, minify: (options: InternalOptions<T>) => Promise<MinimizedResult> }} MinimizerWorker
*/
/**
@@ -135,12 +147,12 @@ const {
*/
/**
* @typedef {Object} BasePluginOptions
* @property {Rules} [test]
* @property {Rules} [include]
* @property {Rules} [exclude]
* @property {ExtractCommentsOptions} [extractComments]
* @property {Parallel} [parallel]
* @typedef {object} BasePluginOptions
* @property {Rules=} test test rule
* @property {Rules=} include include rile
* @property {Rules=} exclude exclude rule
* @property {ExtractCommentsOptions=} extractComments extract comments options
* @property {Parallel=} parallel parallel option
*/
/**
@@ -153,19 +165,15 @@ const {
* @typedef {BasePluginOptions & { minimizer: { implementation: MinimizerImplementation<T>, options: MinimizerOptions<T> } }} InternalPluginOptions
*/
const getTraceMapping = memoize(() =>
// eslint-disable-next-line global-require
require("@jridgewell/trace-mapping"));
const getSerializeJavascript = memoize(() =>
// eslint-disable-next-line global-require
require("serialize-javascript"));
const getTraceMapping = memoize(() => require("@jridgewell/trace-mapping"));
const getSerializeJavascript = memoize(() => require("serialize-javascript"));
/**
* @template [T=import("terser").MinifyOptions]
*/
class TerserPlugin {
/**
* @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>} [options]
* @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>=} options options
*/
constructor(options) {
validate( /** @type {Schema} */schema, options || {}, {
@@ -203,26 +211,26 @@ class TerserPlugin {
/**
* @private
* @param {any} input
* @returns {boolean}
* @param {unknown} input Input to check
* @returns {boolean} Whether input is a source map
*/
static isSourceMap(input) {
// All required options for `new TraceMap(...options)`
// https://github.com/jridgewell/trace-mapping#usage
return Boolean(input && input.version && input.sources && Array.isArray(input.sources) && typeof input.mappings === "string");
return Boolean(input && typeof input === "object" && input !== null && "version" in input && "sources" in input && Array.isArray(input.sources) && "mappings" in input && typeof input.mappings === "string");
}
/**
* @private
* @param {unknown} warning
* @param {string} file
* @returns {Error}
* @param {unknown} warning warning
* @param {string} file file
* @returns {Error} built warning
*/
static buildWarning(warning, file) {
/**
* @type {Error & { hideStack: true, file: string }}
*/
// @ts-ignore
// @ts-expect-error
const builtWarning = new Error(warning.toString());
builtWarning.name = "Warning";
builtWarning.hideStack = true;
@@ -232,11 +240,11 @@ class TerserPlugin {
/**
* @private
* @param {any} error
* @param {string} file
* @param {TraceMap} [sourceMap]
* @param {Compilation["requestShortener"]} [requestShortener]
* @returns {Error}
* @param {Error | ErrorObject | string} error error
* @param {string} file file
* @param {TraceMap=} sourceMap source map
* @param {Compilation["requestShortener"]=} requestShortener request shortener
* @returns {Error} built error
*/
static buildError(error, file, sourceMap, requestShortener) {
/**
@@ -248,17 +256,21 @@ class TerserPlugin {
builtError.file = file;
return builtError;
}
if (error.line) {
if ( /** @type {ErrorObject} */error.line) {
const {
line,
column
} = /** @type {ErrorObject & { line: number, column: number }} */error;
const original = sourceMap && getTraceMapping().originalPositionFor(sourceMap, {
line: error.line,
column: error.col
line,
column
});
if (original && original.source && requestShortener) {
builtError = new Error(`${file} from Terser plugin\n${error.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${error.line},${error.col}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
builtError = new Error(`${file} from Terser plugin\n${error.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
builtError.file = file;
return builtError;
}
builtError = new Error(`${file} from Terser plugin\n${error.message} [${file}:${error.line},${error.col}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
builtError = new Error(`${file} from Terser plugin\n${error.message} [${file}:${line},${column}]${error.stack ? `\n${error.stack.split("\n").slice(1).join("\n")}` : ""}`);
builtError.file = file;
return builtError;
}
@@ -274,13 +286,17 @@ class TerserPlugin {
/**
* @private
* @param {Parallel} parallel
* @returns {number}
* @param {Parallel} parallel value of the `parallel` option
* @returns {number} number of cores for parallelism
*/
static getAvailableNumberOfCores(parallel) {
// In some cases cpus() returns undefined
// https://github.com/nodejs/node/issues/19022
const cpus = typeof os.availableParallelism === "function" ? {
const cpus =
// eslint-disable-next-line n/no-unsupported-features/node-builtins
typeof os.availableParallelism === "function" ?
// eslint-disable-next-line n/no-unsupported-features/node-builtins
{
length: os.availableParallelism()
} : os.cpus() || {
length: 1
@@ -290,10 +306,10 @@ class TerserPlugin {
/**
* @private
* @param {Compiler} compiler
* @param {Compilation} compilation
* @param {Record<string, import("webpack").sources.Source>} assets
* @param {{availableNumberOfCores: number}} optimizeOptions
* @param {Compiler} compiler compiler
* @param {Compilation} compilation compilation
* @param {Record<string, import("webpack").sources.Source>} assets assets
* @param {{ availableNumberOfCores: number }} optimizeOptions optimize options
* @returns {Promise<void>}
*/
async optimize(compiler, compilation, assets, optimizeOptions) {
@@ -310,9 +326,7 @@ class TerserPlugin {
info.extractedComments) {
return false;
}
if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(
// eslint-disable-next-line no-undefined
undefined, this.options)(name)) {
if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {
return false;
}
return true;
@@ -349,13 +363,10 @@ class TerserPlugin {
if (optimizeOptions.availableNumberOfCores > 0) {
// Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
numberOfWorkers = Math.min(numberOfAssets, optimizeOptions.availableNumberOfCores);
// eslint-disable-next-line consistent-return
getWorker = () => {
if (initializedWorker) {
return initializedWorker;
}
// eslint-disable-next-line global-require
const {
Worker
} = require("jest-worker");
@@ -401,7 +412,7 @@ class TerserPlugin {
} = asset;
if (!output) {
let input;
/** @type {SourceMapInput | undefined} */
/** @type {RawSourceMap | undefined} */
let inputSourceMap;
const {
source: sourceFromInputSource,
@@ -410,10 +421,9 @@ class TerserPlugin {
input = sourceFromInputSource;
if (map) {
if (!TerserPlugin.isSourceMap(map)) {
compilation.warnings.push( /** @type {WebpackError} */
new Error(`${name} contains invalid source map`));
compilation.warnings.push(new Error(`${name} contains invalid source map`));
} else {
inputSourceMap = /** @type {SourceMapInput} */map;
inputSourceMap = /** @type {RawSourceMap} */map;
}
}
if (Buffer.isBuffer(input)) {
@@ -429,7 +439,6 @@ class TerserPlugin {
inputSourceMap,
minimizer: {
implementation: this.options.minimizer.implementation,
// @ts-ignore https://github.com/Microsoft/TypeScript/issues/10727
options: {
...this.options.minimizer.options
}
@@ -455,25 +464,19 @@ class TerserPlugin {
output = await (getWorker ? getWorker().transform(getSerializeJavascript()(options)) : minify(options));
} catch (error) {
const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
compilation.errors.push( /** @type {WebpackError} */
TerserPlugin.buildError(error, name, hasSourceMap ? new (getTraceMapping().TraceMap)( /** @type {SourceMapInput} */inputSourceMap) :
// eslint-disable-next-line no-undefined
undefined,
// eslint-disable-next-line no-undefined
hasSourceMap ? compilation.requestShortener : undefined));
compilation.errors.push(TerserPlugin.buildError( /** @type {Error | ErrorObject | string} */
error, name, hasSourceMap ? new (getTraceMapping().TraceMap)( /** @type {RawSourceMap} */
inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
return;
}
if (typeof output.code === "undefined") {
compilation.errors.push( /** @type {WebpackError} */
new Error(`${name} from Terser plugin\nMinimizer doesn't return result`));
return;
compilation.errors.push(new Error(`${name} from Terser plugin\nMinimizer doesn't return result`));
}
if (output.warnings && output.warnings.length > 0) {
output.warnings = output.warnings.map(
/**
* @param {Error | string} item
* @param {Error | string} item a warning
* @returns {Error} built warning with extra info
*/
item => TerserPlugin.buildWarning(item, name));
}
@@ -481,60 +484,61 @@ class TerserPlugin {
const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
output.errors = output.errors.map(
/**
* @param {Error | string} item
* @param {Error | string} item an error
* @returns {Error} built error with extra info
*/
item => TerserPlugin.buildError(item, name, hasSourceMap ? new (getTraceMapping().TraceMap)( /** @type {SourceMapInput} */inputSourceMap) :
// eslint-disable-next-line no-undefined
undefined,
// eslint-disable-next-line no-undefined
hasSourceMap ? compilation.requestShortener : undefined));
item => TerserPlugin.buildError(item, name, hasSourceMap ? new (getTraceMapping().TraceMap)( /** @type {RawSourceMap} */
inputSourceMap) : undefined, hasSourceMap ? compilation.requestShortener : undefined));
}
let shebang;
if ( /** @type {ExtractCommentsObject} */
this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith("#!")) {
const firstNewlinePosition = output.code.indexOf("\n");
shebang = output.code.substring(0, firstNewlinePosition);
output.code = output.code.substring(firstNewlinePosition + 1);
}
if (output.map) {
output.source = new SourceMapSource(output.code, name, output.map, input, /** @type {SourceMapInput} */inputSourceMap, true);
} else {
output.source = new RawSource(output.code);
}
if (output.extractedComments && output.extractedComments.length > 0) {
const commentsFilename = /** @type {ExtractCommentsObject} */
this.options.extractComments.filename || "[file].LICENSE.txt[query]";
let query = "";
let filename = name;
const querySplit = filename.indexOf("?");
if (querySplit >= 0) {
query = filename.slice(querySplit);
filename = filename.slice(0, querySplit);
}
const lastSlashIndex = filename.lastIndexOf("/");
const basename = lastSlashIndex === -1 ? filename : filename.slice(lastSlashIndex + 1);
const data = {
filename,
basename,
query
};
output.commentsFilename = compilation.getPath(commentsFilename, data);
let banner;
// Add a banner to the original file
if (output.code) {
let shebang;
if ( /** @type {ExtractCommentsObject} */
this.options.extractComments.banner !== false) {
banner = /** @type {ExtractCommentsObject} */
this.options.extractComments.banner || `For license information please see ${path.relative(path.dirname(name), output.commentsFilename).replace(/\\/g, "/")}`;
if (typeof banner === "function") {
banner = banner(output.commentsFilename);
}
if (banner) {
output.source = new ConcatSource(shebang ? `${shebang}\n` : "", `/*! ${banner} */\n`, output.source);
}
this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith("#!")) {
const firstNewlinePosition = output.code.indexOf("\n");
shebang = output.code.slice(0, Math.max(0, firstNewlinePosition));
output.code = output.code.slice(Math.max(0, firstNewlinePosition + 1));
}
if (output.map) {
output.source = new SourceMapSource(output.code, name, output.map, input, /** @type {RawSourceMap} */
inputSourceMap, true);
} else {
output.source = new RawSource(output.code);
}
if (output.extractedComments && output.extractedComments.length > 0) {
const commentsFilename = /** @type {ExtractCommentsObject} */
this.options.extractComments.filename || "[file].LICENSE.txt[query]";
let query = "";
let filename = name;
const querySplit = filename.indexOf("?");
if (querySplit >= 0) {
query = filename.slice(querySplit);
filename = filename.slice(0, querySplit);
}
const lastSlashIndex = filename.lastIndexOf("/");
const basename = lastSlashIndex === -1 ? filename : filename.slice(lastSlashIndex + 1);
const data = {
filename,
basename,
query
};
output.commentsFilename = compilation.getPath(commentsFilename, data);
let banner;
// Add a banner to the original file
if ( /** @type {ExtractCommentsObject} */
this.options.extractComments.banner !== false) {
banner = /** @type {ExtractCommentsObject} */
this.options.extractComments.banner || `For license information please see ${path.relative(path.dirname(name), output.commentsFilename).replace(/\\/g, "/")}`;
if (typeof banner === "function") {
banner = banner(output.commentsFilename);
}
if (banner) {
output.source = new ConcatSource(shebang ? `${shebang}\n` : "", `/*! ${banner} */\n`, output.source);
}
}
const extractedCommentsString = output.extractedComments.sort().join("\n\n");
output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
}
const extractedCommentsString = output.extractedComments.sort().join("\n\n");
output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
}
await cacheItem.storePromise({
source: output.source,
@@ -546,16 +550,19 @@ class TerserPlugin {
}
if (output.warnings && output.warnings.length > 0) {
for (const warning of output.warnings) {
compilation.warnings.push( /** @type {WebpackError} */warning);
compilation.warnings.push(warning);
}
}
if (output.errors && output.errors.length > 0) {
for (const error of output.errors) {
compilation.errors.push( /** @type {WebpackError} */error);
compilation.errors.push(error);
}
}
if (!output.source) {
return;
}
/** @type {Record<string, any>} */
/** @type {AssetInfo} */
const newInfo = {
minimized: true
};
@@ -586,15 +593,15 @@ class TerserPlugin {
await initializedWorker.end();
}
/** @typedef {{ source: import("webpack").sources.Source, commentsFilename: string, from: string }} ExtractedCommentsInfoWIthFrom */
await Array.from(allExtractedComments).sort().reduce(
/** @typedef {{ source: import("webpack").sources.Source, commentsFilename: string, from: string }} ExtractedCommentsInfoWithFrom */
await [...allExtractedComments].sort().reduce(
/**
* @param {Promise<unknown>} previousPromise
* @param {[string, ExtractedCommentsInfo]} extractedComments
* @returns {Promise<ExtractedCommentsInfoWIthFrom>}
* @param {Promise<unknown>} previousPromise previous result
* @param {[string, ExtractedCommentsInfo]} extractedComments extracted comments
* @returns {Promise<ExtractedCommentsInfoWithFrom>} extract comments with info
*/
async (previousPromise, [from, value]) => {
const previous = /** @type {ExtractedCommentsInfoWIthFrom | undefined} **/
const previous = /** @type {ExtractedCommentsInfoWithFrom | undefined} * */
await previousPromise;
const {
commentsFilename,
@@ -610,7 +617,7 @@ class TerserPlugin {
const eTag = [prevSource, extractedCommentsSource].map(item => cache.getLazyHashedEtag(item)).reduce((previousValue, currentValue) => cache.mergeEtags(previousValue, currentValue));
let source = await cache.getPromise(name, eTag);
if (!source) {
source = new ConcatSource(Array.from(new Set([... /** @type {string}*/prevSource.source().split("\n\n"), ... /** @type {string}*/extractedCommentsSource.source().split("\n\n")])).join("\n\n"));
source = new ConcatSource([...new Set([... /** @type {string} */prevSource.source().split("\n\n"), ... /** @type {string} */extractedCommentsSource.source().split("\n\n")])].join("\n\n"));
await cache.storePromise(name, eTag, source);
}
compilation.updateAsset(commentsFilename, source);
@@ -641,8 +648,8 @@ class TerserPlugin {
/**
* @private
* @param {any} environment
* @returns {number}
* @param {NonNullable<NonNullable<Configuration["output"]>["environment"]>} environment environment
* @returns {number} ecma version
*/
static getEcmaVersion(environment) {
// ES 6th
@@ -658,7 +665,7 @@ class TerserPlugin {
}
/**
* @param {Compiler} compiler
* @param {Compiler} compiler compiler
* @returns {void}
*/
apply(compiler) {
@@ -685,7 +692,7 @@ class TerserPlugin {
stats.hooks.print.for("asset.info.minimized").tap("terser-webpack-plugin", (minimized, {
green,
formatFlag
}) => minimized ? /** @type {Function} */green( /** @type {Function} */formatFlag("minimized")) : "");
}) => minimized ? /** @type {(text: string) => string} */green( /** @type {(flag: string) => string} */formatFlag("minimized")) : "");
});
});
}

15
node_modules/terser-webpack-plugin/dist/minify.js generated vendored Executable file → Normal file
View File

@@ -5,8 +5,8 @@
/**
* @template T
* @param {import("./index.js").InternalOptions<T>} options
* @returns {Promise<MinimizedResult>}
* @param {import("./index.js").InternalOptions<T>} options options
* @returns {Promise<MinimizedResult>} minified result
*/
async function minify(options) {
const {
@@ -25,21 +25,22 @@ async function minify(options) {
}
/**
* @param {string} options
* @returns {Promise<MinimizedResult>}
* @param {string} options options
* @returns {Promise<MinimizedResult>} minified result
*/
async function transform(options) {
// 'use strict' => this === undefined (Clean Scope)
// Safer for possible security issues, albeit not critical at all here
// eslint-disable-next-line no-param-reassign
const evaluatedOptions =
/**
* @template T
* @type {import("./index.js").InternalOptions<T>}
* */
*/
// eslint-disable-next-line no-new-func
new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`)(exports, require, module, __filename, __dirname);
new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`) // eslint-disable-next-line n/exports-style
(exports, require, module, __filename, __dirname);
return minify(evaluatedOptions);
}
module.exports = {

20
node_modules/terser-webpack-plugin/dist/options.json generated vendored Executable file → Normal file
View File

@@ -39,7 +39,7 @@
"properties": {
"test": {
"description": "Include all modules that pass test assertion.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#test",
"link": "https://github.com/webpack/terser-webpack-plugin#test",
"oneOf": [
{
"$ref": "#/definitions/Rules"
@@ -48,7 +48,7 @@
},
"include": {
"description": "Include all modules matching any of these conditions.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#include",
"link": "https://github.com/webpack/terser-webpack-plugin#include",
"oneOf": [
{
"$ref": "#/definitions/Rules"
@@ -57,7 +57,7 @@
},
"exclude": {
"description": "Exclude all modules matching any of these conditions.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#exclude",
"link": "https://github.com/webpack/terser-webpack-plugin#exclude",
"oneOf": [
{
"$ref": "#/definitions/Rules"
@@ -66,13 +66,13 @@
},
"terserOptions": {
"description": "Options for `terser` (by default) or custom `minify` function.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions",
"link": "https://github.com/webpack/terser-webpack-plugin#terseroptions",
"additionalProperties": true,
"type": "object"
},
"extractComments": {
"description": "Whether comments shall be extracted to a separate file.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#extractcomments",
"link": "https://github.com/webpack/terser-webpack-plugin#extractcomments",
"anyOf": [
{
"type": "boolean"
@@ -107,7 +107,7 @@
}
],
"description": "Condition what comments you need extract.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#condition"
"link": "https://github.com/webpack/terser-webpack-plugin#condition"
},
"filename": {
"anyOf": [
@@ -120,7 +120,7 @@
}
],
"description": "The file where the extracted comments will be stored. Default is to append the suffix .LICENSE.txt to the original filename.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#filename"
"link": "https://github.com/webpack/terser-webpack-plugin#filename"
},
"banner": {
"anyOf": [
@@ -136,7 +136,7 @@
}
],
"description": "The banner text that points to the extracted file and will be added on top of the original file",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#banner"
"link": "https://github.com/webpack/terser-webpack-plugin#banner"
}
},
"type": "object"
@@ -145,7 +145,7 @@
},
"parallel": {
"description": "Use multi-process parallel running to improve the build speed.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#parallel",
"link": "https://github.com/webpack/terser-webpack-plugin#parallel",
"anyOf": [
{
"type": "boolean"
@@ -157,7 +157,7 @@
},
"minify": {
"description": "Allows you to override default minify function.",
"link": "https://github.com/webpack-contrib/terser-webpack-plugin#number",
"link": "https://github.com/webpack/terser-webpack-plugin#number",
"instanceof": "Function"
}
}

363
node_modules/terser-webpack-plugin/dist/utils.js generated vendored Executable file → Normal file
View File

@@ -1,12 +1,12 @@
"use strict";
/** @typedef {import("@jridgewell/trace-mapping").SourceMapInput} SourceMapInput */
/** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */
/** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */
/** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */
/** @typedef {import("./index.js").Input} Input */
/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
/**
* @template T
@@ -17,7 +17,7 @@
* @typedef {Array<string>} ExtractedComments
*/
const notSettled = Symbol(`not-settled`);
const notSettled = Symbol("not-settled");
/**
* @template T
@@ -27,19 +27,15 @@ const notSettled = Symbol(`not-settled`);
/**
* Run tasks with limited concurrency.
* @template T
* @param {number} limit - Limit of tasks that run at once.
* @param {Task<T>[]} tasks - List of tasks to run.
* @param {number} limit Limit of tasks that run at once.
* @param {Task<T>[]} tasks List of tasks to run.
* @returns {Promise<T[]>} A promise that fulfills to an array of the results
*/
function throttleAll(limit, tasks) {
if (!Number.isInteger(limit) || limit < 1) {
throw new TypeError(`Expected \`limit\` to be a finite number > 0, got \`${limit}\` (${typeof limit})`);
}
if (!Array.isArray(tasks) || !tasks.every(task => typeof task === `function`)) {
throw new TypeError(`Expected \`tasks\` to be a list of functions returning a promise`);
}
return new Promise((resolve, reject) => {
const result = Array(tasks.length).fill(notSettled);
const result = Array.from({
length: tasks.length
}).fill(notSettled);
const entries = tasks.entries();
const next = () => {
const {
@@ -48,46 +44,51 @@ function throttleAll(limit, tasks) {
} = entries.next();
if (done) {
const isLast = !result.includes(notSettled);
if (isLast) resolve( /** @type{T[]} **/result);
if (isLast) resolve(result);
return;
}
const [index, task] = value;
/**
* @param {T} x
* @param {T} resultValue Result value
*/
const onFulfilled = x => {
result[index] = x;
const onFulfilled = resultValue => {
result[index] = resultValue;
next();
};
task().then(onFulfilled, reject);
};
Array(limit).fill(0).forEach(next);
for (let i = 0; i < limit; i++) {
next();
}
});
}
/* istanbul ignore next */
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @param {ExtractCommentsOptions | undefined} extractComments
* @return {Promise<MinimizedResult>}
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @param {ExtractCommentsOptions=} extractComments extract comments option
* @returns {Promise<MinimizedResult>} minimized result
*/
async function terserMinify(input, sourceMap, minimizerOptions, extractComments) {
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @param {any} value
* @returns {boolean}
* @param {unknown} value value
* @returns {value is object} true when value is object or function
*/
const isObject = value => {
const type = typeof value;
// eslint-disable-next-line no-eq-null, eqeqeq
return value != null && (type === "object" || type === "function");
};
/**
* @param {import("terser").MinifyOptions & { sourceMap: undefined } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} terserOptions
* @param {ExtractedComments} extractedComments
* @returns {ExtractCommentsFunction}
* @param {import("terser").MinifyOptions & { sourceMap: import("terser").SourceMapOptions | undefined } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} terserOptions terser options
* @param {ExtractedComments} extractedComments extracted comments
* @returns {ExtractCommentsFunction} function to extract comments
*/
const buildComments = (terserOptions, extractedComments) => {
/** @type {{ [index: string]: ExtractCommentsCondition }} */
@@ -119,7 +120,7 @@ async function terserMinify(input, sourceMap, minimizerOptions, extractComments)
}
// Ensure that both conditions are functions
["preserve", "extract"].forEach(key => {
for (const key of ["preserve", "extract"]) {
/** @type {undefined | string} */
let regexStr;
/** @type {undefined | RegExp} */
@@ -149,7 +150,7 @@ async function terserMinify(input, sourceMap, minimizerOptions, extractComments)
condition[key] = /** @type {ExtractCommentsFunction} */
(astNode, comment) => /** @type {RegExp} */regex.test(comment.value);
}
});
}
// Redefine the comments function to extract and preserve
// comments according to the two conditions
@@ -168,58 +169,63 @@ async function terserMinify(input, sourceMap, minimizerOptions, extractComments)
};
/**
* @param {PredefinedOptions<import("terser").MinifyOptions> & import("terser").MinifyOptions} [terserOptions={}]
* @returns {import("terser").MinifyOptions & { sourceMap: undefined } & { compress: import("terser").CompressOptions } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })}
* @param {PredefinedOptions<import("terser").MinifyOptions> & import("terser").MinifyOptions=} terserOptions terser options
* @returns {import("terser").MinifyOptions & { sourceMap: import("terser").SourceMapOptions | undefined } & { compress: import("terser").CompressOptions } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} built terser options
*/
const buildTerserOptions = (terserOptions = {}) => {
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
const buildTerserOptions = (terserOptions = {}) => (
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
{
...terserOptions,
compress: typeof terserOptions.compress === "boolean" ? terserOptions.compress ? {} : false : {
...terserOptions.compress
},
// ecma: terserOptions.ecma,
// ie8: terserOptions.ie8,
// keep_classnames: terserOptions.keep_classnames,
// keep_fnames: terserOptions.keep_fnames,
mangle:
// eslint-disable-next-line no-eq-null, eqeqeq
terserOptions.mangle == null ? true : typeof terserOptions.mangle === "boolean" ? terserOptions.mangle : {
...terserOptions.mangle
},
// module: terserOptions.module,
// nameCache: { ...terserOptions.toplevel },
// the `output` option is deprecated
...(terserOptions.format ? {
format: {
beautify: false,
...terserOptions.format
}
} : {
output: {
beautify: false,
...terserOptions.output
}
}),
parse: {
...terserOptions.parse
},
// safari10: terserOptions.safari10,
// Ignoring sourceMap from options
sourceMap: undefined
// toplevel: terserOptions.toplevel
});
let minify;
try {
({
minify
} = require("terser"));
} catch (err) {
return {
...terserOptions,
compress: typeof terserOptions.compress === "boolean" ? terserOptions.compress ? {} : false : {
...terserOptions.compress
},
// ecma: terserOptions.ecma,
// ie8: terserOptions.ie8,
// keep_classnames: terserOptions.keep_classnames,
// keep_fnames: terserOptions.keep_fnames,
mangle: terserOptions.mangle == null ? true : typeof terserOptions.mangle === "boolean" ? terserOptions.mangle : {
...terserOptions.mangle
},
// module: terserOptions.module,
// nameCache: { ...terserOptions.toplevel },
// the `output` option is deprecated
...(terserOptions.format ? {
format: {
beautify: false,
...terserOptions.format
}
} : {
output: {
beautify: false,
...terserOptions.output
}
}),
parse: {
...terserOptions.parse
},
// safari10: terserOptions.safari10,
// Ignoring sourceMap from options
// eslint-disable-next-line no-undefined
sourceMap: undefined
// toplevel: terserOptions.toplevel
errors: [( /** @type {Error} */err)]
};
};
}
// eslint-disable-next-line global-require
const {
minify
} = require("terser");
// Copy `terser` options
const terserOptions = buildTerserOptions(minimizerOptions);
// Let terser generate a SourceMap
if (sourceMap) {
// @ts-ignore
terserOptions.sourceMap = {
asObject: true
};
@@ -248,55 +254,55 @@ async function terserMinify(input, sourceMap, minimizerOptions, extractComments)
[filename]: code
}, terserOptions);
return {
code: ( /** @type {string} **/result.code),
// @ts-ignore
// eslint-disable-next-line no-undefined
map: result.map ? ( /** @type {SourceMapInput} **/result.map) : undefined,
code: ( /** @type {string} * */result.code),
map: result.map ? ( /** @type {RawSourceMap} * */result.map) : undefined,
extractedComments
};
}
/**
* @returns {string | undefined}
* @returns {string | undefined} the minimizer version
*/
terserMinify.getMinimizerVersion = () => {
let packageJson;
try {
// eslint-disable-next-line global-require
packageJson = require("terser/package.json");
} catch (error) {
} catch (_err) {
// Ignore
}
return packageJson && packageJson.version;
};
/**
* @returns {boolean | undefined}
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
terserMinify.supportsWorkerThreads = () => true;
/* istanbul ignore next */
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @param {ExtractCommentsOptions | undefined} extractComments
* @return {Promise<MinimizedResult>}
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @param {ExtractCommentsOptions=} extractComments extract comments option
* @returns {Promise<MinimizedResult>} minimized result
*/
async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComments) {
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @param {any} value
* @returns {boolean}
* @param {unknown} value value
* @returns {value is object} true when value is object or function
*/
const isObject = value => {
const type = typeof value;
// eslint-disable-next-line no-eq-null, eqeqeq
return value != null && (type === "object" || type === "function");
};
/**
* @param {import("uglify-js").MinifyOptions & { sourceMap: undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}} uglifyJsOptions
* @param {ExtractedComments} extractedComments
* @returns {ExtractCommentsFunction}
* @param {import("uglify-js").MinifyOptions & { sourceMap: boolean | import("uglify-js").SourceMapOptions | undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}} uglifyJsOptions uglify-js options
* @param {ExtractedComments} extractedComments extracted comments
* @returns {ExtractCommentsFunction} extract comments function
*/
const buildComments = (uglifyJsOptions, extractedComments) => {
/** @type {{ [index: string]: ExtractCommentsCondition }} */
@@ -321,7 +327,7 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
}
// Ensure that both conditions are functions
["preserve", "extract"].forEach(key => {
for (const key of ["preserve", "extract"]) {
/** @type {undefined | string} */
let regexStr;
/** @type {undefined | RegExp} */
@@ -351,7 +357,7 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
condition[key] = /** @type {ExtractCommentsFunction} */
(astNode, comment) => /** @type {RegExp} */regex.test(comment.value);
}
});
}
// Redefine the comments function to extract and preserve
// comments according to the two conditions
@@ -370,14 +376,16 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
};
/**
* @param {PredefinedOptions<import("uglify-js").MinifyOptions> & import("uglify-js").MinifyOptions} [uglifyJsOptions={}]
* @returns {import("uglify-js").MinifyOptions & { sourceMap: undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}}
* @param {PredefinedOptions<import("uglify-js").MinifyOptions> & import("uglify-js").MinifyOptions=} uglifyJsOptions uglify-js options
* @returns {import("uglify-js").MinifyOptions & { sourceMap: boolean | import("uglify-js").SourceMapOptions | undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean }}} uglify-js options
*/
const buildUglifyJsOptions = (uglifyJsOptions = {}) => {
// eslint-disable-next-line no-param-reassign
delete minimizerOptions.ecma;
// eslint-disable-next-line no-param-reassign
delete minimizerOptions.module;
if (typeof uglifyJsOptions.ecma !== "undefined") {
delete uglifyJsOptions.ecma;
}
if (typeof uglifyJsOptions.module !== "undefined") {
delete uglifyJsOptions.module;
}
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
return {
@@ -389,7 +397,9 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
compress: typeof uglifyJsOptions.compress === "boolean" ? uglifyJsOptions.compress : {
...uglifyJsOptions.compress
},
mangle: uglifyJsOptions.mangle == null ? true : typeof uglifyJsOptions.mangle === "boolean" ? uglifyJsOptions.mangle : {
mangle:
// eslint-disable-next-line no-eq-null, eqeqeq
uglifyJsOptions.mangle == null ? true : typeof uglifyJsOptions.mangle === "boolean" ? uglifyJsOptions.mangle : {
...uglifyJsOptions.mangle
},
output: {
@@ -397,7 +407,7 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
...uglifyJsOptions.output
},
// Ignoring sourceMap from options
// eslint-disable-next-line no-undefined
sourceMap: undefined
// toplevel: uglifyJsOptions.toplevel
// nameCache: { ...uglifyJsOptions.toplevel },
@@ -405,25 +415,29 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
// keep_fnames: uglifyJsOptions.keep_fnames,
};
};
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
const {
minify
} = require("uglify-js");
let minify;
try {
({
minify
} = require("uglify-js"));
} catch (err) {
return {
errors: [( /** @type {Error} */err)]
};
}
// Copy `uglify-js` options
const uglifyJsOptions = buildUglifyJsOptions(minimizerOptions);
// Let terser generate a SourceMap
if (sourceMap) {
// @ts-ignore
uglifyJsOptions.sourceMap = true;
}
/** @type {ExtractedComments} */
const extractedComments = [];
// @ts-ignore
// @ts-expect-error wrong types in uglify-js
uglifyJsOptions.output.comments = buildComments(uglifyJsOptions, extractedComments);
const [[filename, code]] = Object.entries(input);
const result = await minify({
@@ -431,7 +445,6 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
}, uglifyJsOptions);
return {
code: result.code,
// eslint-disable-next-line no-undefined
map: result.map ? JSON.parse(result.map) : undefined,
errors: result.error ? [result.error] : [],
warnings: result.warnings || [],
@@ -440,65 +453,70 @@ async function uglifyJsMinify(input, sourceMap, minimizerOptions, extractComment
}
/**
* @returns {string | undefined}
* @returns {string | undefined} the minimizer version
*/
uglifyJsMinify.getMinimizerVersion = () => {
let packageJson;
try {
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
packageJson = require("uglify-js/package.json");
} catch (error) {
} catch (_err) {
// Ignore
}
return packageJson && packageJson.version;
};
/**
* @returns {boolean | undefined}
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
uglifyJsMinify.supportsWorkerThreads = () => true;
/* istanbul ignore next */
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @return {Promise<MinimizedResult>}
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @returns {Promise<MinimizedResult>} minimized result
*/
async function swcMinify(input, sourceMap, minimizerOptions) {
/**
* @param {PredefinedOptions<import("@swc/core").JsMinifyOptions> & import("@swc/core").JsMinifyOptions} [swcOptions={}]
* @returns {import("@swc/core").JsMinifyOptions & { sourceMap: undefined } & { compress: import("@swc/core").TerserCompressOptions }}
* @param {PredefinedOptions<import("@swc/core").JsMinifyOptions> & import("@swc/core").JsMinifyOptions=} swcOptions swc options
* @returns {import("@swc/core").JsMinifyOptions & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options
*/
const buildSwcOptions = (swcOptions = {}) => {
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
return {
...swcOptions,
compress: typeof swcOptions.compress === "boolean" ? swcOptions.compress ? {} : false : {
...swcOptions.compress
},
mangle: swcOptions.mangle == null ? true : typeof swcOptions.mangle === "boolean" ? swcOptions.mangle : {
...swcOptions.mangle
},
// ecma: swcOptions.ecma,
// keep_classnames: swcOptions.keep_classnames,
// keep_fnames: swcOptions.keep_fnames,
// module: swcOptions.module,
// safari10: swcOptions.safari10,
// toplevel: swcOptions.toplevel
// eslint-disable-next-line no-undefined
sourceMap: undefined
};
};
const buildSwcOptions = (swcOptions = {}) => (
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
{
...swcOptions,
compress: typeof swcOptions.compress === "boolean" ? swcOptions.compress ? {} : false : {
...swcOptions.compress
},
mangle:
// eslint-disable-next-line no-eq-null, eqeqeq
swcOptions.mangle == null ? true : typeof swcOptions.mangle === "boolean" ? swcOptions.mangle : {
...swcOptions.mangle
},
// ecma: swcOptions.ecma,
// keep_classnames: swcOptions.keep_classnames,
// keep_fnames: swcOptions.keep_fnames,
// module: swcOptions.module,
// safari10: swcOptions.safari10,
// toplevel: swcOptions.toplevel
sourceMap: undefined
});
let swc;
try {
swc = require("@swc/core");
} catch (err) {
return {
errors: [( /** @type {Error} */err)]
};
}
// eslint-disable-next-line import/no-extraneous-dependencies, global-require
const swc = require("@swc/core");
// Copy `swc` options
const swcOptions = buildSwcOptions(minimizerOptions);
// Let `swc` generate a SourceMap
if (sourceMap) {
// @ts-ignore
swcOptions.sourceMap = true;
}
if (swcOptions.compress) {
@@ -529,45 +547,40 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
}
/**
* @returns {string | undefined}
* @returns {string | undefined} the minimizer version
*/
swcMinify.getMinimizerVersion = () => {
let packageJson;
try {
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
packageJson = require("@swc/core/package.json");
} catch (error) {
} catch (_err) {
// Ignore
}
return packageJson && packageJson.version;
};
/**
* @returns {boolean | undefined}
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
swcMinify.supportsWorkerThreads = () => false;
/* istanbul ignore next */
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @return {Promise<MinimizedResult>}
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @returns {Promise<MinimizedResult>} minimized result
*/
async function esbuildMinify(input, sourceMap, minimizerOptions) {
/**
* @param {PredefinedOptions<import("esbuild").TransformOptions> & import("esbuild").TransformOptions} [esbuildOptions={}]
* @returns {import("esbuild").TransformOptions}
* @param {PredefinedOptions<import("esbuild").TransformOptions> & import("esbuild").TransformOptions=} esbuildOptions esbuild options
* @returns {import("esbuild").TransformOptions} built esbuild options
*/
const buildEsbuildOptions = (esbuildOptions = {}) => {
// eslint-disable-next-line no-param-reassign
delete esbuildOptions.ecma;
if (esbuildOptions.module) {
// eslint-disable-next-line no-param-reassign
esbuildOptions.format = "esm";
}
// eslint-disable-next-line no-param-reassign
delete esbuildOptions.module;
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
@@ -578,9 +591,14 @@ async function esbuildMinify(input, sourceMap, minimizerOptions) {
sourcemap: false
};
};
// eslint-disable-next-line import/no-extraneous-dependencies, global-require
const esbuild = require("esbuild");
let esbuild;
try {
esbuild = require("esbuild");
} catch (err) {
return {
errors: [( /** @type {Error} */err)]
};
}
// Copy `esbuild` options
const esbuildOptions = buildEsbuildOptions(minimizerOptions);
@@ -595,7 +613,6 @@ async function esbuildMinify(input, sourceMap, minimizerOptions) {
const result = await esbuild.transform(code, esbuildOptions);
return {
code: result.code,
// eslint-disable-next-line no-undefined
map: result.map ? JSON.parse(result.map) : undefined,
warnings: result.warnings.length > 0 ? result.warnings.map(item => {
const plugin = item.pluginName ? `\nPlugin Name: ${item.pluginName}` : "";
@@ -607,28 +624,32 @@ async function esbuildMinify(input, sourceMap, minimizerOptions) {
}
/**
* @returns {string | undefined}
* @returns {string | undefined} the minimizer version
*/
esbuildMinify.getMinimizerVersion = () => {
let packageJson;
try {
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
packageJson = require("esbuild/package.json");
} catch (error) {
} catch (_err) {
// Ignore
}
return packageJson && packageJson.version;
};
/**
* @returns {boolean | undefined}
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
esbuildMinify.supportsWorkerThreads = () => false;
/**
* @template T
* @param fn {(function(): any) | undefined}
* @returns {function(): T}
* @typedef {() => T} FunctionReturning
*/
/**
* @template T
* @param {FunctionReturning<T>} fn memorized function
* @returns {FunctionReturning<T>} new function
*/
function memoize(fn) {
let cache = false;
@@ -638,20 +659,20 @@ function memoize(fn) {
if (cache) {
return result;
}
result = /** @type {function(): any} */fn();
result = fn();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
// eslint-disable-next-line no-undefined, no-param-reassign
/** @type {FunctionReturning<T> | undefined} */
fn = undefined;
return result;
return /** @type {T} */result;
};
}
module.exports = {
throttleAll,
esbuildMinify,
memoize,
terserMinify,
uglifyJsMinify,
swcMinify,
esbuildMinify
terserMinify,
throttleAll,
uglifyJsMinify
};

209
node_modules/terser-webpack-plugin/package.json generated vendored Executable file → Normal file
View File

@@ -1,105 +1,7 @@
{
"name": "terser-webpack-plugin",
"version": "5.3.14",
"version": "5.3.15",
"description": "Terser plugin for webpack",
"license": "MIT",
"repository": "webpack-contrib/terser-webpack-plugin",
"author": "webpack Contrib Team",
"homepage": "https://github.com/webpack-contrib/terser-webpack-plugin",
"bugs": "https://github.com/webpack-contrib/terser-webpack-plugin/issues",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"main": "dist/index.js",
"types": "types/index.d.ts",
"engines": {
"node": ">= 10.13.0"
},
"scripts": {
"clean": "del-cli dist types",
"prebuild": "npm run clean",
"build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write",
"build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files",
"build": "npm-run-all -p \"build:**\"",
"commitlint": "commitlint --from=master",
"security": "npm audit --production",
"lint:prettier": "prettier --list-different .",
"lint:js": "eslint --cache .",
"lint:spelling": "cspell \"**/*.*\"",
"lint:types": "tsc --pretty --noEmit",
"lint": "npm-run-all -l -p \"lint:**\"",
"fix:js": "npm run lint:js -- --fix",
"fix:prettier": "npm run lint:prettier -- --write",
"fix": "npm-run-all -l fix:js fix:prettier",
"test:only": "cross-env NODE_ENV=test jest",
"test:watch": "npm run test:only -- --watch",
"test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
"pretest": "npm run lint",
"test": "npm run test:coverage",
"prepare": "husky install && npm run build",
"release": "standard-version"
},
"files": [
"dist",
"types"
],
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"uglify-js": {
"optional": true
},
"esbuild": {
"optional": true
}
},
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"devDependencies": {
"@babel/cli": "^7.24.7",
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@swc/core": "^1.3.102",
"@types/node": "^18.15.11",
"@types/serialize-javascript": "^5.0.2",
"@types/uglify-js": "^3.17.5",
"@webpack-contrib/eslint-config-webpack": "^3.0.0",
"babel-jest": "^28.1.2",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^7.0.3",
"cspell": "^6.31.2",
"del": "^6.0.0",
"del-cli": "^3.0.1",
"esbuild": "^0.19.11",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.9.0",
"eslint-plugin-import": "^2.28.1",
"file-loader": "^6.2.0",
"husky": "^7.0.2",
"jest": "^27.5.1",
"lint-staged": "^13.2.3",
"memfs": "^3.4.13",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"standard-version": "^9.3.1",
"typescript": "^4.9.5",
"uglify-js": "^3.18.0",
"webpack": "^5.92.1",
"webpack-cli": "^4.10.0",
"worker-loader": "^3.0.8"
},
"keywords": [
"uglify",
"uglify-js",
@@ -116,5 +18,112 @@
"minify",
"optimize",
"optimizer"
]
],
"homepage": "https://github.com/webpack/terser-webpack-plugin",
"bugs": "https://github.com/webpack/terser-webpack-plugin/issues",
"repository": "webpack/terser-webpack-plugin",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"license": "MIT",
"author": "webpack Contrib Team",
"main": "dist/index.js",
"types": "types/index.d.ts",
"files": [
"dist",
"types"
],
"scripts": {
"clean": "del-cli dist types",
"prebuild": "npm run clean",
"build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write",
"build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files",
"build": "npm-run-all -p \"build:**\"",
"commitlint": "commitlint --from=main",
"security": "npm audit --production",
"lint:prettier": "prettier --list-different .",
"lint:code": "eslint --cache .",
"lint:spelling": "cspell \"**/*.*\"",
"lint:types": "tsc --pretty --noEmit",
"lint": "npm-run-all -l -p \"lint:**\"",
"fix:code": "npm run lint:code -- --fix",
"fix:prettier": "npm run lint:prettier -- --write",
"fix": "npm-run-all -l fix:code fix:prettier",
"test:only": "cross-env NODE_ENV=test jest",
"test:watch": "npm run test:only -- --watch",
"test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
"pretest": "npm run lint",
"test": "npm run test:coverage",
"prepare": "husky install && npm run build",
"release": "standard-version"
},
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"devDependencies": {
"@babel/cli": "^7.24.7",
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@eslint/js": "^9.29.0",
"@eslint/markdown": "^7.1.0",
"@stylistic/eslint-plugin": "^5.2.2",
"@swc/core": "^1.3.102",
"@types/node": "^24.2.1",
"@types/serialize-javascript": "^5.0.2",
"@types/uglify-js": "^3.17.5",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^7.0.3",
"cspell": "^6.31.2",
"del": "^6.0.0",
"del-cli": "^3.0.1",
"esbuild": "^0.25.0",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.1",
"eslint-config-webpack": "^4.5.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^54.0.0",
"eslint-plugin-n": "^17.21.0",
"eslint-plugin-prettier": "^5.5.0",
"eslint-plugin-unicorn": "^60.0.0",
"file-loader": "^6.2.0",
"husky": "^7.0.2",
"jest": "^27.5.1",
"lint-staged": "^13.2.3",
"memfs": "^3.4.13",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.0",
"prettier-2": "npm:prettier@^2",
"standard-version": "^9.3.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
"uglify-js": "^3.19.3",
"webpack": "^5.101.0",
"webpack-cli": "^4.10.0",
"worker-loader": "^3.0.8"
},
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"uglify-js": {
"optional": true
},
"esbuild": {
"optional": true
}
},
"engines": {
"node": ">= 10.13.0"
}
}

216
node_modules/terser-webpack-plugin/types/index.d.ts generated vendored Executable file → Normal file
View File

@@ -5,45 +5,45 @@ export = TerserPlugin;
declare class TerserPlugin<T = import("terser").MinifyOptions> {
/**
* @private
* @param {any} input
* @returns {boolean}
* @param {unknown} input Input to check
* @returns {boolean} Whether input is a source map
*/
private static isSourceMap;
/**
* @private
* @param {unknown} warning
* @param {string} file
* @returns {Error}
* @param {unknown} warning warning
* @param {string} file file
* @returns {Error} built warning
*/
private static buildWarning;
/**
* @private
* @param {any} error
* @param {string} file
* @param {TraceMap} [sourceMap]
* @param {Compilation["requestShortener"]} [requestShortener]
* @returns {Error}
* @param {Error | ErrorObject | string} error error
* @param {string} file file
* @param {TraceMap=} sourceMap source map
* @param {Compilation["requestShortener"]=} requestShortener request shortener
* @returns {Error} built error
*/
private static buildError;
/**
* @private
* @param {Parallel} parallel
* @returns {number}
* @param {Parallel} parallel value of the `parallel` option
* @returns {number} number of cores for parallelism
*/
private static getAvailableNumberOfCores;
/**
* @private
* @param {any} environment
* @returns {number}
* @param {NonNullable<NonNullable<Configuration["output"]>["environment"]>} environment environment
* @returns {number} ecma version
*/
private static getEcmaVersion;
/**
* @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>} [options]
* @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>=} options options
*/
constructor(
options?:
| (BasePluginOptions & DefinedDefaultMinimizerAndOptions<T>)
| undefined
| undefined,
);
/**
* @private
@@ -52,15 +52,15 @@ declare class TerserPlugin<T = import("terser").MinifyOptions> {
private options;
/**
* @private
* @param {Compiler} compiler
* @param {Compilation} compilation
* @param {Record<string, import("webpack").sources.Source>} assets
* @param {{availableNumberOfCores: number}} optimizeOptions
* @param {Compiler} compiler compiler
* @param {Compilation} compilation compilation
* @param {Record<string, import("webpack").sources.Source>} assets assets
* @param {{ availableNumberOfCores: number }} optimizeOptions optimize options
* @returns {Promise<void>}
*/
private optimize;
/**
* @param {Compiler} compiler
* @param {Compiler} compiler compiler
* @returns {void}
*/
apply(compiler: Compiler): void;
@@ -74,10 +74,11 @@ declare namespace TerserPlugin {
Schema,
Compiler,
Compilation,
WebpackError,
Configuration,
Asset,
AssetInfo,
JestWorker,
SourceMapInput,
RawSourceMap,
TraceMap,
Rule,
Rules,
@@ -87,6 +88,7 @@ declare namespace TerserPlugin {
ExtractCommentsBanner,
ExtractCommentsObject,
ExtractCommentsOptions,
ErrorObject,
MinimizedResult,
Input,
CustomOptions,
@@ -104,34 +106,22 @@ declare namespace TerserPlugin {
InternalPluginOptions,
};
}
type Compiler = import("webpack").Compiler;
type BasePluginOptions = {
test?: Rules | undefined;
include?: Rules | undefined;
exclude?: Rules | undefined;
extractComments?: ExtractCommentsOptions | undefined;
parallel?: Parallel;
};
type DefinedDefaultMinimizerAndOptions<T> =
T extends import("terser").MinifyOptions
? {
minify?: MinimizerImplementation<T> | undefined;
terserOptions?: MinimizerOptions<T> | undefined;
}
: {
minify: MinimizerImplementation<T>;
terserOptions?: MinimizerOptions<T> | undefined;
};
import { terserMinify } from "./utils";
import { uglifyJsMinify } from "./utils";
import { swcMinify } from "./utils";
import { esbuildMinify } from "./utils";
type Schema = import("schema-utils/declarations/validate").Schema;
type Compiler = import("webpack").Compiler;
type Compilation = import("webpack").Compilation;
type WebpackError = import("webpack").WebpackError;
type Configuration = import("webpack").Configuration;
type Asset = import("webpack").Asset;
type AssetInfo = import("webpack").AssetInfo;
type JestWorker = import("jest-worker").Worker;
type SourceMapInput = import("@jridgewell/trace-mapping").SourceMapInput;
type RawSourceMap = import("@jridgewell/trace-mapping").EncodedSourceMap & {
sources: string[];
sourcesContent?: string[];
file: string;
};
type TraceMap = import("@jridgewell/trace-mapping").TraceMap;
type Rule = RegExp | string;
type Rules = Rule[] | Rule;
@@ -143,7 +133,7 @@ type ExtractCommentsFunction = (
pos: number;
line: number;
col: number;
}
},
) => boolean;
type ExtractCommentsCondition =
| boolean
@@ -153,21 +143,63 @@ type ExtractCommentsCondition =
| ExtractCommentsFunction;
type ExtractCommentsFilename = string | ((fileData: any) => string);
type ExtractCommentsBanner =
| string
| boolean
| string
| ((commentsFile: string) => string);
type ExtractCommentsObject = {
/**
* condition which comments need to be expected
*/
condition?: ExtractCommentsCondition | undefined;
/**
* filename for extracted comments
*/
filename?: ExtractCommentsFilename | undefined;
/**
* banner in filename for extracted comments
*/
banner?: ExtractCommentsBanner | undefined;
};
type ExtractCommentsOptions = ExtractCommentsCondition | ExtractCommentsObject;
type ErrorObject = {
/**
* message
*/
message: string;
/**
* line number
*/
line?: number | undefined;
/**
* column number
*/
column?: number | undefined;
/**
* error stack trace
*/
stack?: string | undefined;
};
type MinimizedResult = {
code: string;
map?: import("@jridgewell/trace-mapping").SourceMapInput | undefined;
errors?: (string | Error)[] | undefined;
warnings?: (string | Error)[] | undefined;
extractedComments?: string[] | undefined;
/**
* code
*/
code?: string | undefined;
/**
* source map
*/
map?: RawSourceMap | undefined;
/**
* errors
*/
errors?: Array<Error | string> | undefined;
/**
* warnings
*/
warnings?: Array<Error | string> | undefined;
/**
* extracted comments
*/
extractedComments?: Array<string> | undefined;
};
type Input = {
[file: string]: string;
@@ -177,53 +209,111 @@ type CustomOptions = {
};
type InferDefaultType<T> = T extends infer U ? U : CustomOptions;
type PredefinedOptions<T> = {
/**
* true when code is a EC module, otherwise false
*/
module?:
| (T extends {
module?: infer P | undefined;
module?: infer P;
}
? P
: string | boolean)
: boolean | string)
| undefined;
/**
* ecma version
*/
ecma?:
| (T extends {
ecma?: infer P_1 | undefined;
ecma?: infer P;
}
? P_1
: string | number)
? P
: number | string)
| undefined;
};
type MinimizerOptions<T> = PredefinedOptions<T> & InferDefaultType<T>;
type BasicMinimizerImplementation<T> = (
input: Input,
sourceMap: SourceMapInput | undefined,
sourceMap: RawSourceMap | undefined,
minifyOptions: MinimizerOptions<T>,
extractComments: ExtractCommentsOptions | undefined
) => Promise<MinimizedResult>;
extractComments: ExtractCommentsOptions | undefined,
) => Promise<MinimizedResult> | MinimizedResult;
type MinimizeFunctionHelpers = {
/**
* function that returns version of minimizer
*/
getMinimizerVersion?: (() => string | undefined) | undefined;
/**
* true when minimizer support worker threads, otherwise false
*/
supportsWorkerThreads?: (() => boolean | undefined) | undefined;
};
type MinimizerImplementation<T> = BasicMinimizerImplementation<T> &
MinimizeFunctionHelpers;
type InternalOptions<T> = {
/**
* name
*/
name: string;
/**
* input
*/
input: string;
inputSourceMap: SourceMapInput | undefined;
/**
* input source map
*/
inputSourceMap: RawSourceMap | undefined;
/**
* extract comments option
*/
extractComments: ExtractCommentsOptions | undefined;
/**
* minimizer
*/
minimizer: {
implementation: MinimizerImplementation<T>;
options: MinimizerOptions<T>;
};
};
type MinimizerWorker<T> = import("jest-worker").Worker & {
transform: (options: string) => MinimizedResult;
minify: (options: InternalOptions<T>) => MinimizedResult;
type MinimizerWorker<T> = JestWorker & {
transform: (options: string) => Promise<MinimizedResult>;
minify: (options: InternalOptions<T>) => Promise<MinimizedResult>;
};
type Parallel = undefined | boolean | number;
type BasePluginOptions = {
/**
* test rule
*/
test?: Rules | undefined;
/**
* include rile
*/
include?: Rules | undefined;
/**
* exclude rule
*/
exclude?: Rules | undefined;
/**
* extract comments options
*/
extractComments?: ExtractCommentsOptions | undefined;
/**
* parallel option
*/
parallel?: Parallel | undefined;
};
type DefinedDefaultMinimizerAndOptions<T> =
T extends import("terser").MinifyOptions
? {
minify?: MinimizerImplementation<T> | undefined;
terserOptions?: MinimizerOptions<T> | undefined;
}
: {
minify: MinimizerImplementation<T>;
terserOptions?: MinimizerOptions<T> | undefined;
};
type InternalPluginOptions<T> = BasePluginOptions & {
minimizer: {
implementation: MinimizerImplementation<T>;
options: MinimizerOptions<T>;
};
};
import { minify } from "./minify";

10
node_modules/terser-webpack-plugin/types/minify.d.ts generated vendored Executable file → Normal file
View File

@@ -4,14 +4,14 @@ export type CustomOptions = import("./index.js").CustomOptions;
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
/**
* @template T
* @param {import("./index.js").InternalOptions<T>} options
* @returns {Promise<MinimizedResult>}
* @param {import("./index.js").InternalOptions<T>} options options
* @returns {Promise<MinimizedResult>} minified result
*/
export function minify<T>(
options: import("./index.js").InternalOptions<T>
options: import("./index.js").InternalOptions<T>,
): Promise<MinimizedResult>;
/**
* @param {string} options
* @returns {Promise<MinimizedResult>}
* @param {string} options options
* @returns {Promise<MinimizedResult>} minified result
*/
export function transform(options: string): Promise<MinimizedResult>;

173
node_modules/terser-webpack-plugin/types/utils.d.ts generated vendored Executable file → Normal file
View File

@@ -1,5 +1,5 @@
export type Task<T> = () => Promise<T>;
export type SourceMapInput = import("@jridgewell/trace-mapping").SourceMapInput;
export type FunctionReturning<T> = () => T;
export type ExtractCommentsOptions =
import("./index.js").ExtractCommentsOptions;
export type ExtractCommentsFunction =
@@ -9,8 +9,84 @@ export type ExtractCommentsCondition =
export type Input = import("./index.js").Input;
export type MinimizedResult = import("./index.js").MinimizedResult;
export type CustomOptions = import("./index.js").CustomOptions;
export type RawSourceMap = import("./index.js").RawSourceMap;
export type PredefinedOptions<T> = import("./index.js").PredefinedOptions<T>;
export type ExtractedComments = Array<string>;
/**
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @returns {Promise<MinimizedResult>} minimized result
*/
export function esbuildMinify(
input: Input,
sourceMap?: RawSourceMap | undefined,
minimizerOptions?: CustomOptions | undefined,
): Promise<MinimizedResult>;
export namespace esbuildMinify {
/**
* @returns {string | undefined} the minimizer version
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @template T
* @typedef {() => T} FunctionReturning
*/
/**
* @template T
* @param {FunctionReturning<T>} fn memorized function
* @returns {FunctionReturning<T>} new function
*/
export function memoize<T>(fn: FunctionReturning<T>): FunctionReturning<T>;
/**
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @returns {Promise<MinimizedResult>} minimized result
*/
export function swcMinify(
input: Input,
sourceMap?: RawSourceMap | undefined,
minimizerOptions?: CustomOptions | undefined,
): Promise<MinimizedResult>;
export namespace swcMinify {
/**
* @returns {string | undefined} the minimizer version
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @param {ExtractCommentsOptions=} extractComments extract comments option
* @returns {Promise<MinimizedResult>} minimized result
*/
export function terserMinify(
input: Input,
sourceMap?: RawSourceMap | undefined,
minimizerOptions?: CustomOptions | undefined,
extractComments?: ExtractCommentsOptions | undefined,
): Promise<MinimizedResult>;
export namespace terserMinify {
/**
* @returns {string | undefined} the minimizer version
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @template T
* @typedef {() => Promise<T>} Task
@@ -18,102 +94,31 @@ export type ExtractedComments = Array<string>;
/**
* Run tasks with limited concurrency.
* @template T
* @param {number} limit - Limit of tasks that run at once.
* @param {Task<T>[]} tasks - List of tasks to run.
* @param {number} limit Limit of tasks that run at once.
* @param {Task<T>[]} tasks List of tasks to run.
* @returns {Promise<T[]>} A promise that fulfills to an array of the results
*/
export function throttleAll<T>(limit: number, tasks: Task<T>[]): Promise<T[]>;
/**
* @template T
* @param fn {(function(): any) | undefined}
* @returns {function(): T}
*/
export function memoize<T>(fn: (() => any) | undefined): () => T;
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @param {ExtractCommentsOptions | undefined} extractComments
* @return {Promise<MinimizedResult>}
*/
export function terserMinify(
input: Input,
sourceMap: SourceMapInput | undefined,
minimizerOptions: CustomOptions,
extractComments: ExtractCommentsOptions | undefined
): Promise<MinimizedResult>;
export namespace terserMinify {
/**
* @returns {string | undefined}
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined}
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @param {ExtractCommentsOptions | undefined} extractComments
* @return {Promise<MinimizedResult>}
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @param {ExtractCommentsOptions=} extractComments extract comments option
* @returns {Promise<MinimizedResult>} minimized result
*/
export function uglifyJsMinify(
input: Input,
sourceMap: SourceMapInput | undefined,
minimizerOptions: CustomOptions,
extractComments: ExtractCommentsOptions | undefined
sourceMap?: RawSourceMap | undefined,
minimizerOptions?: CustomOptions | undefined,
extractComments?: ExtractCommentsOptions | undefined,
): Promise<MinimizedResult>;
export namespace uglifyJsMinify {
/**
* @returns {string | undefined}
* @returns {string | undefined} the minimizer version
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined}
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @return {Promise<MinimizedResult>}
*/
export function swcMinify(
input: Input,
sourceMap: SourceMapInput | undefined,
minimizerOptions: CustomOptions
): Promise<MinimizedResult>;
export namespace swcMinify {
/**
* @returns {string | undefined}
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined}
*/
function supportsWorkerThreads(): boolean | undefined;
}
/**
* @param {Input} input
* @param {SourceMapInput | undefined} sourceMap
* @param {CustomOptions} minimizerOptions
* @return {Promise<MinimizedResult>}
*/
export function esbuildMinify(
input: Input,
sourceMap: SourceMapInput | undefined,
minimizerOptions: CustomOptions
): Promise<MinimizedResult>;
export namespace esbuildMinify {
/**
* @returns {string | undefined}
*/
function getMinimizerVersion(): string | undefined;
/**
* @returns {boolean | undefined}
* @returns {boolean | undefined} true if worker thread is supported, false otherwise
*/
function supportsWorkerThreads(): boolean | undefined;
}

View File

@@ -298,7 +298,11 @@ module.exports = function updateDB(print = defaultPrint) {
yarnCommand + ' up -R caniuse-lite baseline-browser-mapping'
)
} else if (lock.mode === 'pnpm') {
updateWith(print, 'pnpm up --no-save caniuse-lite baseline-browser-mapping')
let lockContent = readFileSync(lock.file).toString()
let packages = lockContent.includes('baseline-browser-mapping')
? 'caniuse-lite baseline-browser-mapping'
: 'caniuse-lite'
updateWith(print, 'pnpm up --depth=Infinity --no-save ' + packages)
} else if (lock.mode === 'bun') {
updateWith(print, 'bun update caniuse-lite baseline-browser-mapping')
} else {

View File

@@ -1,6 +1,6 @@
{
"name": "update-browserslist-db",
"version": "1.2.0",
"version": "1.2.2",
"description": "CLI tool to update caniuse-lite to refresh target browsers from Browserslist config",
"keywords": [
"caniuse",

48
package-lock.json generated
View File

@@ -2658,9 +2658,9 @@
}
},
"node_modules/@jqhtml/core": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.11.tgz",
"integrity": "sha512-HxkBSauw+bHIzU0jnLTuE2EWArthVxpGVV7zhG4q/zurGh+tjog1Jst2UbquHSoqB8Yr32FHZ2wAVm1FVZFKVg==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.12.tgz",
"integrity": "sha512-IaYxg5N/ikNvS560hr71y+fmcd3LprMlFMZdtYnQjOA2vN3ccCISstXkDG7wErvicMJt2mD6H46FvXJOoAhefg==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2684,9 +2684,9 @@
}
},
"node_modules/@jqhtml/parser": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.11.tgz",
"integrity": "sha512-vXyB1+KjkwDlv8vKbe1b0HZkIAM4oQMnx1pMk/xAfEtGP+WZyqXAvuiaboV20vyZUG/JhveZ9pdHvB8dZNlW/A==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.12.tgz",
"integrity": "sha512-/srJqbPVw6XK9hhtuqGh+BhwBeuvCk/UtFGYcaum/AEuJq/6vNLBbz9h4IWUmfQb7MoW9I24d/lgMcnNBE3QwQ==",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
@@ -2724,9 +2724,9 @@
}
},
"node_modules/@jqhtml/vscode-extension": {
"version": "2.3.11",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.11.tgz",
"integrity": "sha512-VHg5Tu1hPtU6LPhp/GGQ2sXCI67bMtrUynUgQp/V5KZnL0PRbY1C0usgF+71yzNoC2YvZY1ahKjrwznq+m0WCA==",
"version": "2.3.12",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.12.tgz",
"integrity": "sha512-obVJOhLKB9KRP732gsQyge8KHew0mt4aRWOZMM0/XryK0fXS6cck1+l+QKdU5XyCnVwivDHF3LdEAzwvEn93nA==",
"license": "MIT",
"engines": {
"vscode": "^1.74.0"
@@ -4957,9 +4957,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz",
"integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz",
"integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -6622,9 +6622,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.263",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz",
"integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==",
"version": "1.5.266",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
"license": "ISC"
},
"node_modules/elliptic": {
@@ -10018,9 +10018,9 @@
}
},
"node_modules/nwsapi": {
"version": "2.2.22",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz",
"integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/object-inspect": {
@@ -12760,9 +12760,9 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"version": "5.3.15",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz",
"integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
@@ -13269,9 +13269,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz",
"integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
"funding": [
{
"type": "opencollective",