Add npm man page, --clean flag for bundle:compile, fix subclass checks

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-09 21:16:16 +00:00
parent 46b45e6762
commit d71f7a6615
6 changed files with 315 additions and 26 deletions

View File

@@ -16,7 +16,8 @@ class Bundle_Compile_Command extends Command
*/ */
protected $signature = 'rsx:bundle:compile protected $signature = 'rsx:bundle:compile
{bundle? : Bundle class name to compile (optional, compiles all if not specified)} {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. * 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 // Prevent being called via $this->call() - must use passthru for fresh process
$this->prevent_call_from_another_command(); $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'); $bundle_arg = $this->argument('bundle');
// Get all MODULE bundle classes from manifest (not asset bundles) // Get all MODULE bundle classes from manifest (not asset bundles)
@@ -56,11 +75,10 @@ class Bundle_Compile_Command extends Command
$bundle_classes = []; $bundle_classes = [];
foreach ($manifest_data as $file_info) { foreach ($manifest_data as $file_info) {
if (isset($file_info['extends']) && $file_info['extends'] === 'Rsx_Module_Bundle_Abstract') { $class_name = $file_info['class'] ?? null;
$fqcn = $file_info['fqcn'] ?? $file_info['class'] ?? null; if ($class_name && Manifest::php_is_subclass_of($class_name, 'Rsx_Module_Bundle_Abstract')) {
if ($fqcn) { $fqcn = $file_info['fqcn'] ?? $class_name;
$bundle_classes[$fqcn] = $file_info['class'] ?? $fqcn; $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(' JS: ' . $this->format_size($js_size) . " → app.{$bundle_hash}.js");
$this->line(' CSS: ' . $this->format_size($css_size) . " → app.{$bundle_hash}.css"); $this->line(' CSS: ' . $this->format_size($css_size) . " → app.{$bundle_hash}.css");
} else { } 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'])) { if (isset($compiled['app_js_bundle_path'])) {
$app_js_size = filesize("{$bundle_dir}/{$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'])) { if (isset($compiled['app_css_bundle_path'])) {
$app_css_size = filesize("{$bundle_dir}/{$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']));
} }
} }

View File

@@ -35,11 +35,11 @@ class Bundle_Show_Command extends Command
$bundle_classes = []; $bundle_classes = [];
foreach ($manifest_data as $file_info) { foreach ($manifest_data as $file_info) {
if (isset($file_info['extends']) && $file_info['extends'] === 'Rsx_Bundle_Abstract') { // Only show Module Bundles (compilable page bundles), not Asset Bundles
$fqcn = $file_info['fqcn'] ?? $file_info['class'] ?? null; $class_name = $file_info['class'] ?? null;
if ($fqcn) { if ($class_name && Manifest::php_is_subclass_of($class_name, 'Rsx_Module_Bundle_Abstract')) {
$bundle_classes[$fqcn] = $file_info['class'] ?? $fqcn; $fqcn = $file_info['fqcn'] ?? $class_name;
} $bundle_classes[$fqcn] = $class_name;
} }
} }

View File

@@ -551,12 +551,10 @@ abstract class Rsx_Bundle_Abstract
// Search manifest for any class ending with the bundle name that extends Rsx_Bundle_Abstract // Search manifest for any class ending with the bundle name that extends Rsx_Bundle_Abstract
$manifest_data = Manifest::get_all(); $manifest_data = Manifest::get_all();
foreach ($manifest_data as $file_info) { foreach ($manifest_data as $file_info) {
if (isset($file_info['class']) && $class_name = $file_info['class'] ?? null;
str_ends_with($file_info['class'], $bundle) && if ($class_name &&
isset($file_info['extends']) && str_ends_with($class_name, $bundle) &&
($file_info['extends'] === 'Rsx_Bundle_Abstract' || $file_info['extends'] === 'RsxBundle' || Manifest::php_is_subclass_of($class_name, 'Rsx_Bundle_Abstract')) {
$file_info['extends'] === 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract' ||
$file_info['extends'] === 'App\\RSpade\\Core\\Bundle\\RsxBundle')) {
return $file_info['fqcn']; return $file_info['fqcn'];
} }
} }

View File

@@ -276,7 +276,7 @@ class Scss_ManifestModule extends ManifestModule_Abstract
// Check for JavaScript class extending Component // Check for JavaScript class extending Component
if (isset($file_data['extension']) && $file_data['extension'] === 'js' && if (isset($file_data['extension']) && $file_data['extension'] === 'js' &&
isset($file_data['class']) && $file_data['class'] === $class_name && 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; $found_match = true;
} }
} }

View File

@@ -212,6 +212,32 @@ BUNDLE PLACEMENT
/rsx/app/login/signup/... /rsx/app/login/signup/...
... and all files in subdirectories ... 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 INCLUDE TYPES
Module Aliases Module Aliases
Predefined in config/rsx.php: Predefined in config/rsx.php:
@@ -235,9 +261,7 @@ INCLUDE TYPES
'rsx/theme/variables.scss' 'rsx/theme/variables.scss'
NPM Modules NPM Modules
Include from node_modules: Use Asset Bundles with the 'npm' array. See npm(3) for details.
'npm:axios'
'npm:moment'
CDN Assets CDN Assets
External resources: External resources:
@@ -551,6 +575,6 @@ TROUBLESHOOTING
Verify file extension matches. Verify file extension matches.
SEE ALSO 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) RSX Framework 2025-12-09 BUNDLE_API(3)

241
app/RSpade/man/npm.txt Executable file
View File

@@ -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 <package-name>
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 <package>
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)