Skip to content

Commit c3d7f2e

Browse files
committed
scaffold refactor
1 parent 6a54559 commit c3d7f2e

9 files changed

Lines changed: 1161 additions & 401 deletions

File tree

packages/b2c-cli/src/commands/scaffold/validate.ts

Lines changed: 11 additions & 264 deletions
Original file line numberDiff line numberDiff line change
@@ -3,229 +3,17 @@
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';
76
import path from 'node:path';
87
import {Args, Flags} from '@oclif/core';
9-
import {glob} from 'glob';
108
import {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';
1210
import {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

Comments
 (0)