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:
@@ -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']));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
RSX Framework 2025-12-09 BUNDLE_API(3)
|
||||
241
app/RSpade/man/npm.txt
Executable file
241
app/RSpade/man/npm.txt
Executable 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)
|
||||
Reference in New Issue
Block a user