190 lines
5.9 KiB
JavaScript
190 lines
5.9 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.mkdocsConfigService = void 0;
|
|
const promises_1 = require("fs/promises");
|
|
const http_1 = require("http");
|
|
const env_1 = require("../../config/env");
|
|
const logger_1 = require("../../utils/logger");
|
|
const yaml_1 = require("yaml");
|
|
const DOCKER_SOCKET = '/var/run/docker.sock';
|
|
/**
|
|
* Custom YAML tag schema to preserve !!python/name: and !!python/object: tags.
|
|
* Without this, the yaml library would reject these custom tags.
|
|
*/
|
|
const pythonNameTag = {
|
|
identify: () => false,
|
|
tag: '!python/name',
|
|
collection: undefined,
|
|
resolve: (str) => str,
|
|
};
|
|
const pythonObjectTag = {
|
|
identify: () => false,
|
|
tag: '!python/object',
|
|
collection: undefined,
|
|
resolve: (str) => str,
|
|
};
|
|
/**
|
|
* Parse mkdocs.yml content with support for !!python/name: tags.
|
|
* Returns a yaml Document that preserves comments and formatting.
|
|
*/
|
|
function parseConfig(content) {
|
|
return (0, yaml_1.parseDocument)(content, {
|
|
customTags: (tags) => [
|
|
...tags,
|
|
pythonNameTag,
|
|
pythonObjectTag,
|
|
],
|
|
keepSourceTokens: true,
|
|
});
|
|
}
|
|
/**
|
|
* Validate that a string is valid YAML (with python tags support).
|
|
* Returns null if valid, error message if invalid.
|
|
*/
|
|
function validateYaml(content) {
|
|
try {
|
|
const doc = parseConfig(content);
|
|
const errors = doc.errors;
|
|
if (errors.length > 0) {
|
|
return errors.map(e => e.message).join('; ');
|
|
}
|
|
return null;
|
|
}
|
|
catch (err) {
|
|
return err.message;
|
|
}
|
|
}
|
|
async function readConfig() {
|
|
return (0, promises_1.readFile)(env_1.env.MKDOCS_CONFIG_PATH, 'utf-8');
|
|
}
|
|
async function writeConfig(content) {
|
|
// Validate YAML first
|
|
const error = validateYaml(content);
|
|
if (error) {
|
|
throw new Error(`Invalid YAML: ${error}`);
|
|
}
|
|
// Create backup
|
|
try {
|
|
await (0, promises_1.copyFile)(env_1.env.MKDOCS_CONFIG_PATH, `${env_1.env.MKDOCS_CONFIG_PATH}.bak`);
|
|
}
|
|
catch {
|
|
logger_1.logger.warn('Could not create backup of mkdocs.yml');
|
|
}
|
|
await (0, promises_1.writeFile)(env_1.env.MKDOCS_CONFIG_PATH, content, 'utf-8');
|
|
}
|
|
/**
|
|
* Make a request to the Docker Engine API over Unix socket.
|
|
*/
|
|
function dockerRequest(method, path, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
socketPath: DOCKER_SOCKET,
|
|
path,
|
|
method,
|
|
headers: body
|
|
? { 'Content-Type': 'application/json' }
|
|
: undefined,
|
|
};
|
|
const req = (0, http_1.request)(options, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
resolve({
|
|
statusCode: res.statusCode || 0,
|
|
body: Buffer.concat(chunks).toString(),
|
|
});
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) {
|
|
req.write(JSON.stringify(body));
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
/**
|
|
* Read raw output from a Docker exec start stream.
|
|
* Docker multiplexes stdout/stderr with 8-byte headers.
|
|
*/
|
|
function demuxDockerStream(raw) {
|
|
const lines = [];
|
|
let offset = 0;
|
|
while (offset < raw.length) {
|
|
if (offset + 8 > raw.length)
|
|
break;
|
|
// byte 0: stream type (1=stdout, 2=stderr)
|
|
const size = raw.readUInt32BE(offset + 4);
|
|
offset += 8;
|
|
if (offset + size > raw.length) {
|
|
lines.push(raw.subarray(offset).toString('utf-8'));
|
|
break;
|
|
}
|
|
lines.push(raw.subarray(offset, offset + size).toString('utf-8'));
|
|
offset += size;
|
|
}
|
|
return lines.join('');
|
|
}
|
|
/**
|
|
* Execute `mkdocs build` inside the running MkDocs container via Docker Engine API.
|
|
*/
|
|
async function triggerBuild() {
|
|
const containerName = env_1.env.MKDOCS_CONTAINER_NAME;
|
|
const startTime = Date.now();
|
|
try {
|
|
// 1. Create exec instance
|
|
const execCreate = await dockerRequest('POST', `/containers/${containerName}/exec`, {
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Cmd: ['mkdocs', 'build', '--clean'],
|
|
});
|
|
if (execCreate.statusCode !== 201) {
|
|
throw new Error(`Failed to create exec: ${execCreate.body}`);
|
|
}
|
|
const { Id: execId } = JSON.parse(execCreate.body);
|
|
// 2. Start exec and collect output
|
|
const execOutput = await new Promise((resolve, reject) => {
|
|
const options = {
|
|
socketPath: DOCKER_SOCKET,
|
|
path: `/exec/${execId}/start`,
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
};
|
|
const req = (0, http_1.request)(options, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
});
|
|
req.on('error', reject);
|
|
req.write(JSON.stringify({ Detach: false, Tty: false }));
|
|
req.end();
|
|
});
|
|
const output = demuxDockerStream(execOutput);
|
|
// 3. Check exit code
|
|
const execInspect = await dockerRequest('GET', `/exec/${execId}/json`);
|
|
const inspectData = JSON.parse(execInspect.body);
|
|
const exitCode = inspectData.ExitCode ?? -1;
|
|
const duration = Date.now() - startTime;
|
|
return {
|
|
success: exitCode === 0,
|
|
output: output || '(no output)',
|
|
duration,
|
|
};
|
|
}
|
|
catch (err) {
|
|
const duration = Date.now() - startTime;
|
|
logger_1.logger.error('MkDocs build failed', err);
|
|
return {
|
|
success: false,
|
|
output: `Build error: ${err.message}`,
|
|
duration,
|
|
};
|
|
}
|
|
}
|
|
exports.mkdocsConfigService = {
|
|
readConfig,
|
|
writeConfig,
|
|
validateYaml,
|
|
parseConfig,
|
|
triggerBuild,
|
|
};
|
|
//# sourceMappingURL=mkdocs-config.service.js.map
|