diff --git a/.env.dist b/.env.dist
index 77dfe57f3..ddc848c2e 100755
--- a/.env.dist
+++ b/.env.dist
@@ -4,6 +4,8 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
+# RSX Application Mode: development, debug, or production
+
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=info
@@ -54,3 +56,4 @@ GATEKEEPER_SUBTITLE="This is a restricted development preview site. Please enter
SSR_FPC_ENABLED=true
LOG_BROWSER_ERRORS=false
AJAX_DISABLE_BATCHING=false
+RSX_MODE=development
diff --git a/.gitignore b/.gitignore
index 8a0fa2b40..ae5331508 100755
--- a/.gitignore
+++ b/.gitignore
@@ -22,8 +22,11 @@ yarn-error.log
._*
Thumbs.db
-# Entire storage directory - will be created on bootstrap
-/storage/
+# Storage directory (except cdn-cache which is committed)
+/storage/*
+!/storage/rsx-build/
+/storage/rsx-build/*
+!/storage/rsx-build/cdn-cache/
# Supervisor files
supervisord.log*
diff --git a/app/RSpade/Commands/Migrate/Migrate_Normalize_Schema_Command.php b/app/RSpade/Commands/Migrate/Migrate_Normalize_Schema_Command.php
index 7ab860fb0..97eb024ab 100644
--- a/app/RSpade/Commands/Migrate/Migrate_Normalize_Schema_Command.php
+++ b/app/RSpade/Commands/Migrate/Migrate_Normalize_Schema_Command.php
@@ -261,6 +261,12 @@ class Migrate_Normalize_Schema_Command extends Command
if (in_array($tableName, $ajaxableTables) && !Schema::hasColumn($tableName, 'version')) {
DB::statement("ALTER TABLE $tableName ADD COLUMN version INT(11) NOT NULL DEFAULT 1");
}
+
+ // Order column normalization
+ // Tables with `order` column get: BIGINT DEFAULT NULL, order_idx index, auto-increment triggers
+ if (Schema::hasColumn($tableName, 'order')) {
+ $this->normalizeOrderColumn($tableName);
+ }
}
// Convert table to utf8mb4 if needed
@@ -407,4 +413,100 @@ class Migrate_Normalize_Schema_Command extends Command
return count($indexes) > 0;
}
+
+ /**
+ * Check if a trigger exists
+ *
+ * @param string $triggerName The name of the trigger
+ * @return bool True if the trigger exists
+ */
+ private function triggerExists($triggerName)
+ {
+ $triggers = DB::select("SHOW TRIGGERS WHERE `Trigger` = ?", [$triggerName]);
+
+ return count($triggers) > 0;
+ }
+
+ /**
+ * Normalize order column for a table
+ *
+ * Ensures:
+ * - Column is BIGINT DEFAULT NULL
+ * - Index order_idx exists on (order)
+ * - Triggers exist for auto-incrementing NULL values on INSERT/UPDATE
+ *
+ * @param string $tableName The name of the table
+ */
+ private function normalizeOrderColumn($tableName)
+ {
+ // 1. Ensure column type is BIGINT DEFAULT NULL
+ $column_info = DB::selectOne(
+ "SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = ?
+ AND COLUMN_NAME = 'order'",
+ [$tableName]
+ );
+
+ if ($column_info) {
+ $needs_modify = false;
+
+ // Check if type is BIGINT (case-insensitive, may include display width)
+ if (stripos($column_info->COLUMN_TYPE, 'bigint') === false) {
+ $needs_modify = true;
+ }
+
+ // Check if nullable
+ if ($column_info->IS_NULLABLE !== 'YES') {
+ $needs_modify = true;
+ }
+
+ // Check if default is NULL
+ if ($column_info->COLUMN_DEFAULT !== null) {
+ $needs_modify = true;
+ }
+
+ if ($needs_modify) {
+ DB::statement("ALTER TABLE `$tableName` MODIFY COLUMN `order` BIGINT DEFAULT NULL");
+ }
+ }
+
+ // 2. Ensure order_idx index exists
+ if (!$this->indexExists($tableName, 'order_idx')) {
+ DB::statement("ALTER TABLE `$tableName` ADD INDEX order_idx(`order`)");
+ }
+
+ // 3. Ensure triggers exist for auto-incrementing NULL values
+ $insert_trigger_name = "{$tableName}_order_insert";
+ $update_trigger_name = "{$tableName}_order_update";
+
+ // Create INSERT trigger if not exists
+ if (!$this->triggerExists($insert_trigger_name)) {
+ DB::unprepared("
+ CREATE TRIGGER `{$insert_trigger_name}`
+ BEFORE INSERT ON `{$tableName}`
+ FOR EACH ROW
+ BEGIN
+ IF NEW.`order` IS NULL THEN
+ SET NEW.`order` = (SELECT COALESCE(MAX(`order`), 0) + 1 FROM `{$tableName}`);
+ END IF;
+ END
+ ");
+ }
+
+ // Create UPDATE trigger if not exists
+ if (!$this->triggerExists($update_trigger_name)) {
+ DB::unprepared("
+ CREATE TRIGGER `{$update_trigger_name}`
+ BEFORE UPDATE ON `{$tableName}`
+ FOR EACH ROW
+ BEGIN
+ IF NEW.`order` IS NULL THEN
+ SET NEW.`order` = (SELECT COALESCE(MAX(`order`), 0) + 1 FROM `{$tableName}`);
+ END IF;
+ END
+ ");
+ }
+ }
}
diff --git a/app/RSpade/Commands/Rsx/Mode_Set_Command.php b/app/RSpade/Commands/Rsx/Mode_Set_Command.php
new file mode 100755
index 000000000..304279446
--- /dev/null
+++ b/app/RSpade/Commands/Rsx/Mode_Set_Command.php
@@ -0,0 +1,241 @@
+argument('mode');
+
+ // Normalize mode aliases
+ $normalized = match (strtolower($mode)) {
+ 'dev', 'development' => Rsx_Mode::DEVELOPMENT,
+ 'debug' => Rsx_Mode::DEBUG,
+ 'prod', 'production' => Rsx_Mode::PRODUCTION,
+ default => null,
+ };
+
+ if ($normalized === null) {
+ $this->error("Invalid mode: {$mode}");
+ $this->line('');
+ $this->line('Valid modes:');
+ $this->line(' dev, development - Auto-rebuild, full debugging');
+ $this->line(' debug - Production optimizations with sourcemaps');
+ $this->line(' prod, production - Full optimization');
+
+ return 1;
+ }
+
+ // Check current mode (for display purposes only - always rebuild)
+ $current_mode = $this->_get_current_env_mode();
+ if ($current_mode === $normalized) {
+ $this->info("Rebuilding {$normalized} mode...");
+ } else {
+ $this->info("Switching to {$normalized} mode...");
+ }
+ $this->newLine();
+
+ // Step 1: Update .env file
+ $this->line(' [1/3] Updating .env...');
+ $this->_update_env_mode($normalized);
+
+ // Clear the cached mode so subsequent calls see the new value
+ Rsx_Mode::clear_cache();
+
+ // Step 2: Clear all caches
+ $this->line(' [2/3] Clearing caches...');
+
+ // Clear Laravel caches by deleting files directly (avoids triggering Manifest::init)
+ $this->_clear_laravel_caches();
+
+ // Clear RSX caches directly (avoids triggering Manifest::init via artisan boot)
+ $this->_clear_rsx_caches();
+
+ // Step 3: Build appropriate assets
+ $this->line(' [3/3] Building assets...');
+
+ if ($normalized === Rsx_Mode::DEVELOPMENT) {
+ // In development, pre-warm the bundle cache
+ // Explicitly pass RSX_MODE to ensure subprocess uses correct mode
+ passthru("RSX_MODE={$normalized} php artisan rsx:bundle:compile", $exit_code);
+ if ($exit_code !== 0) {
+ $this->warn('Bundle compilation had warnings, but continuing...');
+ }
+ } else {
+ // In debug/production, run the production build
+ // RSX_FORCE_BUILD allows manifest rebuild even in production mode
+ // Explicitly pass RSX_MODE to ensure subprocess uses correct mode
+ passthru("RSX_MODE={$normalized} RSX_FORCE_BUILD=1 php artisan rsx:prod:build", $exit_code);
+ if ($exit_code !== 0) {
+ $this->error('Production build failed');
+
+ return 1;
+ }
+ }
+
+ $this->newLine();
+ $this->info("[OK] Switched to {$normalized} mode");
+
+ return 0;
+ }
+
+ /**
+ * Get current RSX_MODE from .env file (not from env() which may be cached)
+ */
+ private function _get_current_env_mode(): string
+ {
+ $env_path = base_path('.env');
+ if (!file_exists($env_path)) {
+ return Rsx_Mode::DEVELOPMENT;
+ }
+
+ $contents = file_get_contents($env_path);
+ if (preg_match('/^RSX_MODE=(.*)$/m', $contents, $matches)) {
+ return trim($matches[1]);
+ }
+
+ return Rsx_Mode::DEVELOPMENT;
+ }
+
+ /**
+ * Update RSX_MODE in .env file
+ */
+ private function _update_env_mode(string $mode): void
+ {
+ $env_path = base_path('.env');
+
+ if (!file_exists($env_path)) {
+ // Create .env with just RSX_MODE
+ file_put_contents($env_path, "RSX_MODE={$mode}\n");
+
+ return;
+ }
+
+ $contents = file_get_contents($env_path);
+
+ if (preg_match('/^RSX_MODE=.*$/m', $contents)) {
+ // Replace existing RSX_MODE
+ $contents = preg_replace('/^RSX_MODE=.*$/m', "RSX_MODE={$mode}", $contents);
+ } else {
+ // Add RSX_MODE after APP_DEBUG or APP_URL if they exist, otherwise at end
+ if (preg_match('/^APP_DEBUG=.*$/m', $contents)) {
+ $contents = preg_replace(
+ '/^(APP_DEBUG=.*)$/m',
+ "$1\nRSX_MODE={$mode}",
+ $contents,
+ 1
+ );
+ } elseif (preg_match('/^APP_URL=.*$/m', $contents)) {
+ $contents = preg_replace(
+ '/^(APP_URL=.*)$/m',
+ "$1\nRSX_MODE={$mode}",
+ $contents,
+ 1
+ );
+ } else {
+ // Append to end
+ $contents = rtrim($contents) . "\nRSX_MODE={$mode}\n";
+ }
+ }
+
+ file_put_contents($env_path, $contents);
+ }
+
+ /**
+ * Clear Laravel caches by deleting files directly
+ * (Avoids running artisan commands which trigger Manifest::init)
+ */
+ private function _clear_laravel_caches(): void
+ {
+ $bootstrap_cache = base_path('bootstrap/cache');
+
+ // Config cache
+ $config_cache = "{$bootstrap_cache}/config.php";
+ if (file_exists($config_cache)) {
+ unlink($config_cache);
+ }
+
+ // Route cache
+ $route_cache = "{$bootstrap_cache}/routes-v7.php";
+ if (file_exists($route_cache)) {
+ unlink($route_cache);
+ }
+
+ // Services cache
+ $services_cache = "{$bootstrap_cache}/services.php";
+ if (file_exists($services_cache)) {
+ unlink($services_cache);
+ }
+
+ // Packages cache
+ $packages_cache = "{$bootstrap_cache}/packages.php";
+ if (file_exists($packages_cache)) {
+ unlink($packages_cache);
+ }
+
+ // Compiled views
+ $views_path = storage_path('framework/views');
+ if (is_dir($views_path)) {
+ $files = glob("{$views_path}/*.php");
+ foreach ($files as $file) {
+ unlink($file);
+ }
+ }
+ }
+
+ /**
+ * Clear RSX caches by deleting directories directly
+ * (Avoids running artisan commands which trigger Manifest::init)
+ */
+ private function _clear_rsx_caches(): void
+ {
+ // Clear rsx-build directory
+ $build_path = storage_path('rsx-build');
+ if (is_dir($build_path)) {
+ $this->_clear_directory_contents($build_path);
+ }
+
+ // Clear rsx-tmp directory
+ $tmp_path = storage_path('rsx-tmp');
+ if (is_dir($tmp_path)) {
+ $this->_clear_directory_contents($tmp_path);
+ }
+ }
+
+ /**
+ * Recursively clear directory contents
+ */
+ private function _clear_directory_contents(string $path): void
+ {
+ if (!is_dir($path)) {
+ return;
+ }
+
+ $files = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ rmdir($file->getRealPath());
+ } else {
+ unlink($file->getRealPath());
+ }
+ }
+ }
+}
diff --git a/app/RSpade/Commands/Rsx/Prod_Build_Command.php b/app/RSpade/Commands/Rsx/Prod_Build_Command.php
new file mode 100755
index 000000000..1860eea08
--- /dev/null
+++ b/app/RSpade/Commands/Rsx/Prod_Build_Command.php
@@ -0,0 +1,180 @@
+warn('Warning: Building production assets in development mode.');
+ $this->line('Assets will be built but will not be used until you switch modes:');
+ $this->line(' php artisan rsx:mode:set debug');
+ $this->line(' php artisan rsx:mode:set production');
+ $this->newLine();
+ }
+
+ $this->info('Building production assets...');
+ $this->newLine();
+
+ $start_time = microtime(true);
+
+ // Step 1: Rebuild manifest
+ $this->line('[1/3] Building manifest...');
+ try {
+ // Enable force build mode to allow rebuilding in production-like modes
+ Manifest::$_force_build = true;
+
+ // Force a fresh manifest rebuild by clearing and re-initializing
+ Manifest::clear();
+ Manifest::init();
+ $this->line(' Manifest built successfully');
+ } catch (Exception $e) {
+ $this->error(' Failed to build manifest: ' . $e->getMessage());
+
+ return 1;
+ } finally {
+ Manifest::$_force_build = false;
+ }
+
+ // Step 2: Compile all bundles
+ $this->line('[2/3] Compiling bundles...');
+
+ // Force restart minify server to pick up any code changes
+ if (Rsx_Mode::is_production()) {
+ Minifier::force_restart();
+ }
+
+ $manifest_data = Manifest::get_all();
+ $bundle_classes = [];
+
+ foreach ($manifest_data as $file_info) {
+ $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;
+ }
+ }
+
+ if (empty($bundle_classes)) {
+ $this->warn(' No bundles found in manifest');
+ } else {
+ // Ensure storage directory exists
+ $bundle_dir = storage_path('rsx-build/bundles');
+ if (!is_dir($bundle_dir)) {
+ mkdir($bundle_dir, 0755, true);
+ }
+
+ $compiled_count = 0;
+ $failed_bundles = [];
+
+ foreach ($bundle_classes as $fqcn => $class_name) {
+ $this->line(" Compiling: {$class_name}");
+
+ try {
+ $compiler = new BundleCompiler();
+ $compiled = $compiler->compile($fqcn, ['force_build' => true]);
+
+ // Get output file info (always vendor/app split)
+ $js_size = 0;
+ $css_size = 0;
+
+ if (isset($compiled['vendor_js_bundle_path'])) {
+ $js_size += filesize("{$bundle_dir}/{$compiled['vendor_js_bundle_path']}");
+ }
+ if (isset($compiled['app_js_bundle_path'])) {
+ $js_size += filesize("{$bundle_dir}/{$compiled['app_js_bundle_path']}");
+ }
+ if (isset($compiled['vendor_css_bundle_path'])) {
+ $css_size += filesize("{$bundle_dir}/{$compiled['vendor_css_bundle_path']}");
+ }
+ if (isset($compiled['app_css_bundle_path'])) {
+ $css_size += filesize("{$bundle_dir}/{$compiled['app_css_bundle_path']}");
+ }
+
+ $this->line(' JS: ' . $this->_format_size($js_size) . ', CSS: ' . $this->_format_size($css_size));
+ $compiled_count++;
+ } catch (Exception $e) {
+ $this->error(" Failed: {$e->getMessage()}");
+ $failed_bundles[$class_name] = $e->getMessage();
+ }
+ }
+
+ if (!empty($failed_bundles)) {
+ $this->error(" {$compiled_count} compiled, " . count($failed_bundles) . ' failed');
+
+ return 1;
+ }
+
+ $this->line(" {$compiled_count} bundles compiled");
+ }
+
+ // Step 3: Laravel caches
+ if (!$this->option('skip-laravel-cache')) {
+ $this->line('[3/3] Building Laravel caches...');
+
+ passthru('php artisan config:cache 2>/dev/null', $exit_code);
+ if ($exit_code === 0) {
+ $this->line(' Config cached');
+ }
+
+ passthru('php artisan route:cache 2>/dev/null', $exit_code);
+ if ($exit_code === 0) {
+ $this->line(' Routes cached');
+ }
+
+ passthru('php artisan view:cache 2>/dev/null', $exit_code);
+ if ($exit_code === 0) {
+ $this->line(' Views cached');
+ }
+ } else {
+ $this->line('[3/3] Skipping Laravel caches (--skip-laravel-cache)');
+ }
+
+ $elapsed = round(microtime(true) - $start_time, 2);
+
+ $this->newLine();
+ $this->info("[OK] Production build complete ({$elapsed}s)");
+
+ if ($mode === Rsx_Mode::DEVELOPMENT) {
+ $this->newLine();
+ $this->line('To use these assets, switch to production mode:');
+ $this->line(' php artisan rsx:mode:set production');
+ }
+
+ return 0;
+ }
+
+ private function _format_size(int $bytes): string
+ {
+ if ($bytes < 1024) {
+ return "{$bytes} B";
+ }
+ if ($bytes < 1048576) {
+ return round($bytes / 1024, 1) . ' KB';
+ }
+
+ return round($bytes / 1048576, 2) . ' MB';
+ }
+}
diff --git a/app/RSpade/Commands/Rsx/Prod_Export_Command.php b/app/RSpade/Commands/Rsx/Prod_Export_Command.php
new file mode 100755
index 000000000..56c49339a
--- /dev/null
+++ b/app/RSpade/Commands/Rsx/Prod_Export_Command.php
@@ -0,0 +1,231 @@
+option('path');
+
+ // Resolve relative path from base_path parent (since we're in /system)
+ if (str_starts_with($export_path, './')) {
+ $export_path = dirname(base_path()) . '/' . substr($export_path, 2);
+ } elseif (!str_starts_with($export_path, '/')) {
+ $export_path = dirname(base_path()) . '/' . $export_path;
+ }
+
+ $this->info('Exporting application for deployment...');
+ $this->line(" Destination: {$export_path}");
+ $this->newLine();
+
+ // Step 1: Run production build (unless --skip-build)
+ if (!$this->option('skip-build')) {
+ $this->line('[1/3] Running production build...');
+ passthru('php artisan rsx:prod:build', $exit_code);
+ if ($exit_code !== 0) {
+ $this->error('Production build failed');
+
+ return 1;
+ }
+ $this->newLine();
+ } else {
+ $this->line('[1/3] Skipping production build (--skip-build)');
+ }
+
+ // Step 2: Prepare export directory
+ $this->line('[2/3] Preparing export directory...');
+
+ // Clear existing export if it exists
+ if (is_dir($export_path)) {
+ $this->line(' Clearing existing export...');
+ $this->_clear_directory($export_path);
+ }
+
+ // Create export directory
+ if (!mkdir($export_path, 0755, true) && !is_dir($export_path)) {
+ $this->error("Failed to create export directory: {$export_path}");
+
+ return 1;
+ }
+
+ // Step 3: Copy files
+ $this->line('[3/3] Copying files...');
+
+ $base = dirname(base_path()); // Parent of /system
+ $copied_files = 0;
+ $copied_dirs = 0;
+
+ // Directories to copy
+ $dirs_to_copy = [
+ 'system' => 'system',
+ 'rsx' => 'rsx',
+ 'node_modules' => 'node_modules',
+ 'vendor' => 'vendor',
+ ];
+
+ // Also include rsx-build from storage
+ $storage_rsx_build = base_path('storage/rsx-build');
+ if (is_dir($storage_rsx_build)) {
+ $dirs_to_copy['system/storage/rsx-build'] = 'system/storage/rsx-build';
+ }
+
+ foreach ($dirs_to_copy as $src_rel => $dest_rel) {
+ $src = "{$base}/{$src_rel}";
+ $dest = "{$export_path}/{$dest_rel}";
+
+ if (!is_dir($src)) {
+ $this->warn(" Skipping {$src_rel} (not found)");
+ continue;
+ }
+
+ $this->line(" Copying {$src_rel}/...");
+ $count = $this->_copy_directory($src, $dest, $src_rel);
+ $copied_files += $count;
+ $copied_dirs++;
+ }
+
+ // Copy *.json files from root
+ $json_files = glob("{$base}/*.json");
+ foreach ($json_files as $json_file) {
+ $filename = basename($json_file);
+ copy($json_file, "{$export_path}/{$filename}");
+ $copied_files++;
+ }
+ if (!empty($json_files)) {
+ $this->line(' Copying *.json files...');
+ }
+
+ // Create .gitignore for export directory
+ file_put_contents(
+ "{$export_path}/.gitignore",
+ "# This export should not be committed to version control\n*\n"
+ );
+
+ $this->newLine();
+ $this->info("[OK] Export complete");
+ $this->line(" {$copied_dirs} directories, {$copied_files} files");
+ $this->line(" Location: {$export_path}");
+ $this->newLine();
+ $this->line('Next steps:');
+ $this->line(' 1. Copy the export directory to your production server');
+ $this->line(' 2. Configure .env on the production server');
+ $this->line(' 3. Point your web server to the deployment');
+
+ return 0;
+ }
+
+ /**
+ * Copy a directory recursively, excluding certain paths
+ */
+ private function _copy_directory(string $src, string $dest, string $rel_path): int
+ {
+ // Paths to exclude (relative to source)
+ $exclude_patterns = [
+ '.env',
+ '.env.example',
+ 'storage/app',
+ 'storage/logs',
+ 'storage/framework/cache',
+ 'storage/framework/sessions',
+ 'storage/framework/views',
+ 'storage/rsx-tmp',
+ 'rsx-export',
+ '.git',
+ '.idea',
+ '.vscode',
+ 'tests',
+ ];
+
+ // For system directory, exclude additional paths
+ if ($rel_path === 'system') {
+ $exclude_patterns[] = 'storage/rsx-tmp';
+ }
+
+ if (!is_dir($dest)) {
+ mkdir($dest, 0755, true);
+ }
+
+ $copied = 0;
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($src, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $item) {
+ $sub_path = $iterator->getSubPathname();
+
+ // Check exclusions
+ $skip = false;
+ foreach ($exclude_patterns as $pattern) {
+ if (str_starts_with($sub_path, $pattern) || str_contains($sub_path, "/{$pattern}")) {
+ $skip = true;
+ break;
+ }
+ }
+
+ if ($skip) {
+ continue;
+ }
+
+ $dest_path = "{$dest}/{$sub_path}";
+
+ if ($item->isDir()) {
+ if (!is_dir($dest_path)) {
+ mkdir($dest_path, 0755, true);
+ }
+ } else {
+ // Ensure parent directory exists
+ $parent = dirname($dest_path);
+ if (!is_dir($parent)) {
+ mkdir($parent, 0755, true);
+ }
+ copy($item->getPathname(), $dest_path);
+ $copied++;
+ }
+ }
+
+ return $copied;
+ }
+
+ /**
+ * Clear a directory recursively
+ */
+ private function _clear_directory(string $path): void
+ {
+ if (!is_dir($path)) {
+ return;
+ }
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($iterator as $item) {
+ if ($item->isDir()) {
+ rmdir($item->getPathname());
+ } else {
+ unlink($item->getPathname());
+ }
+ }
+
+ rmdir($path);
+ }
+}
diff --git a/app/RSpade/Core/Bundle/BundleCompiler.php b/app/RSpade/Core/Bundle/BundleCompiler.php
index abff36683..83e8281bf 100644
--- a/app/RSpade/Core/Bundle/BundleCompiler.php
+++ b/app/RSpade/Core/Bundle/BundleCompiler.php
@@ -7,10 +7,13 @@ use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
+use App\RSpade\Core\Bundle\Cdn_Cache;
+use App\RSpade\Core\Bundle\Minifier;
use App\RSpade\Core\Bundle\Rsx_Asset_Bundle_Abstract;
use App\RSpade\Core\Bundle\Rsx_Module_Bundle_Abstract;
use App\RSpade\Core\Locks\RsxLocks;
use App\RSpade\Core\Manifest\Manifest;
+use App\RSpade\Core\Mode\Rsx_Mode;
/**
* BundleCompiler - Compiles RSX bundles into JS and CSS files
@@ -121,18 +124,25 @@ class BundleCompiler
public function compile(string $bundle_class, array $options = []): array
{
$this->bundle_name = $this->_get_bundle_name($bundle_class);
- $this->is_production = app()->environment('production');
+ $this->is_production = Rsx_Mode::is_production_like();
+ $force_build = $options['force_build'] ?? false;
- console_debug('BUNDLE', "Compiling {$this->bundle_name} (production: " . ($this->is_production ? 'yes' : 'no') . ')');
+ console_debug('BUNDLE', "Compiling {$this->bundle_name} (mode: " . Rsx_Mode::get() . ')');
- // Step 1: Check production cache
- if ($this->is_production) {
+ // Step 1: In production-like modes, require pre-built bundles (unless force_build)
+ if ($this->is_production && !$force_build) {
$existing = $this->_check_production_cache();
if ($existing) {
console_debug('BUNDLE', 'Using existing production bundle');
return $existing;
}
+
+ // In production-like modes, don't auto-rebuild - error instead
+ throw new RuntimeException(
+ "Bundle '{$this->bundle_name}' not compiled for production mode. " .
+ 'Run: php artisan rsx:prod:build'
+ );
}
// Step 2: Mark the bundle we're compiling as already resolved
@@ -148,9 +158,18 @@ class BundleCompiler
// Step 5: Always split into vendor/app
$this->_split_vendor_app();
- // Step 6: Check individual bundle caches
- $need_compile = $this->_check_bundle_caches();
- console_debug('BUNDLE', 'Need compile: ' . json_encode($need_compile));
+ // Step 6: Check individual bundle caches (or force all if force_build)
+ if ($force_build) {
+ $need_compile = ['vendor', 'app'];
+ $this->cache_keys = [];
+ foreach ($need_compile as $type) {
+ $this->cache_keys[$type] = $this->_get_cache_key($type);
+ }
+ console_debug('BUNDLE', 'Force build - compiling all types');
+ } else {
+ $need_compile = $this->_check_bundle_caches();
+ console_debug('BUNDLE', 'Need compile: ' . json_encode($need_compile));
+ }
// Step 7-10: Process bundles that need compilation
if (!empty($need_compile)) {
@@ -205,15 +224,16 @@ class BundleCompiler
// Step 13: Return data for render() method (not CLI)
// Include CDN assets and proper file paths for HTML generation
- $bundle_dir = storage_path('rsx-build/bundles');
$result = [];
- // Add CDN assets
+ // CDN assets are always output as separate tags
+ // In development mode: loaded directly from CDN URLs
+ // In production-like modes: served from /_vendor/ (cached locally)
if (!empty($this->cdn_assets['js'])) {
- $result['cdn_js'] = $this->cdn_assets['js'];
+ $result['cdn_js'] = $this->_prepare_cdn_assets($this->cdn_assets['js'], 'js');
}
if (!empty($this->cdn_assets['css'])) {
- $result['cdn_css'] = $this->cdn_assets['css'];
+ $result['cdn_css'] = $this->_prepare_cdn_assets($this->cdn_assets['css'], 'css');
}
// Add public directory assets
@@ -224,60 +244,18 @@ class BundleCompiler
$result['public_css'] = $this->public_assets['css'];
}
- // Add bundle file paths for development
- if (!$this->is_production) {
- if (isset($outputs['vendor_js'])) {
- $result['vendor_js_bundle_path'] = $outputs['vendor_js'];
- }
- if (isset($outputs['app_js'])) {
- $result['app_js_bundle_path'] = $outputs['app_js'];
- }
- if (isset($outputs['vendor_css'])) {
- $result['vendor_css_bundle_path'] = $outputs['vendor_css'];
- }
- if (isset($outputs['app_css'])) {
- $result['app_css_bundle_path'] = $outputs['app_css'];
- }
- } else {
- // Production mode - simple concatenation of vendor + app
- $js_content = '';
- if (isset($outputs['vendor_js'])) {
- $js_content = file_get_contents("{$bundle_dir}/{$outputs['vendor_js']}");
- }
- if (isset($outputs['app_js'])) {
- // Simple concatenation with newline separator
- if ($js_content) {
- $js_content .= "\n";
- }
- $js_content .= file_get_contents("{$bundle_dir}/{$outputs['app_js']}");
- }
-
- $css_content = '';
- if (isset($outputs['vendor_css'])) {
- $css_content = file_get_contents("{$bundle_dir}/{$outputs['vendor_css']}");
- }
- if (isset($outputs['app_css'])) {
- // Simple concatenation with newline separator
- if ($css_content) {
- $css_content .= "\n";
- }
- $css_content .= file_get_contents("{$bundle_dir}/{$outputs['app_css']}");
- }
-
- // Write combined files with content hash
- if ($js_content) {
- $js_hash = substr(md5($js_content), 0, 16);
- $js_file = "app.{$js_hash}.js";
- file_put_contents("{$bundle_dir}/{$js_file}", $js_content);
- $result['js_bundle_path'] = $js_file;
- }
-
- if ($css_content) {
- $css_hash = substr(md5($css_content), 0, 16);
- $css_file = "app.{$css_hash}.css";
- file_put_contents("{$bundle_dir}/{$css_file}", $css_content);
- $result['css_bundle_path'] = $css_file;
- }
+ // Always return vendor/app split paths
+ if (isset($outputs['vendor_js'])) {
+ $result['vendor_js_bundle_path'] = $outputs['vendor_js'];
+ }
+ if (isset($outputs['app_js'])) {
+ $result['app_js_bundle_path'] = $outputs['app_js'];
+ }
+ if (isset($outputs['vendor_css'])) {
+ $result['vendor_css_bundle_path'] = $outputs['vendor_css'];
+ }
+ if (isset($outputs['app_css'])) {
+ $result['app_css_bundle_path'] = $outputs['app_css'];
}
// Add config if present
@@ -441,28 +419,137 @@ class BundleCompiler
return end($parts);
}
+ /**
+ * Get bundle FQCN from simple class name
+ */
+ protected function _get_bundle_fqcn(string $bundle_name): string
+ {
+ // If already a FQCN, return as-is
+ if (str_contains($bundle_name, '\\')) {
+ return $bundle_name;
+ }
+
+ // Look up in Manifest
+ $metadata = Manifest::php_get_metadata_by_class($bundle_name);
+
+ return $metadata['fqcn'];
+ }
+
/**
* Check if production bundle already exists
*/
protected function _check_production_cache(): ?array
{
$bundle_dir = storage_path('rsx-build/bundles');
- $js_pattern = "{$bundle_dir}/app.*.js";
- $css_pattern = "{$bundle_dir}/app.*.css";
- $js_files = glob($js_pattern);
- $css_files = glob($css_pattern);
+ // Look for split vendor/app files (current output format)
+ // Future: support merged files when Rsx_Mode::should_merge_bundles()
+ $vendor_js_pattern = "{$bundle_dir}/{$this->bundle_name}__vendor.*.js";
+ $app_js_pattern = "{$bundle_dir}/{$this->bundle_name}__app.*.js";
+ $vendor_css_pattern = "{$bundle_dir}/{$this->bundle_name}__vendor.*.css";
+ $app_css_pattern = "{$bundle_dir}/{$this->bundle_name}__app.*.css";
- if (!empty($js_files) || !empty($css_files)) {
- return [
- 'js_bundle_path' => !empty($js_files) ? basename($js_files[0]) : null,
- 'css_bundle_path' => !empty($css_files) ? basename($css_files[0]) : null,
+ $vendor_js_files = glob($vendor_js_pattern);
+ $app_js_files = glob($app_js_pattern);
+ $vendor_css_files = glob($vendor_css_pattern);
+ $app_css_files = glob($app_css_pattern);
+
+ // Need at least one app file (JS is typically required)
+ if (!empty($app_js_files)) {
+ $result = [
+ 'vendor_js_bundle_path' => !empty($vendor_js_files) ? basename($vendor_js_files[0]) : null,
+ 'app_js_bundle_path' => !empty($app_js_files) ? basename($app_js_files[0]) : null,
+ 'vendor_css_bundle_path' => !empty($vendor_css_files) ? basename($vendor_css_files[0]) : null,
+ 'app_css_bundle_path' => !empty($app_css_files) ? basename($app_css_files[0]) : null,
];
+
+ // Also resolve CDN assets - they're served separately via /_vendor/ URLs
+ // We need to resolve the bundle includes to get CDN asset definitions
+ $this->_resolve_cdn_assets_only();
+
+ if (!empty($this->cdn_assets['js'])) {
+ $result['cdn_js'] = $this->_prepare_cdn_assets($this->cdn_assets['js'], 'js');
+ }
+ if (!empty($this->cdn_assets['css'])) {
+ $result['cdn_css'] = $this->_prepare_cdn_assets($this->cdn_assets['css'], 'css');
+ }
+
+ return $result;
}
return null;
}
+ /**
+ * Resolve only CDN assets without full bundle compilation
+ *
+ * Used when serving from production cache to get CDN asset URLs
+ * without re-resolving and re-compiling all bundle files.
+ */
+ protected function _resolve_cdn_assets_only(): void
+ {
+ // Process required bundles first (they may have CDN assets)
+ $required_bundles = config('rsx.required_bundles', []);
+ $bundle_aliases = config('rsx.bundle_aliases', []);
+
+ foreach ($required_bundles as $alias) {
+ if (isset($bundle_aliases[$alias])) {
+ $this->_collect_cdn_assets_from_include($bundle_aliases[$alias]);
+ }
+ }
+
+ // Get the bundle's own CDN assets
+ $fqcn = $this->_get_bundle_fqcn($this->bundle_name);
+ $definition = $fqcn::define();
+
+ // Add CDN assets from the bundle definition (same format as main resolution)
+ if (!empty($definition['cdn_assets'])) {
+ if (!empty($definition['cdn_assets']['js'])) {
+ $this->cdn_assets['js'] = array_merge($this->cdn_assets['js'], $definition['cdn_assets']['js']);
+ }
+ if (!empty($definition['cdn_assets']['css'])) {
+ $this->cdn_assets['css'] = array_merge($this->cdn_assets['css'], $definition['cdn_assets']['css']);
+ }
+ }
+
+ // Also check for Asset Bundle includes that may have CDN assets
+ foreach ($definition['include'] ?? [] as $include) {
+ $this->_collect_cdn_assets_from_include($include);
+ }
+ }
+
+ /**
+ * Collect CDN assets from a bundle include without full resolution
+ */
+ protected function _collect_cdn_assets_from_include($include): void
+ {
+ // Handle config array format (from bundle aliases like jquery, lodash)
+ if (is_array($include) && isset($include['cdn'])) {
+ foreach ($include['cdn'] as $cdn_item) {
+ // Determine type from URL extension
+ $url = $cdn_item['url'] ?? '';
+ $type = str_ends_with($url, '.css') ? 'css' : 'js';
+ $this->cdn_assets[$type][] = $cdn_item;
+ }
+ return;
+ }
+
+ // Handle class name includes (could be Asset Bundles with CDN assets)
+ if (is_string($include) && Manifest::php_find_class($include)) {
+ if (Manifest::php_is_subclass_of($include, 'Rsx_Asset_Bundle_Abstract')) {
+ $asset_def = $include::define();
+ if (!empty($asset_def['cdn_assets'])) {
+ if (!empty($asset_def['cdn_assets']['js'])) {
+ $this->cdn_assets['js'] = array_merge($this->cdn_assets['js'], $asset_def['cdn_assets']['js']);
+ }
+ if (!empty($asset_def['cdn_assets']['css'])) {
+ $this->cdn_assets['css'] = array_merge($this->cdn_assets['css'], $asset_def['cdn_assets']['css']);
+ }
+ }
+ }
+ }
+ }
+
/**
* Process required bundles (jquery, lodash, jqhtml)
*/
@@ -1856,7 +1943,18 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Compile JS
if (!empty($files['js'])) {
- $js_content = $this->_compile_js_files($files['js']);
+ $js_files = $files['js'];
+
+ // CDN assets are served separately via /_vendor/ URLs, not merged into bundle
+ // This avoids complex concatenation issues with third-party code
+
+ $js_content = $this->_compile_js_files($js_files);
+
+ // Minify JS in production mode only (strips sourcemaps)
+ if (Rsx_Mode::is_production()) {
+ $js_content = Minifier::minify_js($js_content, "{$this->bundle_name}__{$type}.js");
+ }
+
$js_file = "{$this->bundle_name}__{$type}.{$hash}.js";
file_put_contents("{$bundle_dir}/{$js_file}", $js_content);
$outputs["{$type}_js"] = $js_file;
@@ -1864,7 +1962,18 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Compile CSS
if (!empty($files['css'])) {
- $css_content = $this->_compile_css_files($files['css']);
+ $css_files = $files['css'];
+
+ // CDN assets are served separately via /_vendor/ URLs, not merged into bundle
+ // This avoids complex concatenation issues with third-party code
+
+ $css_content = $this->_compile_css_files($css_files);
+
+ // Minify CSS in production mode only (strips sourcemaps)
+ if (Rsx_Mode::is_production()) {
+ $css_content = Minifier::minify_css($css_content, "{$this->bundle_name}__{$type}.css");
+ }
+
$css_file = "{$this->bundle_name}__{$type}.{$hash}.css";
file_put_contents("{$bundle_dir}/{$css_file}", $css_content);
$outputs["{$type}_css"] = $css_file;
@@ -2189,8 +2298,11 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// This preserves dependency sort order - we substitute babel versions during concat
foreach ($files as $file) {
- // Skip temp files and already processed files
- if (str_contains($file, 'storage/rsx-tmp/') || str_contains($file, 'storage/rsx-build/')) {
+ // Skip temp files, already processed files, and CDN cache files
+ // CDN files are third-party production code - don't transform them
+ if (str_contains($file, 'storage/rsx-tmp/') ||
+ str_contains($file, 'storage/rsx-build/') ||
+ str_contains($file, '.cdn-cache/')) {
continue;
}
@@ -2276,11 +2388,6 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
}
@unlink($output_file);
- // Minify in production (TODO: preserve source maps when minifying)
- if ($this->is_production) {
- // TODO: Add minification that preserves source maps
- }
-
return $js;
}
@@ -2332,12 +2439,103 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Clean up temp file
@unlink($output_file);
- // Minify in production
- if ($this->is_production) {
- // TODO: Add minification that preserves source maps
+ return $css;
+ }
+
+ /**
+ * Prepare CDN assets for rendering
+ *
+ * In development mode: returns assets as-is (loaded from CDN URLs)
+ * In production-like modes: ensures assets are cached and adds cached_filename
+ * so rendering can use /_vendor/{filename} URLs
+ *
+ * @param array $assets CDN assets array
+ * @param string $type 'js' or 'css'
+ * @return array Prepared assets with cached_filename in production modes
+ */
+ protected function _prepare_cdn_assets(array $assets, string $type): array
+ {
+ // In development mode, return as-is (use CDN URLs directly)
+ if (!$this->is_production) {
+ return $assets;
}
- return $css;
+ // In production-like modes, ensure cached and add filename
+ $prepared = [];
+ foreach ($assets as $asset) {
+ $url = $asset['url'] ?? '';
+ if (empty($url)) {
+ continue;
+ }
+
+ // Ensure the asset is cached (downloads if not already)
+ Cdn_Cache::get($url, $type);
+
+ // Add cached filename for /_vendor/ URL generation
+ $asset['cached_filename'] = Cdn_Cache::get_cache_filename($url, $type);
+ $prepared[] = $asset;
+ }
+
+ return $prepared;
+ }
+
+ /**
+ * Get local file paths for cached CDN assets (DEPRECATED)
+ *
+ * Used in production-like modes to include CDN assets in concat scripts
+ * for proper sourcemap handling.
+ *
+ * @param string $type 'js' or 'css'
+ * @return array Array of local file paths to cached CDN files
+ * @deprecated CDN assets are now served via /_vendor/ URLs, not merged into bundles
+ */
+ protected function _get_cdn_cache_file_paths(string $type): array
+ {
+ $file_paths = [];
+ $assets = $this->cdn_assets[$type] ?? [];
+
+ if (empty($assets)) {
+ return $file_paths;
+ }
+
+ // Sort assets: jQuery first, then others alphabetically
+ $jquery_assets = [];
+ $other_assets = [];
+
+ foreach ($assets as $asset) {
+ $url = $asset['url'] ?? '';
+ if (stripos($url, 'jquery') !== false) {
+ $jquery_assets[] = $asset;
+ } else {
+ $other_assets[] = $asset;
+ }
+ }
+
+ usort($other_assets, function ($a, $b) {
+ return strcmp($a['url'] ?? '', $b['url'] ?? '');
+ });
+
+ $sorted_assets = array_merge($jquery_assets, $other_assets);
+
+ // Get cache file path for each asset (downloads if not cached)
+ // If download fails, let it throw - CDN assets are required
+ foreach ($sorted_assets as $asset) {
+ $url = $asset['url'] ?? '';
+ if (empty($url)) {
+ continue;
+ }
+
+ // This will download and cache if not already cached
+ Cdn_Cache::get($url, $type);
+
+ // Get the cache file path
+ $cache_path = Cdn_Cache::get_cache_path($url, $type);
+ if (file_exists($cache_path)) {
+ $file_paths[] = $cache_path;
+ }
+ }
+
+ return $file_paths;
}
/**
diff --git a/app/RSpade/Core/Bundle/Cdn_Cache.php b/app/RSpade/Core/Bundle/Cdn_Cache.php
new file mode 100755
index 000000000..cc0db6a85
--- /dev/null
+++ b/app/RSpade/Core/Bundle/Cdn_Cache.php
@@ -0,0 +1,291 @@
+&1',
+ escapeshellarg($script),
+ escapeshellarg($base_url),
+ escapeshellarg($temp_input),
+ escapeshellarg($temp_output)
+ );
+
+ // Use shell_exec with exit code capture
+ $full_cmd = "({$cmd}); echo \$?";
+ $result = shell_exec($full_cmd);
+
+ // Clean up temp input
+ @unlink($temp_input);
+
+ // Parse exit code from last line
+ $lines = explode("\n", trim($result ?? ''));
+ $exit_code = (int) array_pop($lines);
+ $output = implode("\n", $lines);
+
+ if ($exit_code !== 0) {
+ // Clean up temp output if it exists
+ @unlink($temp_output);
+
+ throw new RuntimeException(
+ "Failed to inline CSS URLs for {$base_url}:\n{$output}"
+ );
+ }
+
+ // Read processed CSS
+ if (!file_exists($temp_output)) {
+ throw new RuntimeException(
+ "CSS URL inlining did not produce output for {$base_url}"
+ );
+ }
+
+ $result = file_get_contents($temp_output);
+
+ // Clean up temp output
+ @unlink($temp_output);
+
+ return $result;
+ }
+
+ /**
+ * Download URL content using curl
+ */
+ private static function _download(string $url): string|false
+ {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_USERAGENT => 'RSpade/1.0',
+ CURLOPT_SSL_VERIFYPEER => true,
+ ]);
+ $content = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code !== 200 || $content === false) {
+ return false;
+ }
+
+ return $content;
+ }
+
+ /**
+ * Clear the CDN cache
+ */
+ public static function clear(): void
+ {
+ $cache_dir = self::_get_cache_dir();
+
+ if (!is_dir($cache_dir)) {
+ return;
+ }
+
+ $files = glob("{$cache_dir}/*.js") + glob("{$cache_dir}/*.css");
+ foreach ($files as $file) {
+ unlink($file);
+ }
+ }
+
+ /**
+ * Get all cached files
+ *
+ * @return array Array of ['path' => string, 'url' => string, 'type' => string]
+ */
+ public static function get_cached_files(): array
+ {
+ $cache_dir = self::_get_cache_dir();
+
+ if (!is_dir($cache_dir)) {
+ return [];
+ }
+
+ $files = [];
+ foreach (glob("{$cache_dir}/*") as $file) {
+ if (is_file($file)) {
+ $ext = pathinfo($file, PATHINFO_EXTENSION);
+ if (in_array($ext, ['js', 'css'])) {
+ // Try to extract URL from file header
+ $content = file_get_contents($file, false, null, 0, 500);
+ $url = '';
+ if (preg_match('/CDN Source: (.+?) \*/', $content, $matches)) {
+ $url = $matches[1];
+ }
+ $files[] = [
+ 'path' => $file,
+ 'url' => $url,
+ 'type' => $ext,
+ ];
+ }
+ }
+ }
+
+ return $files;
+ }
+}
diff --git a/app/RSpade/Core/Bundle/Minifier.php b/app/RSpade/Core/Bundle/Minifier.php
new file mode 100755
index 000000000..25dbdd3c7
--- /dev/null
+++ b/app/RSpade/Core/Bundle/Minifier.php
@@ -0,0 +1,306 @@
+ static::$request_id,
+ 'method' => 'minify',
+ 'files' => [
+ [
+ 'type' => $type,
+ 'content' => $content,
+ 'filename' => $filename
+ ]
+ ]
+ ]) . "\n";
+
+ fwrite($socket, $request);
+
+ $response = fgets($socket);
+ fclose($socket);
+
+ if (!$response) {
+ throw new RuntimeException("No response from minify RPC server");
+ }
+
+ $result = json_decode($response, true);
+
+ if (!isset($result['results'][$filename])) {
+ throw new RuntimeException("Invalid response from minify RPC server");
+ }
+
+ $file_result = $result['results'][$filename];
+
+ if ($file_result['status'] === 'error') {
+ $error = $file_result['error'];
+ throw new RuntimeException(
+ "Minification failed for {$filename}: {$error['message']}"
+ );
+ }
+
+ return $file_result['result'];
+ }
+
+ /**
+ * Force restart the RPC server
+ *
+ * Call this before production builds to ensure code changes take effect.
+ */
+ public static function force_restart(): void
+ {
+ static::stop_rpc_server(force: true);
+ static::$server_started = false;
+ static::start_rpc_server();
+ }
+
+ /**
+ * Start RPC server for minification
+ */
+ public static function start_rpc_server(): void
+ {
+ $socket_path = base_path(self::RPC_SOCKET);
+ $server_script = base_path(self::RPC_SERVER_SCRIPT);
+
+ if (!file_exists($server_script)) {
+ throw new RuntimeException("Minify RPC server script not found at {$server_script}");
+ }
+
+ // If socket exists, check if it's stale
+ if (file_exists($socket_path)) {
+ if (static::ping_rpc_server()) {
+ static::$server_started = true;
+ return;
+ }
+ static::stop_rpc_server(force: true);
+ }
+
+ // Ensure socket directory exists
+ $socket_dir = dirname($socket_path);
+ if (!is_dir($socket_dir)) {
+ mkdir($socket_dir, 0755, true);
+ }
+
+ // Start RPC server
+ $process = new Process([
+ 'node',
+ $server_script,
+ '--socket=' . $socket_path
+ ]);
+
+ $process->setWorkingDirectory(base_path());
+ $process->setTimeout(null);
+ $process->start();
+
+ static::$rpc_server_process = $process;
+
+ // Wait for server to be ready
+ $max_wait_ms = 10000;
+ $wait_interval_ms = 50;
+ $iterations = $max_wait_ms / $wait_interval_ms;
+
+ for ($i = 0; $i < $iterations; $i++) {
+ usleep($wait_interval_ms * 1000);
+
+ if (static::ping_rpc_server()) {
+ static::$server_started = true;
+ register_shutdown_function([self::class, 'stop_rpc_server']);
+ return;
+ }
+ }
+
+ throw new RuntimeException(
+ "Minify RPC server failed to start within {$max_wait_ms}ms.\n" .
+ "Check that Node.js, Terser, and cssnano are installed."
+ );
+ }
+
+ /**
+ * Ping the RPC server
+ */
+ public static function ping_rpc_server(): bool
+ {
+ $socket_path = base_path(self::RPC_SOCKET);
+
+ if (!file_exists($socket_path)) {
+ return false;
+ }
+
+ try {
+ $socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
+ if (!$socket) {
+ return false;
+ }
+
+ stream_set_blocking($socket, true);
+
+ static::$request_id++;
+ $request = json_encode([
+ 'id' => static::$request_id,
+ 'method' => 'ping'
+ ]) . "\n";
+
+ fwrite($socket, $request);
+ $response = fgets($socket);
+ fclose($socket);
+
+ if (!$response) {
+ return false;
+ }
+
+ $result = json_decode($response, true);
+ return isset($result['result']) && $result['result'] === 'pong';
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Stop the RPC server
+ */
+ public static function stop_rpc_server(bool $force = false): void
+ {
+ $socket_path = base_path(self::RPC_SOCKET);
+
+ if ($force) {
+ if (file_exists($socket_path)) {
+ try {
+ $socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
+ if ($socket) {
+ stream_set_blocking($socket, true);
+
+ static::$request_id++;
+ $request = json_encode([
+ 'id' => static::$request_id,
+ 'method' => 'shutdown'
+ ]) . "\n";
+
+ fwrite($socket, $request);
+ fclose($socket);
+ }
+ } catch (\Exception $e) {
+ // Ignore errors during force shutdown
+ }
+
+ usleep(100000); // 100ms
+
+ if (file_exists($socket_path)) {
+ @unlink($socket_path);
+ }
+ }
+
+ if (static::$rpc_server_process) {
+ if (static::$rpc_server_process->isRunning()) {
+ static::$rpc_server_process->stop(1, SIGTERM);
+ }
+ static::$rpc_server_process = null;
+ }
+
+ static::$server_started = false;
+ return;
+ }
+
+ // Graceful shutdown
+ if (file_exists($socket_path)) {
+ try {
+ $socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
+ if ($socket) {
+ stream_set_blocking($socket, true);
+
+ static::$request_id++;
+ $request = json_encode([
+ 'id' => static::$request_id,
+ 'method' => 'shutdown'
+ ]) . "\n";
+
+ fwrite($socket, $request);
+ fclose($socket);
+ }
+ } catch (\Exception $e) {
+ // Ignore errors
+ }
+ }
+
+ static::$server_started = false;
+ }
+}
diff --git a/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php b/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php
index 09a288412..7d74f4b20 100644
--- a/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php
+++ b/app/RSpade/Core/Bundle/Rsx_Bundle_Abstract.php
@@ -6,6 +6,7 @@ use RuntimeException;
use App\RSpade\CodeQuality\RuntimeChecks\BundleErrors;
use App\RSpade\Core\Bundle\BundleCompiler;
use App\RSpade\Core\Manifest\Manifest;
+use App\RSpade\Core\Mode\Rsx_Mode;
use App\RSpade\Core\Session\Session;
/**
@@ -127,7 +128,7 @@ abstract class Rsx_Bundle_Abstract
\App\RSpade\Core\Debug\Debugger::dump_console_debug_messages_to_html();
// In development mode, validate path coverage
- if (!app()->environment('production')) {
+ if (Rsx_Mode::is_development()) {
static::__validate_path_coverage($bundle_class);
}
@@ -227,23 +228,13 @@ abstract class Rsx_Bundle_Abstract
throw new RuntimeException('BundleCompiler returned non-array: ' . gettype($compiled));
}
- // In development, we should have vendor/app split files
- // In production, we should have combined files
- $is_production = app()->environment('production');
-
- if (!$is_production) {
- // Development mode - expect vendor/app bundle paths
- if (!isset($compiled['vendor_js_bundle_path']) &&
- !isset($compiled['app_js_bundle_path']) &&
- !isset($compiled['vendor_css_bundle_path']) &&
- !isset($compiled['app_css_bundle_path'])) {
- throw new RuntimeException('BundleCompiler missing expected vendor/app bundle paths. Got keys: ' . implode(', ', array_keys($compiled)));
- }
- } else {
- // Production mode - expect combined bundle paths
- if (!isset($compiled['js_bundle_path']) && !isset($compiled['css_bundle_path'])) {
- throw new RuntimeException('BundleCompiler missing expected js/css bundle paths. Got keys: ' . implode(', ', array_keys($compiled)));
- }
+ // Expect vendor/app split files (merging not yet implemented)
+ // Future: check Rsx_Mode::should_merge_bundles() for combined files
+ if (!isset($compiled['vendor_js_bundle_path']) &&
+ !isset($compiled['app_js_bundle_path']) &&
+ !isset($compiled['vendor_css_bundle_path']) &&
+ !isset($compiled['app_css_bundle_path'])) {
+ throw new RuntimeException('BundleCompiler missing expected vendor/app bundle paths. Got keys: ' . implode(', ', array_keys($compiled)));
}
$html = [];
@@ -269,12 +260,16 @@ abstract class Rsx_Bundle_Abstract
}
// Add runtime data
- $rsxapp_data['debug'] = !app()->environment('production');
+ $rsxapp_data['debug'] = Rsx_Mode::is_development();
$rsxapp_data['current_controller'] = \App\RSpade\Core\Rsx::get_current_controller();
$rsxapp_data['current_action'] = \App\RSpade\Core\Rsx::get_current_action();
$rsxapp_data['is_auth'] = Session::is_logged_in();
$rsxapp_data['is_spa'] = \App\RSpade\Core\Rsx::is_spa();
- $rsxapp_data['ajax_disable_batching'] = config('rsx.development.ajax_disable_batching', false);
+
+ // Only include ajax_disable_batching in development mode
+ if (Rsx_Mode::is_development()) {
+ $rsxapp_data['ajax_disable_batching'] = config('rsx.development.ajax_disable_batching', false);
+ }
// Add current params (always set to reduce state variations)
$current_params = \App\RSpade\Core\Rsx::get_current_params();
@@ -305,8 +300,8 @@ abstract class Rsx_Bundle_Abstract
$rsxapp_data['server_time'] = \App\RSpade\Core\Time\Rsx_Time::now_iso();
$rsxapp_data['user_timezone'] = \App\RSpade\Core\Time\Rsx_Time::get_user_timezone();
- // Add console_debug config in non-production mode
- if (!app()->environment('production')) {
+ // Add console_debug config only in development mode
+ if (Rsx_Mode::should_include_debug_info()) {
$console_debug_config = config('rsx.console_debug', []);
// Build console_debug settings
@@ -371,10 +366,8 @@ abstract class Rsx_Bundle_Abstract
// Filter out keys starting with single underscore (but allow double underscore like __MODEL)
$rsxapp_data = static::__filter_underscore_keys($rsxapp_data);
- // Pretty print JSON in non-production environments
- $rsxapp_json = app()->environment('production')
- ? json_encode($rsxapp_data, JSON_UNESCAPED_SLASHES)
- : json_encode($rsxapp_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ // Always pretty print rsxapp for debuggability in browser dev tools
+ $rsxapp_json = json_encode($rsxapp_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$html[] = '';
@@ -413,13 +406,20 @@ abstract class Rsx_Bundle_Abstract
// Add CSS: jQuery first, then others
foreach (array_merge($jquery_css, $other_css) as $asset) {
- $tag = '';
+ } else {
+ // Development mode: use CDN URL directly
+ $tag = '';
+ } else {
+ // Development mode: use CDN URL directly
+ $tag = '
+
+```
+
+- `dist/csstree.esm.js` – minified ES module
+```html
+
+```
+
+One of CDN services like `unpkg` or `jsDelivr` can be used. By default (for short path) a ESM version is exposing. For IIFE version a full path to a bundle should be specified:
+
+```html
+
+
+
+
+
+
+```
+
## Top level API
-
+
## License
diff --git a/node_modules/css-tree/cjs/convertor/create.cjs b/node_modules/css-tree/cjs/convertor/create.cjs
new file mode 100644
index 000000000..55c655b24
--- /dev/null
+++ b/node_modules/css-tree/cjs/convertor/create.cjs
@@ -0,0 +1,32 @@
+'use strict';
+
+const List = require('../utils/List.cjs');
+
+function createConvertor(walk) {
+ return {
+ fromPlainObject(ast) {
+ walk(ast, {
+ enter(node) {
+ if (node.children && node.children instanceof List.List === false) {
+ node.children = new List.List().fromArray(node.children);
+ }
+ }
+ });
+
+ return ast;
+ },
+ toPlainObject(ast) {
+ walk(ast, {
+ leave(node) {
+ if (node.children && node.children instanceof List.List) {
+ node.children = node.children.toArray();
+ }
+ }
+ });
+
+ return ast;
+ }
+ };
+}
+
+exports.createConvertor = createConvertor;
diff --git a/node_modules/css-tree/cjs/convertor/index.cjs b/node_modules/css-tree/cjs/convertor/index.cjs
new file mode 100644
index 000000000..665427855
--- /dev/null
+++ b/node_modules/css-tree/cjs/convertor/index.cjs
@@ -0,0 +1,8 @@
+'use strict';
+
+const create = require('./create.cjs');
+const index$1 = require('../walker/index.cjs');
+
+const index = create.createConvertor(index$1);
+
+module.exports = index;
diff --git a/node_modules/css-tree/cjs/data-patch.cjs b/node_modules/css-tree/cjs/data-patch.cjs
new file mode 100755
index 000000000..9103ea4c2
--- /dev/null
+++ b/node_modules/css-tree/cjs/data-patch.cjs
@@ -0,0 +1,7 @@
+'use strict';
+
+const patch = require('../data/patch.json');
+
+const patch$1 = patch;
+
+module.exports = patch$1;
diff --git a/node_modules/css-tree/cjs/data.cjs b/node_modules/css-tree/cjs/data.cjs
new file mode 100755
index 000000000..258ac6a31
--- /dev/null
+++ b/node_modules/css-tree/cjs/data.cjs
@@ -0,0 +1,120 @@
+'use strict';
+
+const dataPatch = require('./data-patch.cjs');
+
+const mdnAtrules = require('mdn-data/css/at-rules.json');
+const mdnProperties = require('mdn-data/css/properties.json');
+const mdnSyntaxes = require('mdn-data/css/syntaxes.json');
+
+const hasOwn = Object.hasOwn || ((object, property) => Object.prototype.hasOwnProperty.call(object, property));
+const extendSyntax = /^\s*\|\s*/;
+
+function preprocessAtrules(dict) {
+ const result = Object.create(null);
+
+ for (const [atruleName, atrule] of Object.entries(dict)) {
+ let descriptors = null;
+
+ if (atrule.descriptors) {
+ descriptors = Object.create(null);
+
+ for (const [name, descriptor] of Object.entries(atrule.descriptors)) {
+ descriptors[name] = descriptor.syntax;
+ }
+ }
+
+ result[atruleName.substr(1)] = {
+ prelude: atrule.syntax.trim().replace(/\{(.|\s)+\}/, '').match(/^@\S+\s+([^;\{]*)/)[1].trim() || null,
+ descriptors
+ };
+ }
+
+ return result;
+}
+
+function patchDictionary(dict, patchDict) {
+ const result = Object.create(null);
+
+ // copy all syntaxes for an original dict
+ for (const [key, value] of Object.entries(dict)) {
+ if (value) {
+ result[key] = value.syntax || value;
+ }
+ }
+
+ // apply a patch
+ for (const key of Object.keys(patchDict)) {
+ if (hasOwn(dict, key)) {
+ if (patchDict[key].syntax) {
+ result[key] = extendSyntax.test(patchDict[key].syntax)
+ ? result[key] + ' ' + patchDict[key].syntax.trim()
+ : patchDict[key].syntax;
+ } else {
+ delete result[key];
+ }
+ } else {
+ if (patchDict[key].syntax) {
+ result[key] = patchDict[key].syntax.replace(extendSyntax, '');
+ }
+ }
+ }
+
+ return result;
+}
+
+function preprocessPatchAtrulesDescritors(declarations) {
+ const result = {};
+
+ for (const [key, value] of Object.entries(declarations || {})) {
+ result[key] = typeof value === 'string'
+ ? { syntax: value }
+ : value;
+ }
+
+ return result;
+}
+
+function patchAtrules(dict, patchDict) {
+ const result = {};
+
+ // copy all syntaxes for an original dict
+ for (const key in dict) {
+ if (patchDict[key] === null) {
+ continue;
+ }
+
+ const atrulePatch = patchDict[key] || {};
+
+ result[key] = {
+ prelude: key in patchDict && 'prelude' in atrulePatch
+ ? atrulePatch.prelude
+ : dict[key].prelude || null,
+ descriptors: patchDictionary(
+ dict[key].descriptors || {},
+ preprocessPatchAtrulesDescritors(atrulePatch.descriptors)
+ )
+ };
+ }
+
+ // apply a patch
+ for (const [key, atrulePatch] of Object.entries(patchDict)) {
+ if (atrulePatch && !hasOwn(dict, key)) {
+ result[key] = {
+ prelude: atrulePatch.prelude || null,
+ descriptors: atrulePatch.descriptors
+ ? patchDictionary({}, preprocessPatchAtrulesDescritors(atrulePatch.descriptors))
+ : null
+ };
+ }
+ }
+
+ return result;
+}
+
+const definitions = {
+ types: patchDictionary(mdnSyntaxes, dataPatch.types),
+ atrules: patchAtrules(preprocessAtrules(mdnAtrules), dataPatch.atrules),
+ properties: patchDictionary(mdnProperties, dataPatch.properties)
+};
+
+module.exports = definitions;
diff --git a/node_modules/css-tree/cjs/definition-syntax/SyntaxError.cjs b/node_modules/css-tree/cjs/definition-syntax/SyntaxError.cjs
new file mode 100644
index 000000000..d24e7ceda
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/SyntaxError.cjs
@@ -0,0 +1,16 @@
+'use strict';
+
+const createCustomError = require('../utils/create-custom-error.cjs');
+
+function SyntaxError(message, input, offset) {
+ return Object.assign(createCustomError.createCustomError('SyntaxError', message), {
+ input,
+ offset,
+ rawMessage: message,
+ message: message + '\n' +
+ ' ' + input + '\n' +
+ '--' + new Array((offset || input.length) + 1).join('-') + '^'
+ });
+}
+
+exports.SyntaxError = SyntaxError;
diff --git a/node_modules/css-tree/cjs/definition-syntax/generate.cjs b/node_modules/css-tree/cjs/definition-syntax/generate.cjs
new file mode 100644
index 000000000..ff9f0ad47
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/generate.cjs
@@ -0,0 +1,139 @@
+'use strict';
+
+function noop(value) {
+ return value;
+}
+
+function generateMultiplier(multiplier) {
+ const { min, max, comma } = multiplier;
+
+ if (min === 0 && max === 0) {
+ return comma ? '#?' : '*';
+ }
+
+ if (min === 0 && max === 1) {
+ return '?';
+ }
+
+ if (min === 1 && max === 0) {
+ return comma ? '#' : '+';
+ }
+
+ if (min === 1 && max === 1) {
+ return '';
+ }
+
+ return (
+ (comma ? '#' : '') +
+ (min === max
+ ? '{' + min + '}'
+ : '{' + min + ',' + (max !== 0 ? max : '') + '}'
+ )
+ );
+}
+
+function generateTypeOpts(node) {
+ switch (node.type) {
+ case 'Range':
+ return (
+ ' [' +
+ (node.min === null ? '-∞' : node.min) +
+ ',' +
+ (node.max === null ? '∞' : node.max) +
+ ']'
+ );
+
+ default:
+ throw new Error('Unknown node type `' + node.type + '`');
+ }
+}
+
+function generateSequence(node, decorate, forceBraces, compact) {
+ const combinator = node.combinator === ' ' || compact ? node.combinator : ' ' + node.combinator + ' ';
+ const result = node.terms
+ .map(term => internalGenerate(term, decorate, forceBraces, compact))
+ .join(combinator);
+
+ if (node.explicit || forceBraces) {
+ return (compact || result[0] === ',' ? '[' : '[ ') + result + (compact ? ']' : ' ]');
+ }
+
+ return result;
+}
+
+function internalGenerate(node, decorate, forceBraces, compact) {
+ let result;
+
+ switch (node.type) {
+ case 'Group':
+ result =
+ generateSequence(node, decorate, forceBraces, compact) +
+ (node.disallowEmpty ? '!' : '');
+ break;
+
+ case 'Multiplier':
+ // return since node is a composition
+ return (
+ internalGenerate(node.term, decorate, forceBraces, compact) +
+ decorate(generateMultiplier(node), node)
+ );
+
+ case 'Boolean':
+ result = '';
+ break;
+
+ case 'Type':
+ result = '<' + node.name + (node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') + '>';
+ break;
+
+ case 'Property':
+ result = '<\'' + node.name + '\'>';
+ break;
+
+ case 'Keyword':
+ result = node.name;
+ break;
+
+ case 'AtKeyword':
+ result = '@' + node.name;
+ break;
+
+ case 'Function':
+ result = node.name + '(';
+ break;
+
+ case 'String':
+ case 'Token':
+ result = node.value;
+ break;
+
+ case 'Comma':
+ result = ',';
+ break;
+
+ default:
+ throw new Error('Unknown node type `' + node.type + '`');
+ }
+
+ return decorate(result, node);
+}
+
+function generate(node, options) {
+ let decorate = noop;
+ let forceBraces = false;
+ let compact = false;
+
+ if (typeof options === 'function') {
+ decorate = options;
+ } else if (options) {
+ forceBraces = Boolean(options.forceBraces);
+ compact = Boolean(options.compact);
+ if (typeof options.decorate === 'function') {
+ decorate = options.decorate;
+ }
+ }
+
+ return internalGenerate(node, decorate, forceBraces, compact);
+}
+
+exports.generate = generate;
diff --git a/node_modules/css-tree/cjs/definition-syntax/index.cjs b/node_modules/css-tree/cjs/definition-syntax/index.cjs
new file mode 100644
index 000000000..0afb505c9
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/index.cjs
@@ -0,0 +1,13 @@
+'use strict';
+
+const SyntaxError = require('./SyntaxError.cjs');
+const generate = require('./generate.cjs');
+const parse = require('./parse.cjs');
+const walk = require('./walk.cjs');
+
+
+
+exports.SyntaxError = SyntaxError.SyntaxError;
+exports.generate = generate.generate;
+exports.parse = parse.parse;
+exports.walk = walk.walk;
diff --git a/node_modules/css-tree/cjs/definition-syntax/parse.cjs b/node_modules/css-tree/cjs/definition-syntax/parse.cjs
new file mode 100644
index 000000000..b17b26792
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/parse.cjs
@@ -0,0 +1,556 @@
+'use strict';
+
+const scanner = require('./scanner.cjs');
+
+const TAB = 9;
+const N = 10;
+const F = 12;
+const R = 13;
+const SPACE = 32;
+const EXCLAMATIONMARK = 33; // !
+const NUMBERSIGN = 35; // #
+const AMPERSAND = 38; // &
+const APOSTROPHE = 39; // '
+const LEFTPARENTHESIS = 40; // (
+const RIGHTPARENTHESIS = 41; // )
+const ASTERISK = 42; // *
+const PLUSSIGN = 43; // +
+const COMMA = 44; // ,
+const HYPERMINUS = 45; // -
+const LESSTHANSIGN = 60; // <
+const GREATERTHANSIGN = 62; // >
+const QUESTIONMARK = 63; // ?
+const COMMERCIALAT = 64; // @
+const LEFTSQUAREBRACKET = 91; // [
+const RIGHTSQUAREBRACKET = 93; // ]
+const LEFTCURLYBRACKET = 123; // {
+const VERTICALLINE = 124; // |
+const RIGHTCURLYBRACKET = 125; // }
+const INFINITY = 8734; // ∞
+const COMBINATOR_PRECEDENCE = {
+ ' ': 1,
+ '&&': 2,
+ '||': 3,
+ '|': 4
+};
+
+function readMultiplierRange(scanner) {
+ let min = null;
+ let max = null;
+
+ scanner.eat(LEFTCURLYBRACKET);
+ scanner.skipWs();
+
+ min = scanner.scanNumber(scanner);
+ scanner.skipWs();
+
+ if (scanner.charCode() === COMMA) {
+ scanner.pos++;
+ scanner.skipWs();
+
+ if (scanner.charCode() !== RIGHTCURLYBRACKET) {
+ max = scanner.scanNumber(scanner);
+ scanner.skipWs();
+ }
+ } else {
+ max = min;
+ }
+
+ scanner.eat(RIGHTCURLYBRACKET);
+
+ return {
+ min: Number(min),
+ max: max ? Number(max) : 0
+ };
+}
+
+function readMultiplier(scanner) {
+ let range = null;
+ let comma = false;
+
+ switch (scanner.charCode()) {
+ case ASTERISK:
+ scanner.pos++;
+
+ range = {
+ min: 0,
+ max: 0
+ };
+
+ break;
+
+ case PLUSSIGN:
+ scanner.pos++;
+
+ range = {
+ min: 1,
+ max: 0
+ };
+
+ break;
+
+ case QUESTIONMARK:
+ scanner.pos++;
+
+ range = {
+ min: 0,
+ max: 1
+ };
+
+ break;
+
+ case NUMBERSIGN:
+ scanner.pos++;
+
+ comma = true;
+
+ if (scanner.charCode() === LEFTCURLYBRACKET) {
+ range = readMultiplierRange(scanner);
+ } else if (scanner.charCode() === QUESTIONMARK) {
+ // https://www.w3.org/TR/css-values-4/#component-multipliers
+ // > the # and ? multipliers may be stacked as #?
+ // In this case just treat "#?" as a single multiplier
+ // { min: 0, max: 0, comma: true }
+ scanner.pos++;
+ range = {
+ min: 0,
+ max: 0
+ };
+ } else {
+ range = {
+ min: 1,
+ max: 0
+ };
+ }
+
+ break;
+
+ case LEFTCURLYBRACKET:
+ range = readMultiplierRange(scanner);
+ break;
+
+ default:
+ return null;
+ }
+
+ return {
+ type: 'Multiplier',
+ comma,
+ min: range.min,
+ max: range.max,
+ term: null
+ };
+}
+
+function maybeMultiplied(scanner, node) {
+ const multiplier = readMultiplier(scanner);
+
+ if (multiplier !== null) {
+ multiplier.term = node;
+
+ // https://www.w3.org/TR/css-values-4/#component-multipliers
+ // > The + and # multipliers may be stacked as +#;
+ // Represent "+#" as nested multipliers:
+ // { ...,
+ // term: {
+ // ...,
+ // term: node
+ // }
+ // }
+ if (scanner.charCode() === NUMBERSIGN &&
+ scanner.charCodeAt(scanner.pos - 1) === PLUSSIGN) {
+ return maybeMultiplied(scanner, multiplier);
+ }
+
+ return multiplier;
+ }
+
+ return node;
+}
+
+function maybeToken(scanner) {
+ const ch = scanner.peek();
+
+ if (ch === '') {
+ return null;
+ }
+
+ return maybeMultiplied(scanner, {
+ type: 'Token',
+ value: ch
+ });
+}
+
+function readProperty(scanner) {
+ let name;
+
+ scanner.eat(LESSTHANSIGN);
+ scanner.eat(APOSTROPHE);
+
+ name = scanner.scanWord();
+
+ scanner.eat(APOSTROPHE);
+ scanner.eat(GREATERTHANSIGN);
+
+ return maybeMultiplied(scanner, {
+ type: 'Property',
+ name
+ });
+}
+
+// https://drafts.csswg.org/css-values-3/#numeric-ranges
+// 4.1. Range Restrictions and Range Definition Notation
+//
+// Range restrictions can be annotated in the numeric type notation using CSS bracketed
+// range notation—[min,max]—within the angle brackets, after the identifying keyword,
+// indicating a closed range between (and including) min and max.
+// For example, indicates an integer between 0 and 10, inclusive.
+function readTypeRange(scanner) {
+ // use null for Infinity to make AST format JSON serializable/deserializable
+ let min = null; // -Infinity
+ let max = null; // Infinity
+ let sign = 1;
+
+ scanner.eat(LEFTSQUAREBRACKET);
+
+ if (scanner.charCode() === HYPERMINUS) {
+ scanner.peek();
+ sign = -1;
+ }
+
+ if (sign == -1 && scanner.charCode() === INFINITY) {
+ scanner.peek();
+ } else {
+ min = sign * Number(scanner.scanNumber(scanner));
+
+ if (scanner.isNameCharCode()) {
+ min += scanner.scanWord();
+ }
+ }
+
+ scanner.skipWs();
+ scanner.eat(COMMA);
+ scanner.skipWs();
+
+ if (scanner.charCode() === INFINITY) {
+ scanner.peek();
+ } else {
+ sign = 1;
+
+ if (scanner.charCode() === HYPERMINUS) {
+ scanner.peek();
+ sign = -1;
+ }
+
+ max = sign * Number(scanner.scanNumber(scanner));
+
+ if (scanner.isNameCharCode()) {
+ max += scanner.scanWord();
+ }
+ }
+
+ scanner.eat(RIGHTSQUAREBRACKET);
+
+ return {
+ type: 'Range',
+ min,
+ max
+ };
+}
+
+function readType(scanner) {
+ let name;
+ let opts = null;
+
+ scanner.eat(LESSTHANSIGN);
+ name = scanner.scanWord();
+
+ // https://drafts.csswg.org/css-values-5/#boolean
+ if (name === 'boolean-expr') {
+ scanner.eat(LEFTSQUAREBRACKET);
+
+ const implicitGroup = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
+
+ scanner.eat(RIGHTSQUAREBRACKET);
+ scanner.eat(GREATERTHANSIGN);
+
+ return maybeMultiplied(scanner, {
+ type: 'Boolean',
+ term: implicitGroup.terms.length === 1
+ ? implicitGroup.terms[0]
+ : implicitGroup
+ });
+ }
+
+ if (scanner.charCode() === LEFTPARENTHESIS &&
+ scanner.nextCharCode() === RIGHTPARENTHESIS) {
+ scanner.pos += 2;
+ name += '()';
+ }
+
+ if (scanner.charCodeAt(scanner.findWsEnd(scanner.pos)) === LEFTSQUAREBRACKET) {
+ scanner.skipWs();
+ opts = readTypeRange(scanner);
+ }
+
+ scanner.eat(GREATERTHANSIGN);
+
+ return maybeMultiplied(scanner, {
+ type: 'Type',
+ name,
+ opts
+ });
+}
+
+function readKeywordOrFunction(scanner) {
+ const name = scanner.scanWord();
+
+ if (scanner.charCode() === LEFTPARENTHESIS) {
+ scanner.pos++;
+
+ return {
+ type: 'Function',
+ name
+ };
+ }
+
+ return maybeMultiplied(scanner, {
+ type: 'Keyword',
+ name
+ });
+}
+
+function regroupTerms(terms, combinators) {
+ function createGroup(terms, combinator) {
+ return {
+ type: 'Group',
+ terms,
+ combinator,
+ disallowEmpty: false,
+ explicit: false
+ };
+ }
+
+ let combinator;
+
+ combinators = Object.keys(combinators)
+ .sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]);
+
+ while (combinators.length > 0) {
+ combinator = combinators.shift();
+
+ let i = 0;
+ let subgroupStart = 0;
+
+ for (; i < terms.length; i++) {
+ const term = terms[i];
+
+ if (term.type === 'Combinator') {
+ if (term.value === combinator) {
+ if (subgroupStart === -1) {
+ subgroupStart = i - 1;
+ }
+ terms.splice(i, 1);
+ i--;
+ } else {
+ if (subgroupStart !== -1 && i - subgroupStart > 1) {
+ terms.splice(
+ subgroupStart,
+ i - subgroupStart,
+ createGroup(terms.slice(subgroupStart, i), combinator)
+ );
+ i = subgroupStart + 1;
+ }
+ subgroupStart = -1;
+ }
+ }
+ }
+
+ if (subgroupStart !== -1 && combinators.length) {
+ terms.splice(
+ subgroupStart,
+ i - subgroupStart,
+ createGroup(terms.slice(subgroupStart, i), combinator)
+ );
+ }
+ }
+
+ return combinator;
+}
+
+function readImplicitGroup(scanner, stopCharCode) {
+ const combinators = Object.create(null);
+ const terms = [];
+ let token;
+ let prevToken = null;
+ let prevTokenPos = scanner.pos;
+
+ while (scanner.charCode() !== stopCharCode && (token = peek(scanner, stopCharCode))) {
+ if (token.type !== 'Spaces') {
+ if (token.type === 'Combinator') {
+ // check for combinator in group beginning and double combinator sequence
+ if (prevToken === null || prevToken.type === 'Combinator') {
+ scanner.pos = prevTokenPos;
+ scanner.error('Unexpected combinator');
+ }
+
+ combinators[token.value] = true;
+ } else if (prevToken !== null && prevToken.type !== 'Combinator') {
+ combinators[' '] = true; // a b
+ terms.push({
+ type: 'Combinator',
+ value: ' '
+ });
+ }
+
+ terms.push(token);
+ prevToken = token;
+ prevTokenPos = scanner.pos;
+ }
+ }
+
+ // check for combinator in group ending
+ if (prevToken !== null && prevToken.type === 'Combinator') {
+ scanner.pos -= prevTokenPos;
+ scanner.error('Unexpected combinator');
+ }
+
+ return {
+ type: 'Group',
+ terms,
+ combinator: regroupTerms(terms, combinators) || ' ',
+ disallowEmpty: false,
+ explicit: false
+ };
+}
+
+function readGroup(scanner, stopCharCode) {
+ let result;
+
+ scanner.eat(LEFTSQUAREBRACKET);
+ result = readImplicitGroup(scanner, stopCharCode);
+ scanner.eat(RIGHTSQUAREBRACKET);
+
+ result.explicit = true;
+
+ if (scanner.charCode() === EXCLAMATIONMARK) {
+ scanner.pos++;
+ result.disallowEmpty = true;
+ }
+
+ return result;
+}
+
+function peek(scanner, stopCharCode) {
+ let code = scanner.charCode();
+
+ switch (code) {
+ case RIGHTSQUAREBRACKET:
+ // don't eat, stop scan a group
+ break;
+
+ case LEFTSQUAREBRACKET:
+ return maybeMultiplied(scanner, readGroup(scanner, stopCharCode));
+
+ case LESSTHANSIGN:
+ return scanner.nextCharCode() === APOSTROPHE
+ ? readProperty(scanner)
+ : readType(scanner);
+
+ case VERTICALLINE:
+ return {
+ type: 'Combinator',
+ value: scanner.substringToPos(
+ scanner.pos + (scanner.nextCharCode() === VERTICALLINE ? 2 : 1)
+ )
+ };
+
+ case AMPERSAND:
+ scanner.pos++;
+ scanner.eat(AMPERSAND);
+
+ return {
+ type: 'Combinator',
+ value: '&&'
+ };
+
+ case COMMA:
+ scanner.pos++;
+ return {
+ type: 'Comma'
+ };
+
+ case APOSTROPHE:
+ return maybeMultiplied(scanner, {
+ type: 'String',
+ value: scanner.scanString()
+ });
+
+ case SPACE:
+ case TAB:
+ case N:
+ case R:
+ case F:
+ return {
+ type: 'Spaces',
+ value: scanner.scanSpaces()
+ };
+
+ case COMMERCIALAT:
+ code = scanner.nextCharCode();
+
+ if (scanner.isNameCharCode(code)) {
+ scanner.pos++;
+ return {
+ type: 'AtKeyword',
+ name: scanner.scanWord()
+ };
+ }
+
+ return maybeToken(scanner);
+
+ case ASTERISK:
+ case PLUSSIGN:
+ case QUESTIONMARK:
+ case NUMBERSIGN:
+ case EXCLAMATIONMARK:
+ // prohibited tokens (used as a multiplier start)
+ break;
+
+ case LEFTCURLYBRACKET:
+ // LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting
+ // check next char isn't a number, because it's likely a disjoined multiplier
+ code = scanner.nextCharCode();
+
+ if (code < 48 || code > 57) {
+ return maybeToken(scanner);
+ }
+
+ break;
+
+ default:
+ if (scanner.isNameCharCode(code)) {
+ return readKeywordOrFunction(scanner);
+ }
+
+ return maybeToken(scanner);
+ }
+}
+
+function parse(source) {
+ const scanner$1 = new scanner.Scanner(source);
+ const result = readImplicitGroup(scanner$1);
+
+ if (scanner$1.pos !== source.length) {
+ scanner$1.error('Unexpected input');
+ }
+
+ // reduce redundant groups with single group term
+ if (result.terms.length === 1 && result.terms[0].type === 'Group') {
+ return result.terms[0];
+ }
+
+ return result;
+}
+
+exports.parse = parse;
diff --git a/node_modules/css-tree/cjs/definition-syntax/scanner.cjs b/node_modules/css-tree/cjs/definition-syntax/scanner.cjs
new file mode 100644
index 000000000..0bad36a4a
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/scanner.cjs
@@ -0,0 +1,113 @@
+'use strict';
+
+const SyntaxError = require('./SyntaxError.cjs');
+
+const TAB = 9;
+const N = 10;
+const F = 12;
+const R = 13;
+const SPACE = 32;
+const NAME_CHAR = new Uint8Array(128).map((_, idx) =>
+ /[a-zA-Z0-9\-]/.test(String.fromCharCode(idx)) ? 1 : 0
+);
+
+class Scanner {
+ constructor(str) {
+ this.str = str;
+ this.pos = 0;
+ }
+
+ charCodeAt(pos) {
+ return pos < this.str.length ? this.str.charCodeAt(pos) : 0;
+ }
+ charCode() {
+ return this.charCodeAt(this.pos);
+ }
+ isNameCharCode(code = this.charCode()) {
+ return code < 128 && NAME_CHAR[code] === 1;
+ }
+ nextCharCode() {
+ return this.charCodeAt(this.pos + 1);
+ }
+ nextNonWsCode(pos) {
+ return this.charCodeAt(this.findWsEnd(pos));
+ }
+ skipWs() {
+ this.pos = this.findWsEnd(this.pos);
+ }
+ findWsEnd(pos) {
+ for (; pos < this.str.length; pos++) {
+ const code = this.str.charCodeAt(pos);
+ if (code !== R && code !== N && code !== F && code !== SPACE && code !== TAB) {
+ break;
+ }
+ }
+
+ return pos;
+ }
+ substringToPos(end) {
+ return this.str.substring(this.pos, this.pos = end);
+ }
+ eat(code) {
+ if (this.charCode() !== code) {
+ this.error('Expect `' + String.fromCharCode(code) + '`');
+ }
+
+ this.pos++;
+ }
+ peek() {
+ return this.pos < this.str.length ? this.str.charAt(this.pos++) : '';
+ }
+ error(message) {
+ throw new SyntaxError.SyntaxError(message, this.str, this.pos);
+ }
+
+ scanSpaces() {
+ return this.substringToPos(this.findWsEnd(this.pos));
+ }
+ scanWord() {
+ let end = this.pos;
+
+ for (; end < this.str.length; end++) {
+ const code = this.str.charCodeAt(end);
+ if (code >= 128 || NAME_CHAR[code] === 0) {
+ break;
+ }
+ }
+
+ if (this.pos === end) {
+ this.error('Expect a keyword');
+ }
+
+ return this.substringToPos(end);
+ }
+ scanNumber() {
+ let end = this.pos;
+
+ for (; end < this.str.length; end++) {
+ const code = this.str.charCodeAt(end);
+
+ if (code < 48 || code > 57) {
+ break;
+ }
+ }
+
+ if (this.pos === end) {
+ this.error('Expect a number');
+ }
+
+ return this.substringToPos(end);
+ }
+ scanString() {
+ const end = this.str.indexOf('\'', this.pos + 1);
+
+ if (end === -1) {
+ this.pos = this.str.length;
+ this.error('Expect an apostrophe');
+ }
+
+ return this.substringToPos(end + 1);
+ }
+}
+
+exports.Scanner = Scanner;
diff --git a/node_modules/css-tree/cjs/definition-syntax/tokenizer.cjs b/node_modules/css-tree/cjs/definition-syntax/tokenizer.cjs
new file mode 100644
index 000000000..2b934bd9b
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/tokenizer.cjs
@@ -0,0 +1,59 @@
+'use strict';
+
+const SyntaxError = require('./SyntaxError.cjs');
+
+const TAB = 9;
+const N = 10;
+const F = 12;
+const R = 13;
+const SPACE = 32;
+
+class Tokenizer {
+ constructor(str) {
+ this.str = str;
+ this.pos = 0;
+ }
+ charCodeAt(pos) {
+ return pos < this.str.length ? this.str.charCodeAt(pos) : 0;
+ }
+ charCode() {
+ return this.charCodeAt(this.pos);
+ }
+ nextCharCode() {
+ return this.charCodeAt(this.pos + 1);
+ }
+ nextNonWsCode(pos) {
+ return this.charCodeAt(this.findWsEnd(pos));
+ }
+ skipWs() {
+ this.pos = this.findWsEnd(this.pos);
+ }
+ findWsEnd(pos) {
+ for (; pos < this.str.length; pos++) {
+ const code = this.str.charCodeAt(pos);
+ if (code !== R && code !== N && code !== F && code !== SPACE && code !== TAB) {
+ break;
+ }
+ }
+
+ return pos;
+ }
+ substringToPos(end) {
+ return this.str.substring(this.pos, this.pos = end);
+ }
+ eat(code) {
+ if (this.charCode() !== code) {
+ this.error('Expect `' + String.fromCharCode(code) + '`');
+ }
+
+ this.pos++;
+ }
+ peek() {
+ return this.pos < this.str.length ? this.str.charAt(this.pos++) : '';
+ }
+ error(message) {
+ throw new SyntaxError.SyntaxError(message, this.str, this.pos);
+ }
+}
+
+exports.Tokenizer = Tokenizer;
diff --git a/node_modules/css-tree/cjs/definition-syntax/walk.cjs b/node_modules/css-tree/cjs/definition-syntax/walk.cjs
new file mode 100644
index 000000000..fdba06572
--- /dev/null
+++ b/node_modules/css-tree/cjs/definition-syntax/walk.cjs
@@ -0,0 +1,57 @@
+'use strict';
+
+const noop = function() {};
+
+function ensureFunction(value) {
+ return typeof value === 'function' ? value : noop;
+}
+
+function walk(node, options, context) {
+ function walk(node) {
+ enter.call(context, node);
+
+ switch (node.type) {
+ case 'Group':
+ node.terms.forEach(walk);
+ break;
+
+ case 'Multiplier':
+ case 'Boolean':
+ walk(node.term);
+ break;
+
+ case 'Type':
+ case 'Property':
+ case 'Keyword':
+ case 'AtKeyword':
+ case 'Function':
+ case 'String':
+ case 'Token':
+ case 'Comma':
+ break;
+
+ default:
+ throw new Error('Unknown type: ' + node.type);
+ }
+
+ leave.call(context, node);
+ }
+
+ let enter = noop;
+ let leave = noop;
+
+ if (typeof options === 'function') {
+ enter = options;
+ } else if (options) {
+ enter = ensureFunction(options.enter);
+ leave = ensureFunction(options.leave);
+ }
+
+ if (enter === noop && leave === noop) {
+ throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function');
+ }
+
+ walk(node);
+}
+
+exports.walk = walk;
diff --git a/node_modules/css-tree/cjs/generator/create.cjs b/node_modules/css-tree/cjs/generator/create.cjs
new file mode 100644
index 000000000..87a54b23b
--- /dev/null
+++ b/node_modules/css-tree/cjs/generator/create.cjs
@@ -0,0 +1,102 @@
+'use strict';
+
+const index = require('../tokenizer/index.cjs');
+const sourceMap = require('./sourceMap.cjs');
+const tokenBefore = require('./token-before.cjs');
+const types = require('../tokenizer/types.cjs');
+
+const REVERSESOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\)
+
+function processChildren(node, delimeter) {
+ if (typeof delimeter === 'function') {
+ let prev = null;
+
+ node.children.forEach(node => {
+ if (prev !== null) {
+ delimeter.call(this, prev);
+ }
+
+ this.node(node);
+ prev = node;
+ });
+
+ return;
+ }
+
+ node.children.forEach(this.node, this);
+}
+
+function processChunk(chunk) {
+ index.tokenize(chunk, (type, start, end) => {
+ this.token(type, chunk.slice(start, end));
+ });
+}
+
+function createGenerator(config) {
+ const types$1 = new Map();
+
+ for (let [name, item] of Object.entries(config.node)) {
+ const fn = item.generate || item;
+
+ if (typeof fn === 'function') {
+ types$1.set(name, item.generate || item);
+ }
+ }
+
+ return function(node, options) {
+ let buffer = '';
+ let prevCode = 0;
+ let handlers = {
+ node(node) {
+ if (types$1.has(node.type)) {
+ types$1.get(node.type).call(publicApi, node);
+ } else {
+ throw new Error('Unknown node type: ' + node.type);
+ }
+ },
+ tokenBefore: tokenBefore.safe,
+ token(type, value) {
+ prevCode = this.tokenBefore(prevCode, type, value);
+
+ this.emit(value, type, false);
+
+ if (type === types.Delim && value.charCodeAt(0) === REVERSESOLIDUS) {
+ this.emit('\n', types.WhiteSpace, true);
+ }
+ },
+ emit(value) {
+ buffer += value;
+ },
+ result() {
+ return buffer;
+ }
+ };
+
+ if (options) {
+ if (typeof options.decorator === 'function') {
+ handlers = options.decorator(handlers);
+ }
+
+ if (options.sourceMap) {
+ handlers = sourceMap.generateSourceMap(handlers);
+ }
+
+ if (options.mode in tokenBefore) {
+ handlers.tokenBefore = tokenBefore[options.mode];
+ }
+ }
+
+ const publicApi = {
+ node: (node) => handlers.node(node),
+ children: processChildren,
+ token: (type, value) => handlers.token(type, value),
+ tokenize: processChunk
+ };
+
+ handlers.node(node);
+
+ return handlers.result();
+ };
+}
+
+exports.createGenerator = createGenerator;
diff --git a/node_modules/css-tree/cjs/generator/index.cjs b/node_modules/css-tree/cjs/generator/index.cjs
new file mode 100644
index 000000000..5c87cd341
--- /dev/null
+++ b/node_modules/css-tree/cjs/generator/index.cjs
@@ -0,0 +1,8 @@
+'use strict';
+
+const create = require('./create.cjs');
+const generator = require('../syntax/config/generator.cjs');
+
+const index = create.createGenerator(generator);
+
+module.exports = index;
diff --git a/node_modules/css-tree/cjs/generator/sourceMap.cjs b/node_modules/css-tree/cjs/generator/sourceMap.cjs
new file mode 100644
index 000000000..efbc5b9e5
--- /dev/null
+++ b/node_modules/css-tree/cjs/generator/sourceMap.cjs
@@ -0,0 +1,96 @@
+'use strict';
+
+const sourceMapGenerator_js = require('source-map-js/lib/source-map-generator.js');
+
+const trackNodes = new Set(['Atrule', 'Selector', 'Declaration']);
+
+function generateSourceMap(handlers) {
+ const map = new sourceMapGenerator_js.SourceMapGenerator();
+ const generated = {
+ line: 1,
+ column: 0
+ };
+ const original = {
+ line: 0, // should be zero to add first mapping
+ column: 0
+ };
+ const activatedGenerated = {
+ line: 1,
+ column: 0
+ };
+ const activatedMapping = {
+ generated: activatedGenerated
+ };
+ let line = 1;
+ let column = 0;
+ let sourceMappingActive = false;
+
+ const origHandlersNode = handlers.node;
+ handlers.node = function(node) {
+ if (node.loc && node.loc.start && trackNodes.has(node.type)) {
+ const nodeLine = node.loc.start.line;
+ const nodeColumn = node.loc.start.column - 1;
+
+ if (original.line !== nodeLine ||
+ original.column !== nodeColumn) {
+ original.line = nodeLine;
+ original.column = nodeColumn;
+
+ generated.line = line;
+ generated.column = column;
+
+ if (sourceMappingActive) {
+ sourceMappingActive = false;
+ if (generated.line !== activatedGenerated.line ||
+ generated.column !== activatedGenerated.column) {
+ map.addMapping(activatedMapping);
+ }
+ }
+
+ sourceMappingActive = true;
+ map.addMapping({
+ source: node.loc.source,
+ original,
+ generated
+ });
+ }
+ }
+
+ origHandlersNode.call(this, node);
+
+ if (sourceMappingActive && trackNodes.has(node.type)) {
+ activatedGenerated.line = line;
+ activatedGenerated.column = column;
+ }
+ };
+
+ const origHandlersEmit = handlers.emit;
+ handlers.emit = function(value, type, auto) {
+ for (let i = 0; i < value.length; i++) {
+ if (value.charCodeAt(i) === 10) { // \n
+ line++;
+ column = 0;
+ } else {
+ column++;
+ }
+ }
+
+ origHandlersEmit(value, type, auto);
+ };
+
+ const origHandlersResult = handlers.result;
+ handlers.result = function() {
+ if (sourceMappingActive) {
+ map.addMapping(activatedMapping);
+ }
+
+ return {
+ css: origHandlersResult(),
+ map
+ };
+ };
+
+ return handlers;
+}
+
+exports.generateSourceMap = generateSourceMap;
diff --git a/node_modules/css-tree/cjs/generator/token-before.cjs b/node_modules/css-tree/cjs/generator/token-before.cjs
new file mode 100644
index 000000000..87bf4a3e2
--- /dev/null
+++ b/node_modules/css-tree/cjs/generator/token-before.cjs
@@ -0,0 +1,170 @@
+'use strict';
+
+const types = require('../tokenizer/types.cjs');
+
+const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+)
+const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-)
+
+const code = (type, value) => {
+ if (type === types.Delim) {
+ type = value;
+ }
+
+ if (typeof type === 'string') {
+ const charCode = type.charCodeAt(0);
+ return charCode > 0x7F ? 0x8000 : charCode << 8;
+ }
+
+ return type;
+};
+
+// https://www.w3.org/TR/css-syntax-3/#serialization
+// The only requirement for serialization is that it must "round-trip" with parsing,
+// that is, parsing the stylesheet must produce the same data structures as parsing,
+// serializing, and parsing again, except for consecutive s,
+// which may be collapsed into a single token.
+
+const specPairs = [
+ [types.Ident, types.Ident],
+ [types.Ident, types.Function],
+ [types.Ident, types.Url],
+ [types.Ident, types.BadUrl],
+ [types.Ident, '-'],
+ [types.Ident, types.Number],
+ [types.Ident, types.Percentage],
+ [types.Ident, types.Dimension],
+ [types.Ident, types.CDC],
+ [types.Ident, types.LeftParenthesis],
+
+ [types.AtKeyword, types.Ident],
+ [types.AtKeyword, types.Function],
+ [types.AtKeyword, types.Url],
+ [types.AtKeyword, types.BadUrl],
+ [types.AtKeyword, '-'],
+ [types.AtKeyword, types.Number],
+ [types.AtKeyword, types.Percentage],
+ [types.AtKeyword, types.Dimension],
+ [types.AtKeyword, types.CDC],
+
+ [types.Hash, types.Ident],
+ [types.Hash, types.Function],
+ [types.Hash, types.Url],
+ [types.Hash, types.BadUrl],
+ [types.Hash, '-'],
+ [types.Hash, types.Number],
+ [types.Hash, types.Percentage],
+ [types.Hash, types.Dimension],
+ [types.Hash, types.CDC],
+
+ [types.Dimension, types.Ident],
+ [types.Dimension, types.Function],
+ [types.Dimension, types.Url],
+ [types.Dimension, types.BadUrl],
+ [types.Dimension, '-'],
+ [types.Dimension, types.Number],
+ [types.Dimension, types.Percentage],
+ [types.Dimension, types.Dimension],
+ [types.Dimension, types.CDC],
+
+ ['#', types.Ident],
+ ['#', types.Function],
+ ['#', types.Url],
+ ['#', types.BadUrl],
+ ['#', '-'],
+ ['#', types.Number],
+ ['#', types.Percentage],
+ ['#', types.Dimension],
+ ['#', types.CDC], // https://github.com/w3c/csswg-drafts/pull/6874
+
+ ['-', types.Ident],
+ ['-', types.Function],
+ ['-', types.Url],
+ ['-', types.BadUrl],
+ ['-', '-'],
+ ['-', types.Number],
+ ['-', types.Percentage],
+ ['-', types.Dimension],
+ ['-', types.CDC], // https://github.com/w3c/csswg-drafts/pull/6874
+
+ [types.Number, types.Ident],
+ [types.Number, types.Function],
+ [types.Number, types.Url],
+ [types.Number, types.BadUrl],
+ [types.Number, types.Number],
+ [types.Number, types.Percentage],
+ [types.Number, types.Dimension],
+ [types.Number, '%'],
+ [types.Number, types.CDC], // https://github.com/w3c/csswg-drafts/pull/6874
+
+ ['@', types.Ident],
+ ['@', types.Function],
+ ['@', types.Url],
+ ['@', types.BadUrl],
+ ['@', '-'],
+ ['@', types.CDC], // https://github.com/w3c/csswg-drafts/pull/6874
+
+ ['.', types.Number],
+ ['.', types.Percentage],
+ ['.', types.Dimension],
+
+ ['+', types.Number],
+ ['+', types.Percentage],
+ ['+', types.Dimension],
+
+ ['/', '*']
+];
+// validate with scripts/generate-safe
+const safePairs = specPairs.concat([
+ [types.Ident, types.Hash],
+
+ [types.Dimension, types.Hash],
+
+ [types.Hash, types.Hash],
+
+ [types.AtKeyword, types.LeftParenthesis],
+ [types.AtKeyword, types.String],
+ [types.AtKeyword, types.Colon],
+
+ [types.Percentage, types.Percentage],
+ [types.Percentage, types.Dimension],
+ [types.Percentage, types.Function],
+ [types.Percentage, '-'],
+
+ [types.RightParenthesis, types.Ident],
+ [types.RightParenthesis, types.Function],
+ [types.RightParenthesis, types.Percentage],
+ [types.RightParenthesis, types.Dimension],
+ [types.RightParenthesis, types.Hash],
+ [types.RightParenthesis, '-']
+]);
+
+function createMap(pairs) {
+ const isWhiteSpaceRequired = new Set(
+ pairs.map(([prev, next]) => (code(prev) << 16 | code(next)))
+ );
+
+ return function(prevCode, type, value) {
+ const nextCode = code(type, value);
+ const nextCharCode = value.charCodeAt(0);
+ const emitWs =
+ (nextCharCode === HYPHENMINUS &&
+ type !== types.Ident &&
+ type !== types.Function &&
+ type !== types.CDC) ||
+ (nextCharCode === PLUSSIGN)
+ ? isWhiteSpaceRequired.has(prevCode << 16 | nextCharCode << 8)
+ : isWhiteSpaceRequired.has(prevCode << 16 | nextCode);
+
+ if (emitWs) {
+ this.emit(' ', types.WhiteSpace, true);
+ }
+
+ return nextCode;
+ };
+}
+
+const spec = createMap(specPairs);
+const safe = createMap(safePairs);
+
+exports.safe = safe;
+exports.spec = spec;
diff --git a/node_modules/css-tree/cjs/index.cjs b/node_modules/css-tree/cjs/index.cjs
new file mode 100755
index 000000000..cc6113787
--- /dev/null
+++ b/node_modules/css-tree/cjs/index.cjs
@@ -0,0 +1,65 @@
+'use strict';
+
+const index$1 = require('./syntax/index.cjs');
+const version = require('./version.cjs');
+const create = require('./syntax/create.cjs');
+const List = require('./utils/List.cjs');
+const Lexer = require('./lexer/Lexer.cjs');
+const index = require('./definition-syntax/index.cjs');
+const clone = require('./utils/clone.cjs');
+const names$1 = require('./utils/names.cjs');
+const ident = require('./utils/ident.cjs');
+const string = require('./utils/string.cjs');
+const url = require('./utils/url.cjs');
+const types = require('./tokenizer/types.cjs');
+const names = require('./tokenizer/names.cjs');
+const TokenStream = require('./tokenizer/TokenStream.cjs');
+const OffsetToLocation = require('./tokenizer/OffsetToLocation.cjs');
+
+const {
+ tokenize,
+ parse,
+ generate,
+ lexer,
+ createLexer,
+
+ walk,
+ find,
+ findLast,
+ findAll,
+
+ toPlainObject,
+ fromPlainObject,
+
+ fork
+} = index$1;
+
+exports.version = version.version;
+exports.createSyntax = create;
+exports.List = List.List;
+exports.Lexer = Lexer.Lexer;
+exports.definitionSyntax = index;
+exports.clone = clone.clone;
+exports.isCustomProperty = names$1.isCustomProperty;
+exports.keyword = names$1.keyword;
+exports.property = names$1.property;
+exports.vendorPrefix = names$1.vendorPrefix;
+exports.ident = ident;
+exports.string = string;
+exports.url = url;
+exports.tokenTypes = types;
+exports.tokenNames = names;
+exports.TokenStream = TokenStream.TokenStream;
+exports.OffsetToLocation = OffsetToLocation.OffsetToLocation;
+exports.createLexer = createLexer;
+exports.find = find;
+exports.findAll = findAll;
+exports.findLast = findLast;
+exports.fork = fork;
+exports.fromPlainObject = fromPlainObject;
+exports.generate = generate;
+exports.lexer = lexer;
+exports.parse = parse;
+exports.toPlainObject = toPlainObject;
+exports.tokenize = tokenize;
+exports.walk = walk;
diff --git a/node_modules/css-tree/cjs/lexer/Lexer.cjs b/node_modules/css-tree/cjs/lexer/Lexer.cjs
new file mode 100644
index 000000000..a6d1fcb66
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/Lexer.cjs
@@ -0,0 +1,517 @@
+'use strict';
+
+const error = require('./error.cjs');
+const names = require('../utils/names.cjs');
+const genericConst = require('./generic-const.cjs');
+const generic = require('./generic.cjs');
+const units = require('./units.cjs');
+const prepareTokens = require('./prepare-tokens.cjs');
+const matchGraph = require('./match-graph.cjs');
+const match = require('./match.cjs');
+const trace = require('./trace.cjs');
+const search = require('./search.cjs');
+const structure = require('./structure.cjs');
+const parse = require('../definition-syntax/parse.cjs');
+const generate = require('../definition-syntax/generate.cjs');
+const walk = require('../definition-syntax/walk.cjs');
+
+function dumpMapSyntax(map, compact, syntaxAsAst) {
+ const result = {};
+
+ for (const name in map) {
+ if (map[name].syntax) {
+ result[name] = syntaxAsAst
+ ? map[name].syntax
+ : generate.generate(map[name].syntax, { compact });
+ }
+ }
+
+ return result;
+}
+
+function dumpAtruleMapSyntax(map, compact, syntaxAsAst) {
+ const result = {};
+
+ for (const [name, atrule] of Object.entries(map)) {
+ result[name] = {
+ prelude: atrule.prelude && (
+ syntaxAsAst
+ ? atrule.prelude.syntax
+ : generate.generate(atrule.prelude.syntax, { compact })
+ ),
+ descriptors: atrule.descriptors && dumpMapSyntax(atrule.descriptors, compact, syntaxAsAst)
+ };
+ }
+
+ return result;
+}
+
+function valueHasVar(tokens) {
+ for (let i = 0; i < tokens.length; i++) {
+ if (tokens[i].value.toLowerCase() === 'var(') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function syntaxHasTopLevelCommaMultiplier(syntax) {
+ const singleTerm = syntax.terms[0];
+
+ return (
+ syntax.explicit === false &&
+ syntax.terms.length === 1 &&
+ singleTerm.type === 'Multiplier' &&
+ singleTerm.comma === true
+ );
+}
+
+function buildMatchResult(matched, error, iterations) {
+ return {
+ matched,
+ iterations,
+ error,
+ ...trace
+ };
+}
+
+function matchSyntax(lexer, syntax, value, useCssWideKeywords) {
+ const tokens = prepareTokens(value, lexer.syntax);
+ let result;
+
+ if (valueHasVar(tokens)) {
+ return buildMatchResult(null, new Error('Matching for a tree with var() is not supported'));
+ }
+
+ if (useCssWideKeywords) {
+ result = match.matchAsTree(tokens, lexer.cssWideKeywordsSyntax, lexer);
+ }
+
+ if (!useCssWideKeywords || !result.match) {
+ result = match.matchAsTree(tokens, syntax.match, lexer);
+ if (!result.match) {
+ return buildMatchResult(
+ null,
+ new error.SyntaxMatchError(result.reason, syntax.syntax, value, result),
+ result.iterations
+ );
+ }
+ }
+
+ return buildMatchResult(result.match, null, result.iterations);
+}
+
+class Lexer {
+ constructor(config, syntax, structure$1) {
+ this.cssWideKeywords = genericConst.cssWideKeywords;
+ this.syntax = syntax;
+ this.generic = false;
+ this.units = { ...units };
+ this.atrules = Object.create(null);
+ this.properties = Object.create(null);
+ this.types = Object.create(null);
+ this.structure = structure$1 || structure.getStructureFromConfig(config);
+
+ if (config) {
+ if (config.cssWideKeywords) {
+ this.cssWideKeywords = config.cssWideKeywords;
+ }
+
+ if (config.units) {
+ for (const group of Object.keys(units)) {
+ if (Array.isArray(config.units[group])) {
+ this.units[group] = config.units[group];
+ }
+ }
+ }
+
+ if (config.types) {
+ for (const [name, type] of Object.entries(config.types)) {
+ this.addType_(name, type);
+ }
+ }
+
+ if (config.generic) {
+ this.generic = true;
+ for (const [name, value] of Object.entries(generic.createGenericTypes(this.units))) {
+ this.addType_(name, value);
+ }
+ }
+
+ if (config.atrules) {
+ for (const [name, atrule] of Object.entries(config.atrules)) {
+ this.addAtrule_(name, atrule);
+ }
+ }
+
+ if (config.properties) {
+ for (const [name, property] of Object.entries(config.properties)) {
+ this.addProperty_(name, property);
+ }
+ }
+ }
+
+ this.cssWideKeywordsSyntax = matchGraph.buildMatchGraph(this.cssWideKeywords.join(' | '));
+ }
+
+ checkStructure(ast) {
+ function collectWarning(node, message) {
+ warns.push({ node, message });
+ }
+
+ const structure = this.structure;
+ const warns = [];
+
+ this.syntax.walk(ast, function(node) {
+ if (structure.hasOwnProperty(node.type)) {
+ structure[node.type].check(node, collectWarning);
+ } else {
+ collectWarning(node, 'Unknown node type `' + node.type + '`');
+ }
+ });
+
+ return warns.length ? warns : false;
+ }
+
+ createDescriptor(syntax, type, name, parent = null) {
+ const ref = {
+ type,
+ name
+ };
+ const descriptor = {
+ type,
+ name,
+ parent,
+ serializable: typeof syntax === 'string' || (syntax && typeof syntax.type === 'string'),
+ syntax: null,
+ match: null,
+ matchRef: null // used for properties when a syntax referenced as <'property'> in other syntax definitions
+ };
+
+ if (typeof syntax === 'function') {
+ descriptor.match = matchGraph.buildMatchGraph(syntax, ref);
+ } else {
+ if (typeof syntax === 'string') {
+ // lazy parsing on first access
+ Object.defineProperty(descriptor, 'syntax', {
+ get() {
+ Object.defineProperty(descriptor, 'syntax', {
+ value: parse.parse(syntax)
+ });
+
+ return descriptor.syntax;
+ }
+ });
+ } else {
+ descriptor.syntax = syntax;
+ }
+
+ // lazy graph build on first access
+ Object.defineProperty(descriptor, 'match', {
+ get() {
+ Object.defineProperty(descriptor, 'match', {
+ value: matchGraph.buildMatchGraph(descriptor.syntax, ref)
+ });
+
+ return descriptor.match;
+ }
+ });
+
+ if (type === 'Property') {
+ Object.defineProperty(descriptor, 'matchRef', {
+ get() {
+ const syntax = descriptor.syntax;
+ const value = syntaxHasTopLevelCommaMultiplier(syntax)
+ ? matchGraph.buildMatchGraph({
+ ...syntax,
+ terms: [syntax.terms[0].term]
+ }, ref)
+ : null;
+
+ Object.defineProperty(descriptor, 'matchRef', {
+ value
+ });
+
+ return value;
+ }
+ });
+ }
+ }
+
+ return descriptor;
+ }
+ addAtrule_(name, syntax) {
+ if (!syntax) {
+ return;
+ }
+
+ this.atrules[name] = {
+ type: 'Atrule',
+ name: name,
+ prelude: syntax.prelude ? this.createDescriptor(syntax.prelude, 'AtrulePrelude', name) : null,
+ descriptors: syntax.descriptors
+ ? Object.keys(syntax.descriptors).reduce(
+ (map, descName) => {
+ map[descName] = this.createDescriptor(syntax.descriptors[descName], 'AtruleDescriptor', descName, name);
+ return map;
+ },
+ Object.create(null)
+ )
+ : null
+ };
+ }
+ addProperty_(name, syntax) {
+ if (!syntax) {
+ return;
+ }
+
+ this.properties[name] = this.createDescriptor(syntax, 'Property', name);
+ }
+ addType_(name, syntax) {
+ if (!syntax) {
+ return;
+ }
+
+ this.types[name] = this.createDescriptor(syntax, 'Type', name);
+ }
+
+ checkAtruleName(atruleName) {
+ if (!this.getAtrule(atruleName)) {
+ return new error.SyntaxReferenceError('Unknown at-rule', '@' + atruleName);
+ }
+ }
+ checkAtrulePrelude(atruleName, prelude) {
+ const error = this.checkAtruleName(atruleName);
+
+ if (error) {
+ return error;
+ }
+
+ const atrule = this.getAtrule(atruleName);
+
+ if (!atrule.prelude && prelude) {
+ return new SyntaxError('At-rule `@' + atruleName + '` should not contain a prelude');
+ }
+
+ if (atrule.prelude && !prelude) {
+ if (!matchSyntax(this, atrule.prelude, '', false).matched) {
+ return new SyntaxError('At-rule `@' + atruleName + '` should contain a prelude');
+ }
+ }
+ }
+ checkAtruleDescriptorName(atruleName, descriptorName) {
+ const error$1 = this.checkAtruleName(atruleName);
+
+ if (error$1) {
+ return error$1;
+ }
+
+ const atrule = this.getAtrule(atruleName);
+ const descriptor = names.keyword(descriptorName);
+
+ if (!atrule.descriptors) {
+ return new SyntaxError('At-rule `@' + atruleName + '` has no known descriptors');
+ }
+
+ if (!atrule.descriptors[descriptor.name] &&
+ !atrule.descriptors[descriptor.basename]) {
+ return new error.SyntaxReferenceError('Unknown at-rule descriptor', descriptorName);
+ }
+ }
+ checkPropertyName(propertyName) {
+ if (!this.getProperty(propertyName)) {
+ return new error.SyntaxReferenceError('Unknown property', propertyName);
+ }
+ }
+
+ matchAtrulePrelude(atruleName, prelude) {
+ const error = this.checkAtrulePrelude(atruleName, prelude);
+
+ if (error) {
+ return buildMatchResult(null, error);
+ }
+
+ const atrule = this.getAtrule(atruleName);
+
+ if (!atrule.prelude) {
+ return buildMatchResult(null, null);
+ }
+
+ return matchSyntax(this, atrule.prelude, prelude || '', false);
+ }
+ matchAtruleDescriptor(atruleName, descriptorName, value) {
+ const error = this.checkAtruleDescriptorName(atruleName, descriptorName);
+
+ if (error) {
+ return buildMatchResult(null, error);
+ }
+
+ const atrule = this.getAtrule(atruleName);
+ const descriptor = names.keyword(descriptorName);
+
+ return matchSyntax(this, atrule.descriptors[descriptor.name] || atrule.descriptors[descriptor.basename], value, false);
+ }
+ matchDeclaration(node) {
+ if (node.type !== 'Declaration') {
+ return buildMatchResult(null, new Error('Not a Declaration node'));
+ }
+
+ return this.matchProperty(node.property, node.value);
+ }
+ matchProperty(propertyName, value) {
+ // don't match syntax for a custom property at the moment
+ if (names.property(propertyName).custom) {
+ return buildMatchResult(null, new Error('Lexer matching doesn\'t applicable for custom properties'));
+ }
+
+ const error = this.checkPropertyName(propertyName);
+
+ if (error) {
+ return buildMatchResult(null, error);
+ }
+
+ return matchSyntax(this, this.getProperty(propertyName), value, true);
+ }
+ matchType(typeName, value) {
+ const typeSyntax = this.getType(typeName);
+
+ if (!typeSyntax) {
+ return buildMatchResult(null, new error.SyntaxReferenceError('Unknown type', typeName));
+ }
+
+ return matchSyntax(this, typeSyntax, value, false);
+ }
+ match(syntax, value) {
+ if (typeof syntax !== 'string' && (!syntax || !syntax.type)) {
+ return buildMatchResult(null, new error.SyntaxReferenceError('Bad syntax'));
+ }
+
+ if (typeof syntax === 'string' || !syntax.match) {
+ syntax = this.createDescriptor(syntax, 'Type', 'anonymous');
+ }
+
+ return matchSyntax(this, syntax, value, false);
+ }
+
+ findValueFragments(propertyName, value, type, name) {
+ return search.matchFragments(this, value, this.matchProperty(propertyName, value), type, name);
+ }
+ findDeclarationValueFragments(declaration, type, name) {
+ return search.matchFragments(this, declaration.value, this.matchDeclaration(declaration), type, name);
+ }
+ findAllFragments(ast, type, name) {
+ const result = [];
+
+ this.syntax.walk(ast, {
+ visit: 'Declaration',
+ enter: (declaration) => {
+ result.push.apply(result, this.findDeclarationValueFragments(declaration, type, name));
+ }
+ });
+
+ return result;
+ }
+
+ getAtrule(atruleName, fallbackBasename = true) {
+ const atrule = names.keyword(atruleName);
+ const atruleEntry = atrule.vendor && fallbackBasename
+ ? this.atrules[atrule.name] || this.atrules[atrule.basename]
+ : this.atrules[atrule.name];
+
+ return atruleEntry || null;
+ }
+ getAtrulePrelude(atruleName, fallbackBasename = true) {
+ const atrule = this.getAtrule(atruleName, fallbackBasename);
+
+ return atrule && atrule.prelude || null;
+ }
+ getAtruleDescriptor(atruleName, name) {
+ return this.atrules.hasOwnProperty(atruleName) && this.atrules.declarators
+ ? this.atrules[atruleName].declarators[name] || null
+ : null;
+ }
+ getProperty(propertyName, fallbackBasename = true) {
+ const property = names.property(propertyName);
+ const propertyEntry = property.vendor && fallbackBasename
+ ? this.properties[property.name] || this.properties[property.basename]
+ : this.properties[property.name];
+
+ return propertyEntry || null;
+ }
+ getType(name) {
+ return hasOwnProperty.call(this.types, name) ? this.types[name] : null;
+ }
+
+ validate() {
+ function syntaxRef(name, isType) {
+ return isType ? `<${name}>` : `<'${name}'>`;
+ }
+
+ function validate(syntax, name, broken, descriptor) {
+ if (broken.has(name)) {
+ return broken.get(name);
+ }
+
+ broken.set(name, false);
+ if (descriptor.syntax !== null) {
+ walk.walk(descriptor.syntax, function(node) {
+ if (node.type !== 'Type' && node.type !== 'Property') {
+ return;
+ }
+
+ const map = node.type === 'Type' ? syntax.types : syntax.properties;
+ const brokenMap = node.type === 'Type' ? brokenTypes : brokenProperties;
+
+ if (!hasOwnProperty.call(map, node.name)) {
+ errors.push(`${syntaxRef(name, broken === brokenTypes)} used missed syntax definition ${syntaxRef(node.name, node.type === 'Type')}`);
+ broken.set(name, true);
+ } else if (validate(syntax, node.name, brokenMap, map[node.name])) {
+ errors.push(`${syntaxRef(name, broken === brokenTypes)} used broken syntax definition ${syntaxRef(node.name, node.type === 'Type')}`);
+ broken.set(name, true);
+ }
+ }, this);
+ }
+ }
+
+ const errors = [];
+ let brokenTypes = new Map();
+ let brokenProperties = new Map();
+
+ for (const key in this.types) {
+ validate(this, key, brokenTypes, this.types[key]);
+ }
+
+ for (const key in this.properties) {
+ validate(this, key, brokenProperties, this.properties[key]);
+ }
+
+ const brokenTypesArray = [...brokenTypes.keys()].filter(name => brokenTypes.get(name));
+ const brokenPropertiesArray = [...brokenProperties.keys()].filter(name => brokenProperties.get(name));
+
+ if (brokenTypesArray.length || brokenPropertiesArray.length) {
+ return {
+ errors,
+ types: brokenTypesArray,
+ properties: brokenPropertiesArray
+ };
+ }
+
+ return null;
+ }
+ dump(syntaxAsAst, pretty) {
+ return {
+ generic: this.generic,
+ cssWideKeywords: this.cssWideKeywords,
+ units: this.units,
+ types: dumpMapSyntax(this.types, !pretty, syntaxAsAst),
+ properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst),
+ atrules: dumpAtruleMapSyntax(this.atrules, !pretty, syntaxAsAst)
+ };
+ }
+ toString() {
+ return JSON.stringify(this.dump());
+ }
+}
+
+exports.Lexer = Lexer;
diff --git a/node_modules/css-tree/cjs/lexer/error.cjs b/node_modules/css-tree/cjs/lexer/error.cjs
new file mode 100644
index 000000000..8d252eeb5
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/error.cjs
@@ -0,0 +1,128 @@
+'use strict';
+
+const createCustomError = require('../utils/create-custom-error.cjs');
+const generate = require('../definition-syntax/generate.cjs');
+
+const defaultLoc = { offset: 0, line: 1, column: 1 };
+
+function locateMismatch(matchResult, node) {
+ const tokens = matchResult.tokens;
+ const longestMatch = matchResult.longestMatch;
+ const mismatchNode = longestMatch < tokens.length ? tokens[longestMatch].node || null : null;
+ const badNode = mismatchNode !== node ? mismatchNode : null;
+ let mismatchOffset = 0;
+ let mismatchLength = 0;
+ let entries = 0;
+ let css = '';
+ let start;
+ let end;
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i].value;
+
+ if (i === longestMatch) {
+ mismatchLength = token.length;
+ mismatchOffset = css.length;
+ }
+
+ if (badNode !== null && tokens[i].node === badNode) {
+ if (i <= longestMatch) {
+ entries++;
+ } else {
+ entries = 0;
+ }
+ }
+
+ css += token;
+ }
+
+ if (longestMatch === tokens.length || entries > 1) { // last
+ start = fromLoc(badNode || node, 'end') || buildLoc(defaultLoc, css);
+ end = buildLoc(start);
+ } else {
+ start = fromLoc(badNode, 'start') ||
+ buildLoc(fromLoc(node, 'start') || defaultLoc, css.slice(0, mismatchOffset));
+ end = fromLoc(badNode, 'end') ||
+ buildLoc(start, css.substr(mismatchOffset, mismatchLength));
+ }
+
+ return {
+ css,
+ mismatchOffset,
+ mismatchLength,
+ start,
+ end
+ };
+}
+
+function fromLoc(node, point) {
+ const value = node && node.loc && node.loc[point];
+
+ if (value) {
+ return 'line' in value ? buildLoc(value) : value;
+ }
+
+ return null;
+}
+
+function buildLoc({ offset, line, column }, extra) {
+ const loc = {
+ offset,
+ line,
+ column
+ };
+
+ if (extra) {
+ const lines = extra.split(/\n|\r\n?|\f/);
+
+ loc.offset += extra.length;
+ loc.line += lines.length - 1;
+ loc.column = lines.length === 1 ? loc.column + extra.length : lines.pop().length + 1;
+ }
+
+ return loc;
+}
+
+const SyntaxReferenceError = function(type, referenceName) {
+ const error = createCustomError.createCustomError(
+ 'SyntaxReferenceError',
+ type + (referenceName ? ' `' + referenceName + '`' : '')
+ );
+
+ error.reference = referenceName;
+
+ return error;
+};
+
+const SyntaxMatchError = function(message, syntax, node, matchResult) {
+ const error = createCustomError.createCustomError('SyntaxMatchError', message);
+ const {
+ css,
+ mismatchOffset,
+ mismatchLength,
+ start,
+ end
+ } = locateMismatch(matchResult, node);
+
+ error.rawMessage = message;
+ error.syntax = syntax ? generate.generate(syntax) : '';
+ error.css = css;
+ error.mismatchOffset = mismatchOffset;
+ error.mismatchLength = mismatchLength;
+ error.message = message + '\n' +
+ ' syntax: ' + error.syntax + '\n' +
+ ' value: ' + (css || '') + '\n' +
+ ' --------' + new Array(error.mismatchOffset + 1).join('-') + '^';
+
+ Object.assign(error, start);
+ error.loc = {
+ source: (node && node.loc && node.loc.source) || '',
+ start,
+ end
+ };
+
+ return error;
+};
+
+exports.SyntaxMatchError = SyntaxMatchError;
+exports.SyntaxReferenceError = SyntaxReferenceError;
diff --git a/node_modules/css-tree/cjs/lexer/generic-an-plus-b.cjs b/node_modules/css-tree/cjs/lexer/generic-an-plus-b.cjs
new file mode 100644
index 000000000..a5dfba3e2
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/generic-an-plus-b.cjs
@@ -0,0 +1,235 @@
+'use strict';
+
+const charCodeDefinitions = require('../tokenizer/char-code-definitions.cjs');
+const types = require('../tokenizer/types.cjs');
+const utils = require('../tokenizer/utils.cjs');
+
+const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+)
+const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-)
+const N = 0x006E; // U+006E LATIN SMALL LETTER N (n)
+const DISALLOW_SIGN = true;
+const ALLOW_SIGN = false;
+
+function isDelim(token, code) {
+ return token !== null && token.type === types.Delim && token.value.charCodeAt(0) === code;
+}
+
+function skipSC(token, offset, getNextToken) {
+ while (token !== null && (token.type === types.WhiteSpace || token.type === types.Comment)) {
+ token = getNextToken(++offset);
+ }
+
+ return offset;
+}
+
+function checkInteger(token, valueOffset, disallowSign, offset) {
+ if (!token) {
+ return 0;
+ }
+
+ const code = token.value.charCodeAt(valueOffset);
+
+ if (code === PLUSSIGN || code === HYPHENMINUS) {
+ if (disallowSign) {
+ // Number sign is not allowed
+ return 0;
+ }
+ valueOffset++;
+ }
+
+ for (; valueOffset < token.value.length; valueOffset++) {
+ if (!charCodeDefinitions.isDigit(token.value.charCodeAt(valueOffset))) {
+ // Integer is expected
+ return 0;
+ }
+ }
+
+ return offset + 1;
+}
+
+// ...
+// ... ['+' | '-']
+function consumeB(token, offset_, getNextToken) {
+ let sign = false;
+ let offset = skipSC(token, offset_, getNextToken);
+
+ token = getNextToken(offset);
+
+ if (token === null) {
+ return offset_;
+ }
+
+ if (token.type !== types.Number) {
+ if (isDelim(token, PLUSSIGN) || isDelim(token, HYPHENMINUS)) {
+ sign = true;
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ if (token === null || token.type !== types.Number) {
+ return 0;
+ }
+ } else {
+ return offset_;
+ }
+ }
+
+ if (!sign) {
+ const code = token.value.charCodeAt(0);
+ if (code !== PLUSSIGN && code !== HYPHENMINUS) {
+ // Number sign is expected
+ return 0;
+ }
+ }
+
+ return checkInteger(token, sign ? 0 : 1, sign, offset);
+}
+
+// An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb
+function anPlusB(token, getNextToken) {
+ /* eslint-disable brace-style*/
+ let offset = 0;
+
+ if (!token) {
+ return 0;
+ }
+
+ //
+ if (token.type === types.Number) {
+ return checkInteger(token, 0, ALLOW_SIGN, offset); // b
+ }
+
+ // -n
+ // -n
+ // -n ['+' | '-']
+ // -n-
+ //
+ else if (token.type === types.Ident && token.value.charCodeAt(0) === HYPHENMINUS) {
+ // expect 1st char is N
+ if (!utils.cmpChar(token.value, 1, N)) {
+ return 0;
+ }
+
+ switch (token.value.length) {
+ // -n
+ // -n
+ // -n ['+' | '-']
+ case 2:
+ return consumeB(getNextToken(++offset), offset, getNextToken);
+
+ // -n-
+ case 3:
+ if (token.value.charCodeAt(2) !== HYPHENMINUS) {
+ return 0;
+ }
+
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger(token, 0, DISALLOW_SIGN, offset);
+
+ //
+ default:
+ if (token.value.charCodeAt(2) !== HYPHENMINUS) {
+ return 0;
+ }
+
+ return checkInteger(token, 3, DISALLOW_SIGN, offset);
+ }
+ }
+
+ // '+'? n
+ // '+'? n
+ // '+'? n ['+' | '-']
+ // '+'? n-
+ // '+'?
+ else if (token.type === types.Ident || (isDelim(token, PLUSSIGN) && getNextToken(offset + 1).type === types.Ident)) {
+ // just ignore a plus
+ if (token.type !== types.Ident) {
+ token = getNextToken(++offset);
+ }
+
+ if (token === null || !utils.cmpChar(token.value, 0, N)) {
+ return 0;
+ }
+
+ switch (token.value.length) {
+ // '+'? n
+ // '+'? n
+ // '+'? n ['+' | '-']
+ case 1:
+ return consumeB(getNextToken(++offset), offset, getNextToken);
+
+ // '+'? n-
+ case 2:
+ if (token.value.charCodeAt(1) !== HYPHENMINUS) {
+ return 0;
+ }
+
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger(token, 0, DISALLOW_SIGN, offset);
+
+ // '+'?
+ default:
+ if (token.value.charCodeAt(1) !== HYPHENMINUS) {
+ return 0;
+ }
+
+ return checkInteger(token, 2, DISALLOW_SIGN, offset);
+ }
+ }
+
+ //
+ //
+ //
+ //
+ // ['+' | '-']
+ else if (token.type === types.Dimension) {
+ let code = token.value.charCodeAt(0);
+ let sign = code === PLUSSIGN || code === HYPHENMINUS ? 1 : 0;
+ let i = sign;
+
+ for (; i < token.value.length; i++) {
+ if (!charCodeDefinitions.isDigit(token.value.charCodeAt(i))) {
+ break;
+ }
+ }
+
+ if (i === sign) {
+ // Integer is expected
+ return 0;
+ }
+
+ if (!utils.cmpChar(token.value, i, N)) {
+ return 0;
+ }
+
+ //
+ //
+ // ['+' | '-']
+ if (i + 1 === token.value.length) {
+ return consumeB(getNextToken(++offset), offset, getNextToken);
+ } else {
+ if (token.value.charCodeAt(i + 1) !== HYPHENMINUS) {
+ return 0;
+ }
+
+ //
+ if (i + 2 === token.value.length) {
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger(token, 0, DISALLOW_SIGN, offset);
+ }
+ //
+ else {
+ return checkInteger(token, i + 2, DISALLOW_SIGN, offset);
+ }
+ }
+ }
+
+ return 0;
+}
+
+module.exports = anPlusB;
diff --git a/node_modules/css-tree/cjs/lexer/generic-const.cjs b/node_modules/css-tree/cjs/lexer/generic-const.cjs
new file mode 100644
index 000000000..9b9f6157a
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/generic-const.cjs
@@ -0,0 +1,12 @@
+'use strict';
+
+// https://drafts.csswg.org/css-cascade-5/
+const cssWideKeywords = [
+ 'initial',
+ 'inherit',
+ 'unset',
+ 'revert',
+ 'revert-layer'
+];
+
+exports.cssWideKeywords = cssWideKeywords;
diff --git a/node_modules/css-tree/cjs/lexer/generic-urange.cjs b/node_modules/css-tree/cjs/lexer/generic-urange.cjs
new file mode 100644
index 000000000..ce167bb12
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/generic-urange.cjs
@@ -0,0 +1,149 @@
+'use strict';
+
+const charCodeDefinitions = require('../tokenizer/char-code-definitions.cjs');
+const types = require('../tokenizer/types.cjs');
+const utils = require('../tokenizer/utils.cjs');
+
+const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+)
+const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-)
+const QUESTIONMARK = 0x003F; // U+003F QUESTION MARK (?)
+const U = 0x0075; // U+0075 LATIN SMALL LETTER U (u)
+
+function isDelim(token, code) {
+ return token !== null && token.type === types.Delim && token.value.charCodeAt(0) === code;
+}
+
+function startsWith(token, code) {
+ return token.value.charCodeAt(0) === code;
+}
+
+function hexSequence(token, offset, allowDash) {
+ let hexlen = 0;
+
+ for (let pos = offset; pos < token.value.length; pos++) {
+ const code = token.value.charCodeAt(pos);
+
+ if (code === HYPHENMINUS && allowDash && hexlen !== 0) {
+ hexSequence(token, offset + hexlen + 1, false);
+ return 6; // dissallow following question marks
+ }
+
+ if (!charCodeDefinitions.isHexDigit(code)) {
+ return 0; // not a hex digit
+ }
+
+ if (++hexlen > 6) {
+ return 0; // too many hex digits
+ } }
+
+ return hexlen;
+}
+
+function withQuestionMarkSequence(consumed, length, getNextToken) {
+ if (!consumed) {
+ return 0; // nothing consumed
+ }
+
+ while (isDelim(getNextToken(length), QUESTIONMARK)) {
+ if (++consumed > 6) {
+ return 0; // too many question marks
+ }
+
+ length++;
+ }
+
+ return length;
+}
+
+// https://drafts.csswg.org/css-syntax/#urange
+// Informally, the production has three forms:
+// U+0001
+// Defines a range consisting of a single code point, in this case the code point "1".
+// U+0001-00ff
+// Defines a range of codepoints between the first and the second value, in this case
+// the range between "1" and "ff" (255 in decimal) inclusive.
+// U+00??
+// Defines a range of codepoints where the "?" characters range over all hex digits,
+// in this case defining the same as the value U+0000-00ff.
+// In each form, a maximum of 6 digits is allowed for each hexadecimal number (if you treat "?" as a hexadecimal digit).
+//
+// =
+// u '+' '?'* |
+// u '?'* |
+// u '?'* |
+// u |
+// u |
+// u '+' '?'+
+function urange(token, getNextToken) {
+ let length = 0;
+
+ // should start with `u` or `U`
+ if (token === null || token.type !== types.Ident || !utils.cmpChar(token.value, 0, U)) {
+ return 0;
+ }
+
+ token = getNextToken(++length);
+ if (token === null) {
+ return 0;
+ }
+
+ // u '+' '?'*
+ // u '+' '?'+
+ if (isDelim(token, PLUSSIGN)) {
+ token = getNextToken(++length);
+ if (token === null) {
+ return 0;
+ }
+
+ if (token.type === types.Ident) {
+ // u '+' '?'*
+ return withQuestionMarkSequence(hexSequence(token, 0, true), ++length, getNextToken);
+ }
+
+ if (isDelim(token, QUESTIONMARK)) {
+ // u '+' '?'+
+ return withQuestionMarkSequence(1, ++length, getNextToken);
+ }
+
+ // Hex digit or question mark is expected
+ return 0;
+ }
+
+ // u '?'*
+ // u
+ // u
+ if (token.type === types.Number) {
+ const consumedHexLength = hexSequence(token, 1, true);
+ if (consumedHexLength === 0) {
+ return 0;
+ }
+
+ token = getNextToken(++length);
+ if (token === null) {
+ // u
+ return length;
+ }
+
+ if (token.type === types.Dimension || token.type === types.Number) {
+ // u
+ // u
+ if (!startsWith(token, HYPHENMINUS) || !hexSequence(token, 1, false)) {
+ return 0;
+ }
+
+ return length + 1;
+ }
+
+ // u '?'*
+ return withQuestionMarkSequence(consumedHexLength, length, getNextToken);
+ }
+
+ // u '?'*
+ if (token.type === types.Dimension) {
+ return withQuestionMarkSequence(hexSequence(token, 1, true), ++length, getNextToken);
+ }
+
+ return 0;
+}
+
+module.exports = urange;
diff --git a/node_modules/css-tree/cjs/lexer/generic.cjs b/node_modules/css-tree/cjs/lexer/generic.cjs
new file mode 100644
index 000000000..848991131
--- /dev/null
+++ b/node_modules/css-tree/cjs/lexer/generic.cjs
@@ -0,0 +1,589 @@
+'use strict';
+
+const genericConst = require('./generic-const.cjs');
+const genericAnPlusB = require('./generic-an-plus-b.cjs');
+const genericUrange = require('./generic-urange.cjs');
+const charCodeDefinitions = require('../tokenizer/char-code-definitions.cjs');
+const types = require('../tokenizer/types.cjs');
+const utils = require('../tokenizer/utils.cjs');
+
+const calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc('];
+const balancePair = new Map([
+ [types.Function, types.RightParenthesis],
+ [types.LeftParenthesis, types.RightParenthesis],
+ [types.LeftSquareBracket, types.RightSquareBracket],
+ [types.LeftCurlyBracket, types.RightCurlyBracket]
+]);
+
+// safe char code getter
+function charCodeAt(str, index) {
+ return index < str.length ? str.charCodeAt(index) : 0;
+}
+
+function eqStr(actual, expected) {
+ return utils.cmpStr(actual, 0, actual.length, expected);
+}
+
+function eqStrAny(actual, expected) {
+ for (let i = 0; i < expected.length; i++) {
+ if (eqStr(actual, expected[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// IE postfix hack, i.e. 123\0 or 123px\9
+function isPostfixIeHack(str, offset) {
+ if (offset !== str.length - 2) {
+ return false;
+ }
+
+ return (
+ charCodeAt(str, offset) === 0x005C && // U+005C REVERSE SOLIDUS (\)
+ charCodeDefinitions.isDigit(charCodeAt(str, offset + 1))
+ );
+}
+
+function outOfRange(opts, value, numEnd) {
+ if (opts && opts.type === 'Range') {
+ const num = Number(
+ numEnd !== undefined && numEnd !== value.length
+ ? value.substr(0, numEnd)
+ : value
+ );
+
+ if (isNaN(num)) {
+ return true;
+ }
+
+ // FIXME: when opts.min is a string it's a dimension, skip a range validation
+ // for now since it requires a type covertation which is not implmented yet
+ if (opts.min !== null && num < opts.min && typeof opts.min !== 'string') {
+ return true;
+ }
+
+ // FIXME: when opts.max is a string it's a dimension, skip a range validation
+ // for now since it requires a type covertation which is not implmented yet
+ if (opts.max !== null && num > opts.max && typeof opts.max !== 'string') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function consumeFunction(token, getNextToken) {
+ let balanceCloseType = 0;
+ let balanceStash = [];
+ let length = 0;
+
+ // balanced token consuming
+ scan:
+ do {
+ switch (token.type) {
+ case types.RightCurlyBracket:
+ case types.RightParenthesis:
+ case types.RightSquareBracket:
+ if (token.type !== balanceCloseType) {
+ break scan;
+ }
+
+ balanceCloseType = balanceStash.pop();
+
+ if (balanceStash.length === 0) {
+ length++;
+ break scan;
+ }
+
+ break;
+
+ case types.Function:
+ case types.LeftParenthesis:
+ case types.LeftSquareBracket:
+ case types.LeftCurlyBracket:
+ balanceStash.push(balanceCloseType);
+ balanceCloseType = balancePair.get(token.type);
+ break;
+ }
+
+ length++;
+ } while (token = getNextToken(length));
+
+ return length;
+}
+
+// TODO: implement
+// can be used wherever , , ,