Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions models/Component.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,14 @@ component output="true" accessors="true" {
params=arguments.params
);
} catch ( any e ) {
// Enhance exception for single-file components
if ( _isSingleFileComponent() ) {
throw(
type = "CBWIREException",
message = "Failure when calling onMount(). " & _buildEnhancedErrorMessage( e ),
detail = e.detail ?: ""
);
}
throw( type="CBWIREException", message="Failure when calling onMount(). #e.message#" );
}
}
Expand Down Expand Up @@ -1102,6 +1110,14 @@ component output="true" accessors="true" {
} catch ( ValidationException e ) {
// silently fail so the component can continue to render
} catch( any e ) {
// Enhance exception for single-file components
if ( _isSingleFileComponent() ) {
throw(
type = e.type ?: "CBWIREException",
message = _buildEnhancedErrorMessage( e ),
detail = e.detail ?: ""
);
}
rethrow;
}
} );
Expand Down Expand Up @@ -1706,8 +1722,20 @@ component output="true" accessors="true" {
// should render based on if onSecure exists and allows rendering. render only if true.
var renderedContent = "";
if( _onSecureShouldRender() ){
local.trimmedHTML = isNull( arguments.rendering ) ? trim( onRender() ) : trim( arguments.rendering );
renderedContent = variables._renderService.render( this, local.trimmedHTML );
try {
local.trimmedHTML = isNull( arguments.rendering ) ? trim( onRender() ) : trim( arguments.rendering );
renderedContent = variables._renderService.render( this, local.trimmedHTML );
} catch ( any e ) {
// Enhance exception for single-file components
if ( _isSingleFileComponent() ) {
throw(
type = e.type ?: "CBWIREException",
message = _buildEnhancedErrorMessage( e ),
detail = e.detail ?: ""
);
}
rethrow;
}
}else{
// fireInterceptor event for blocked render

Expand All @@ -1731,6 +1759,113 @@ component output="true" accessors="true" {
return variables._compileTimeKey;
}

/**
* Returns true if this component was generated from a single-file component.
*
* @return boolean
*/
function _isSingleFileComponent() {
return variables.keyExists( "_singleFileSourcePath" ) && len( variables._singleFileSourcePath );
}

/**
* Returns the original source file path for single-file components.
* Returns empty string for regular components.
*
* @return string
*/
function _getSingleFileSourcePath() {
return variables.keyExists( "_singleFileSourcePath" ) ? variables._singleFileSourcePath : "";
}

/**
* Returns the line offset for mapping generated code back to the original source file.
* This is the line number where @startWire begins in the original file.
*
* @return numeric
*/
function _getSingleFileLineOffset() {
return variables.keyExists( "_singleFileLineOffset" ) ? variables._singleFileLineOffset : 0;
}

/**
* Enhances an exception with single-file component source information.
* This helps developers understand where errors originated in their original source file.
*
* @exception any The caught exception to enhance
*
* @return struct Enhanced exception information with source file context
*/
function _enhanceExceptionForSingleFile( required any exception ) {
var enhanced = {
"originalException": arguments.exception,
"isSingleFileComponent": _isSingleFileComponent(),
"message": arguments.exception.message ?: "",
"detail": arguments.exception.detail ?: "",
"type": arguments.exception.type ?: "Application"
};

if ( enhanced.isSingleFileComponent ) {
enhanced[ "singleFileSourcePath" ] = _getSingleFileSourcePath();
enhanced[ "singleFileLineOffset" ] = _getSingleFileLineOffset();
enhanced[ "enhancedMessage" ] = _buildEnhancedErrorMessage( arguments.exception );
}

return enhanced;
}

/**
* Builds an enhanced error message that includes single-file component source information.
*
* @exception any The caught exception
*
* @return string The enhanced error message
*/
function _buildEnhancedErrorMessage( required any exception ) {
var sourcePath = _getSingleFileSourcePath();
var lineOffset = _getSingleFileLineOffset();
var originalMessage = arguments.exception.message ?: "Unknown error";

var enhancedMessage = originalMessage;
enhancedMessage &= chr(10) & chr(10) & "=== CBWIRE Single-File Component Debug Info ===" & chr(10);
enhancedMessage &= "Original source file: " & sourcePath & chr(10);

if ( lineOffset > 0 ) {
enhancedMessage &= "Wire code starts at line: " & lineOffset & " in the original file" & chr(10);
enhancedMessage &= "To find the actual error line in your source file, add " & lineOffset & " to any line numbers shown in the generated component stack trace." & chr(10);
}

// Try to extract line number from the exception if available
if ( structKeyExists( arguments.exception, "tagContext" ) && isArray( arguments.exception.tagContext ) && arguments.exception.tagContext.len() ) {
// Look for the tmp file reference in the stack trace
for ( var ctx in arguments.exception.tagContext ) {
if ( structKeyExists( ctx, "template" ) && ctx.template contains "cbwire" && ctx.template contains "tmp" ) {
var generatedLine = structKeyExists( ctx, "line" ) ? ctx.line : 0;
if ( generatedLine > 0 && lineOffset > 0 ) {
/*
* The generated component file (EmptySingleFileComponent.cfc/.bx) has header lines
* before the {{ CFC_CONTENTS }} placeholder where user code is inserted.
* This includes: component declaration, variable definitions for source path and
* line offset, comments, and blank lines. Currently this is approximately 14 lines.
* See models/EmptySingleFileComponent.cfc for the template structure.
*/
var generatedTemplateHeaderLines = 14;
var estimatedSourceLine = generatedLine - generatedTemplateHeaderLines + lineOffset;
// Only show the estimated line if it's a positive, reasonable value
if ( estimatedSourceLine > 0 && estimatedSourceLine <= 10000 ) {
enhancedMessage &= "Estimated source line: ~" & estimatedSourceLine & chr(10);
}
}
break;
}
}
}

enhancedMessage &= "================================================";

return enhancedMessage;
}

/**
* Fires an interceptor event.
* Standardizes data passed to interceptors fired from base wire Component.cfc
Expand Down
11 changes: 11 additions & 0 deletions models/EmptySingleFileComponent.bx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
class extends="{{ EXTENDS_PATH }}" {

/**
* Path to the original single-file component source for error reporting.
*/
variables._singleFileSourcePath = "{{ SOURCE_PATH }}";

/**
* Line offset for mapping generated code back to original source.
* This is the line number in the original file where @startWire begins.
*/
variables._singleFileLineOffset = {{ LINE_OFFSET }};

{{ CFC_CONTENTS }}

function onRender() {
Expand Down
11 changes: 11 additions & 0 deletions models/EmptySingleFileComponent.cfc
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
component extends="{{ EXTENDS_PATH }}" {

/**
* Path to the original single-file component source for error reporting.
*/
variables._singleFileSourcePath = "{{ SOURCE_PATH }}";

/**
* Line offset for mapping generated code back to original source.
* This is the line number in the original file where @startWire begins.
*/
variables._singleFileLineOffset = {{ LINE_OFFSET }};

{{ CFC_CONTENTS }}

function onRender() {
Expand Down
24 changes: 23 additions & 1 deletion models/SingleFileComponentBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ component accessors="true" singleton {

local.startedWire = false;
local.endedWire = false;
local.lineNumber = 0;
local.startWireLineNumber = 0;

local.isBoxLang = arguments.cfmPath.listToArray(".").last() == "bxm" ? true : false;
local.insideScriptTag = false;
local.REStartScriptTag = local.isBoxLang ? "<\s*bx:script\s*>" : "<\s*cfscript\s*>";
local.REEndScriptTag = local.isBoxLang ? "<\s*/\s*bx:script\s*>" : "<\s*/\s*cfscript\s*>";

for ( local.line in local.fileContents.listToArray( chr( 10 ) ) ) {
local.lineNumber++;

if ( REFindNoCase( local.REStartScriptTag, local.line ) > 0 ) {
local.insideScriptTag = true;
Expand All @@ -80,6 +83,7 @@ component accessors="true" singleton {

if ( local.insideScriptTag && local.line contains "@startWire" ) {
local.startedWire = true;
local.startWireLineNumber = local.lineNumber;
continue;
}
if ( local.insideScriptTag && local.line contains "@endWire" ) {
Expand All @@ -97,7 +101,8 @@ component accessors="true" singleton {
return {
"singleFileContents" : local.singleFileContents,
"remainingContents" : local.remainingContents,
"extendsPath" : local.extendsPath
"extendsPath" : local.extendsPath,
"startWireLineNumber" : local.startWireLineNumber
};
}

Expand Down Expand Up @@ -173,6 +178,23 @@ component accessors="true" singleton {
"one"
);

// Add source path for error reporting (escape backslashes for Windows paths)
local.escapedSourcePath = replace( arguments.sourcePath, "\", "\\", "all" );
local.emptySingleFileComponent = replaceNoCase(
local.emptySingleFileComponent,
"{{ SOURCE_PATH }}",
local.escapedSourcePath,
"one"
);

// Add line offset for error reporting
local.emptySingleFileComponent = replaceNoCase(
local.emptySingleFileComponent,
"{{ LINE_OFFSET }}",
local.parsedContents.startWireLineNumber,
"one"
);

local.uuid = createUUID();

fileWrite( local.tmpClassPath, local.emptySingleFileComponent );
Expand Down
94 changes: 94 additions & 0 deletions test-harness/tests/specs/unit/ComponentSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,100 @@ component extends="coldbox.system.testing.BaseTestCase" {

});

describe( "Single-file component error reporting", function() {

it( "_isSingleFileComponent returns false for regular components", function() {
// Regular components don't have _singleFileSourcePath set
var result = variables.wireComponent._isSingleFileComponent();
expect( result ).toBeFalse();
});

it( "_getSingleFileSourcePath returns empty string for regular components", function() {
var result = variables.wireComponent._getSingleFileSourcePath();
expect( result ).toBe( "" );
});

it( "_getSingleFileLineOffset returns 0 for regular components", function() {
var result = variables.wireComponent._getSingleFileLineOffset();
expect( result ).toBe( 0 );
});

it( "_isSingleFileComponent returns true when _singleFileSourcePath is set", function() {
// Simulate a single-file component by setting the variables
variables.wireComponent.$property( propertyName="_singleFileSourcePath", mock="/path/to/TestComponent.cfm" );
var result = variables.wireComponent._isSingleFileComponent();
expect( result ).toBeTrue();
});

it( "_getSingleFileSourcePath returns the path when set", function() {
var testPath = "/path/to/TestComponent.cfm";
variables.wireComponent.$property( propertyName="_singleFileSourcePath", mock=testPath );
var result = variables.wireComponent._getSingleFileSourcePath();
expect( result ).toBe( testPath );
});

it( "_getSingleFileLineOffset returns the offset when set", function() {
variables.wireComponent.$property( propertyName="_singleFileLineOffset", mock=15 );
var result = variables.wireComponent._getSingleFileLineOffset();
expect( result ).toBe( 15 );
});

it( "_buildEnhancedErrorMessage includes source file path", function() {
variables.wireComponent.$property( propertyName="_singleFileSourcePath", mock="/path/to/TestComponent.cfm" );
variables.wireComponent.$property( propertyName="_singleFileLineOffset", mock=10 );

var mockException = {
"message": "Test error message",
"detail": "Test detail",
"type": "TestException"
};

var result = variables.wireComponent._buildEnhancedErrorMessage( mockException );

expect( result ).toInclude( "Test error message" );
expect( result ).toInclude( "/path/to/TestComponent.cfm" );
expect( result ).toInclude( "CBWIRE Single-File Component Debug Info" );
expect( result ).toInclude( "Wire code starts at line: 10" );
});

it( "_enhanceExceptionForSingleFile returns enhanced struct for single-file components", function() {
variables.wireComponent.$property( propertyName="_singleFileSourcePath", mock="/path/to/TestComponent.cfm" );
variables.wireComponent.$property( propertyName="_singleFileLineOffset", mock=10 );

var mockException = {
"message": "Test error message",
"detail": "Test detail",
"type": "TestException"
};

var result = variables.wireComponent._enhanceExceptionForSingleFile( mockException );

expect( result ).toHaveKey( "isSingleFileComponent" );
expect( result.isSingleFileComponent ).toBeTrue();
expect( result ).toHaveKey( "singleFileSourcePath" );
expect( result.singleFileSourcePath ).toBe( "/path/to/TestComponent.cfm" );
expect( result ).toHaveKey( "singleFileLineOffset" );
expect( result.singleFileLineOffset ).toBe( 10 );
expect( result ).toHaveKey( "enhancedMessage" );
});

it( "_enhanceExceptionForSingleFile returns basic struct for regular components", function() {
var mockException = {
"message": "Test error message",
"detail": "Test detail",
"type": "TestException"
};

var result = variables.wireComponent._enhanceExceptionForSingleFile( mockException );

expect( result ).toHaveKey( "isSingleFileComponent" );
expect( result.isSingleFileComponent ).toBeFalse();
expect( result ).notToHaveKey( "singleFileSourcePath" );
expect( result ).notToHaveKey( "enhancedMessage" );
});

});

});
}

Expand Down
Loading