import * as fs from 'node:fs'; import * as path from 'node:path'; import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks, CompilerFileSystemAsyncCallbacks, CompilerErrorSeverity} from '@keymanapp/developer-utils'; import { fileURLToPath } from 'url'; const { TEST_SAVE_FIXTURES } = process.env; /** * A CompilerCallbacks implementation for testing */ export class TestCompilerCallbacks implements CompilerCallbacks { /* TestCompilerCallbacks */ messages: CompilerEvent[] = []; readonly _net: TestCompilerNetAsyncCallbacks; readonly _fsAsync: DefaultCompilerFileSystemAsyncCallbacks = new DefaultCompilerFileSystemAsyncCallbacks(this); constructor(basePath?: string) { if(basePath) { this._net = new TestCompilerNetAsyncCallbacks(basePath); } } clear() { this.messages = []; } filteredMessages(severity: CompilerErrorSeverity = CompilerErrorSeverity.Info) { return this.messages.filter(m => CompilerError.severity(m.code) >= severity); } printMessages() { if(this.messages.length) { process.stdout.write(CompilerError.formatEvent(this.messages)); } } hasMessage(code: number): boolean { return this.messages.find((item) => item.code == code) === undefined ? false : true; } fileURLToPath(url: string | URL): string { return fileURLToPath(url); } /** true of at least one error */ hasError(): boolean { return CompilerError.hasError(this.messages); } /* CompilerCallbacks */ loadFile(filename: string): Uint8Array { try { return fs.readFileSync(filename); } catch(e) { if (e.code === 'ENOENT') { return null; } else { throw e; } } } fileSize(filename: string): number { return fs.statSync(filename)?.size; } isDirectory(filename: string): boolean { return fs.statSync(filename)?.isDirectory(); } get path(): CompilerPathCallbacks { return { ...path, isAbsolute: path.win32.isAbsolute }; } get fs(): CompilerFileSystemCallbacks { return fs; } get net(): CompilerNetAsyncCallbacks { return this._net; } get fsAsync(): CompilerFileSystemAsyncCallbacks { return this._fsAsync; } resolveFilename(baseFilename: string, filename: string): string { const basePath = baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? baseFilename : path.dirname(baseFilename); // Transform separators to platform separators -- we are agnostic // in our use here but path prefers files may use // either / or \, although older kps files were always \. if(path.sep == '/') { filename = filename.replace(/\\/g, '/'); } else { filename = filename.replace(/\//g, '\\'); } if(!path.isAbsolute(filename)) { filename = path.resolve(basePath, filename); } return filename; } reportMessage(event: CompilerEvent): void { // console.log(event.message); this.messages.push(event); } debug(msg: string) { console.debug(msg); } }; class TestCompilerNetAsyncCallbacks implements CompilerNetAsyncCallbacks { constructor(private basePath: string) { } urlToPath(url: string): string { const p = new URL(url); return path.join(this.basePath, p.hostname, p.pathname.replaceAll(/[^a-z0-9_.!@#$%() -]/ig, '#')); } async fetchBlob(url: string): Promise { const p = this.urlToPath(url); if(TEST_SAVE_FIXTURES) { // When TEST_SAVE_FIXTURES env variable is set, we will do the actual // fetch from the origin so that we can build the fixtures easily console.log(`Downloading file ${url} --> ${p}`); let response: Response; try { response = await fetch(url); } catch(e) { console.error(`failed to download ${url}`); console.error(e); throw e; // yes, we want to abort the download } fs.mkdirSync(path.dirname(p), {recursive: true}); if(!response.ok) { // We won't save a file, just delete any existing file if(fs.existsSync(p)) { fs.rmSync(p); } } else { const data = new Uint8Array(await response.arrayBuffer()); fs.writeFileSync(p, data); } } if(!fs.existsSync(p)) { // missing file, this is okay return null; } const data: Uint8Array = fs.readFileSync(p); return data; } async fetchJSON(url: string): Promise { const data = await this.fetchBlob(url); return data ? JSON.parse(new TextDecoder().decode(data)) : null; } }