@sap/ams-dev
Version:
NodesJS AMS development environment
332 lines (283 loc) • 8.51 kB
JavaScript
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 };