44 * on what counts as "loopback" and how to normalise a free-form URL string.
55 */
66
7+ import * as Schema from "effect/Schema" ;
8+
79const TAB_ID_PREFIX = "tab_" ;
810let nextPreviewTabSequence = 0 ;
911
@@ -45,17 +47,27 @@ export function isPreviewableUrl(rawUrl: string): boolean {
4547 }
4648}
4749
48- export class PreviewUrlNormalizationError extends Error {
49- readonly rawUrl : string ;
50- readonly detail : string ;
51- constructor ( rawUrl : string , detail : string ) {
52- super ( `Invalid preview URL: ${ rawUrl } (${ detail } )` ) ;
53- this . name = "PreviewUrlNormalizationError" ;
54- this . rawUrl = rawUrl ;
55- this . detail = detail ;
50+ export class PreviewUrlNormalizationError extends Schema . TaggedErrorClass < PreviewUrlNormalizationError > ( ) (
51+ "PreviewUrlNormalizationError" ,
52+ {
53+ inputLength : Schema . Number ,
54+ reason : Schema . Literals ( [ "empty" , "parse" , "unsupported-protocol" ] ) ,
55+ protocol : Schema . optional ( Schema . String ) ,
56+ cause : Schema . optional ( Schema . Defect ( ) ) ,
57+ } ,
58+ ) {
59+ override get message ( ) : string {
60+ const protocol = this . protocol === undefined ? "" : `: ${ this . protocol } ` ;
61+ return `Invalid preview URL (${ this . reason } ${ protocol } ; input length ${ this . inputLength } ).` ;
5662 }
5763}
5864
65+ export const isPreviewUrlNormalizationError = Schema . is ( PreviewUrlNormalizationError ) ;
66+
67+ function previewUrlProtocol ( rawUrl : string ) : string | undefined {
68+ return / ^ ( [ A - Z a - z ] [ A - Z a - z \d + . - ] * ) : / . exec ( rawUrl ) ?. [ 1 ] ?. toLowerCase ( ) . concat ( ":" ) ;
69+ }
70+
5971/**
6072 * Normalise a free-form URL string into a fully-qualified `http(s)://` URL.
6173 *
@@ -69,7 +81,7 @@ export class PreviewUrlNormalizationError extends Error {
6981export function normalizePreviewUrl ( rawUrl : string ) : string {
7082 const trimmed = rawUrl . trim ( ) ;
7183 if ( trimmed . length === 0 ) {
72- throw new PreviewUrlNormalizationError ( rawUrl , "empty" ) ;
84+ throw new PreviewUrlNormalizationError ( { inputLength : rawUrl . length , reason : "empty" } ) ;
7385 }
7486 const useHttp = LOOPBACK_PREFIX_PATTERN . test ( trimmed ) ;
7587 const candidate = trimmed . includes ( "://" )
@@ -79,13 +91,19 @@ export function normalizePreviewUrl(rawUrl: string): string {
7991 try {
8092 parsed = new URL ( candidate ) ;
8193 } catch ( cause ) {
82- throw new PreviewUrlNormalizationError (
83- rawUrl ,
84- cause instanceof Error ? cause . message : "unparseable" ,
85- ) ;
94+ throw new PreviewUrlNormalizationError ( {
95+ inputLength : rawUrl . length ,
96+ reason : "parse" ,
97+ protocol : previewUrlProtocol ( candidate ) ,
98+ cause,
99+ } ) ;
86100 }
87101 if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
88- throw new PreviewUrlNormalizationError ( rawUrl , `unsupported protocol ${ parsed . protocol } ` ) ;
102+ throw new PreviewUrlNormalizationError ( {
103+ inputLength : rawUrl . length ,
104+ reason : "unsupported-protocol" ,
105+ protocol : parsed . protocol ,
106+ } ) ;
89107 }
90108 return parsed . href ;
91109}
0 commit comments