Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
<?php
namespace App\RSpade\Core\JavaScript;
use RuntimeException;
/**
* Exception for JavaScript parsing and transformation errors
*/
#[Instantiatable]
class Js_Exception extends RuntimeException
{
protected string $filePath;
protected int $lineNumber = 0;
protected ?int $column = null;
protected ?string $context = null;
protected ?string $suggestion = null;
protected string $rawMessage = '';
/**
* Create a new JavaScript exception
*/
public function __construct(string $message, string $file, int $line = 0, int $column = 0, ?\Throwable $previous = null)
{
// Store the raw message and properties
$this->rawMessage = $message;
$this->filePath = $file;
$this->lineNumber = $line;
$this->column = $column;
// Clean up the file path for display
$displayFile = str_replace(base_path() . '/', '', $file);
// Build the display message
$displayMessage = "JavaScript error in {$displayFile}";
if ($line > 0) {
$displayMessage .= " (line {$line}";
if ($column > 0) {
$displayMessage .= ":{$column}";
}
$displayMessage .= ")";
}
$displayMessage .= "\n{$message}";
// Call parent constructor
parent::__construct($displayMessage, 0, $previous);
// Set file and line for PHP's getFile() and getLine()
$this->file = $file;
$this->line = $line;
}
/**
* Get the column number
*/
public function getColumn(): ?int
{
return $this->column;
}
/**
* Set the column number
*/
public function setColumn(int $column): void
{
$this->column = $column;
}
/**
* Get the code context
*/
public function getContext(): ?string
{
return $this->context;
}
/**
* Set the code context
*/
public function setContext(string $context): void
{
$this->context = $context;
}
/**
* Get the suggestion
*/
public function getSuggestion(): ?string
{
return $this->suggestion;
}
/**
* Set the suggestion
*/
public function setSuggestion(string $suggestion): void
{
$this->suggestion = $suggestion;
}
/**
* Get the raw unformatted message
*/
public function getRawMessage(): string
{
return $this->rawMessage;
}
/**
* Get the file path
*/
public function getFilePath(): string
{
return $this->filePath;
}
/**
* Get the line number
*/
public function getLineNumber(): int
{
return $this->lineNumber;
}
}

View File

