@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
JavaScript
;
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