@@ -9,18 +9,18 @@ const path = require('path');
99const { approve, block, runValidation, findProjectRoot, UUID_REGEX } = require ( '../../../scripts/lib/validation-helpers' ) ;
1010
1111const ALLOWED_FUNCTIONS = [ 'get' , 'post' , 'put' , 'patch' , 'del' ] ;
12- const BROWSER_APIS = [ 'XMLHttpRequest' , 'document\\.' , 'window\\.' , 'setTimeout' , 'setInterval' , 'navigator\\.' ] ;
12+ const BROWSER_APIS = [ 'XMLHttpRequest' , 'document\\.' , 'window\\.' , 'setTimeout' , 'setInterval' , 'navigator\\.' , 'fetch' ] ;
1313
1414runValidation ( ( cwd ) => {
1515 const projectRoot = findProjectRoot ( cwd ) ;
16- if ( ! projectRoot ) approve ( ) ; // Not a Power Pages project, skip
16+ if ( ! projectRoot ) return approve ( ) ; // Not a Power Pages project, skip
1717
1818 // Server logic files live inside .powerpages-site/server-logic/
1919 const serverLogicDir = path . join ( projectRoot , '.powerpages-site' , 'server-logic' ) ;
20- if ( ! fs . existsSync ( serverLogicDir ) ) approve ( ) ; // No server-logic folder, not a server logic session
20+ if ( ! fs . existsSync ( serverLogicDir ) ) return approve ( ) ; // No server-logic folder, not a server logic session
2121
2222 const logicDirs = findServerLogicDirs ( serverLogicDir ) ;
23- if ( logicDirs . length === 0 ) approve ( ) ; // No server logic subdirectories, skip
23+ if ( logicDirs . length === 0 ) return approve ( ) ; // No server logic subdirectories, skip
2424
2525 const errors = [ ] ;
2626
@@ -42,28 +42,54 @@ runValidation((cwd) => {
4242 // Validate YAML contents
4343 const ymlContent = fs . readFileSync ( ymlFile , 'utf8' ) ;
4444
45- // Check id field exists and is a valid UUID
45+ // Check id field exists and is a valid UUID (strip surrounding quotes if present)
4646 const idMatch = ymlContent . match ( / ^ i d : \s * ( .+ ) $ / m) ;
4747 if ( ! idMatch ) {
4848 errors . push ( `${ dirName } .serverlogic.yml: missing 'id' field — PAC CLI requires a GUID` ) ;
49- } else if ( ! UUID_REGEX . test ( idMatch [ 1 ] . trim ( ) ) ) {
50- errors . push ( `${ dirName } .serverlogic.yml: 'id' is not a valid UUID: ${ idMatch [ 1 ] . trim ( ) } ` ) ;
49+ } else {
50+ const idValue = idMatch [ 1 ] . trim ( ) . replace ( / ^ [ ' " ] | [ ' " ] $ / g, '' ) ;
51+ if ( ! UUID_REGEX . test ( idValue ) ) {
52+ errors . push ( `${ dirName } .serverlogic.yml: 'id' is not a valid UUID: ${ idValue } ` ) ;
53+ }
5154 }
5255
53- // Check adx_serverlogic_adx_webrole is present and non-empty
54- if ( ! ymlContent . includes ( 'adx_serverlogic_adx_webrole:' ) ) {
56+ // Check adx_serverlogic_adx_webrole is present and non-empty, and validate GUIDs
57+ const webRoleHeaderMatch = / ^ a d x _ s e r v e r l o g i c _ a d x _ w e b r o l e : \s * $ / m. exec ( ymlContent ) ;
58+ if ( ! webRoleHeaderMatch ) {
5559 errors . push ( `${ dirName } .serverlogic.yml: missing 'adx_serverlogic_adx_webrole' field — at least one web role is required` ) ;
5660 } else {
57- const roleMatches = ymlContent . match ( / ^ \s + - \s + \S + / gm) ;
58- if ( ! roleMatches || roleMatches . length === 0 ) {
61+ const sectionStart = webRoleHeaderMatch . index + webRoleHeaderMatch [ 0 ] . length ;
62+ const rest = ymlContent . slice ( sectionStart ) ;
63+ const nextKeyMatch = rest . match ( / ^ [ A - Z a - z 0 - 9 _ ] + : \s * / m) ;
64+ const sectionEnd = nextKeyMatch ? sectionStart + nextKeyMatch . index : ymlContent . length ;
65+ const webRoleSection = ymlContent . slice ( sectionStart , sectionEnd ) ;
66+
67+ const roleItemRegex = / ^ \s * - \s + ( [ ^ \s # ] + ) / gm;
68+ let match ;
69+ let hasItems = false ;
70+
71+ while ( ( match = roleItemRegex . exec ( webRoleSection ) ) !== null ) {
72+ hasItems = true ;
73+ const roleValue = match [ 1 ] . trim ( ) . replace ( / ^ [ ' " ] | [ ' " ] $ / g, '' ) ;
74+ if ( ! UUID_REGEX . test ( roleValue ) ) {
75+ errors . push ( `${ dirName } .serverlogic.yml: web role value '${ roleValue } ' under 'adx_serverlogic_adx_webrole' is not a valid UUID` ) ;
76+ }
77+ }
78+
79+ if ( ! hasItems ) {
5980 errors . push ( `${ dirName } .serverlogic.yml: 'adx_serverlogic_adx_webrole' array is empty — at least one web role GUID is required` ) ;
6081 }
6182 }
6283
63- // Check name field matches directory name
84+ // Check name field exists and matches directory name (strip surrounding quotes if present)
6485 const nameMatch = ymlContent . match ( / ^ n a m e : \s * ( .+ ) $ / m) ;
65- if ( nameMatch && nameMatch [ 1 ] . trim ( ) !== dirName ) {
66- errors . push ( `${ dirName } .serverlogic.yml: 'name' field '${ nameMatch [ 1 ] . trim ( ) } ' does not match folder name '${ dirName } '` ) ;
86+ if ( ! nameMatch ) {
87+ errors . push ( `${ dirName } .serverlogic.yml: missing 'name' field — it must be present and match the folder name '${ dirName } '` ) ;
88+ } else {
89+ const nameValue = nameMatch [ 1 ] . trim ( ) . replace ( / ^ [ ' " ] | [ ' " ] $ / g, '' ) ;
90+ if ( nameValue !== dirName ) {
91+ errors . push ( `${ dirName } .serverlogic.yml: 'name' field '${ nameValue } ' does not match folder name '${ dirName } '` ) ;
92+ }
6793 }
6894 }
6995
@@ -81,13 +107,37 @@ runValidation((cwd) => {
81107 continue ;
82108 }
83109
84- // Check: each function has try/catch
110+ // Check: no disallowed top-level functions outside the allowlist
111+ const topLevelFnRegex = / (?: ^ | \n ) \s * (?: a s y n c \s + ) ? f u n c t i o n \s + ( [ a - z A - Z 0 - 9 _ ] + ) \s * \( / g;
112+ let fnMatch ;
113+ const disallowedFunctions = new Set ( ) ;
114+ while ( ( fnMatch = topLevelFnRegex . exec ( content ) ) !== null ) {
115+ if ( ! ALLOWED_FUNCTIONS . includes ( fnMatch [ 1 ] ) ) {
116+ disallowedFunctions . add ( fnMatch [ 1 ] ) ;
117+ }
118+ }
119+ const exportRegex = / (?: ^ | \n ) \s * (?: m o d u l e \. e x p o r t s | e x p o r t s ) \. ( [ a - z A - Z 0 - 9 _ ] + ) \s * = / g;
120+ let exportMatch ;
121+ while ( ( exportMatch = exportRegex . exec ( content ) ) !== null ) {
122+ if ( ! ALLOWED_FUNCTIONS . includes ( exportMatch [ 1 ] ) ) {
123+ disallowedFunctions . add ( exportMatch [ 1 ] ) ;
124+ }
125+ }
126+ if ( disallowedFunctions . size > 0 ) {
127+ errors . push ( `${ dirName } .js: only get, post, put, patch, and del functions are allowed; found additional top-level functions: ${ Array . from ( disallowedFunctions ) . join ( ', ' ) } ` ) ;
128+ continue ;
129+ }
130+
131+ // Check: each function has try/catch (scan until next top-level function or end of file)
85132 for ( const fn of foundFunctions ) {
86133 const fnRegex = new RegExp ( `(?:async\\s+)?function\\s+${ fn } \\s*\\([^)]*\\)\\s*\\{` , 'g' ) ;
87134 const match = fnRegex . exec ( content ) ;
88135 if ( match ) {
89- const afterFn = content . substring ( match . index + match [ 0 ] . length , match . index + match [ 0 ] . length + 100 ) ;
90- if ( ! afterFn . includes ( 'try' ) ) {
136+ const bodyStart = match . index + match [ 0 ] . length ;
137+ const nextFnMatch = content . slice ( bodyStart ) . match ( / \n (?: a s y n c \s + ) ? f u n c t i o n \s + [ a - z A - Z ] / ) ;
138+ const bodyEnd = nextFnMatch ? bodyStart + nextFnMatch . index : content . length ;
139+ const fnBody = content . slice ( bodyStart , bodyEnd ) ;
140+ if ( ! / \b t r y \s * \{ / . test ( fnBody ) ) {
91141 errors . push ( `${ dirName } .js: function '${ fn } ' is missing try/catch error handling` ) ;
92142 }
93143 }
@@ -116,9 +166,9 @@ runValidation((cwd) => {
116166 }
117167 }
118168
119- // Check: no console.log
169+ // Check: no console usage
120170 if ( / \b c o n s o l e \s * \. / . test ( content ) ) {
121- errors . push ( `${ dirName } .js: contains console.log — use Server.Logger instead` ) ;
171+ errors . push ( `${ dirName } .js: contains console.* — use Server.Logger instead` ) ;
122172 }
123173
124174 // Check: no 'function delete()'
@@ -142,6 +192,8 @@ function findServerLogicDirs(dir) {
142192 dirs . push ( path . join ( dir , entry . name ) ) ;
143193 }
144194 }
145- } catch { }
195+ } catch ( err ) {
196+ throw new Error ( `Failed to read server logic directory '${ dir } ': ${ err . message } ` ) ;
197+ }
146198 return dirs ;
147199}
0 commit comments