@@ -0,0 +1,502 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\JavaScript;
use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use App\RSpade\Core\JavaScript\Js_Exception;
class Js_Parser
{
/**
* Node.js parser script path
*/
protected const PARSER_SCRIPT = 'app/RSpade/Core/JavaScript/resource/js-parser.js';
/**
* Cache directory for parsed JavaScript files
*/
protected const CACHE_DIR = 'storage/rsx-tmp/persistent/js_parser';
/**
* Parse a JavaScript file using Node.js AST parser with caching
*/
public static function parse($file_path)
{
// Generate cache key using the file hash
$cache_key = _rsx_file_hash_for_build($file_path);
$cache_file = base_path(self::CACHE_DIR . '/' . $cache_key . '.json');
// Check if cached result exists
if (file_exists($cache_file)) {
$cached_data = file_get_contents($cache_file);
$parsed_data = json_decode($cached_data, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $parsed_data;
}
// Cache is corrupt, delete it and continue to parse
@unlink($cache_file);
}
// Parse the file (original logic continues below)
$result = static::_parse_without_cache($file_path);
// Cache the result
static::_cache_result($cache_key, $result);
return $result;
}
/**
* Parse without using cache
*/
protected static function _parse_without_cache($file_path)
{
// Always use advanced parser for decorator support
// The simple parser is deprecated and doesn't support modern ES features
$parser_path = base_path(self::PARSER_SCRIPT);
if (!File::exists($parser_path)) {
throw new \RuntimeException("No JavaScript parser available. Please ensure Node.js and babel parser are installed.");
}
$process = new Process([
'node',
$parser_path,
'--json', // Use JSON output for structured error reporting
$file_path
]);
// Set working directory to base path to find node_modules
$process->setWorkingDirectory(base_path());
$process->setTimeout(10); // 10 second timeout
try {
$process->run();
$output = $process->getOutput();
if (empty($output)) {
throw new \RuntimeException("JavaScript parser returned empty output for {$file_path}");
}
// Parse JSON output
$result = @json_decode($output, true);
if (!$result || !is_array($result)) {
// Handle non-JSON output (shouldn't happen with --json flag)
throw new \RuntimeException(
"JavaScript parser returned invalid JSON for {$file_path}:\n" . $output
);
}
if ($result['status'] === 'success') {
return $result['result'];
}
// Handle error response
if ($result['status'] === 'error' && isset($result['error'])) {
$error = $result['error'];
$error_type = $error['type'] ?? 'Unknown';
$message = $error['message'] ?? 'Unknown error';
$line = $error['line'] ?? 0;
$column = $error['column'] ?? 0;
$code = $error['code'] ?? null;
$suggestion = $error['suggestion'] ?? null;
// Handle specific error types from structure validation
switch ($error_type) {
case 'ModuleExportsFound':
throw new Js_Exception(
"Module exports detected. JavaScript files are concatenated, use direct class references.",
$file_path,
$line
);
case 'CodeOutsideAllowed':
$error_msg = "JavaScript files without classes may only contain function declarations and comments.";
if ($code) {
$error_msg .= "\nFound: {$code}";
}
throw new Js_Exception(
$error_msg,
$file_path,
$line
);
case 'CodeOutsideClass':
$error_msg = "JavaScript files with classes may only contain one class declaration and comments.";
if ($code) {
$error_msg .= "\nFound: {$code}";
}
throw new Js_Exception(
$error_msg,
$file_path,
$line
);
case 'InstanceMethodDecorator':
throw new Js_Exception(
"Decorators only allowed on static methods. Instance methods cannot have decorators.",
$file_path,
$line
);
default:
// Clean up the message - remove redundant file path info
$message = preg_replace('/^Parse error:\s*/', '', $message);
// Create Js_Exception with line/column info
$exception = new Js_Exception(
$message,
$file_path,
$line,
$column
);
if ($suggestion) {
$exception->setSuggestion($suggestion);
}
throw $exception;
}
}
// Unknown response format
throw new \RuntimeException(
"JavaScript parser returned unexpected response for {$file_path}:\n" .
json_encode($result, JSON_PRETTY_PRINT)
);
} catch (ProcessFailedException $e) {
// Process failed to run at all
$error_output = $process->getErrorOutput();
// Check for missing Node.js
if (str_contains($e->getMessage(), 'No such file or directory') &&
str_contains($e->getMessage(), 'node')) {
throw new \RuntimeException(
"Node.js is not installed or not in PATH.\n" .
"Install Node.js to parse JavaScript files."
);
}
// Generic process failure
throw new \RuntimeException(
"Failed to run JavaScript parser for {$file_path}:\n" .
$e->getMessage() . "\n" .
"Error output: " . $error_output
);
} catch (Js_Exception $e) {
// Re-throw JavaScript exceptions
throw $e;
} catch (\Exception $e) {
// Wrap other exceptions
throw new \RuntimeException(
"JavaScript parser error for {$file_path}: " . $e->getMessage()
);
}
}
/**
* Cache the parser result
*/
protected static function _cache_result($cache_key, $result)
{
$cache_dir = base_path(self::CACHE_DIR);
// Ensure cache directory exists
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
$cache_file = $cache_dir . '/' . $cache_key . '.json';
$json_data = json_encode($result);
if (json_last_error() !== JSON_ERROR_NONE) {
// Don't cache if JSON encoding failed
return;
}
file_put_contents($cache_file, $json_data);
}
/**
* Create the Node.js parser script
*/
protected static function __create_parser_script($parser_path)
{
$script = <<<'JS'
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// Get file path from command line
const filePath = process.argv[2];
if (!filePath) {
console.error('Usage: node js-parser.js <file-path>');
process.exit(1);
}
// Read file
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (error) {
console.error(`Error reading file: ${error.message}`);
process.exit(1);
}
// Parse result object
const result = {
classes: {},
functions: {},
exports: {},
imports: []
};
try {
// Parse with Babel
const ast = parser.parse(content, {
sourceType: 'module',
plugins: [
'jsx',
'typescript',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
'decorators-legacy',
'dynamicImport',
'exportDefaultFrom',
'exportNamespaceFrom',
'asyncGenerators',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
'nullishCoalescingOperator'
]
});
// Traverse AST
traverse(ast, {
// Class declarations
ClassDeclaration(path) {
const className = path.node.id.name;
const classInfo = {
name: className,
extends: path.node.superClass ? path.node.superClass.name : null,
methods: {},
staticMethods: {},
properties: {},
staticProperties: {}
};
// Extract methods and properties
path.node.body.body.forEach(member => {
if (member.type === 'ClassMethod') {
const methodInfo = {
name: member.key.name,
params: member.params.map(p => p.name || p.type),
async: member.async,
generator: member.generator
};
if (member.static) {
classInfo.staticMethods[member.key.name] = methodInfo;
} else {
classInfo.methods[member.key.name] = methodInfo;
}
} else if (member.type === 'ClassProperty') {
const propInfo = {
name: member.key.name,
static: member.static,
value: member.value ? getValueType(member.value) : null
};
if (member.static) {
classInfo.staticProperties[member.key.name] = propInfo;
} else {
classInfo.properties[member.key.name] = propInfo;
}
}
});
result.classes[className] = classInfo;
},
// Function declarations
FunctionDeclaration(path) {
const funcName = path.node.id.name;
result.functions[funcName] = {
name: funcName,
params: path.node.params.map(p => p.name || p.type),
async: path.node.async,
generator: path.node.generator
};
},
// Imports
ImportDeclaration(path) {
result.imports.push({
source: path.node.source.value,
specifiers: path.node.specifiers.map(spec => ({
type: spec.type,
local: spec.local.name,
imported: spec.imported ? spec.imported.name : null
}))
});
},
// Exports
ExportNamedDeclaration(path) {
if (path.node.declaration) {
if (path.node.declaration.type === 'ClassDeclaration') {
result.exports[path.node.declaration.id.name] = 'class';
} else if (path.node.declaration.type === 'FunctionDeclaration') {
result.exports[path.node.declaration.id.name] = 'function';
}
}
},
ExportDefaultDeclaration(path) {
result.exports.default = getExportType(path.node.declaration);
}
});
} catch (error) {
console.error(`Parse error: ${error.message}`);
process.exit(1);
}
// Helper functions
function getValueType(node) {
if (node.type === 'StringLiteral') return node.value;
if (node.type === 'NumericLiteral') return node.value;
if (node.type === 'BooleanLiteral') return node.value;
if (node.type === 'NullLiteral') return null;
if (node.type === 'Identifier') return node.name;
return node.type;
}
function getExportType(node) {
if (node.type === 'ClassDeclaration') return 'class';
if (node.type === 'FunctionDeclaration') return 'function';
if (node.type === 'Identifier') return 'identifier';
return node.type;
}
// Output result as JSON
console.log(JSON.stringify(result, null, 2));
JS;
File::ensureDirectoryExists(dirname($parser_path));
File::put($parser_path, $script);
// Also create package.json for dependencies
$package_json = [
'name' => 'rsx-js-parser',
'version' => '1.0.0',
'description' => 'JavaScript parser for RSX manifest system',
'dependencies' => [
'@babel/parser' => '^7.22.0',
'@babel/traverse' => '^7.22.0'
]
];
File::put(dirname($parser_path) . '/package.json', json_encode($package_json, JSON_PRETTY_PRINT));
}
/**
* Extract JavaScript metadata in manifest-ready format
* This is the high-level method that Manifest should call
*
* @param string $file_path Path to JavaScript file
* @return array Manifest-ready metadata
*/
public static function extract_metadata(string $file_path): array
{
$data = [];
// Use static parser to get raw parsed data
$parsed = static::parse($file_path);
if (!empty($parsed['classes'])) {
$first_class = reset($parsed['classes']);
$data['class'] = $first_class['name'];
if ($first_class['extends']) {
// Check for period in extends clause
if (str_contains($first_class['extends'], '.')) {
\App\RSpade\Core\Manifest\ManifestErrors::js_extends_with_period($file_path, $first_class['name'], $first_class['extends']);
}
$data['extends'] = $first_class['extends'];
}
// For JS files, we use consistent naming with PHP
$data['public_instance_methods'] = $first_class['public_instance_methods'] ?? [];
$data['public_static_methods'] = $first_class['public_static_methods'] ?? [];
$data['static_properties'] = $first_class['staticProperties'] ?? [];
// Store decorators if present
// JS decorators now use same compact format as PHP: [[name, [args]], ...]
if (!empty($first_class['decorators'])) {
$data['decorators'] = $first_class['decorators'];
}
// Extract method decorators in compact format
// Note: js-parser.js already returns decorators in compact format [[name, [args]], ...]
// so we don't need to call compact_decorators() here
$method_decorators = [];
// Process regular methods
if (!empty($first_class['public_instance_methods'])) {
foreach ($first_class['public_instance_methods'] as $method_name => $method_info) {
if (!empty($method_info['decorators'])) {
$method_decorators[$method_name] = $method_info['decorators'];
}
}
}
// Process static methods
if (!empty($first_class['public_static_methods'])) {
foreach ($first_class['public_static_methods'] as $method_name => $method_info) {
if (!empty($method_info['decorators'])) {
$method_decorators[$method_name] = $method_info['decorators'];
}
}
}
// Store method decorators if any found
if (!empty($method_decorators)) {
$data['method_decorators'] = $method_decorators;
}
}
if (!empty($parsed['imports'])) {
$data['imports'] = $parsed['imports'];
}
if (!empty($parsed['exports'])) {
$data['exports'] = $parsed['exports'];
}
// Store global function names for uniqueness checking
if (!empty($parsed['globalFunctions'])) {
$data['global_function_names'] = $parsed['globalFunctions'];
}
// Store global const names for uniqueness checking
if (!empty($parsed['globalConstants'])) {
$data['global_const_names'] = $parsed['globalConstants'];
}
// Store global functions that have decorators
if (!empty($parsed['functionsWithDecorators'])) {
$data['global_functions_with_decorators'] = $parsed['functionsWithDecorators'];
}
return $data;
}
}

