33 * SPDX-License-Identifier: Apache-2
44 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55 */
6- import fs from 'node:fs/promises' ;
76import path from 'node:path' ;
87import { Args , Flags } from '@oclif/core' ;
9- import { glob } from 'glob' ;
108import { BaseCommand } from '@salesforce/b2c-tooling-sdk/cli' ;
11- import { validateScaffoldManifest , type FileMapping , type ScaffoldManifest } from '@salesforce/b2c-tooling-sdk/scaffold' ;
9+ import { validateScaffoldDirectory , type ValidationResult } from '@salesforce/b2c-tooling-sdk/scaffold' ;
1210import { t , withDocs } from '../../i18n/index.js' ;
1311
14- /**
15- * Validation issue severity.
16- */
17- type IssueSeverity = 'error' | 'warning' ;
18-
19- /**
20- * A validation issue found in the scaffold.
21- */
22- interface ValidationIssue {
23- severity : IssueSeverity ;
24- message : string ;
25- file ?: string ;
26- }
27-
2812/**
2913 * Response type for the validate command.
3014 */
31- interface ScaffoldValidateResponse {
15+ interface ScaffoldValidateResponse extends ValidationResult {
3216 path : string ;
33- valid : boolean ;
34- errors : number ;
35- warnings : number ;
36- issues : ValidationIssue [ ] ;
37- }
38-
39- /**
40- * Check if a file exists
41- */
42- async function fileExists ( filePath : string ) : Promise < boolean > {
43- try {
44- await fs . access ( filePath ) ;
45- return true ;
46- } catch {
47- return false ;
48- }
49- }
50-
51- /**
52- * Check if a path is a directory
53- */
54- async function isDirectory ( dirPath : string ) : Promise < boolean > {
55- try {
56- const stat = await fs . stat ( dirPath ) ;
57- return stat . isDirectory ( ) ;
58- } catch {
59- return false ;
60- }
61- }
62-
63- /**
64- * Load and parse manifest file
65- */
66- async function loadManifest ( manifestPath : string ) : Promise < { manifest : null | ScaffoldManifest ; error : null | string } > {
67- try {
68- const content = await fs . readFile ( manifestPath , 'utf8' ) ;
69- return { manifest : JSON . parse ( content ) as ScaffoldManifest , error : null } ;
70- } catch ( error ) {
71- if ( ( error as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
72- return { manifest : null , error : 'scaffold.json not found' } ;
73- }
74- return { manifest : null , error : 'scaffold.json is not valid JSON' } ;
75- }
76- }
77-
78- /**
79- * Validate manifest structure and return issues
80- */
81- function validateManifestStructure ( manifest : ScaffoldManifest ) : ValidationIssue [ ] {
82- const issues : ValidationIssue [ ] = [ ] ;
83-
84- const manifestErrors = validateScaffoldManifest ( manifest ) ;
85- for ( const error of manifestErrors ) {
86- issues . push ( { severity : 'error' , message : error , file : 'scaffold.json' } ) ;
87- }
88-
89- if ( ! manifest . postInstructions ) {
90- issues . push ( {
91- severity : 'warning' ,
92- message : 'Consider adding postInstructions to guide users after generation' ,
93- file : 'scaffold.json' ,
94- } ) ;
95- }
96-
97- return issues ;
98- }
99-
100- /**
101- * Check template files exist and return issues
102- */
103- async function checkTemplateFiles ( filesDir : string , manifest : ScaffoldManifest ) : Promise < ValidationIssue [ ] > {
104- const issues : ValidationIssue [ ] = [ ] ;
105- const filesToCheck : Array < { path : string ; template : string } > = [ ] ;
106-
107- // Collect files from file mappings
108- if ( manifest . files && Array . isArray ( manifest . files ) ) {
109- for ( const mapping of manifest . files as FileMapping [ ] ) {
110- filesToCheck . push ( { path : path . join ( filesDir , mapping . template ) , template : mapping . template } ) ;
111- }
112- }
113-
114- // Collect files from modifications
115- if ( manifest . modifications ) {
116- for ( const mod of manifest . modifications ) {
117- if ( mod . contentTemplate ) {
118- filesToCheck . push ( { path : path . join ( filesDir , mod . contentTemplate ) , template : mod . contentTemplate } ) ;
119- }
120- }
121- }
122-
123- // Check all files in parallel
124- const results = await Promise . all (
125- filesToCheck . map ( async ( { path : filePath , template} ) => {
126- const exists = await fileExists ( filePath ) ;
127- return { template, exists} ;
128- } ) ,
129- ) ;
130-
131- for ( const { template, exists} of results ) {
132- if ( ! exists ) {
133- issues . push ( {
134- severity : 'error' ,
135- message : `Template file not found: ${ template } ` ,
136- file : `files/${ template } ` ,
137- } ) ;
138- }
139- }
140-
141- return issues ;
142- }
143-
144- /**
145- * Check for orphaned template files
146- */
147- function checkOrphanedFiles ( allTemplates : string [ ] , manifest : ScaffoldManifest ) : ValidationIssue [ ] {
148- const issues : ValidationIssue [ ] = [ ] ;
149- const referencedTemplates = new Set < string > ( ) ;
150-
151- if ( manifest . files ) {
152- for ( const f of manifest . files as FileMapping [ ] ) {
153- referencedTemplates . add ( f . template ) ;
154- }
155- }
156-
157- if ( manifest . modifications ) {
158- for ( const mod of manifest . modifications ) {
159- if ( mod . contentTemplate ) {
160- referencedTemplates . add ( mod . contentTemplate ) ;
161- }
162- }
163- }
164-
165- for ( const template of allTemplates ) {
166- if ( ! referencedTemplates . has ( template ) ) {
167- issues . push ( {
168- severity : 'warning' ,
169- message : `Template file not referenced in manifest: ${ template } ` ,
170- file : `files/${ template } ` ,
171- } ) ;
172- }
173- }
174-
175- return issues ;
176- }
177-
178- /**
179- * Validate EJS syntax in template content
180- */
181- function validateEjsSyntax ( content : string , filename : string ) : ValidationIssue [ ] {
182- const issues : ValidationIssue [ ] = [ ] ;
183-
184- // Check for unclosed EJS tags
185- const openTags = ( content . match ( / < % / g) || [ ] ) . length ;
186- const closeTags = ( content . match ( / % > / g) || [ ] ) . length ;
187-
188- if ( openTags !== closeTags ) {
189- issues . push ( {
190- severity : 'error' ,
191- message : `Mismatched EJS tags: ${ openTags } opening, ${ closeTags } closing` ,
192- file : `files/${ filename } ` ,
193- } ) ;
194- }
195-
196- // Check for common EJS errors
197- const invalidPatterns = [
198- { pattern : / < % [ ^ % = _ \- \s # ] / , message : 'Invalid EJS tag opening (missing space or modifier)' } ,
199- { pattern : / < % = \s * % > / , message : 'Empty EJS output tag' } ,
200- ] ;
201-
202- for ( const { pattern, message} of invalidPatterns ) {
203- if ( pattern . test ( content ) ) {
204- issues . push ( { severity : 'warning' , message, file : `files/${ filename } ` } ) ;
205- }
206- }
207-
208- return issues ;
209- }
210-
211- /**
212- * Validate EJS syntax in all template files
213- */
214- async function validateAllEjsTemplates ( filesDir : string , allTemplates : string [ ] ) : Promise < ValidationIssue [ ] > {
215- const ejsTemplates = allTemplates . filter ( ( t ) => t . endsWith ( '.ejs' ) ) ;
216-
217- const results = await Promise . all (
218- ejsTemplates . map ( async ( template ) => {
219- try {
220- const content = await fs . readFile ( path . join ( filesDir , template ) , 'utf8' ) ;
221- return validateEjsSyntax ( content , template ) ;
222- } catch {
223- return [ ] ;
224- }
225- } ) ,
226- ) ;
227-
228- return results . flat ( ) ;
22917}
23018
23119/**
@@ -261,56 +49,15 @@ export default class ScaffoldValidate extends BaseCommand<typeof ScaffoldValidat
26149
26250 async run ( ) : Promise < ScaffoldValidateResponse > {
26351 const scaffoldPath = path . resolve ( this . args . path ) ;
264- const issues : ValidationIssue [ ] = [ ] ;
265-
266- // Check if path exists and is a directory
267- if ( ! ( await isDirectory ( scaffoldPath ) ) ) {
268- this . error ( `Path does not exist or is not a directory: ${ scaffoldPath } ` ) ;
269- }
27052
271- // Load and validate manifest
272- const manifestPath = path . join ( scaffoldPath , 'scaffold.json' ) ;
273- const { manifest, error : manifestError } = await loadManifest ( manifestPath ) ;
274-
275- if ( manifestError ) {
276- issues . push ( { severity : 'error' , message : manifestError , file : 'scaffold.json' } ) ;
277- }
278-
279- if ( manifest ) {
280- issues . push ( ...validateManifestStructure ( manifest ) ) ;
281- }
282-
283- // Check files directory
284- const filesDir = path . join ( scaffoldPath , 'files' ) ;
285- const filesExist = await isDirectory ( filesDir ) ;
286-
287- if ( ! filesExist ) {
288- issues . push ( { severity : 'error' , message : 'files/ directory not found' } ) ;
289- }
290-
291- if ( filesExist && manifest ) {
292- // Get all template files
293- const allTemplates = await glob ( '**/*' , { cwd : filesDir , nodir : true , dot : true } ) ;
294-
295- // Check template files exist, orphans, and EJS syntax
296- issues . push (
297- ...( await checkTemplateFiles ( filesDir , manifest ) ) ,
298- ...checkOrphanedFiles ( allTemplates , manifest ) ,
299- ...( await validateAllEjsTemplates ( filesDir , allTemplates ) ) ,
300- ) ;
301- }
302-
303- // Calculate results
304- const errors = issues . filter ( ( i ) => i . severity === 'error' ) . length ;
305- const warnings = issues . filter ( ( i ) => i . severity === 'warning' ) . length ;
306- const valid = this . flags . strict ? errors === 0 && warnings === 0 : errors === 0 ;
53+ // Use SDK validation function
54+ const result = await validateScaffoldDirectory ( scaffoldPath , {
55+ strict : this . flags . strict ,
56+ } ) ;
30757
30858 const response : ScaffoldValidateResponse = {
30959 path : scaffoldPath ,
310- valid,
311- errors,
312- warnings,
313- issues,
60+ ...result ,
31461 } ;
31562
31663 if ( this . jsonEnabled ( ) ) {
@@ -322,20 +69,20 @@ export default class ScaffoldValidate extends BaseCommand<typeof ScaffoldValidat
32269 this . log ( `Validating scaffold at: ${ scaffoldPath } ` ) ;
32370 this . log ( '' ) ;
32471
325- if ( issues . length === 0 ) {
72+ if ( result . issues . length === 0 ) {
32673 this . log ( 'No issues found.' ) ;
32774 } else {
328- for ( const issue of issues ) {
75+ for ( const issue of result . issues ) {
32976 const prefix = issue . severity === 'error' ? 'ERROR' : 'WARN' ;
33077 const fileInfo = issue . file ? ` (${ issue . file } )` : '' ;
33178 this . log ( ` ${ prefix } : ${ issue . message } ${ fileInfo } ` ) ;
33279 }
33380 }
33481
33582 this . log ( '' ) ;
336- this . log ( `Summary: ${ errors } error(s), ${ warnings } warning(s)` ) ;
83+ this . log ( `Summary: ${ result . errors } error(s), ${ result . warnings } warning(s)` ) ;
33784
338- if ( valid ) {
85+ if ( result . valid ) {
33986 this . log ( '' ) ;
34087 this . log ( 'Scaffold is valid.' ) ;
34188 } else {
0 commit comments