@@ -586,40 +586,94 @@ export const createRenderRuntimeController = ({
586586
587587 const { byId, byModuleKey } = buildWorkspaceTabLookup ( tabs )
588588 const visited = new Set ( )
589+ const visiting = new Set ( )
590+ const visitStack = [ ]
589591 const dependencyOrder = [ ]
592+ const importsByTabId = new Map ( )
590593
591- const visit = tab => {
592- if ( ! tab || visited . has ( tab . id ) ) {
593- return
594+ const toCycleTabLabel = tab => toTabModuleKey ( tab ) || tab . id
595+
596+ const getParsedImportsForTab = tab => {
597+ if ( ! tab || typeof tab . id !== 'string' || tab . id . length === 0 ) {
598+ return [ ]
594599 }
595600
596- visited . add ( tab . id )
601+ if ( importsByTabId . has ( tab . id ) ) {
602+ return importsByTabId . get ( tab . id )
603+ }
597604
598605 const source = typeof tab . content === 'string' ? tab . content : ''
599606 const imports = parseWorkspaceImports ( { source, transformJsxSource } )
607+ importsByTabId . set ( tab . id , imports )
608+ return imports
609+ }
610+
611+ const visit = ( tab , { viaSpecifier = '' } = { } ) => {
612+ if ( ! tab ) {
613+ return
614+ }
600615
601- workspaceGraphCache . upsert ( {
602- tabId : tab . id ,
603- imports : imports . map ( entry => entry ?. source ) . filter ( Boolean ) ,
604- lastUpdated : Date . now ( ) ,
616+ if ( visiting . has ( tab . id ) ) {
617+ const cycleStartIndex = visitStack . findIndex ( entry => entry . id === tab . id )
618+ const cycleEntries =
619+ cycleStartIndex >= 0 ? visitStack . slice ( cycleStartIndex ) : [ ...visitStack ]
620+ const cycleLabels = [
621+ ...cycleEntries . map ( entry => entry . label ) ,
622+ toCycleTabLabel ( tab ) ,
623+ ] . join ( ' -> ' )
624+ const importPath = [
625+ ...cycleEntries . slice ( 1 ) . map ( entry => entry . viaSpecifier ) ,
626+ viaSpecifier ,
627+ ]
628+ . filter ( Boolean )
629+ . join ( ' -> ' )
630+
631+ const importHint = importPath ? ` Import chain: ${ importPath } .` : ''
632+ throw new Error (
633+ `Preview entry contains circular workspace import: ${ cycleLabels } .${ importHint } ` ,
634+ )
635+ }
636+
637+ if ( visited . has ( tab . id ) ) {
638+ return
639+ }
640+
641+ visiting . add ( tab . id )
642+ visitStack . push ( {
643+ id : tab . id ,
644+ label : toCycleTabLabel ( tab ) ,
645+ viaSpecifier,
605646 } )
647+ try {
648+ const imports = getParsedImportsForTab ( tab )
606649
607- for ( const entry of imports ) {
608- if ( ! isRelativeSpecifier ( entry ?. source ) ) {
609- continue
610- }
650+ workspaceGraphCache . upsert ( {
651+ tabId : tab . id ,
652+ imports : imports . map ( entry => entry ?. source ) . filter ( Boolean ) ,
653+ lastUpdated : Date . now ( ) ,
654+ } )
611655
612- const target = byModuleKey . get ( entry . source )
613- if ( ! target ) {
614- throw new Error (
615- `Preview entry references missing workspace module: ${ entry . source } ` ,
616- )
656+ for ( const entry of imports ) {
657+ if ( ! isRelativeSpecifier ( entry ?. source ) ) {
658+ continue
659+ }
660+
661+ const target = byModuleKey . get ( entry . source )
662+ if ( ! target ) {
663+ throw new Error (
664+ `Preview entry references missing workspace module: ${ entry . source } ` ,
665+ )
666+ }
667+
668+ visit ( target , { viaSpecifier : entry . source } )
617669 }
618670
619- visit ( target )
671+ visited . add ( tab . id )
672+ dependencyOrder . push ( tab . id )
673+ } finally {
674+ visitStack . pop ( )
675+ visiting . delete ( tab . id )
620676 }
621-
622- dependencyOrder . push ( tab . id )
623677 }
624678
625679 visit ( entryTab )
@@ -634,7 +688,7 @@ export const createRenderRuntimeController = ({
634688 }
635689
636690 const source = typeof tab . content === 'string' ? tab . content : ''
637- const imports = parseWorkspaceImports ( { source , transformJsxSource } )
691+ const imports = getParsedImportsForTab ( tab )
638692 const aliases = createRelativeImportAliases ( imports )
639693 const withoutRelativeImports = stripImportDeclarationsBy ( source , imports , entry =>
640694 isRelativeSpecifier ( entry ?. source ) ,
0 commit comments