View File

@@ -0,0 +1,290 @@
<?php
/**
* JavaScript Transformer using Babel
*
* Transpiles modern JavaScript features (decorators) to compatible code.
* Private fields (#private) are NOT transpiled - native browser support is used.
*
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\JavaScript;
use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
use RuntimeException;
class Js_Transformer
{
/**
* Node.js transformer script path
*/
protected const TRANSFORMER_SCRIPT = 'app/RSpade/Core/JavaScript/resource/js-transformer.js';
/**
* Cache directory for transformed JavaScript files
*/
protected const CACHE_DIR = 'storage/rsx-tmp/babel_cache';
/**
* Transform a JavaScript file using Babel
*
* @param string $file_path Path to JavaScript file
* @param string $target Target environment (modern, es6, es5)
* @return string Transformed JavaScript code
*/
public static function transform(string $file_path, string $target = 'modern'): string
{
// Generate cache key using file hash and target
$cache_key = _rsx_file_hash_for_build($file_path) . '_' . $target;
$cache_file = base_path(self::CACHE_DIR . '/' . $cache_key . '.js');
// Check if cached result exists
if (file_exists($cache_file)) {
$mtime_cache = filemtime($cache_file);
$mtime_source = filemtime($file_path);
if ($mtime_cache >= $mtime_source) {
return file_get_contents($cache_file);
}
}
// Transform the file
$result = static::_transform_without_cache($file_path, $target);
// Cache the result
static::_cache_result($cache_key, $result);
return $result;
}
/**
* Transform a JavaScript string using Babel
*
* @param string $js_code JavaScript code to transform
* @param string $file_path Original file path (for hash generation)
* @param string $target Target environment (modern, es6, es5)
* @return string Transformed JavaScript code
*/
public static function transform_string(string $js_code, string $file_path, string $target = 'modern'): string
{
// Create temporary file
$temp_file = tempnam(sys_get_temp_dir(), 'babel_');
file_put_contents($temp_file, $js_code);
try {
// Transform using temporary file
$result = static::_transform_without_cache($temp_file, $target, $file_path);
return $result;
} finally {
// Clean up temporary file
@unlink($temp_file);
}
}
/**
* Transform without using cache
*
* @param string $file_path Path to file to transform
* @param string $target Target environment
* @param string|null $original_path Original file path for hash (if using temp file)
* @return string Transformed code
*/
protected static function _transform_without_cache(string $file_path, string $target, ?string $original_path = null): string
{
$transformer_path = base_path(self::TRANSFORMER_SCRIPT);
if (!File::exists($transformer_path)) {
throw new RuntimeException("Babel transformer script not found at {$transformer_path}");
}
// Use original path for hash generation if provided (for temp files)
$hash_path = $original_path ?: $file_path;
$process = new Process([
'node',
$transformer_path,
'--json', // Use JSON output for structured error reporting
$file_path,
$target,
$hash_path // Pass the path for hash generation
]);
// Set working directory to base path to find node_modules
$process->setWorkingDirectory(base_path());
$process->setTimeout(30); // 30 second timeout
try {
$process->run();
$output = $process->getOutput();
if (empty($output)) {
throw new RuntimeException("Babel transformer returned empty output for {$file_path}");
}
// Parse JSON output
$result = @json_decode($output, true);
if (!$result || !is_array($result)) {
throw new RuntimeException(
"Babel transformer returned invalid JSON for {$file_path}:\n" . $output
);
}
if ($result['status'] === 'success') {
return $result['result'];
}
// Handle error response
if ($result['status'] === 'error' && isset($result['error'])) {
$error = $result['error'];
$message = $error['message'] ?? 'Unknown error';
$line = $error['line'] ?? null;
$column = $error['column'] ?? null;
$suggestion = $error['suggestion'] ?? null;
// Build error message
$error_msg = "JavaScript transformation failed";
if ($line && $column) {
$error_msg .= " at line {$line}, column {$column}";
}
$error_msg .= " in {$file_path}:\n{$message}";
if ($suggestion) {
$error_msg .= "\n\n{$suggestion}";
}
// Check for specific error types
if (str_contains($message, 'Cannot find module')) {
throw new RuntimeException(
"Babel packages not installed.\n" .
"Run: npm install\n" .
"Error: {$message}"
);
}
if (str_contains($message, 'No such file or directory') &&
str_contains($message, 'node')) {
throw new RuntimeException(
"Node.js is not installed or not in PATH.\n" .
"Install Node.js to use JavaScript transformation."
);
}
throw new RuntimeException($error_msg);
}
// Unknown response format
throw new RuntimeException(
"Babel transformer returned unexpected response for {$file_path}:\n" .
json_encode($result, JSON_PRETTY_PRINT)
);
} catch (ProcessFailedException $e) {
// Process failed to run at all
$error_output = $process->getErrorOutput();
// Check for missing Node.js
if (str_contains($e->getMessage(), 'No such file or directory') &&
str_contains($e->getMessage(), 'node')) {
throw new RuntimeException(
"Node.js is not installed or not in PATH.\n" .
"Install Node.js to use JavaScript transformation."
);
}
// Generic process failure
throw new RuntimeException(
"Failed to run Babel transformer for {$file_path}:\n" .
$e->getMessage() . "\n" .
"Error output: " . $error_output
);
}
}
/**
* Cache the transformer result
*
* @param string $cache_key Cache key
* @param string $result Transformed code
*/
protected static function _cache_result(string $cache_key, string $result): void
{
$cache_dir = base_path(self::CACHE_DIR);
// Ensure cache directory exists
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
$cache_file = $cache_dir . '/' . $cache_key . '.js';
file_put_contents($cache_file, $result);
}
/**
* Clear the transformation cache
*/
public static function clear_cache(): void
{
$cache_dir = base_path(self::CACHE_DIR);
if (is_dir($cache_dir)) {
$files = glob($cache_dir . '/*.js');
foreach ($files as $file) {
@unlink($file);
}
}
}
/**
* Check if Babel is properly installed
*
* @return bool
*/
public static function is_available(): bool
{
$transformer_path = base_path(self::TRANSFORMER_SCRIPT);
if (!File::exists($transformer_path)) {
return false;
}
// Check if node_modules exists
$node_modules = dirname($transformer_path) . '/node_modules';
if (!is_dir($node_modules)) {
return false;
}
// Check for required packages
$required_packages = [
'@babel/core',
'@babel/preset-env',
'@babel/plugin-proposal-decorators'
];
foreach ($required_packages as $package) {
if (!is_dir($node_modules . '/' . $package)) {
return false;
}
}
return true;
}
/**
* Get list of required npm packages
*
* @return array
*/
public static function get_required_packages(): array
{
return [
'@babel/core' => '^7.24.0',
'@babel/preset-env' => '^7.24.0',
'@babel/plugin-proposal-decorators' => '^7.24.0'
];
}
}

