@@ -15,6 +15,7 @@ import {
1515 watchDebounced ,
1616 isNodeError ,
1717} from '../../utils/command-helpers.js'
18+ import { MultiMap } from '../../utils/multimap.js'
1819import { getPathInProject } from '../settings.js'
1920
2021import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.js'
@@ -28,6 +29,8 @@ type EdgeFunctionEvent = 'buildError' | 'loaded' | 'reloaded' | 'reloading' | 'r
2829type Route = Omit < Manifest [ 'routes' ] [ 0 ] , 'pattern' > & { pattern : RegExp }
2930type RunIsolate = Awaited < ReturnType < typeof import ( '@netlify/edge-bundler' ) . serve > >
3031
32+ type ModuleJson = ModuleGraph [ 'modules' ] [ number ]
33+
3134const featureFlags = { edge_functions_correct_order : true }
3235
3336interface EdgeFunctionsRegistryOptions {
@@ -44,6 +47,38 @@ interface EdgeFunctionsRegistryOptions {
4447 importMapFromTOML ?: string
4548}
4649
50+ /**
51+ * Helper method which, given a edge bundler graph module and an index of modules by path, traverses its dependency tree
52+ * and returns an array of all of ist local dependencies
53+ */
54+ function traverseLocalDependencies (
55+ { dependencies = [ ] } : ModuleJson ,
56+ modulesByPath : Map < string , ModuleJson > ,
57+ ) : string [ ] {
58+ return dependencies . flatMap ( ( dependency ) => {
59+ // We're interested in tracking local dependencies, so we only look at
60+ // specifiers with the `file:` protocol.
61+ if (
62+ dependency . code === undefined ||
63+ typeof dependency . code . specifier !== 'string' ||
64+ ! dependency . code . specifier . startsWith ( 'file://' )
65+ ) {
66+ return [ ]
67+ }
68+ const { specifier : dependencyURL } = dependency . code
69+ const dependencyPath = fileURLToPath ( dependencyURL )
70+ const dependencyModule = modulesByPath . get ( dependencyPath )
71+
72+ // No module indexed for this dependency
73+ if ( dependencyModule === undefined ) {
74+ return [ dependencyPath ]
75+ }
76+
77+ // Keep traversing the child dependencies and return the current dependency path
78+ return [ ...traverseLocalDependencies ( dependencyModule , modulesByPath ) , dependencyPath ]
79+ } )
80+ }
81+
4782export class EdgeFunctionsRegistry {
4883 private buildError : Error | null = null
4984 private bundler : typeof import ( '@netlify/edge-bundler' )
@@ -52,19 +87,28 @@ export class EdgeFunctionsRegistry {
5287 private importMapFromTOML ?: string
5388 private declarationsFromDeployConfig : Declaration [ ] = [ ]
5489 private declarationsFromTOML : Declaration [ ]
55- private dependencyPaths = new Map < string , string [ ] > ( )
90+
91+ // Mapping file URLs to names of functions that use them as dependencies.
92+ private dependencyPaths = new MultiMap < string , string > ( )
93+
5694 private directories : string [ ]
5795 private directoryWatchers = new Map < string , import ( 'chokidar' ) . FSWatcher > ( )
5896 private env : Record < string , string >
97+
98+ private userFunctions : EdgeFunction [ ] = [ ]
99+ private internalFunctions : EdgeFunction [ ] = [ ]
100+
101+ // a Map from `this.functions` that maps function paths to function
102+ // names. This allows us to match modules against functions in O(1) time as
103+ // opposed to O(n).
59104 private functionPaths = new Map < string , string > ( )
105+
60106 private getUpdatedConfig : ( ) => Promise < Config >
61107 private initialScan : Promise < void >
62- private internalFunctions : EdgeFunction [ ] = [ ]
63108 private manifest : Manifest | null = null
64109 private routes : Route [ ] = [ ]
65110 private runIsolate : RunIsolate
66111 private servePath : string
67- private userFunctions : EdgeFunction [ ] = [ ]
68112 private projectDir : string
69113
70114 constructor ( {
@@ -91,13 +135,6 @@ export class EdgeFunctionsRegistry {
91135 this . declarationsFromTOML = EdgeFunctionsRegistry . getDeclarationsFromTOML ( config )
92136 this . env = EdgeFunctionsRegistry . getEnvironmentVariables ( env )
93137
94- this . buildError = null
95- this . directoryWatchers = new Map ( )
96- this . dependencyPaths = new Map ( )
97- this . functionPaths = new Map ( )
98- this . userFunctions = [ ]
99- this . internalFunctions = [ ]
100-
101138 this . initialScan = this . doInitialScan ( )
102139
103140 this . setupWatchers ( )
@@ -416,50 +453,37 @@ export class EdgeFunctionsRegistry {
416453 return
417454 }
418455
419- // Creating a Map from `this.functions` that maps function paths to function
420- // names. This allows us to match modules against functions in O(1) time as
421- // opposed to O(n).
422- // eslint-disable-next-line unicorn/prefer-spread
423- const functionPaths = new Map ( Array . from ( this . functions , ( func ) => [ func . path , func . name ] ) )
456+ this . dependencyPaths = new MultiMap < string , string > ( )
424457
425- // Mapping file URLs to names of functions that use them as dependencies .
426- const dependencyPaths = new Map < string , string [ ] > ( )
458+ // Mapping file URLs to modules. Used by the traversal function .
459+ const modulesByPath = new Map < string , ModuleJson > ( )
427460
428- const { modules } = graph
429-
430- modules . forEach ( ( { dependencies = [ ] , specifier } ) => {
461+ // a set of edge function modules that we'll use to start traversing the dependency tree from
462+ const functionModules = new Set < { functionName : string ; module : ModuleJson } > ( )
463+ graph . modules . forEach ( ( module ) => {
464+ // We're interested in tracking local dependencies, so we only look at
465+ // specifiers with the `file:` protocol.
466+ const { specifier } = module
431467 if ( ! specifier . startsWith ( 'file://' ) ) {
432468 return
433469 }
434470
435471 const path = fileURLToPath ( specifier )
436- const functionMatch = functionPaths . get ( path )
472+ modulesByPath . set ( path , module )
437473
438- if ( ! functionMatch ) {
439- return
474+ const functionName = this . functionPaths . get ( path )
475+ if ( functionName ) {
476+ functionModules . add ( { functionName, module } )
440477 }
478+ } )
441479
442- dependencies . forEach ( ( dependency ) => {
443- // We're interested in tracking local dependencies, so we only look at
444- // specifiers with the `file:` protocol.
445- if (
446- dependency . code === undefined ||
447- typeof dependency . code . specifier !== 'string' ||
448- ! dependency . code . specifier . startsWith ( 'file://' )
449- ) {
450- return
451- }
452-
453- const { specifier : dependencyURL } = dependency . code
454- const dependencyPath = fileURLToPath ( dependencyURL )
455- const functions = dependencyPaths . get ( dependencyPath ) || [ ]
456-
457- dependencyPaths . set ( dependencyPath , [ ...functions , functionMatch ] )
480+ // We start from our functions and we traverse through their dependency tree
481+ functionModules . forEach ( ( { functionName, module } ) => {
482+ const traversedPaths = traverseLocalDependencies ( module , modulesByPath )
483+ traversedPaths . forEach ( ( dependencyPath ) => {
484+ this . dependencyPaths . add ( dependencyPath , functionName )
458485 } )
459486 } )
460-
461- this . dependencyPaths = dependencyPaths
462- this . functionPaths = functionPaths
463487 }
464488
465489 /**
@@ -541,10 +565,19 @@ export class EdgeFunctionsRegistry {
541565 this . internalFunctions = internalFunctions
542566 this . userFunctions = userFunctions
543567
568+ // eslint-disable-next-line unicorn/prefer-spread
569+ this . functionPaths = new Map ( Array . from ( this . functions , ( func ) => [ func . path , func . name ] ) )
570+
544571 return { all : functions , new : newFunctions , deleted : deletedFunctions }
545572 }
546573
547574 private async setupWatchers ( ) {
575+ // While functions are guaranteed to be inside one of the configured
576+ // directories, they might be importing files that are located in
577+ // parent directories. So we watch the entire project directory for
578+ // changes.
579+ await this . setupWatcherForDirectory ( )
580+
548581 if ( ! this . configPath ) {
549582 return
550583 }
@@ -560,12 +593,6 @@ export class EdgeFunctionsRegistry {
560593 await this . checkForAddedOrDeletedFunctions ( )
561594 } ,
562595 } )
563-
564- // While functions are guaranteed to be inside one of the configured
565- // directories, they might be importing files that are located in
566- // parent directories. So we watch the entire project directory for
567- // changes.
568- await this . setupWatcherForDirectory ( )
569596 }
570597
571598 private async setupWatcherForDirectory ( ) {
0 commit comments