From d71f7a661545ba617a4b405dbf6aff19ba9c32a7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 9 Dec 2025 21:16:16 +0000 Subject: [PATCH] Add npm man page, --clean flag for bundle:compile, fix subclass checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Commands/Rsx/Bundle_Compile_Command.php | 44 +++- .../Commands/Rsx/Bundle_Show_Command.php | 10 +- .../Core/Bundle/Rsx_Bundle_Abstract.php | 10 +- .../Integrations/Scss/Scss_ManifestModule.php | 2 +- app/RSpade/man/bundle_api.txt | 34 ++- app/RSpade/man/npm.txt | 241 ++++++++++++++++++ 6 files changed, 315 insertions(+), 26 deletions(-) create mode 100755 app/RSpade/man/npm.txt diff --git a/app/RSpade/Commands/Rsx/Bundle_Compile_Command.php b/app/RSpade/Commands/Rsx/Bundle_Compile_Command.php index a39ae10e4..0522dd9e7 100755 --- a/app/RSpade/Commands/Rsx/Bundle_Compile_Command.php +++ b/app/RSpade/Commands/Rsx/Bundle_Compile_Command.php @@ -16,7 +16,8 @@ class Bundle_Compile_Command extends Command */ protected $signature = 'rsx:bundle:compile {bundle? : Bundle class name to compile (optional, compiles all if not specified)} - {--build-debug : Enable verbose build output (shows detailed compilation steps)}'; + {--build-debug : Enable verbose build output (shows detailed compilation steps)} + {--clean : Clear all caches before compiling (runs rsx:clean first)}'; /** * The console command description. @@ -47,6 +48,24 @@ class Bundle_Compile_Command extends Command // Prevent being called via $this->call() - must use passthru for fresh process $this->prevent_call_from_another_command(); + // Handle --clean flag: run rsx:clean then re-invoke without --clean + if ($this->option('clean')) { + $this->info('Clearing caches before compile...'); + passthru('php artisan rsx:clean'); + + // Build command with all args except --clean + $cmd = 'php artisan rsx:bundle:compile'; + if ($this->argument('bundle')) { + $cmd .= ' ' . escapeshellarg($this->argument('bundle')); + } + if ($this->option('build-debug')) { + $cmd .= ' --build-debug'; + } + + passthru($cmd, $exit_code); + exit($exit_code); + } + $bundle_arg = $this->argument('bundle'); // Get all MODULE bundle classes from manifest (not asset bundles) @@ -56,11 +75,10 @@ class Bundle_Compile_Command extends Command $bundle_classes = []; foreach ($manifest_data as $file_info) { - if (isset($file_info['extends']) && $file_info['extends'] === 'Rsx_Module_Bundle_Abstract') { - $fqcn = $file_info['fqcn'] ?? $file_info['class'] ?? null; - if ($fqcn) { - $bundle_classes[$fqcn] = $file_info['class'] ?? $fqcn; - } + $class_name = $file_info['class'] ?? null; + if ($class_name && Manifest::php_is_subclass_of($class_name, 'Rsx_Module_Bundle_Abstract')) { + $fqcn = $file_info['fqcn'] ?? $class_name; + $bundle_classes[$fqcn] = $class_name; } } @@ -180,14 +198,22 @@ class Bundle_Compile_Command extends Command $this->line(' JS: ' . $this->format_size($js_size) . " → app.{$bundle_hash}.js"); $this->line(' CSS: ' . $this->format_size($css_size) . " → app.{$bundle_hash}.css"); } else { - // In dev mode, show the actual split files + // In dev mode, show the actual split files (vendor and app) + if (isset($compiled['vendor_js_bundle_path'])) { + $vendor_js_size = filesize("{$bundle_dir}/{$compiled['vendor_js_bundle_path']}"); + $this->line(' Vendor JS: ' . $this->format_size($vendor_js_size) . ' → ' . basename($compiled['vendor_js_bundle_path'])); + } + if (isset($compiled['vendor_css_bundle_path'])) { + $vendor_css_size = filesize("{$bundle_dir}/{$compiled['vendor_css_bundle_path']}"); + $this->line(' Vendor CSS: ' . $this->format_size($vendor_css_size) . ' → ' . basename($compiled['vendor_css_bundle_path'])); + } if (isset($compiled['app_js_bundle_path'])) { $app_js_size = filesize("{$bundle_dir}/{$compiled['app_js_bundle_path']}"); - $this->line(' JS: ' . $this->format_size($app_js_size) . ' → ' . basename($compiled['app_js_bundle_path'])); + $this->line(' App JS: ' . $this->format_size($app_js_size) . ' → ' . basename($compiled['app_js_bundle_path'])); } if (isset($compiled['app_css_bundle_path'])) { $app_css_size = filesize("{$bundle_dir}/{$compiled['app_css_bundle_path']}"); - $this->line(' CSS: ' . $this->format_size($app_css_size) . ' → ' . basename($compiled['app_css_bundle_path'])); + $this->line(' App CSS: ' . $this->format_size($app_css_size) . ' → ' . basename($compiled['app_css_bundle_path'])); } } diff --git a/app/RSpade/Commands/Rsx/Bundle_Show_Command.php b/app/RSpade/Commands/Rsx/Bundle_Show_Command.php index efbe6bfea..950bc9a41 100755 --- a/app/RSpade/Commands/Rsx/Bundle_Show_Command.php +++ b/app/RSpade/Commands/Rsx/Bundle_Show_Command.php @@ -35,11 +35,11 @@ class Bundle_Show_Command extends Command $bundle_classes = []; foreach ($manifest_data as $file_info) { - if (isset($file_info['extends']) && $file_info['extends'] === 'Rsx_Bundle_Abstract') { - $fqcn = $file_info['fqcn'] ?? $file_info['class'] ?? null; - if ($fqcn) { - $bundle_classes[$fqcn] = $file_info['class'] ?? $fqcn; - } + // Only show Module Bundles (compilable page bundles), not Asset Bundles + $class_name = $file_info['class'] ?? null; + if ($class_name && Manifest::php_is_subclass_of($class_name, 'Rsx_Module_Bundle_Abstract')) { + $fqcn = $file_info['fqcn'] ?? $class_name; + $bundle_classes[$fqcn] = $class_name; } } diff --git a/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php b/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php index eb3cb2f56..08db72e21 100755 --- a/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php +++ b/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php @@ -551,12 +551,10 @@ abstract class Rsx_Bundle_Abstract // Search manifest for any class ending with the bundle name that extends Rsx_Bundle_Abstract $manifest_data = Manifest::get_all(); foreach ($manifest_data as $file_info) { - if (isset($file_info['class']) && - str_ends_with($file_info['class'], $bundle) && - isset($file_info['extends']) && - ($file_info['extends'] === 'Rsx_Bundle_Abstract' || $file_info['extends'] === 'RsxBundle' || - $file_info['extends'] === 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract' || - $file_info['extends'] === 'App\\RSpade\\Core\\Bundle\\RsxBundle')) { + $class_name = $file_info['class'] ?? null; + if ($class_name && + str_ends_with($class_name, $bundle) && + Manifest::php_is_subclass_of($class_name, 'Rsx_Bundle_Abstract')) { return $file_info['fqcn']; } } diff --git a/app/RSpade/Integrations/Scss/Scss_ManifestModule.php b/app/RSpade/Integrations/Scss/Scss_ManifestModule.php index ed47f13b7..c73cfeead 100755 --- a/app/RSpade/Integrations/Scss/Scss_ManifestModule.php +++ b/app/RSpade/Integrations/Scss/Scss_ManifestModule.php @@ -276,7 +276,7 @@ class Scss_ManifestModule extends ManifestModule_Abstract // Check for JavaScript class extending Component if (isset($file_data['extension']) && $file_data['extension'] === 'js' && isset($file_data['class']) && $file_data['class'] === $class_name && - isset($file_data['extends']) && $file_data['extends'] === 'Component') { + \App\RSpade\Core\Manifest\Manifest::js_is_subclass_of($class_name, 'Component')) { $found_match = true; } } diff --git a/app/RSpade/man/bundle_api.txt b/app/RSpade/man/bundle_api.txt index 2169b2aa4..c8f1e8a8a 100755 --- a/app/RSpade/man/bundle_api.txt +++ b/app/RSpade/man/bundle_api.txt @@ -212,6 +212,32 @@ BUNDLE PLACEMENT /rsx/app/login/signup/... ... and all files in subdirectories +ADDING CUSTOM NPM MODULES + Application developers can include third-party npm packages in their bundles + by creating Asset Bundles co-located with their components: + + // /rsx/theme/components/charts/Chart_JS_Bundle.php + class Chart_JS_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'Chart' => "import { Chart } from 'chart.js/auto'", + ], + ]; + } + } + + The 'npm' array maps global variable names to ES module import statements. + Each entry creates one global variable accessible in your JavaScript. + + The package is included only when Module Bundles scan that directory. + This keeps bundle sizes smaller when components aren't used. + + For detailed npm integration documentation including import formats, + troubleshooting, and examples, see npm(3). + INCLUDE TYPES Module Aliases Predefined in config/rsx.php: @@ -235,9 +261,7 @@ INCLUDE TYPES 'rsx/theme/variables.scss' NPM Modules - Include from node_modules: - 'npm:axios' - 'npm:moment' + Use Asset Bundles with the 'npm' array. See npm(3) for details. CDN Assets External resources: @@ -551,6 +575,6 @@ TROUBLESHOOTING Verify file extension matches. SEE ALSO - manifest_api(3), jqhtml(3), controller(3) + npm(3), manifest_api(3), jqhtml(3), controller(3) -RSX Framework 2025-09-17 BUNDLE_API(3) \ No newline at end of file +RSX Framework 2025-12-09 BUNDLE_API(3) \ No newline at end of file diff --git a/app/RSpade/man/npm.txt b/app/RSpade/man/npm.txt new file mode 100755 index 000000000..bbfbfb295 --- /dev/null +++ b/app/RSpade/man/npm.txt @@ -0,0 +1,241 @@ +NPM(3) RSX Framework Manual NPM(3) + +NAME + npm - Including npm packages in RSX bundles + +SYNOPSIS + // Asset Bundle with npm imports + class My_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'moment' => "import moment from 'moment'", + 'axios' => "import axios from 'axios'", + ], + ]; + } + } + + // Usage in JavaScript + const now = moment().format('YYYY-MM-DD'); + const response = await axios.get('/api/data'); + +DESCRIPTION + RSX bundles can include npm packages through Asset Bundles. The bundle + system uses esbuild to compile npm imports into the vendor bundle, + exposing them as global variables accessible throughout your application. + + Unlike traditional webpack/vite setups where you write import statements + in your JavaScript files, RSX centralizes npm dependencies in Asset + Bundle definitions. This provides: + + - Explicit dependency declaration in PHP + - Automatic vendor/app bundle splitting + - Tree-shaking of unused exports + - No import statements needed in application code + +PACKAGE INSTALLATION + All npm packages must be installed in the system directory: + + cd /var/www/html/system + npm install + + The framework's esbuild runs from system/, so packages must exist in + system/node_modules/. The project root package.json is for reference + only - actual dependencies live in system/. + +ASSET BUNDLE SYNTAX + Create an Asset Bundle extending Rsx_Asset_Bundle_Abstract with an + 'npm' array in the define() method: + + class My_Library_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'global_name' => "import statement", + ], + ]; + } + } + + Each entry maps a global variable name to an ES module import statement. + The global becomes available to all JavaScript in bundles that include + this Asset Bundle. + +IMPORT FORMATS + DEFAULT EXPORT + Package exports a single default value: + + 'moment' => "import moment from 'moment'", + 'axios' => "import axios from 'axios'", + 'lodash' => "import _ from 'lodash'", + + Usage: moment(), axios.get(), _.map() + + NAMED EXPORT + Package exports multiple named values. Each named export needs its + own entry: + + // CORRECT - separate entries for each named export + 'createApp' => "import { createApp } from 'vue'", + 'ref' => "import { ref } from 'vue'", + 'computed' => "import { computed } from 'vue'", + + Usage: createApp(), ref(), computed() + + IMPORTANT: You cannot combine multiple named exports into one global. + This does NOT work: + + // WRONG - creates single value, not object with both + 'vue' => "import { createApp, ref } from 'vue'", + + The global 'vue' would only contain createApp, not both functions. + + NAMESPACE IMPORT + Import all exports as a namespace object: + + 'Shiki' => "import * as Shiki from 'shiki'", + + Usage: Shiki.createHighlighter(), Shiki.bundledLanguages + + SUB-PATH IMPORTS + Many packages offer sub-path imports for tree-shaking: + + // Full package - includes everything + 'shiki' => "import { createHighlighter } from 'shiki'", + + // Sub-path - only what you need + 'createHighlighter' => "import { createHighlighterCore } from '@shikijs/core'", + 'darkPlus' => "import darkPlus from '@shikijs/themes/dark-plus'", + + Sub-path imports significantly reduce bundle size. + +BUNDLE PLACEMENT + Asset Bundles are auto-discovered when Module Bundles scan directories. + Place your npm Asset Bundle alongside the components that use it: + + rsx/app/docs/components/ + shiki_bundle.php # Asset Bundle with npm imports + Docs_Code_Block.js # Component using shiki globals + Docs_Code_Block.jqhtml + + The Module Bundle scanning rsx/app/docs/ will automatically include + the Shiki_Bundle and its npm dependencies. + +VENDOR BUNDLE COMPILATION + npm packages compile into the vendor bundle, separate from app code. + This provides better caching since vendor code changes less frequently. + + REBUILD TRIGGERS + The vendor bundle rebuilds when: + - npm entries in Asset Bundles change + - First HTTP request after cache clear + - Running rsx:clean followed by page request + + IMPORTANT: Running rsx:bundle:compile alone may not rebuild the + vendor bundle if the cache key hasn't changed. To force a complete + rebuild: + + php artisan rsx:bundle:compile --clean + + Or trigger via HTTP request: + + curl http://localhost/your-page + php artisan rsx:debug /your-page + +TROUBLESHOOTING + NPM MODULE NOT FOUND ERROR + Error: RSX Framework Error: NPM module "xyz" not found. + Expected window._rsx_npm.xyz to be defined by the vendor bundle. + + Causes: + 1. Package not installed in system/node_modules/ + 2. Vendor bundle not rebuilt after adding npm entry + 3. Import statement syntax error + + Solutions: + 1. cd system && npm install + 2. php artisan rsx:bundle:compile --clean + 3. Verify import syntax matches package's actual exports + + VERIFYING PACKAGE EXPORTS + Check what a package actually exports: + + # Default export + grep -E "^export default" node_modules/pkg/dist/index.mjs + + # Named exports + grep -E "^export \{|^export function|^export const" \ + node_modules/pkg/dist/index.mjs + + BUNDLE SIZE OPTIMIZATION + Use sub-path imports when available: + + // Before: 9.7 MB (all languages, themes) + 'shiki' => "import { createHighlighter } from 'shiki'", + + // After: 2.4 MB (only what's needed) + 'createHighlighter' => "import { createHighlighterCore } from '@shikijs/core'", + 'langJs' => "import js from '@shikijs/langs/javascript'", + 'themeDark' => "import dark from '@shikijs/themes/dark-plus'", + +EXAMPLES + CHART.JS + class Chart_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'Chart' => "import { Chart } from 'chart.js/auto'", + ], + ]; + } + } + + // Usage + new Chart(ctx, { type: 'bar', data: {...} }); + + DATE-FNS (Tree-Shaking Friendly) + class DateFns_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'format' => "import { format } from 'date-fns'", + 'parseISO' => "import { parseISO } from 'date-fns'", + 'addDays' => "import { addDays } from 'date-fns'", + ], + ]; + } + } + + // Usage + format(parseISO('2024-01-15'), 'MMM d, yyyy'); + + VUE 3 + class Vue_Bundle extends Rsx_Asset_Bundle_Abstract + { + public static function define(): array + { + return [ + 'npm' => [ + 'createApp' => "import { createApp } from 'vue'", + 'ref' => "import { ref } from 'vue'", + 'reactive' => "import { reactive } from 'vue'", + 'computed' => "import { computed } from 'vue'", + 'watch' => "import { watch } from 'vue'", + ], + ]; + } + } + +SEE ALSO + bundle_api(3), jqhtml(3) + +RSX Framework 2025-12-09 NPM(3)