View File

@@ -0,0 +1,815 @@
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generate = require('@babel/generator').default;
// Parse command line arguments
let filePath = null;
let jsonOutput = false;
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === '--json') {
jsonOutput = true;
} else if (!filePath) {
filePath = arg;
}
}
// Error helper for JSON output
function outputError(error) {
if (jsonOutput) {
const errorObj = {
status: 'error',
error: {
type: error.constructor.name,
message: error.message,
line: error.loc ? error.loc.line : null,
column: error.loc ? error.loc.column : null,
file: filePath,
context: null,
suggestion: null
}
};
// Add suggestions for common errors
if (error.message.includes('Unexpected token')) {
errorObj.error.suggestion = 'Check JavaScript syntax - may have invalid characters or missing punctuation';
} else if (error.message.includes('Unterminated')) {
errorObj.error.suggestion = 'Check for unclosed strings, comments, or brackets';
}
console.log(JSON.stringify(errorObj));
} else {
console.error(`Parse error: ${error.message}`);
}
}
// Custom error for structure violations
function structureError(type, message, line, code = null) {
if (jsonOutput) {
console.log(JSON.stringify({
status: 'error',
error: {
type: type,
message: message,
line: line,
code: code,
file: filePath
}
}));
} else {
console.error(`${type}: ${message} at line ${line}`);
if (code) {
console.error(` Code: ${code}`);
}
}
process.exit(1);
}
if (!filePath) {
if (jsonOutput) {
console.log(JSON.stringify({
status: 'error',
error: {
type: 'ArgumentError',
message: 'No input file specified',
suggestion: 'Usage: node js-parser.js [--json] <file-path>'
}
}));
} else {
console.error('Usage: node js-parser.js [--json] <file-path>');
}
process.exit(1);
}
// Read file
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (error) {
error.message = `Error reading file: ${error.message}`;
outputError(error);
process.exit(1);
}
// Parse result object
const result = {
classes: {},
functions: {},
exports: {},
imports: [],
globalFunctions: [], // All global function names
globalConstants: [], // All global const names
functionsWithDecorators: {}, // Global functions that have decorators
instanceMethodsWithDecorators: {} // Instance methods with decorators (for error detection)
};
// Tracking for validation
let hasES6Class = false;
let moduleExportsFound = null;
let codeOutsideAllowed = [];
// Helper function to extract decorator information in PHP-compatible compact format
function extractDecorators(decorators) {
if (!decorators || decorators.length === 0) {
return [];
}
return decorators.map(decorator => {
const expr = decorator.expression;
// Handle simple decorators like @readonly
if (t.isIdentifier(expr)) {
return [expr.name, []];
}
// Handle decorators with arguments like @Route('/api')
if (t.isCallExpression(expr)) {
const name = t.isIdentifier(expr.callee) ? expr.callee.name :
t.isMemberExpression(expr.callee) ? getMemberExpressionName(expr.callee) :
'unknown';
const args = expr.arguments.map(arg => extractArgumentValue(arg));
return [name, args];
}
// Handle member expression decorators like @Namespace.Decorator
if (t.isMemberExpression(expr)) {
return [getMemberExpressionName(expr), []];
}
return ['unknown', []];
});
}
// Helper to get full name from member expression
function getMemberExpressionName(node) {
if (t.isIdentifier(node.object) && t.isIdentifier(node.property)) {
return `${node.object.name}.${node.property.name}`;
}
if (t.isIdentifier(node.property)) {
return node.property.name;
}
return 'unknown';
}
// Helper to extract argument values
function extractArgumentValue(node) {
if (t.isStringLiteral(node)) {
return node.value;
}
if (t.isNumericLiteral(node)) {
return node.value;
}
if (t.isBooleanLiteral(node)) {
return node.value;
}
if (t.isNullLiteral(node)) {
return null;
}
if (t.isIdentifier(node)) {
return { identifier: node.name };
}
if (t.isObjectExpression(node)) {
const obj = {};
node.properties.forEach(prop => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
obj[prop.key.name] = extractArgumentValue(prop.value);
}
});
return obj;
}
if (t.isArrayExpression(node)) {
return node.elements.map(el => el ? extractArgumentValue(el) : null);
}
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
// Simple template literal without expressions
return node.quasis[0].value.raw;
}
// For complex expressions, store the type
return { type: node.type };
}
// Helper to get snippet of code for error messages
function getCodeSnippet(node) {
try {
const generated = generate(node, { compact: true });
let code = generated.code;
// Truncate long code snippets
if (code.length > 60) {
code = code.substring(0, 60) + '...';
}
return code;
} catch (e) {
return node.type;
}
}
// Helper to check if a value is static (no function calls)
function isStaticValue(node) {
if (!node) return false;
// Literals are always static
if (t.isStringLiteral(node) || t.isNumericLiteral(node) ||
t.isBooleanLiteral(node) || t.isNullLiteral(node)) {
return true;
}
// Template literals without expressions are static
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
return true;
}
// Binary expressions with static operands (e.g., 3 * 365)
if (t.isBinaryExpression(node)) {
return isStaticValue(node.left) && isStaticValue(node.right);
}
// Unary expressions with static operands (e.g., -5, !true)
if (t.isUnaryExpression(node)) {
return isStaticValue(node.argument);
}
// Arrays with all static elements
if (t.isArrayExpression(node)) {
return node.elements.every(el => el === null || isStaticValue(el));
}
// Objects with all static properties
if (t.isObjectExpression(node)) {
return node.properties.every(prop => {
if (t.isObjectProperty(prop)) {
return isStaticValue(prop.value);
}
return false; // Methods are not static
});
}
// Everything else (identifiers, function calls, etc.) is not static
return false;
}
/**
* Helper function to check if a JSDoc comment contains @decorator tag
*/
function hasDecoratorTag(commentValue) {
if (!commentValue) return false;
// Parse JSDoc-style comment
// Look for @decorator anywhere in the comment
const lines = commentValue.split('\n');
for (const line of lines) {
// Remove leading asterisk and whitespace
const cleanLine = line.replace(/^\s*\*\s*/, '').trim();
// Check if line contains @decorator tag
if (cleanLine === '@decorator' || cleanLine.startsWith('@decorator ')) {
return true;
}
}
return false;
}
/**
* Extract JSDoc decorators from comments (e.g., @Instantiatable, @Monoprogenic)
* Returns compact format matching PHP: [[name, [args]], ...]
*/
function extractJSDocDecorators(leadingComments) {
const decorators = [];
if (!leadingComments) return decorators;
for (const comment of leadingComments) {
if (comment.type !== 'CommentBlock') continue;
const lines = comment.value.split('\n');
for (const line of lines) {
// Remove leading asterisk and whitespace
const cleanLine = line.replace(/^\s*\*\s*/, '').trim();
// Match @DecoratorName pattern (capital letter start, no spaces)
const match = cleanLine.match(/^@([A-Z][A-Za-z0-9_]*)\s*$/);
if (match) {
const decoratorName = match[1];
// Store in compact format matching PHP: [name, [args]]
decorators.push([decoratorName, []]);
}
}
}
return decorators;
}
try {
// No preprocessing needed - parse content directly
const processedContent = content;
// Parse with Babel
const ast = parser.parse(processedContent, {
sourceType: 'module',
attachComment: true, // Attach comments to AST nodes for /** @decorator */ detection
plugins: [
'jsx',
'typescript',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
['decorators', { decoratorsBeforeExport: true }],
'dynamicImport',
'exportDefaultFrom',
'exportNamespaceFrom',
'asyncGenerators',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
'nullishCoalescingOperator',
'classStaticBlock'
]
});
// First pass: Check for classes and track top-level statements
ast.program.body.forEach(node => {
if (t.isClassDeclaration(node) || (t.isExportNamedDeclaration(node) && t.isClassDeclaration(node.declaration))) {
hasES6Class = true;
}
});
// Traverse AST
traverse(ast, {
// Class declarations
ClassDeclaration(path) {
const className = path.node.id.name;
// Extract both ES decorators and JSDoc decorators, merge them
const esDecorators = extractDecorators(path.node.decorators);
const jsdocDecorators = extractJSDocDecorators(path.node.leadingComments);
const allDecorators = [...esDecorators, ...jsdocDecorators];
const classInfo = {
name: className,
extends: path.node.superClass ? path.node.superClass.name : null,
public_instance_methods: {},
public_static_methods: {},
properties: {},
staticProperties: {},
decorators: allDecorators
};
// Extract methods and properties
path.node.body.body.forEach(member => {
if (t.isClassMethod(member)) {
// Check for @decorator in JSDoc comments on the method
let hasDecoratorComment = false;
if (member.leadingComments) {
hasDecoratorComment = member.leadingComments.some(comment => {
if (comment.type !== 'CommentBlock') return false;
return hasDecoratorTag(comment.value);
});
}
// Determine visibility: private if starts with #, otherwise public
const methodName = member.key.name || (t.isPrivateName(member.key) ? '#' + member.key.id.name : 'unknown');
const visibility = methodName.startsWith('#') ? 'private' : 'public';
const methodInfo = {
// PHP-compatible fields
name: methodName,
static: member.static,
visibility: visibility,
line: member.loc ? member.loc.start.line : null,
// JavaScript-specific fields (keep existing)
params: member.params.map(p => {
if (t.isIdentifier(p)) return p.name;
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return '...' + p.argument.name;
if (t.isObjectPattern(p)) return '{object}';
if (t.isArrayPattern(p)) return '[array]';
return p.type;
}),
// PHP-compatible parameters structure
parameters: member.params.map(p => {
const paramInfo = {};
if (t.isIdentifier(p)) {
paramInfo.name = p.name;
} else if (t.isRestElement(p) && t.isIdentifier(p.argument)) {
paramInfo.name = '...' + p.argument.name;
} else if (t.isObjectPattern(p)) {
paramInfo.name = '{object}';
} else if (t.isArrayPattern(p)) {
paramInfo.name = '[array]';
} else {
paramInfo.name = p.type;
}
return paramInfo;
}),
async: member.async,
generator: member.generator,
kind: member.kind,
decorators: extractDecorators(member.decorators),
isDecoratorFunction: hasDecoratorComment
};
if (member.static) {
classInfo.public_static_methods[member.key.name] = methodInfo;
// Track static methods that are decorator functions
if (hasDecoratorComment) {
if (!result.functionsWithDecorators[className]) {
result.functionsWithDecorators[className] = {};
}
result.functionsWithDecorators[className][member.key.name] = {
decorators: [['decorator', []]],
line: member.loc ? member.loc.start.line : null
};
}
} else {
classInfo.public_instance_methods[member.key.name] = methodInfo;
}
} else if (t.isClassProperty(member) || t.isClassPrivateProperty(member)) {
const propName = t.isIdentifier(member.key) ? member.key.name :
t.isPrivateName(member.key) ? '#' + member.key.id.name :
'unknown';
const propInfo = {
name: propName,
static: member.static,
value: member.value ? getValueType(member.value) : null,
decorators: extractDecorators(member.decorators)
};
if (member.static) {
classInfo.staticProperties[propName] = propInfo;
} else {
classInfo.properties[propName] = propInfo;
}
}
});
result.classes[className] = classInfo;
},
// Function declarations
FunctionDeclaration(path) {
if (path.node.id) {
const funcName = path.node.id.name;
// Add to global functions list
result.globalFunctions.push(funcName);
// Check if it has decorators (either @decorator syntax or /** @decorator */ comment)
const decorators = extractDecorators(path.node.decorators);
// Check for @decorator in JSDoc comments
let hasDecoratorComment = false;
if (path.node.leadingComments) {
hasDecoratorComment = path.node.leadingComments.some(comment => {
if (comment.type !== 'CommentBlock') return false;
// Check if JSDoc contains @decorator tag
return hasDecoratorTag(comment.value);
});
}
if (decorators || hasDecoratorComment) {
result.functionsWithDecorators[funcName] = {
decorators: hasDecoratorComment ? [['decorator', []]] : decorators,
line: path.node.loc ? path.node.loc.start.line : null
};
}
result.functions[funcName] = {
name: funcName,
params: path.node.params.map(p => {
if (t.isIdentifier(p)) return p.name;
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return '...' + p.argument.name;
if (t.isObjectPattern(p)) return '{object}';
if (t.isArrayPattern(p)) return '[array]';
return p.type;
}),
async: path.node.async,
generator: path.node.generator,
decorators: decorators
};
}
},
// Variable declarations (check for function expressions)
VariableDeclaration(path) {
// Only check top-level variables (directly in Program body)
if (path.parent && path.parent.type === 'Program') {
path.node.declarations.forEach(decl => {
if (t.isIdentifier(decl.id)) {
const varName = decl.id.name;
// Check if it's a function expression
if (decl.init && (t.isFunctionExpression(decl.init) || t.isArrowFunctionExpression(decl.init))) {
// Add to global functions list
result.globalFunctions.push(varName);
// Check for decorators (function expressions don't support decorators directly)
// But we still track them as functions
result.functions[varName] = {
name: varName,
params: decl.init.params ? decl.init.params.map(p => {
if (t.isIdentifier(p)) return p.name;
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return '...' + p.argument.name;
if (t.isObjectPattern(p)) return '{object}';
if (t.isArrayPattern(p)) return '[array]';
return p.type;
}) : [],
async: decl.init.async || false,
generator: decl.init.generator || false,
decorators: null
};
} else if (path.node.kind === 'const' && decl.init && isStaticValue(decl.init)) {
// Const with static value - track it
result.globalConstants.push(varName);
} else if (!hasES6Class) {
// Non-allowed variable in a file without classes
// (not a function, not a const with static value)
codeOutsideAllowed.push({
line: path.node.loc ? path.node.loc.start.line : null,
code: getCodeSnippet(decl)
});
}
}
});
}
},
// Check for module.exports
AssignmentExpression(path) {
const left = path.node.left;
if (t.isMemberExpression(left)) {
if ((t.isIdentifier(left.object) && left.object.name === 'module') &&
(t.isIdentifier(left.property) && left.property.name === 'exports')) {
moduleExportsFound = path.node.loc ? path.node.loc.start.line : 1;
}
}
},
// Check for exports.something =
MemberExpression(path) {
if (path.parent && t.isAssignmentExpression(path.parent) && path.parent.left === path.node) {
if (t.isIdentifier(path.node.object) && path.node.object.name === 'exports') {
moduleExportsFound = path.node.loc ? path.node.loc.start.line : 1;
}
}
},
// Imports
ImportDeclaration(path) {
const importInfo = {
source: path.node.source.value,
specifiers: []
};
path.node.specifiers.forEach(spec => {
if (t.isImportDefaultSpecifier(spec)) {
importInfo.specifiers.push({
type: 'default',
local: spec.local.name
});
} else if (t.isImportSpecifier(spec)) {
importInfo.specifiers.push({
type: 'named',
imported: spec.imported.name,
local: spec.local.name
});
} else if (t.isImportNamespaceSpecifier(spec)) {
importInfo.specifiers.push({
type: 'namespace',
local: spec.local.name
});
}
});
result.imports.push(importInfo);
},
// Exports
ExportNamedDeclaration(path) {
if (path.node.declaration) {
if (t.isClassDeclaration(path.node.declaration) && path.node.declaration.id) {
result.exports[path.node.declaration.id.name] = 'class';
} else if (t.isFunctionDeclaration(path.node.declaration) && path.node.declaration.id) {
result.exports[path.node.declaration.id.name] = 'function';
} else if (t.isVariableDeclaration(path.node.declaration)) {
path.node.declaration.declarations.forEach(decl => {
if (t.isIdentifier(decl.id)) {
result.exports[decl.id.name] = 'variable';
}
});
}
}
// Handle export specifiers
if (path.node.specifiers) {
path.node.specifiers.forEach(spec => {
if (t.isExportSpecifier(spec)) {
result.exports[spec.exported.name] = 'named';
}
});
}
},
ExportDefaultDeclaration(path) {
result.exports.default = getExportType(path.node.declaration);
},
Program: {
exit(path) {
// After traversal, check structure violations
// Check for module.exports
if (moduleExportsFound) {
structureError(
'ModuleExportsFound',
'Module exports detected. JavaScript files are concatenated, use direct class references.',
moduleExportsFound,
null
);
}
// Check if this is a compiled/generated file (not originally a .js file)
// These files are generated from other sources (.jqhtml, etc) and should be exempt
const fileContent = fs.readFileSync(filePath, 'utf8');
const firstLine = fileContent.split('\n')[0];
if (firstLine && firstLine.includes('Compiled from:')) {
// This is a compiled file, skip all structure validation
return;
}
// TEMPORARY: Exempt specific utility files from strict validation
// TODO: Replace this with a proper @RULE comment-based system
const fileName = filePath.split('/').pop();
const exemptFiles = ['functions.js'];
if (exemptFiles.includes(fileName)) {
// Skip validation for these files temporarily
return;
}
// Check structure based on whether file has ES6 class
if (hasES6Class) {
// Files with classes should only have class and comments
let invalidCode = null;
path.node.body.forEach(node => {
// Allow comments (handled by parser)
if (t.isClassDeclaration(node)) {
// Class is allowed
return;
}
if (t.isExportNamedDeclaration(node) && t.isClassDeclaration(node.declaration)) {
// Exported class is allowed
return;
}
if (t.isExportDefaultDeclaration(node) && t.isClassExpression(node.declaration)) {
// Export default class is allowed
return;
}
if (t.isImportDeclaration(node)) {
// Imports are allowed (they're removed during bundling)
return;
}
// Anything else is not allowed
if (!invalidCode) {
invalidCode = {
line: node.loc ? node.loc.start.line : null,
code: getCodeSnippet(node)
};
}
});
if (invalidCode) {
structureError(
'CodeOutsideClass',
'JavaScript files with classes may only contain one class declaration and comments.',
invalidCode.line,
invalidCode.code
);
}
} else {
// Files without classes should only have functions and comments
let invalidCode = null;
path.node.body.forEach(node => {
// Allow function declarations
if (t.isFunctionDeclaration(node)) {
return;
}
// Allow variable declarations that are functions or static const values
if (t.isVariableDeclaration(node)) {
const allAllowed = node.declarations.every(decl => {
// Functions are always allowed
if (decl.init && (
t.isFunctionExpression(decl.init) ||
t.isArrowFunctionExpression(decl.init)
)) {
return true;
}
// Const declarations with static values are allowed
if (node.kind === 'const' && decl.init && isStaticValue(decl.init)) {
return true;
}
return false;
});
if (allAllowed) {
return;
}
}
// Allow imports (they're removed during bundling)
if (t.isImportDeclaration(node)) {
return;
}
// Allow exports that wrap functions
if (t.isExportNamedDeclaration(node)) {
if (t.isFunctionDeclaration(node.declaration)) {
return;
}
if (!node.declaration && node.specifiers) {
// Export of existing identifiers
return;
}
}
// Anything else is not allowed
if (!invalidCode) {
invalidCode = {
line: node.loc ? node.loc.start.line : null,
code: getCodeSnippet(node)
};
}
});
if (invalidCode) {
structureError(
'CodeOutsideAllowed',
'JavaScript files without classes may only contain function declarations, const variables with static values, and comments.',
invalidCode.line,
invalidCode.code
);
}
}
}
}
});
} catch (error) {
// Parse Babel error location if available
if (!error.loc && error.message) {
// Try to extract from message (e.g., "Unexpected token (10:5)")
const match = error.message.match(/\((\d+):(\d+)\)/);
if (match) {
error.loc = {
line: parseInt(match[1]),
column: parseInt(match[2])
};
}
}
outputError(error);
process.exit(1);
}
// Helper functions
function getValueType(node) {
if (t.isStringLiteral(node)) return `"${node.value}"`;
if (t.isNumericLiteral(node)) return node.value;
if (t.isBooleanLiteral(node)) return node.value;
if (t.isNullLiteral(node)) return null;
if (t.isIdentifier(node)) return node.name;
if (t.isArrayExpression(node)) return 'array';
if (t.isObjectExpression(node)) return 'object';
if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) return 'function';
return node.type;
}
function getExportType(node) {
if (t.isClassDeclaration(node)) return 'class';
if (t.isFunctionDeclaration(node)) return 'function';
if (t.isIdentifier(node)) return 'identifier';
if (t.isCallExpression(node)) return 'expression';
return node.type;
}
// Output result
if (jsonOutput) {
console.log(JSON.stringify({
status: 'success',
result: result,
file: filePath
}));
} else {
console.log(JSON.stringify(result, null, 2));
}

