144 lines
4.3 KiB
JavaScript
144 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Preflight JS Syntax Validation (5B.4)
|
|
*
|
|
* Parses all JavaScript files in web/ as ESM modules to catch syntax errors
|
|
* before runtime. Uses acorn parser for accurate ESM parsing.
|
|
*
|
|
* Usage: npm run preflight:js
|
|
* Exit code: 0 = success, 1 = syntax errors found
|
|
*/
|
|
|
|
import { readdir, readFile } from 'fs/promises';
|
|
import { join, relative } from 'path';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import * as acorn from 'acorn';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const WEB_DIR = join(__dirname, '..', 'web');
|
|
export const NODE_POLICY = {
|
|
minMajor: 18,
|
|
maxMajorExclusive: 26,
|
|
label: 'Node.js >=18 <26',
|
|
};
|
|
|
|
export function parseNodeMajor(version) {
|
|
const raw = String(version || '').trim().replace(/^v/, '');
|
|
const [majorText] = raw.split('.');
|
|
const major = Number(majorText);
|
|
return Number.isInteger(major) ? major : null;
|
|
}
|
|
|
|
export function isSupportedNodeMajor(major) {
|
|
return Number.isInteger(major)
|
|
&& major >= NODE_POLICY.minMajor
|
|
&& major < NODE_POLICY.maxMajorExclusive;
|
|
}
|
|
|
|
export function getUnsupportedNodeVersionMessage(version) {
|
|
const major = parseNodeMajor(version);
|
|
if (!isSupportedNodeMajor(major)) {
|
|
return [
|
|
`FAIL Node.js ${version || 'unknown'} detected. This project requires ${NODE_POLICY.label}.`,
|
|
'Use a Doctor-supported Node runtime from Node 18 through Node 25.',
|
|
'Node 26+ is blocked until validated against Doctor\'s Playwright harness.',
|
|
].join(' ');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function ensureSupportedNodeVersion() {
|
|
const message = getUnsupportedNodeVersionMessage(process.versions.node);
|
|
if (message) {
|
|
console.error(message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Recursively find all .js files
|
|
async function findJsFiles(dir) {
|
|
const files = [];
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
// Skip node_modules and lib (vendor files)
|
|
if (entry.name === 'node_modules' || entry.name === 'lib') continue;
|
|
files.push(...await findJsFiles(fullPath));
|
|
} else if (entry.name.endsWith('.js')) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
// Validate a single JS file as ESM
|
|
async function validateFile(filePath) {
|
|
const content = await readFile(filePath, 'utf-8');
|
|
const relativePath = relative(join(__dirname, '..'), filePath);
|
|
|
|
try {
|
|
acorn.parse(content, {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
locations: true
|
|
});
|
|
return { file: relativePath, valid: true };
|
|
} catch (error) {
|
|
return {
|
|
file: relativePath,
|
|
valid: false,
|
|
error: error.message,
|
|
line: error.loc?.line || null,
|
|
column: error.loc?.column || null
|
|
};
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
async function main() {
|
|
ensureSupportedNodeVersion();
|
|
console.log('INFO: Preflight JS Syntax Check (acorn)\n');
|
|
console.log(`Scanning: ${WEB_DIR}\n`);
|
|
|
|
const files = await findJsFiles(WEB_DIR);
|
|
console.log(`Found ${files.length} JavaScript files\n`);
|
|
|
|
const results = await Promise.all(files.map(validateFile));
|
|
|
|
const errors = results.filter(r => !r.valid);
|
|
const valid = results.filter(r => r.valid);
|
|
|
|
// Report valid files
|
|
for (const r of valid) {
|
|
console.log(`PASS ${r.file}`);
|
|
}
|
|
|
|
// Report errors
|
|
if (errors.length > 0) {
|
|
console.log('\nFAIL Syntax Errors Found:\n');
|
|
for (const r of errors) {
|
|
console.log(` FAIL ${r.file}`);
|
|
console.log(` ${r.error}`);
|
|
if (r.line) console.log(` Line: ${r.line}, Column: ${r.column}`);
|
|
console.log('');
|
|
}
|
|
console.log(`\nABORT: ${errors.length} file(s) with syntax errors`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\nPASS All ${valid.length} files passed syntax check`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const isCliEntrypoint = process.argv[1]
|
|
&& import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
|
|
if (isCliEntrypoint) {
|
|
main().catch(err => {
|
|
console.error('Preflight check failed:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|