UNPKG

@mdfriday/foundry

Version:

The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.

453 lines 19.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MockHttpClient = exports.FetchHttpClient = exports.NodeHttpClient = void 0; exports.newHttpClient = newHttpClient; exports.newMockHttpClient = newMockHttpClient; const type_1 = require("../type"); const path = __importStar(require("path")); const http = __importStar(require("http")); const https = __importStar(require("https")); const log_1 = require("../../../../pkg/log"); // Create domain-specific logger for http operations const log = (0, log_1.getDomainLogger)('module', { component: 'httpclient' }); /** * Node.js HTTP client implementation using http/https modules * Alternative to fetch API for better compatibility */ class NodeHttpClient { constructor(fs, timeout = 30000, headers = {}) { this.fs = fs; this.timeout = timeout; this.headers = headers; this.defaultTimeout = 30000; // 30 seconds this.defaultHeaders = { 'User-Agent': 'MDFriday-CLI/1.0.0', }; } /** * Download file from URL to target path using Node.js http/https */ async download(downloadUrl, targetPath, options) { return new Promise((resolve, reject) => { try { // Use new URL constructor instead of deprecated url.parse const parsedUrl = new URL(downloadUrl); const isHttps = parsedUrl.protocol === 'https:'; const httpModule = isHttps ? https : http; const requestHeaders = { ...this.defaultHeaders, ...this.headers, ...options?.headers, }; const requestOptions = { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: requestHeaders, timeout: options?.timeout || this.timeout, }; const request = httpModule.request(requestOptions, async (response) => { if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { reject(new type_1.ModuleError(`HTTP ${response.statusCode}: ${response.statusMessage}`, 'HTTP_ERROR')); return; } const total = parseInt(response.headers['content-length'] || '0', 10); let loaded = 0; let lastProgressTime = Date.now(); let lastLoggedPercentage = -1; // Track last logged percentage to avoid duplicate logs try { // Ensure target directory exists const targetDir = path.dirname(targetPath); await this.fs.mkdirAll(targetDir, 0o755); // Create target file const file = await this.fs.create(targetPath); const chunks = []; response.on('data', (chunk) => { chunks.push(chunk); loaded += chunk.length; // Report progress if (options?.onProgress && total > 0) { const now = Date.now(); const percentage = Math.round((loaded / total) * 100); // More aggressive progress reporting for better visibility // Use 100ms throttle, and also report on significant percentage changes const timePassed = now - lastProgressTime >= 100; const significantChange = percentage - lastLoggedPercentage >= 5; // Report every 5% change if ((timePassed && percentage !== lastLoggedPercentage) || significantChange) { const progress = { loaded, total, percentage: percentage }; try { options.onProgress(progress); lastLoggedPercentage = percentage; } catch (progressError) { log.error(`Progress callback error: ${progressError}`); } lastProgressTime = now; } } }); response.on('end', async () => { try { // Write all chunks to file const buffer = Buffer.concat(chunks); const uint8Array = new Uint8Array(buffer); await file.write(uint8Array); await file.sync(); await file.close(); // Final progress callback for 100% if (options?.onProgress && total > 0) { const progress = { loaded: total, total, percentage: 100 }; try { options.onProgress(progress); } catch (progressError) { log.warn(`Progress callback error: ${progressError}`); } } resolve(); } catch (error) { const message = error instanceof Error ? error.message : String(error); reject(new type_1.ModuleError(`File write failed: ${message}`, 'WRITE_FAILED')); } }); response.on('error', (error) => { log.error(`Response error: ${error.message}`); reject(new type_1.ModuleError(`Download failed: ${error.message}`, 'DOWNLOAD_FAILED')); }); } catch (error) { const message = error instanceof Error ? error.message : String(error); reject(new type_1.ModuleError(`File system error: ${message}`, 'FS_ERROR')); } }); request.on('error', (error) => { log.error(`Request error:`, error); reject(new type_1.ModuleError(`Request failed: ${error.message}`, 'REQUEST_FAILED')); }); request.on('timeout', () => { request.destroy(); log.error(`Request timeout for ${downloadUrl}`); reject(new type_1.ModuleError('Request timeout', 'TIMEOUT')); }); request.end(); } catch (error) { const message = error instanceof Error ? error.message : String(error); reject(new type_1.ModuleError(`Download failed: ${message}`, 'DOWNLOAD_FAILED')); } }); } /** * Perform GET request using Node.js http/https */ async get(requestUrl, options) { return new Promise((resolve, reject) => { try { // Use new URL constructor instead of deprecated url.parse const parsedUrl = new URL(requestUrl); const isHttps = parsedUrl.protocol === 'https:'; const httpModule = isHttps ? https : http; const requestHeaders = { ...this.defaultHeaders, ...this.headers, ...options?.headers, }; const requestOptions = { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: requestHeaders, timeout: options?.timeout || this.timeout, }; const request = httpModule.request(requestOptions, (response) => { const chunks = []; const responseHeaders = {}; // Convert headers Object.entries(response.headers).forEach(([key, value]) => { responseHeaders[key] = Array.isArray(value) ? value.join(', ') : (value || ''); }); response.on('data', (chunk) => { chunks.push(chunk); }); response.on('end', () => { const buffer = Buffer.concat(chunks); const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); resolve({ data: arrayBuffer, headers: responseHeaders, status: response.statusCode || 0, }); }); response.on('error', (error) => { reject(new type_1.ModuleError(`Response error: ${error.message}`, 'RESPONSE_ERROR')); }); }); request.on('error', (error) => { reject(new type_1.ModuleError(`GET request failed: ${error.message}`, 'REQUEST_FAILED')); }); request.on('timeout', () => { request.destroy(); reject(new type_1.ModuleError('GET request timeout', 'TIMEOUT')); }); request.end(); } catch (error) { const message = error instanceof Error ? error.message : String(error); reject(new type_1.ModuleError(`GET request failed: ${message}`, 'REQUEST_FAILED')); } }); } } exports.NodeHttpClient = NodeHttpClient; /** * Default HTTP client implementation using fetch API * TypeScript replacement for Go's module download mechanism */ class FetchHttpClient { constructor(fs, timeout = 30000, headers = {}) { this.fs = fs; this.timeout = timeout; this.headers = headers; this.defaultTimeout = 30000; // 30 seconds this.defaultHeaders = { 'User-Agent': 'AuPro-CLI/1.0.0', }; } /** * Download file from URL to target path */ async download(url, targetPath, options) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, options?.timeout || this.timeout); const headers = { ...this.defaultHeaders, ...this.headers, ...options?.headers, }; const response = await fetch(url, { headers, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new type_1.ModuleError(`HTTP ${response.status}: ${response.statusText}`, 'HTTP_ERROR'); } const total = parseInt(response.headers.get('content-length') || '0', 10); const reader = response.body?.getReader(); if (!reader) { throw new type_1.ModuleError('Unable to read response body', 'RESPONSE_ERROR'); } // Ensure target directory exists const targetDir = path.dirname(targetPath); await this.fs.mkdirAll(targetDir, 0o755); // Create target file const file = await this.fs.create(targetPath); try { let loaded = 0; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; // Report progress if (options?.onProgress && total > 0) { const percentage = Math.round((loaded / total) * 100); const progress = { loaded, total, percentage: percentage }; try { options.onProgress(progress); } catch (progressError) { log.warn(`Progress callback error: ${progressError}`); } } } // Write all chunks to file const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const buffer = new Uint8Array(totalSize); let offset = 0; for (const chunk of chunks) { buffer.set(chunk, offset); offset += chunk.length; } await file.write(buffer); await file.sync(); } finally { await file.close(); } } catch (error) { if (error instanceof type_1.ModuleError) { throw error; } const message = error instanceof Error ? error.message : String(error); throw new type_1.ModuleError(`Download failed: ${message}`, 'DOWNLOAD_FAILED'); } } /** * Perform GET request */ async get(url, options) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, options?.timeout || this.timeout); const headers = { ...this.defaultHeaders, ...this.headers, ...options?.headers, }; const response = await fetch(url, { headers, signal: controller.signal, }); clearTimeout(timeoutId); const data = await response.arrayBuffer(); const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); return { data, headers: responseHeaders, status: response.status, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new type_1.ModuleError(`GET request failed: ${message}`, 'REQUEST_FAILED'); } } /** * Set default headers */ setHeaders(headers) { this.headers = { ...this.headers, ...headers }; } /** * Set timeout */ setTimeout(timeout) { this.timeout = timeout; } } exports.FetchHttpClient = FetchHttpClient; /** * Creates a new HTTP client instance */ function newHttpClient(fs, timeout, headers) { // Use NodeHttpClient instead of FetchHttpClient for better compatibility return new NodeHttpClient(fs, timeout, headers); } /** * Mock HTTP client for testing */ class MockHttpClient { constructor() { this.mockResponses = new Map(); } setMockResponse(url, data, status = 200, headers = {}) { this.mockResponses.set(url, { data, headers, status }); } async download(url, targetPath, options) { const response = this.mockResponses.get(url); if (!response) { // Simulate progress with default data if (options?.onProgress) { const total = 1000; for (let loaded = 0; loaded <= total; loaded += Math.ceil(total / 10)) { options.onProgress({ loaded: Math.min(loaded, total), total, percentage: Math.round((Math.min(loaded, total) / total) * 100), }); // Small delay to simulate real download await new Promise(resolve => setTimeout(resolve, 10)); } } return; } // Simulate progress if (options?.onProgress) { const total = response.data.byteLength; for (let loaded = 0; loaded <= total; loaded += Math.ceil(total / 10)) { options.onProgress({ loaded: Math.min(loaded, total), total, percentage: Math.round((Math.min(loaded, total) / total) * 100), }); // Small delay to simulate real download await new Promise(resolve => setTimeout(resolve, 10)); } } } async get(url, options) { const response = this.mockResponses.get(url); if (!response) { throw new type_1.ModuleError(`Mock response not found for ${url}`, 'MOCK_NOT_FOUND'); } return response; } } exports.MockHttpClient = MockHttpClient; /** * Creates a mock HTTP client for testing */ function newMockHttpClient() { return new MockHttpClient(); } //# sourceMappingURL=httpclient.js.map