View File

@@ -0,0 +1,292 @@
#!/usr/bin/env node
const fs = require('fs');
const crypto = require('crypto');
const babel = require('@babel/core');
// Parse command line arguments
let filePath = null;
let target = 'modern';
let hashPath = null;
let jsonOutput = false;
// Process arguments
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === '--json') {
jsonOutput = true;
} else if (!filePath) {
filePath = arg;
} else if (!target || target === 'modern') {
target = arg;
} else if (!hashPath) {
hashPath = arg;
}
}
// Default hashPath to filePath if not provided
if (!hashPath) {
hashPath = filePath;
}
// Error helper for JSON output
function outputError(error) {
if (jsonOutput) {
const errorObj = {
status: 'error',
error: {
type: error.constructor.name,
message: error.message,
line: error.loc ? error.loc.line : null,
column: error.loc ? error.loc.column : null,
file: filePath,
context: null,
suggestion: null
}
};
// Add suggestions for common errors
if (error.message.includes('Cannot find module')) {
errorObj.error.suggestion = 'Missing Babel dependencies. Run: npm install';
} else if (error.message.includes('Unexpected token')) {
errorObj.error.suggestion = 'Check JavaScript syntax in the source file';
} else if (error.message.includes('decorator')) {
errorObj.error.suggestion = 'Ensure decorators are properly formatted (e.g., @decorator before class/method)';
}
console.log(JSON.stringify(errorObj));
} else {
console.error(`Transformation error: ${error.message}`);
if (error.loc) {
console.error(` at line ${error.loc.line}, column ${error.loc.column}`);
}
// Provide helpful error messages
if (error.message.includes('Cannot find module')) {
console.error('\nMissing dependencies. Please run:');
console.error(`cd ${__dirname} && npm install`);
} else if (error.message.includes('Unexpected token')) {
console.error('\nSyntax error in source file. The file may contain invalid JavaScript.');
} else if (error.message.includes('decorator')) {
console.error('\nDecorator syntax error. Ensure decorators are properly formatted.');
}
}
}
if (!filePath) {
const error = new Error('No input file specified');
if (jsonOutput) {
console.log(JSON.stringify({
status: 'error',
error: {
type: 'ArgumentError',
message: error.message,
suggestion: 'Usage: node js-transformer.js [--json] <file-path> [target] [hash-path]'
}
}));
} else {
console.error('Usage: node js-transformer.js [--json] <file-path> [target] [hash-path]');
console.error('Targets: modern, es6, es5');
}
process.exit(1);
}
// Read file
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (error) {
error.message = `Error reading file: ${error.message}`;
outputError(error);
process.exit(1);
}
/**
* Preprocessor to handle @decorator on standalone functions
* Converts @decorator to decorator comment when no ES6 classes are present
*/
function preprocessDecorators(content, filePath) {
// Check if file contains ES6 class declarations
// Using regex to avoid parsing errors from decorators
const es6ClassRegex = /^\s*class\s+[A-Z]\w*\s*(?:extends\s+\w+\s*)?\{/m;
const hasES6Class = es6ClassRegex.test(content);
if (hasES6Class) {
// File has ES6 classes, leave @decorator syntax unchanged
return content;
}
// No ES6 classes, convert @decorator to /** @decorator */
// Match @decorator at the start of a line (with optional whitespace)
// that appears before a function declaration
const decoratorRegex = /^(\s*)@decorator\s*\n(\s*(?:async\s+)?function\s+\w+)/gm;
const processed = content.replace(decoratorRegex, (match, indent, funcDecl) => {
return `${indent}/** @decorator */\n${funcDecl}`;
});
return processed;
}
// Preprocess content before transformation
content = preprocessDecorators(content, filePath);
// Generate file hash for prefixing (HARDCODED - NOT CONFIGURABLE)
// This prevents namespace collisions when files are concatenated in bundles
const fileHash = crypto.createHash('md5')
.update(hashPath)
.digest('hex')
.substring(0, 8);
// Target environment presets
const targetPresets = {
modern: {
targets: {
chrome: '90',
firefox: '88',
safari: '14',
edge: '90'
}
},
es6: {
targets: {
chrome: '60',
firefox: '60',
safari: '10.1',
edge: '15'
}
},
es5: {
targets: {
ie: '11'
}
}
};
// Create custom plugin to prefix generated WeakMap variables and Babel helper functions
// This plugin runs AFTER all other transformations to catch Babel-generated helpers
const prefixGeneratedVariables = function() {
return {
name: 'prefix-generated-variables',
post(file) {
// Run after all transformations are complete
const program = file.path;
// Track all top-level variables and functions that start with underscore
const generatedNames = new Set();
// First pass: collect all generated variable and function names at top level
for (const statement of program.node.body) {
if (statement.type === 'VariableDeclaration') {
for (const declarator of statement.declarations) {
const name = declarator.id?.name;
if (name && name.startsWith('_')) {
generatedNames.add(name);
}
}
} else if (statement.type === 'FunctionDeclaration') {
const name = statement.id?.name;
if (name && name.startsWith('_')) {
generatedNames.add(name);
}
}
}
// Second pass: rename all references
if (generatedNames.size > 0) {
program.traverse({
Identifier(idPath) {
if (generatedNames.has(idPath.node.name)) {
// Don't rename if it's already prefixed
if (!idPath.node.name.startsWith(`_${fileHash}`)) {
const newName = `_${fileHash}${idPath.node.name}`;
idPath.scope.rename(idPath.node.name, newName);
}
}
}
});
}
}
};
};
try {
// Configure Babel transformation
const result = babel.transformSync(content, {
filename: filePath,
sourceMaps: 'inline',
presets: [
['@babel/preset-env', targetPresets[target] || targetPresets.modern]
],
plugins: [
// Apply custom prefixing plugin first
prefixGeneratedVariables,
// Transform decorators (Stage 3 proposal)
// Note: We're NOT transforming private fields - native support only
['@babel/plugin-proposal-decorators', {
version: '2023-11',
// Ensure decorators are transpiled to compatible code
}],
// Transform class properties
'@babel/plugin-transform-class-properties',
// Transform optional chaining and nullish coalescing
'@babel/plugin-transform-optional-chaining',
'@babel/plugin-transform-nullish-coalescing-operator'
]
});
if (!result || !result.code) {
const error = new Error('Babel transformation produced no output');
outputError(error);
process.exit(1);
}
// Add comment header with file information
const header = `/* Transformed from: ${hashPath} (hash: ${fileHash}) */\n`;
const output = header + result.code;
// Output result
if (jsonOutput) {
console.log(JSON.stringify({
status: 'success',
result: output,
file: filePath,
hash: fileHash
}));
} else {
console.log(output);
}
} catch (error) {
// Parse Babel error location if available
if (error.loc) {
// Babel provides loc.line and loc.column
} else if (error.codeFrame) {
// Try to extract line/column from codeFrame
const lineMatch = error.codeFrame.match(/>\s*(\d+)\s*\|/);
const colMatch = error.codeFrame.match(/\n\s+\|\s+(\^+)/);
if (lineMatch) {
error.loc = {
line: parseInt(lineMatch[1]),
column: colMatch ? colMatch[1].indexOf('^') + 1 : 0
};
}
} else if (error.message) {
// Try to extract from message (e.g., "file.js: Unexpected token (10:5)")
const match = error.message.match(/\((\d+):(\d+)\)/);
if (match) {
error.loc = {
line: parseInt(match[1]),
column: parseInt(match[2])
};
}
}
outputError(error);
process.exit(1);
}