UNPKG

@sap/ams-dev

Version:

NodesJS AMS development environment

332 lines (283 loc) 8.51 kB
const fs = require('fs'); const path = require('path'); const { DclCompiler } = require('./compiler'); /** * Simple argument parser (replaces yargs) */ function parseArgs(args) { const options = { d: null, o: null, l: 'error', f: 'error', E: false }; const aliases = { dcl: 'd', output: 'o', 'log-level': 'l', failOn: 'f', 'force-encoding': 'E' }; const booleans = new Set(['E']); const choices = { l: ['info', 'error', 'silent'], f: ['error', 'deprecation', 'warning'] }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); } let key = null; let value = null; if (arg.startsWith('--')) { // Long option: --dcl=value or --dcl value const eqIndex = arg.indexOf('='); if (eqIndex !== -1) { key = arg.slice(2, eqIndex); value = arg.slice(eqIndex + 1); } else { key = arg.slice(2); } // Convert alias to short form if (aliases[key]) { key = aliases[key]; } // Boolean flags don't consume the next argument if (booleans.has(key)) { value = true; } else if (value === null) { value = args[++i]; } } else if (arg.startsWith('-') && arg.length === 2) { // Short option: -d value or -E (boolean) key = arg[1]; if (booleans.has(key)) { value = true; } else { value = args[++i]; } } if (key && value !== undefined) { // Validate choices if (choices[key] && !choices[key].includes(value)) { console.error(`Invalid value '${value}' for option -${key}. Allowed: ${choices[key].join(', ')}`); process.exit(1); } options[key] = value; } } // Check required options if (!options.d) { console.error('Error: Missing required option -d/--dcl'); printHelp(); process.exit(1); } if (!options.o) { console.error('Error: Missing required option -o/--output'); printHelp(); process.exit(1); } return options; } function printHelp() { console.log(` Usage: compile-dcl --dcl [DCL_SRC_ROOT_DIR] --output [DCN_OUTPUT_ROOT_DIR] Options: -d, --dcl path to DCL root directory [string] [required] -o, --output path to DCN output root directory [string] [required] -l, --log-level log level [choices: "info", "error", "silent"] [default: "error"] -f, --failOn fail on error, deprecation or warning [choices: "error", "deprecation", "warning"] [default: "error"] -h, --help Show help Examples: compile-dcl -d src/dcl -o build/dcn compiles DCL in root directory src/dcl to DCN in output root directory build/dcn/ `); } /** * Check if DCL directories exist, create output if needed */ function checkDirsExist(src, target, logLevel) { // Check dcl resources dir try { fs.accessSync(src); } catch { if (logLevel === "error") { console.error(`DCL resource dir: ${src} does not exist.`); } return false; } // Check dcl target dir try { fs.accessSync(target); } catch { if (logLevel !== "silent") { console.info(`compile target dir: ${target} does not exist. creating it.`); } fs.mkdirSync(target, { recursive: true }); } return true; } /** * Check if compilation should fail based on findings and failOn setting */ function checkShouldFail(findings, failOn) { if (!findings || findings.length === 0) { return false; } for (const finding of findings) { const severity = finding.severity.toLowerCase(); if (failOn === 'warning' && (severity === 'warning' || severity === 'error' || severity === 'deprecation')) { return true; } if (failOn === 'deprecation' && (severity === 'deprecation' || severity === 'error')) { return true; } if (failOn === 'error' && severity === 'error') { return true; } } return false; } const colors = { red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', gray: '\x1b[90m', reset: '\x1b[0m', }; const severityColor = { Error: colors.red, Warning: colors.yellow, Info: colors.blue, }; function formatLogMessage(f) { const c = severityColor[f.severity] || colors.gray; const sev = `${c}${f.severity}${colors.reset}`; if (!f.location.file) { return `${sev} ${f.name}(${f.code}) - ${f.message}`; } return `${sev} ${f.name}(${f.code}) in ${colors.gray}${f.location.file}${colors.reset} at ${colors.gray}${f.location.line}:${f.location.column}${colors.reset} - ${f.message}`; } /** * Parses CLI args into the resolved options used by both the async and sync * compile paths. Returns null if the input/output directories are not usable. */ function resolveCompileOptions(args) { // Skip node and script path if present (when called from CLI) const cleanArgs = args[0]?.includes('node') || args[0]?.endsWith('.js') ? args.slice(2) : args; const options = parseArgs(cleanArgs); let outputPath = options.o; let dclPath = options.d; // Adjust the path in case relative paths are used if (!path.isAbsolute(outputPath)) outputPath = path.join(process.cwd(), outputPath); if (!path.isAbsolute(dclPath)) dclPath = path.join(process.cwd(), dclPath); if (!checkDirsExist(dclPath, outputPath, options.l)) { return null; } return { failOn: options.f, logLevel: options.l, forceEncoding: options.E === true, outputPath, dclPath }; } /** * Shared post-processing of a compile result: logs findings, applies the * failOn policy and writes the DCN output files. Returns the process exit code. */ function handleCompileResult(result, { failOn, logLevel, forceEncoding, outputPath }) { // Log findings based on log level if (result.findings && result.findings.length > 0) { for (const finding of result.findings) { const msg = formatLogMessage(finding); const severity = finding.severity.toLowerCase(); if (logLevel === 'info') { console.info(msg); } else if (logLevel === 'error' && (severity === 'error' || severity === 'warning')) { console.error(msg); } } } // Check if we should fail based on failOn setting if (checkShouldFail(result.findings, failOn) && !forceEncoding) { return 255; } // Write output files for (const [filename, content] of Object.entries(result.dcnOutputs)) { // Change extension from .dcl to .dcn (the content is JSON) const outputFilename = filename.replace(/\.dcl$/, '.dcn'); const outputFile = path.join(outputPath, outputFilename); // Ensure directory exists fs.mkdirSync(path.dirname(outputFile), { recursive: true }); // Pretty print JSON const jsonContent = JSON.parse(content); fs.writeFileSync(outputFile, JSON.stringify(jsonContent, null, 2)); } if (logLevel !== 'silent') { console.info(`Compiled ${Object.keys(result.dcnOutputs).length} DCL files to ${outputPath}`); } return 0; } const COMPILE_CONFIG = { tenantPackages: false, transitiveErrors: true }; /** * Compile DCL files to DCN using WASM */ async function compileDcl(args) { const opts = resolveCompileOptions(args); if (opts === null) { return 255; } try { // Initialize WASM compiler const compiler = new DclCompiler(); await compiler.init(); // Compile directory const result = await compiler.compileDirectory(opts.dclPath, { config: COMPILE_CONFIG, extensions: ['.dcl'], forceEncoding: opts.forceEncoding }); return handleCompileResult(result, opts); } catch (error) { if (opts.logLevel === 'error') { console.error(`Compilation error: ${error.message}`); } return 255; } } /** * Synchronous counterpart to compileDcl. Compiles DCL files to DCN without * any await, suitable for callers that need a fully synchronous API. */ function compileDclSync(args) { const opts = resolveCompileOptions(args); if (opts === null) { return 255; } try { // Initialize WASM compiler synchronously const compiler = new DclCompiler(); compiler.initSync(); // Compile directory synchronously const result = compiler.compileDirectorySync(opts.dclPath, { config: COMPILE_CONFIG, extensions: ['.dcl'], forceEncoding: opts.forceEncoding }); return handleCompileResult(result, opts); } catch (error) { if (opts.logLevel === 'error') { console.error(`Compilation error: ${error.message}`); } return 255; } } /** * Get the path to the WASM compiler */ function getCompilerPath() { return path.join(__dirname, 'lib', 'wasm', 'compiler.wasm'); } module.exports = { compileDcl, compileDclSync, getCompilerPath };