Route Discovery
Route discovery is the process by which Scully identifies all the routes in your Angular application. This is a critical first step that determines which pages will be pre-rendered into static HTML files.
Overview
Scully uses a combination of static analysis and configuration to discover routes:
Automatic traversal of Angular routing modules using the guess-parser library
Manual specification through the extraRoutes configuration option
Plugin-based expansion of parameterized routes into concrete paths
Route discovery happens once per build unless you use the --scanRoutes flag to force a fresh scan.
The Traversal Process
The route traversal is handled by the traverseAppRoutes plugin:
// From libs/scully/src/lib/routerPlugins/traverseAppRoutesPlugin.ts
const plugin = async ( forceScan = scanRoutes ) : Promise < string []> => {
const appRootFolder = scullyConfig . projectRoot ;
const routesPath = join (
scullyConfig . homeFolder ,
'node_modules/.cache/@scullyio' ,
` ${ scullyConfig . projectName } .unhandledRoutes.json`
);
const extraRoutes = await addExtraRoutes ();
let routes = [] as string [];
if ( ! scullyConfig . bareProject ) {
// Read from cache when exists and not forced to scan
if ( forceScan === false && existFolder ( routesPath )) {
const result = JSON . parse ( readFileSync ( routesPath ). toString ());
logWarn ( 'Using stored unhandled routes!' );
return [ ... new Set ([ ... result , ... extraRoutes ]). values ()];
}
// Parse Angular routes
routes = parseAngularRoutes ( file , excludedFiles ). map (( r ) => r . path );
// Make sure root route is always rendered
if ( routes . findIndex (( r ) => r . trim () === '' || r . trim () === '/' ) === - 1 ) {
routes . push ( '/' );
}
// Cache the scanned routes
writeFileSync ( routesPath , JSON . stringify ( routes ));
}
// De-duplicate routes
return [ ... new Set ([ ... routes , ... extraRoutes ]). values ()];
};
What Gets Discovered
Scully automatically discovers:
Static Routes
Lazy-Loaded Routes
Parameterized Routes
Nested Routes
Routes with no parameters: const routes : Routes = [
{ path: '' , component: HomeComponent },
{ path: 'about' , component: AboutComponent },
{ path: 'contact' , component: ContactComponent }
];
Discovered routes: Routes loaded via loadChildren: const routes : Routes = [
{
path: 'blog' ,
loadChildren : () => import ( './blog/blog.module' ). then ( m => m . BlogModule )
}
];
Scully analyzes the lazy-loaded module and discovers its routes too. Routes with parameters: const routes : Routes = [
{ path: 'user/:userId' , component: UserComponent },
{ path: 'blog/:slug' , component: BlogPostComponent }
];
Discovered as unhandled routes: /user/:userId
/blog/:slug
Parameterized routes require additional configuration to be rendered. See Unhandled Routes for details. Child routes within a parent: const routes : Routes = [
{
path: 'user/:userId' ,
component: UserComponent ,
children: [
{ path: '' , component: PostsComponent },
{ path: 'friend/:friendCode' , component: FriendComponent },
{ path: 'post/:postId' , component: PostComponent }
]
}
];
Discovered routes: /user/:userId
/user/:userId/friend/:friendCode
/user/:userId/post/:postId
Caching Mechanism
Scully caches discovered routes to improve build performance:
node_modules/.cache/@scullyio/
└── your-project-name.unhandledRoutes.json
When routes are cached, you’ll see this warning:
----------------------------------
Using stored unhandled routes!.
To discover new routes in the angular app use "npx scully --scanRoutes"
----------------------------------
Always use --scanRoutes after adding new routes to your Angular application to ensure they’re discovered and rendered.
Sometimes Scully cannot automatically discover all routes in your application. The extraRoutes configuration allows you to manually specify additional routes:
String Routes
// scully.your-project.config.ts
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
extraRoutes: '/hidden-route'
};
Array of Routes
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
extraRoutes: [
'/special-page' ,
'/legacy-route' ,
'/unlisted-content'
]
};
Promise-Based Routes
For dynamic route discovery, use a Promise:
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
extraRoutes: fetchRoutesFromAPI ()
};
async function fetchRoutesFromAPI () : Promise < string []> {
const response = await fetch ( 'https://api.example.com/routes' );
const data = await response . json ();
return data . routes ; // Returns array of route strings
}
Real-World Example
Fetching archived URLs from the Wayback Machine:
import { httpGetJson } from './http-client' ;
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
extraRoutes: httpGetJson (
'http://web.archive.org/cdx/search/cdx?url=scully.io*&output=json'
). then ( cleanupUrls )
};
function cleanupUrls ( data : any []) : string [] {
return data
. filter ( item => item . statuscode === '200' )
. map ( item => new URL ( item . original ). pathname )
. filter ( path => isValidAppRoute ( path ));
}
The extraRoutes configuration is particularly useful when:
Using custom route matchers that can’t be statically analyzed
Working with ng-upgrade applications that have AngularJS routes
Building non-Angular applications with Scully
Dynamically generating routes from external data sources
The Route Discovery Pipeline
Here’s the complete flow of route discovery:
From Discovery to Enrichment
Once routes are discovered, they move to the enrichment phase:
// From libs/scully/src/lib/utils/handlers/routeDiscovery.ts
export async function routeDiscovery (
unhandledRoutes : string [],
localBaseFilter : string
) : Promise < HandledRoute []> {
performance . mark ( 'startDiscovery' );
performanceIds . add ( 'Discovery' );
printProgress ( undefined , 'Pulling in data to create additional routes.' );
let handledRoutes = [] as HandledRoute [];
// Apply filters
const baseFilterRegexs = wildCardStringToRegEx ( localBaseFilter , {
addTrailingStar: true ,
});
const routeFilterRegexs = wildCardStringToRegEx ( routeFilter );
// Transform unhandled to handled routes
handledRoutes = (
await addOptionalRoutes (
unhandledRoutes . filter (( r : string ) =>
typeof r === 'string' &&
baseFilterRegexs . some (( reg ) => r . match ( reg ) !== null )
)
)
). filter (
( r ) =>
! r . route . endsWith ( '*' ) &&
( routeFilter === '' || routeFilterRegexs . some (( reg ) => r . route . match ( reg ) !== null ))
);
performance . mark ( 'stopDiscovery' );
return handledRoutes ;
}
This enrichment process:
Filters routes based on command-line arguments
Invokes router plugins to expand parameterized routes
Transforms unhandled routes into handled routes with full metadata
Filtering Routes
You can limit which routes are processed using command-line filters:
Base filter
Route filter
Wildcard patterns
# Only process routes starting with /blog
npx scully --baseFilter /blog
Filters are useful during development to quickly test changes on a subset of routes without building the entire site.
Troubleshooting Route Discovery
Routes Not Found
Lazy-loaded routes not discovered
Problem : Routes in lazy-loaded modules aren’t being found.Solution : Ensure your tsconfig.app.json is in the expected location and includes all module files. Check the Scully logs for parsing errors.// Verify your tsconfig.app.json includes:
{
"extends" : "./tsconfig.json" ,
"compilerOptions" : {
"outDir" : "./out-tsc/app" ,
"types" : []
},
"files" : [
"src/main.ts" ,
"src/polyfills.ts"
],
"include" : [
"src/**/*.d.ts"
]
}
Custom route matchers not working
Problem : Routes using custom UrlMatcher functions aren’t discovered.Solution : Custom matchers can’t be statically analyzed. Add these routes manually via extraRoutes:export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
extraRoutes: [
'/custom-matched-route-1' ,
'/custom-matched-route-2'
]
};
Routes discovered but not rendered
Problem : Routes appear in the cache but don’t generate HTML files.Solution : Parameterized routes need router plugin configuration. See Handled Routes for configuration examples.export const config : ScullyConfig = {
routes: {
'/blog/:slug' : {
type: 'contentFolder' ,
slug: {
folder: './blog'
}
}
}
};
Parser Errors
If you encounter parser errors:
We encountered a problem while reading the routes from your applications source.
This might happen when there are lazy-loaded routes, that are not loaded,
Or when there are paths we can not resolve statically.
Use --showGuessError to see detailed error information:
npx scully --showGuessError
Route discovery timing depends on:
Application size : More routes and modules take longer to analyze
Lazy loading : Multiple lazy-loaded modules increase parsing time
Cache status : Cached routes are retrieved in milliseconds
Typical performance:
Small app (< 20 routes): 500ms - 1s
Medium app (20-100 routes): 1s - 3s
Large app (> 100 routes): 3s - 10s
After the initial scan, subsequent builds use the cache and complete in under 100ms.
Next Steps
Unhandled Routes Learn what happens to routes with parameters
Handled Routes Understand how routes are enriched with metadata
Router Plugins Create plugins to handle custom route types
Configuration Configure route discovery settings