@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
1,394 lines (1,381 loc) โข 304 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_logger = require("./logger-BY80-2wN.js");
let fs_extra = require("fs-extra");
fs_extra = require_logger.__toESM(fs_extra);
let path = require("path");
path = require_logger.__toESM(path);
let crypto = require("crypto");
crypto = require_logger.__toESM(crypto);
let child_process = require("child_process");
let os = require("os");
os = require_logger.__toESM(os);
let http = require("http");
http = require_logger.__toESM(http);
//#region src/template-playground/handlebars-helpers.ts
function registerTemplatePlaygroundHandlebarsHelpers(Handlebars, context) {
Handlebars.registerHelper("t", function() {
console.log("T HELPER CALLED");
const key = arguments[0];
return {
components: "Components",
modules: "Modules",
interfaces: "Interfaces",
classes: "Classes",
injectables: "Injectables",
pipes: "Pipes",
directives: "Directives",
guards: "Guards",
interceptors: "Interceptors",
entities: "Entities",
controllers: "Controllers",
info: "Info",
readme: "Readme",
source: "Source",
template: "Template",
styles: "Styles",
"dom-tree": "DOM Tree",
file: "File",
description: "Description",
implements: "Implements",
metadata: "Metadata",
index: "Index",
methods: "Methods",
properties: "Properties"
}[key] || key;
});
Handlebars.registerHelper("relativeURL", (depth, ...args) => {
const depthValue = typeof depth === "number" ? depth : context.depth || 0;
return "../".repeat(depthValue) + args.slice(0, -1).join("/");
});
Handlebars.registerHelper("compare", function() {
const context = this;
const a = arguments[0];
const operator = arguments[1];
const b = arguments[2];
const options = arguments[3];
if (arguments.length < 4) throw new Error("handlebars Helper {{compare}} expects 4 arguments");
let result = false;
switch (operator) {
case "indexof":
result = b.indexOf(a) !== -1;
break;
case "===":
result = a === b;
break;
case "!==":
result = a !== b;
break;
case ">":
result = a > b;
break;
case "<":
result = a < b;
break;
case ">=":
result = a >= b;
break;
case "<=":
result = a <= b;
break;
case "==":
result = a == b;
break;
case "!=":
result = a != b;
break;
default: throw new Error("helper {{compare}}: invalid operator: `" + operator + "`");
}
if (result === false) return options.inverse(context);
return options.fn(context);
});
Handlebars.registerHelper("isTabEnabled", function() {
const context = this;
const navTabs = arguments[0];
const tabId = arguments[1];
const options = arguments[2];
if (navTabs && navTabs.some((tab) => tab.id === tabId)) return options.fn(context);
else return options.inverse(context);
});
Handlebars.registerHelper("isInitialTab", function() {
const navTabs = arguments[0];
const tabId = arguments[1];
if (navTabs && navTabs.length > 0 && navTabs[0].id === tabId) return "active in";
return "";
});
Handlebars.registerHelper("orLength", function(...args) {
const options = args.pop();
if (args.some((arg) => arg && (Array.isArray(arg) ? arg.length > 0 : arg))) return options.fn(this);
else return options.inverse(this);
});
Handlebars.registerHelper("breakComma", function(array) {
if (Array.isArray(array)) return array.join(", ");
return array;
});
Handlebars.registerHelper("parseDescription", function(description, depth) {
return new Handlebars.SafeString(description || "");
});
Handlebars.registerHelper("escapeSimpleQuote", function(text) {
if (typeof text === "string") return text.replace(/'/g, "\\'");
return text;
});
Handlebars.registerHelper("jsdoc-code-example", function(jsdoctags, options) {
return options.fn({ tags: jsdoctags || [] });
});
Handlebars.registerHelper("link-type", function(type, options) {
if (type && type.href) return new Handlebars.SafeString(`<a href="${type.href}" target="${type.target || "_self"}">${type.raw || type}</a>`);
return type;
});
Handlebars.registerHelper("each", Handlebars.helpers.each);
Handlebars.registerHelper("if", Handlebars.helpers.if);
Handlebars.registerHelper("unless", Handlebars.helpers.unless);
Handlebars.registerHelper("with", Handlebars.helpers.with);
Handlebars.registerPartial("component-detail", `
{{#if component.description}}
<p class="comment">
<h3>{{t "description"}}</h3>
</p>
<p class="comment">
{{{parseDescription component.description depth}}}
</p>
{{/if}}
{{#if component.implements}}
<p class="comment">
<h3>{{t "implements"}}</h3>
</p>
<p class="comment">
{{#each component.implements}}
<code>{{this}}</code>{{#unless @last}}, {{/unless}}
{{/each}}
</p>
{{/if}}
<section data-compodoc="block-metadata">
<h3>{{t "metadata"}}</h3>
<table class="table table-sm table-hover metadata">
<tbody>
{{#if component.selector}}
<tr>
<td class="col-md-3">selector</td>
<td class="col-md-9"><code>{{component.selector}}</code></td>
</tr>
{{/if}}
{{#if component.templateUrl}}
<tr>
<td class="col-md-3">templateUrl</td>
<td class="col-md-9"><code>{{component.templateUrl}}</code></td>
</tr>
{{/if}}
{{#if component.styleUrls}}
<tr>
<td class="col-md-3">styleUrls</td>
<td class="col-md-9"><code>{{breakComma component.styleUrls}}</code></td>
</tr>
{{/if}}
</tbody>
</table>
</section>
{{#orLength component.properties component.methods component.inputs component.outputs}}
<section data-compodoc="block-index">
<h3 id="index">{{t "index"}}</h3>
<table class="table table-sm table-bordered index-table">
<tbody>
{{#if component.methods}}
<tr>
<td class="col-md-4">
<h6><b>{{t "methods"}}</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
{{#each component.methods}}
<li><a href="#{{name}}">{{name}}</a></li>
{{/each}}
</ul>
</td>
</tr>
{{/if}}
{{#if component.properties}}
<tr>
<td class="col-md-4">
<h6><b>{{t "properties"}}</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
{{#each component.properties}}
<li><a href="#{{name}}">{{name}}</a></li>
{{/each}}
</ul>
</td>
</tr>
{{/if}}
</tbody>
</table>
</section>
{{/orLength}}
{{#if component.methods}}
<section data-compodoc="block-methods">
<h3 id="methods">{{t "methods"}}</h3>
{{#each component.methods}}
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="{{name}}"></a>
<span class="name">
<span><b>{{name}}</b></span>
<a href="#{{name}}"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<code>{{name}}({{#each args}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}})</code>
</td>
</tr>
{{#if description}}
<tr>
<td class="col-md-4">
<div class="io-description">{{description}}</div>
<div class="io-description">
<b>Returns : </b><code>{{type}}</code>
</div>
</td>
</tr>
{{/if}}
</tbody>
</table>
{{/each}}
</section>
{{/if}}
`);
Handlebars.registerPartial("index", "<!-- Index partial placeholder -->");
Handlebars.registerPartial("component-api", "<!-- Component API partial placeholder -->");
Handlebars.registerPartial("interface-api", "<!-- Interface API partial placeholder -->");
Handlebars.registerPartial("block-relationships", "<!-- Relationships block partial placeholder -->");
Handlebars.registerPartial("link-type", "<code>{{type}}</code>");
Handlebars.registerPartial("reference-badge", "<span class=\"reference-badge\" aria-hidden=\"true\">{{letter}}</span>");
Handlebars.registerPartial("file-path", `
{{#unless disableFilePath}}
{{#if filePath}}
<div class="io-file io-file-path">
<span class="icon ion-ios-document" aria-hidden="true"></span>
<code>{{filePath}}</code>
</div>
{{/if}}
{{/unless}}
`);
}
//#endregion
//#region src/template-playground/session-template-mock-data.ts
function createSessionTemplateMockData(templateName) {
let additionalContext = {};
let templateVariables;
if (templateName.includes("component")) {
templateVariables = {
name: "UserProfileComponent",
description: "A comprehensive user profile management component that handles user information display and editing capabilities.",
file: "src/app/components/user-profile/user-profile.component.ts",
selector: "app-user-profile",
templateUrl: "./user-profile.component.html",
styleUrls: ["./user-profile.component.scss", "./user-profile.theme.scss"],
encapsulation: "ViewEncapsulation.Emulated",
changeDetection: "ChangeDetectionStrategy.OnPush",
type: "component",
sourceCode: "export class UserProfileComponent implements OnInit, OnDestroy { ... }",
rawFile: "user-profile.component.ts",
templateData: "<div class=\"user-profile\">\\n <h2>{{user.name}}</h2>\\n <p>{{user.email}}</p>\\n</div>",
styleUrlsData: [".user-profile { padding: 20px; }\\n.user-profile h2 { color: #333; }"],
stylesData: [":host { display: block; margin: 10px; }"],
inputs: [
{
name: "user",
type: "User",
description: "The user object containing profile information",
decorators: ["@Input()"],
optional: false,
defaultValue: null
},
{
name: "editable",
type: "boolean",
description: "Whether the profile can be edited",
decorators: ["@Input()"],
optional: true,
defaultValue: "false"
},
{
name: "showAvatar",
type: "boolean",
description: "Controls avatar visibility",
decorators: ["@Input()"],
optional: true,
defaultValue: "true"
}
],
outputs: [{
name: "userUpdated",
type: "EventEmitter<User>",
description: "Emitted when user profile is updated",
decorators: ["@Output()"]
}, {
name: "avatarClicked",
type: "EventEmitter<MouseEvent>",
description: "Emitted when user clicks on avatar",
decorators: ["@Output()"]
}],
methods: [
{
name: "ngOnInit",
type: "void",
description: "Angular lifecycle hook for component initialization",
args: [],
returnType: "void",
modifierKind: "public"
},
{
name: "updateProfile",
type: "Promise<void>",
description: "Updates the user profile with new information",
args: [{
name: "userData",
type: "Partial<User>"
}],
returnType: "Promise<void>",
modifierKind: "public"
},
{
name: "validateForm",
type: "boolean",
description: "Validates the profile form data",
args: [],
returnType: "boolean",
modifierKind: "private"
}
],
properties: [{
name: "isLoading",
type: "boolean",
description: "Indicates if component is in loading state",
defaultValue: "false",
modifierKind: "public"
}, {
name: "form",
type: "FormGroup",
description: "Reactive form for user profile editing",
modifierKind: "public"
}],
hostListeners: [{
name: "click",
args: ["$event"],
description: "Handles click events on the component"
}],
hostBindings: [{
name: "class.active",
value: "isActive"
}],
implements: [
"OnInit",
"OnDestroy",
"AfterViewInit"
],
constructorObj: {
name: "constructor",
description: "Component constructor with dependency injection",
args: [
{
name: "userService",
type: "UserService"
},
{
name: "router",
type: "Router"
},
{
name: "cd",
type: "ChangeDetectorRef"
}
]
},
providers: ["UserService"],
viewProviders: [],
queries: [],
exportAs: "userProfile",
jsdoctags: [{
tagName: { text: "example" },
comment: "<app-user-profile [user]=\"currentUser\" [editable]=\"true\"></app-user-profile>"
}],
coveragePercent: 85,
coverageCount: "17/20",
status: "good"
};
additionalContext = {
depth: 1,
breadcrumbs: [{
name: "Components",
url: "../components.html"
}, {
name: "UserProfileComponent",
url: "#"
}]
};
} else if (templateName.includes("service") || templateName.includes("injectable")) templateVariables = {
name: "UserService",
description: "Service responsible for managing user data and authentication operations",
file: "src/app/services/user.service.ts",
type: "injectable",
providedIn: "root",
decorators: ["@Injectable()"],
methods: [
{
name: "getUser",
type: "Observable<User>",
description: "Retrieves user data by ID",
args: [{
name: "id",
type: "string"
}],
returnType: "Observable<User>",
modifierKind: "public"
},
{
name: "updateUser",
type: "Observable<User>",
description: "Updates user information",
args: [{
name: "id",
type: "string"
}, {
name: "userData",
type: "Partial<User>"
}],
returnType: "Observable<User>",
modifierKind: "public"
},
{
name: "deleteUser",
type: "Observable<void>",
description: "Deletes a user account",
args: [{
name: "id",
type: "string"
}],
returnType: "Observable<void>",
modifierKind: "public"
}
],
properties: [{
name: "currentUser$",
type: "BehaviorSubject<User | null>",
description: "Observable stream of current user state",
modifierKind: "private"
}, {
name: "apiUrl",
type: "string",
description: "Base URL for user API endpoints",
defaultValue: "\"/api/users\"",
modifierKind: "private"
}],
constructorObj: {
name: "constructor",
description: "Service constructor with HTTP client injection",
args: [{
name: "http",
type: "HttpClient"
}, {
name: "config",
type: "AppConfig"
}]
},
coveragePercent: 92,
coverageCount: "23/25"
};
else if (templateName.includes("module")) templateVariables = {
name: "UserModule",
description: "Feature module containing user-related components and services",
file: "src/app/modules/user/user.module.ts",
type: "module",
declarations: [
{
name: "UserProfileComponent",
type: "component"
},
{
name: "UserListComponent",
type: "component"
},
{
name: "UserCardDirective",
type: "directive"
}
],
imports: [
{
name: "CommonModule",
type: "module"
},
{
name: "ReactiveFormsModule",
type: "module"
},
{
name: "RouterModule",
type: "module"
}
],
exports: [{
name: "UserProfileComponent",
type: "component"
}, {
name: "UserListComponent",
type: "component"
}],
providers: [{
name: "UserService",
type: "service"
}, {
name: "UserResolver",
type: "resolver"
}],
bootstrap: [],
schemas: []
};
else if (templateName.includes("interface")) templateVariables = {
name: "User",
description: "Interface defining the structure of user objects",
file: "src/app/interfaces/user.interface.ts",
type: "interface",
properties: [
{
name: "id",
type: "string",
description: "Unique identifier for the user",
optional: false
},
{
name: "email",
type: "string",
description: "User email address",
optional: false
},
{
name: "name",
type: "string",
description: "Full name of the user",
optional: false
},
{
name: "avatar",
type: "string",
description: "URL to user avatar image",
optional: true
},
{
name: "role",
type: "UserRole",
description: "User role permissions",
optional: true
}
],
methods: [],
indexSignatures: []
};
else templateVariables = {
name: "ExampleItem",
description: "A sample item for demonstration purposes",
file: "src/app/example.ts",
type: "class",
methods: [{
name: "ngOnInit",
type: "void",
description: "Lifecycle hook",
args: [],
returnType: "void"
}],
properties: [{
name: "isActive",
type: "boolean",
description: "Active state",
defaultValue: "false"
}]
};
return {
templateVariables,
additionalContext
};
}
//#endregion
//#region src/template-playground/template-playground-server.ts
const polka = require("polka");
const sirv = require("sirv");
const { json, urlencoded } = require("body-parser");
const { ZipArchive } = require("archiver");
function send(res, statusCode, body) {
res.statusCode = statusCode;
if (body === void 0) {
res.end();
return;
}
if (typeof body === "string" || Buffer.isBuffer(body)) {
res.end(body);
return;
}
if (!res.getHeader("Content-Type")) res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(body));
}
var TemplatePlaygroundServer = class {
constructor(port) {
this.sessions = /* @__PURE__ */ new Map();
this.ipToSessionId = /* @__PURE__ */ new Map();
this.debounceTimers = /* @__PURE__ */ new Map();
this.signalHandlers = /* @__PURE__ */ new Map();
this.port = port || parseInt(process.env.PLAYGROUND_PORT || process.env.PORT || "3001", 10);
this.app = polka();
this.setupPaths();
this.initializeHandlebars();
this.setupMiddleware();
this.setupRoutes();
this.startSessionCleanup();
this.setupSignalHandlers();
}
/**
* Get the underlying HTTP server instance for testing purposes
* @returns HTTP server instance or null if not started
*/
getHttpServer() {
return this.server?.server || null;
}
setupSignalHandlers() {
if (process.env.NODE_ENV === "test") return;
[
"SIGINT",
"SIGTERM",
"SIGUSR2"
].forEach((signal) => {
const handler = async () => {
require_logger.logger.info(`Received ${signal}, shutting down Template Playground server gracefully...`);
try {
await this.stop();
require_logger.logger.info("Server shutdown complete");
process.exit(0);
} catch (error) {
require_logger.logger.error("Error during server shutdown:", error);
process.exit(1);
}
};
this.signalHandlers.set(signal, handler);
process.on(signal, handler);
});
if (process.listenerCount("uncaughtException") === 0) {
const uncaughtHandler = async (error) => {
require_logger.logger.error("Uncaught exception:", error);
try {
await this.stop();
} catch (stopError) {
require_logger.logger.error("Error during emergency shutdown:", stopError);
}
process.exit(1);
};
this.signalHandlers.set("uncaughtException", uncaughtHandler);
process.on("uncaughtException", uncaughtHandler);
}
if (process.listenerCount("unhandledRejection") === 0) {
const rejectionHandler = async (reason, promise) => {
require_logger.logger.error("Unhandled rejection at:", promise, "reason:", reason);
try {
await this.stop();
} catch (stopError) {
require_logger.logger.error("Error during emergency shutdown:", stopError);
}
process.exit(1);
};
this.signalHandlers.set("unhandledRejection", rejectionHandler);
process.on("unhandledRejection", rejectionHandler);
}
}
setupPaths() {
const distributedFakeProjectPath = path.join(__dirname, "resources", "playground-demo");
const devFakeProjectPath = path.join(process.cwd(), "src", "playground-demo");
if (fs_extra.existsSync(distributedFakeProjectPath)) this.fakeProjectPath = distributedFakeProjectPath;
else if (fs_extra.existsSync(devFakeProjectPath)) this.fakeProjectPath = devFakeProjectPath;
else throw new Error("playground-demo directory not found. Please ensure it exists.");
const distributedTemplatesPath = path.join(__dirname, "templates");
const devTemplatesPath = path.join(process.cwd(), "src", "templates");
const legacyTemplatesPath = path.join(process.cwd(), "hbs-templates-copy");
if (fs_extra.existsSync(distributedTemplatesPath)) this.originalTemplatesPath = distributedTemplatesPath;
else if (fs_extra.existsSync(devTemplatesPath)) this.originalTemplatesPath = devTemplatesPath;
else if (fs_extra.existsSync(legacyTemplatesPath)) this.originalTemplatesPath = legacyTemplatesPath;
else throw new Error("Templates directory not found. Please ensure src/templates or dist/templates exists.");
}
getClientIP(req) {
const forwarded = req.headers["x-forwarded-for"];
const realIP = req.headers["x-real-ip"];
const remoteAddr = req.socket?.remoteAddress || "unknown";
let ip = forwarded?.split(",")[0] || realIP || remoteAddr || "unknown";
if (ip === "::1" || ip === "::ffff:127.0.0.1") ip = "127.0.0.1";
return ip;
}
createOrGetSessionByIP(ip) {
const existingSessionId = this.ipToSessionId.get(ip);
if (existingSessionId && this.sessions.has(existingSessionId)) {
const session = this.sessions.get(existingSessionId);
session.lastActivity = Date.now();
require_logger.logger.info(`โป๏ธ Reusing existing session for IP ${ip}: ${existingSessionId}`);
return session;
}
const sessionId = crypto.randomBytes(16).toString("hex");
const templateDir = path.join(os.tmpdir(), `hbs-templates-copy-${sessionId}`);
const documentationDir = path.join(os.tmpdir(), `generated-documentation-${sessionId}`);
if (fs_extra.existsSync(templateDir)) fs_extra.removeSync(templateDir);
if (fs_extra.existsSync(documentationDir)) fs_extra.removeSync(documentationDir);
fs_extra.copySync(this.originalTemplatesPath, templateDir);
fs_extra.ensureDirSync(documentationDir);
const session = {
id: sessionId,
templateDir,
documentationDir,
lastActivity: Date.now(),
config: {
hideGenerator: false,
disableSourceCode: false,
disableGraph: false,
disableCoverage: false,
disablePrivate: false,
disableProtected: false,
disableInternal: false
}
};
this.sessions.set(sessionId, session);
this.ipToSessionId.set(ip, sessionId);
require_logger.logger.info(`๐ Created new session for IP ${ip}: ${sessionId}`);
if (process.env.NODE_ENV !== "test") this.generateDocumentation(sessionId);
return session;
}
createNewSession(ip) {
const sessionId = crypto.randomBytes(16).toString("hex");
const templateDir = path.join(os.tmpdir(), `hbs-templates-copy-${sessionId}`);
const documentationDir = path.join(os.tmpdir(), `generated-documentation-${sessionId}`);
if (fs_extra.existsSync(templateDir)) fs_extra.removeSync(templateDir);
if (fs_extra.existsSync(documentationDir)) fs_extra.removeSync(documentationDir);
fs_extra.copySync(this.originalTemplatesPath, templateDir);
fs_extra.ensureDirSync(documentationDir);
const session = {
id: sessionId,
templateDir,
documentationDir,
lastActivity: Date.now(),
config: {
hideGenerator: false,
disableSourceCode: false,
disableGraph: false,
disableCoverage: false,
disablePrivate: false,
disableProtected: false,
disableInternal: false,
disableFilePath: false,
disableOverview: false
}
};
this.sessions.set(sessionId, session);
require_logger.logger.info(`๐ Created new session for IP ${ip}: ${sessionId}`);
if (process.env.NODE_ENV !== "test") this.generateDocumentation(sessionId);
return session;
}
updateSessionActivity(sessionId) {
const session = this.sessions.get(sessionId);
if (session) session.lastActivity = Date.now();
}
generateDocumentation(sessionId, debounce = false) {
if (debounce) {
const existingTimer = this.debounceTimers.get(sessionId);
if (existingTimer) clearTimeout(existingTimer);
const timer = setTimeout(() => {
this.runCompoDocForSession(sessionId);
this.debounceTimers.delete(sessionId);
}, 300);
this.debounceTimers.set(sessionId, timer);
} else this.runCompoDocForSession(sessionId);
}
async runCompoDocForSession(sessionId) {
if (process.env.NODE_ENV === "test") {
require_logger.logger.info(`[test mode] Skipping compodoc generation for session ${sessionId}`);
return;
}
const session = this.sessions.get(sessionId);
if (!session) {
require_logger.logger.error(`Session ${sessionId} not found`);
return;
}
try {
require_logger.logger.info(`๐ Generating documentation for session ${sessionId}`);
const fakeProjectTsConfigPath = path.join(this.fakeProjectPath, "tsconfig.json");
const findCli = (startDir) => {
let dir = startDir;
for (let i = 0; i < 8; i++) {
const candidate = path.join(dir, "bin", "index-cli.js");
if (fs_extra.existsSync(candidate)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
};
const cliPath = process.env.COMPODOC_CLI_PATH ?? findCli(__dirname) ?? path.resolve(__dirname, "../bin", "index-cli.js");
if (process.env.NODE_ENV === "test" && !fs_extra.existsSync(cliPath)) {
require_logger.logger.warn(`CLI not found in test environment: ${cliPath}. Skipping documentation generation.`);
session.documentationGenerated = true;
return;
}
const cmd = [
`node "${cliPath}"`,
`-p "${fakeProjectTsConfigPath}"`,
`-d "${session.documentationDir}"`,
`--templates "${session.templateDir}"`
];
const config = session.config || {};
const booleanFlags = [
"hideGenerator",
"disableSourceCode",
"disableGraph",
"disableCoverage",
"disablePrivate",
"disableProtected",
"disableInternal",
"disableLifeCycleHooks",
"disableConstructors",
"disableRoutesGraph",
"disableSearch",
"disableDependencies",
"disableProperties",
"disableDomTree",
"disableTemplateTab",
"disableStyleTab",
"disableMainGraph",
"disableFilePath",
"disableOverview",
"hideDarkModeToggle",
"minimal",
"serve",
"open",
"watch",
"silent",
"coverageTest",
"coverageTestThresholdFail",
"coverageTestShowOnlyFailed"
];
const valueFlags = [
"theme",
"language",
"base",
"customFavicon",
"customLogo",
"assetsFolder",
"extTheme",
"includes",
"includesName",
"output",
"port",
"hostname",
"exportFormat",
"coverageTestThreshold",
"coverageMinimumPerFile",
"unitTestCoverage",
"gaID",
"gaSite",
"maxSearchResults",
"toggleMenuItems",
"navTabConfig"
];
for (const flag of booleanFlags) if (config[flag] === true) cmd.push(`--${flag}`);
for (const flag of valueFlags) if (config[flag] !== void 0 && config[flag] !== "") {
let value = config[flag];
if (Array.isArray(value) || typeof value === "object") value = JSON.stringify(value);
cmd.push(`--${flag} \"${value}\"`);
}
const fullCmd = cmd.join(" ");
require_logger.logger.info(`๐ Executing CompoDoc command: ${fullCmd}`);
require("fs").appendFileSync("server-commands.log", `${(/* @__PURE__ */ new Date()).toISOString()} - ${fullCmd}\n`);
(0, child_process.execSync)(fullCmd, {
cwd: process.cwd(),
stdio: "inherit"
});
this.updateSessionActivity(sessionId);
require_logger.logger.info(`โ
Documentation generated successfully for session ${sessionId}`);
} catch (error) {
require_logger.logger.error(`โ Error generating documentation for session ${sessionId}:`, error);
}
}
startSessionCleanup() {
this.cleanupInterval = setInterval(() => {
const cutoffTime = Date.now() - 3600 * 1e3;
for (const [sessionId, session] of this.sessions.entries()) if (session.lastActivity < cutoffTime) this.cleanupSession(sessionId);
}, 600 * 1e3);
}
cleanupSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) try {
if (fs_extra.existsSync(session.templateDir)) fs_extra.removeSync(session.templateDir);
if (fs_extra.existsSync(session.documentationDir)) fs_extra.removeSync(session.documentationDir);
const timer = this.debounceTimers.get(sessionId);
if (timer) {
clearTimeout(timer);
this.debounceTimers.delete(sessionId);
}
for (const [ip, id] of this.ipToSessionId.entries()) if (id === sessionId) {
this.ipToSessionId.delete(ip);
break;
}
this.sessions.delete(sessionId);
require_logger.logger.info(`๐งน Cleaned up session: ${sessionId}`);
} catch (error) {
require_logger.logger.error(`Error cleaning up session ${sessionId}:`, error);
}
}
initializeHandlebars() {
this.handlebars = require("handlebars");
this.registerHandlebarsHelpers(this.handlebars, {});
}
async registerAvailablePartials() {
try {
const partialsDir = path.join(process.cwd(), "dist/templates/partials");
require_logger.logger.info(`๐ Looking for partials in: ${partialsDir}`);
require_logger.logger.info(`๐ Partials directory exists: ${fs_extra.existsSync(partialsDir)}`);
if (fs_extra.existsSync(partialsDir)) {
const partialFiles = fs_extra.readdirSync(partialsDir).filter((file) => file.endsWith(".hbs"));
require_logger.logger.info(`๐ Found ${partialFiles.length} partial files: ${JSON.stringify(partialFiles)}`);
for (const file of partialFiles) {
const partialName = file.replace(".hbs", "");
const partialPath = path.join(partialsDir, file);
const partialContent = fs_extra.readFileSync(partialPath, "utf8");
this.handlebars.registerPartial(partialName, partialContent);
require_logger.logger.info(`โ
Registered partial: ${partialName}`);
}
} else require_logger.logger.warn(`โ ๏ธ Partials directory not found at: ${partialsDir}`);
} catch (error) {
require_logger.logger.error(`โ Error registering partials:`, error);
}
}
setupMiddleware() {
this.app.use((req, res, next) => {
const headers = req.headers;
require_logger.logger.info(`๐ REQUEST: ${req.method} ${req.url} - User-Agent: ${headers["user-agent"] || "unknown"}`);
next();
});
this.app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
if (req.method === "OPTIONS") {
res.statusCode = 200;
res.end();
} else next();
});
const compodocResourcesPathDist = path.join(process.cwd(), "dist/resources");
const compodocResourcesPathSrc = path.join(process.cwd(), "src/resources");
const compodocResourcesPath = fs_extra.existsSync(compodocResourcesPathDist) ? compodocResourcesPathDist : compodocResourcesPathSrc;
require_logger.logger.info(`๐ Setting up root-level static files from: ${compodocResourcesPath}`);
require_logger.logger.info(`๐ Compodoc resources path exists: ${fs_extra.existsSync(compodocResourcesPath)}`);
this.app.use("/styles", sirv(path.join(compodocResourcesPath, "styles"), { dev: true }));
this.app.use("/js", sirv(path.join(compodocResourcesPath, "js"), { dev: true }));
this.app.use("/images", sirv(path.join(compodocResourcesPath, "images"), { dev: true }));
this.app.use("/fonts", sirv(path.join(compodocResourcesPath, "fonts"), { dev: true }));
this.app.use("/resources", sirv(compodocResourcesPath, { dev: true }));
const playgroundStaticPathDist = path.join(process.cwd(), "dist/resources/template-playground-app");
const playgroundStaticPathSrc = path.join(process.cwd(), "src/resources/template-playground-app");
const playgroundStaticPath = fs_extra.existsSync(playgroundStaticPathDist) ? playgroundStaticPathDist : playgroundStaticPathSrc;
require_logger.logger.info(`๐ Setting up playground static files from: ${playgroundStaticPath}`);
require_logger.logger.info(`๐ Playground static path exists: ${fs_extra.existsSync(playgroundStaticPath)}`);
this.app.use(sirv(playgroundStaticPath, { dev: true }));
this.app.use(json({ limit: "10mb" }));
this.app.use(urlencoded({
extended: true,
limit: "10mb"
}));
}
setupRoutes() {
this.app.get("/api/templates", this.getTemplates.bind(this));
this.app.get("/api/templates/:templateName", this.getTemplate.bind(this));
this.app.get("/api/example-data/:dataType", this.getExampleData.bind(this));
this.app.post("/api/render", this.renderTemplate.bind(this));
this.app.post("/api/render-page", this.renderCompletePage.bind(this));
this.app.post("/api/generate-docs", this.generateDocs.bind(this));
this.app.post("/api/download-template", this.downloadTemplatePackage.bind(this));
this.app.post("/api/session/:sessionId/download-zip", this.downloadSessionTemplateZip.bind(this));
this.app.post("/api/session/:sessionId/download-all-templates", this.downloadAllSessionTemplates.bind(this));
this.app.get("/api/session/:sessionId/download/all", this.downloadAllSessionTemplates.bind(this));
this.app.post("/api/session", this.createSessionAPI.bind(this));
this.app.post("/api/session/create", this.createSessionAPI.bind(this));
this.app.get("/api/session/:sessionId/templates", this.getSessionTemplates.bind(this));
this.app.get("/api/session/:sessionId/template/*", this.getSessionTemplate.bind(this));
this.app.post("/api/session/:sessionId/template/*", this.saveSessionTemplate.bind(this));
this.app.get("/api/session/:sessionId/template-data/*", this.getSessionTemplateData.bind(this));
this.app.post("/api/session/:sessionId/generate-docs", this.generateSessionDocs.bind(this));
this.app.post("/api/session/:sessionId/generate", this.generateSessionDocs.bind(this));
this.app.get("/api/session/:sessionId/config", this.getSessionConfig.bind(this));
this.app.post("/api/session/:sessionId/config", this.updateSessionConfig.bind(this));
this.app.get("/api/session/:sessionId/docs/*", this.serveSessionDocs.bind(this));
this.app.get("/docs/:sessionId/index.html", (req, res) => {
require_logger.logger.info(`๐ Docs index route hit: /docs/${req.params.sessionId}/index.html`);
const sessionId = req.params.sessionId;
const session = this.sessions.get(sessionId);
if (!session) {
require_logger.logger.error(`โ Session not found: ${sessionId}`);
send(res, 404, {
success: false,
message: "Session not found"
});
return;
}
this.updateSessionActivity(sessionId);
const fullPath = path.join(session.documentationDir, "index.html");
require_logger.logger.info(`๐ Looking for file: ${fullPath}`);
if (fs_extra.existsSync(fullPath)) {
require_logger.logger.info(`โ
Serving file: ${fullPath}`);
const content = fs_extra.readFileSync(fullPath);
res.setHeader("Content-Type", "text/html");
res.end(content);
} else {
require_logger.logger.error(`โ File not found: ${fullPath}`);
res.statusCode = 404;
res.end("Documentation file not found");
}
});
this.app.get("/docs/:sessionId/*", (req, res) => {
const sessionId = req.params.sessionId;
const session = this.sessions.get(sessionId);
if (!session) {
require_logger.logger.error(`โ Session not found: ${sessionId}`);
send(res, 404, {
success: false,
message: "Session not found"
});
return;
}
this.updateSessionActivity(sessionId);
const sessionSirv = sirv(session.documentationDir, {
dev: true,
single: false,
setHeaders: (res, pathname) => {
require_logger.logger.info(`โ
Serving file via sirv: ${pathname}`);
}
});
const originalUrl = req.url;
const sessionPrefix = `/docs/${sessionId}`;
if (originalUrl && originalUrl.startsWith(sessionPrefix)) {
req.url = originalUrl.substring(sessionPrefix.length) || "/";
require_logger.logger.info(`๐ Sirv serving: ${req.url} from ${session.documentationDir}`);
}
sessionSirv(req, res, () => {
req.url = originalUrl;
require_logger.logger.error(`โ File not found in session docs: ${req.url}`);
res.statusCode = 404;
res.end("Documentation file not found");
});
});
this.app.get("/docs/:sessionId", (req, res) => {
require_logger.logger.info(`๐ Docs root route hit: /docs/${req.params.sessionId}`);
const sessionId = req.params.sessionId;
const session = this.sessions.get(sessionId);
if (!session) {
require_logger.logger.error(`โ Session not found: ${sessionId}`);
send(res, 404, {
success: false,
message: "Session not found"
});
return;
}
this.updateSessionActivity(sessionId);
const fullPath = path.join(session.documentationDir, "index.html");
require_logger.logger.info(`๐ Looking for file: ${fullPath}`);
if (fs_extra.existsSync(fullPath)) {
require_logger.logger.info(`โ
Serving file: ${fullPath}`);
const content = fs_extra.readFileSync(fullPath);
res.setHeader("Content-Type", "text/html");
res.end(content);
} else {
require_logger.logger.error(`โ File not found: ${fullPath}`);
res.statusCode = 404;
res.end("Documentation file not found");
}
});
this.app.get("/", (req, res) => {
const indexPathDist = path.join(process.cwd(), "dist/resources/template-playground-app/index.html");
const indexPathSrc = path.join(process.cwd(), "src/resources/template-playground-app/index.html");
const indexPath = fs_extra.existsSync(indexPathDist) ? indexPathDist : indexPathSrc;
if (fs_extra.existsSync(indexPath)) {
const content = fs_extra.readFileSync(indexPath);
res.setHeader("Content-Type", "text/html");
res.end(content);
} else {
res.statusCode = 404;
res.end("Template Playground not built. Please run the build process.");
}
});
this.app.get("*", (req, res) => {
if (req.url.startsWith("/api") || req.url.startsWith("/resources") || req.url.startsWith("/docs")) {
res.statusCode = 404;
res.end("Not Found");
return;
}
require_logger.logger.warn(`โ ๏ธ CATCH-ALL ROUTE HIT: ${req.method} ${req.url}`);
const indexPathDist = path.join(process.cwd(), "dist/resources/template-playground-app/index.html");
const indexPathSrc = path.join(process.cwd(), "src/resources/template-playground-app/index.html");
const indexPath = fs_extra.existsSync(indexPathDist) ? indexPathDist : indexPathSrc;
if (fs_extra.existsSync(indexPath)) {
const content = fs_extra.readFileSync(indexPath);
res.setHeader("Content-Type", "text/html");
res.end(content);
} else {
res.statusCode = 404;
res.end("Template Playground not built. Please run the build process.");
}
});
}
async getTemplates(req, res) {
try {
const templatesDir = path.join(process.cwd(), "dist/templates/partials");
send(res, 200, (await fs_extra.readdir(templatesDir)).filter((file) => file.endsWith(".hbs")).map((file) => ({
name: file.replace(".hbs", ""),
filename: file,
path: path.join(templatesDir, file)
})));
} catch (error) {
require_logger.logger.error("Error reading templates:", error);
send(res, 500, { error: "Failed to read templates" });
}
}
async getTemplate(req, res) {
try {
const templateName = req.params.templateName;
const templatePath = path.join(process.cwd(), "dist/templates/partials", `${templateName}.hbs`);
if (!await fs_extra.pathExists(templatePath)) {
send(res, 404, { error: "Template not found" });
return;
}
send(res, 200, {
name: templateName,
content: await fs_extra.readFile(templatePath, "utf-8"),
path: templatePath
});
} catch (error) {
require_logger.logger.error("Error reading template:", error);
send(res, 500, { error: "Failed to read template" });
}
}
async getExampleData(req, res) {
try {
const dataType = req.params.dataType;
const { EXAMPLE_DATA, TEMPLATE_CONTEXT } = await Promise.resolve().then(() => require("./example-data-B8bAx0B1.js"));
if (!EXAMPLE_DATA[dataType]) {
send(res, 404, { error: "Example data type not found" });
return;
}
send(res, 200, {
data: dataType === "component" || dataType === "directive" || dataType === "pipe" || dataType === "guard" || dataType === "interceptor" || dataType === "injectable" || dataType === "class" || dataType === "interface" || dataType === "entity" ? {
[dataType]: EXAMPLE_DATA[dataType],
...EXAMPLE_DATA[dataType]
} : EXAMPLE_DATA[dataType],
context: TEMPLATE_CONTEXT
});
} catch (error) {
require_logger.logger.error("Error getting example data:", error);
send(res, 500, { error: "Failed to get example data" });
}
}
async renderTemplate(req, res) {
try {
const { templateContent, templateData, templateContext } = req.body;
if (!templateContent) {
send(res, 400, { error: "Template content is required" });
return;
}
send(res, 200, { rendered: this.handlebars.compile(templateContent)(templateData || {}) });
} catch (error) {
require_logger.logger.error("Error rendering template:", error);
send(res, 500, {
error: "Failed to render template",
details: error.message
});
}
}
async renderCompletePage(req, res) {
try {
let { templateContent, templateData, templateContext } = req.body;
if (typeof templateData === "string") try {
templateData = JSON.parse(templateData);
} catch (e) {
templateData = {};
}
if (typeof templateContext === "string") try {
templateContext = JSON.parse(templateContext);
} catch (e) {
templateContext = {};
}
if (!templateContent) {
send(res, 400, { error: "Template content is required" });
return;
}
const completePage = `<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Template Preview - Compodoc</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="/resources/images/favicon.ico">
<link rel="stylesheet" href="/resources/styles/bootstrap.min.css">
<link rel="stylesheet" href="/resources/styles/compodoc.css">
<link rel="stylesheet" href="/resources/styles/prism.css">
<link rel="stylesheet" href="/resources/styles/dark.css">
<link rel="stylesheet" href="/resources/styles/style.css">
</head>
<body>
<script>
// Blocking script to avoid flickering dark mode
var useDark = window.matchMedia('(prefers-color-scheme: dark)');
var darkModeState = useDark.matches;
var darkModeStateLocal = localStorage.getItem('compodoc_darkmode-state');
if (darkModeStateLocal) {
darkModeState = darkModeStateLocal === 'true';
}
if (darkModeState) {
document.body.classList.add('dark');
}
<\/script>
<div class="container-fluid main">
<!-- START CONTENT -->
<div class="content component">
<div class="content-data">
${this.generateCompodocHtml(templateData || {})}
</div>
</div>
<!-- END CONTENT -->
</div>
<script>
var COMPODOC_CURRENT_PAGE_DEPTH = 0;
var COMPODOC_CURRENT_PAGE_CONTEXT = 'component';
var COMPODOC_CURRENT_PAGE_URL = 'component.html';
<\/script>
<script src="/resources/js/libs/bootstrap-native.js"><\/script>
<script src="/resources/js/libs/prism.js"><\/script>
<script src="/resources/js/compodoc.js"><\/script>
<script src="/resources/js/tabs.js"><\/script>
<script src="/resources/js/sourceCode.js"><\/script>
</body>
</html>`;
res.setHeader("Content-Type", "text/html");
res.end(completePage);
} catch (error) {
require_logger.logger.error("Error rendering complete page:", error);
send(res, 500, {
error: "Failed to render complete page",
details: error.message
});
}
}
async generateDocs(req, res) {
try {
const { customTemplateContent, mockData } = req.body;
if (mockData) require_logger.logger.warn("mockData parameter is not directly applicable in this session-based system. It will be ignored.");
const clientIP = this.getClientIP(req);
const session = this.createOrGetSessionByIP(clientIP);
const sessionId = session.id;
if (customTemplateContent && req.body.templatePath) {
const templatePath = path.join(session.templateDir, req.body.templatePath);
await fs_extra.writeFile(templatePath, customTemplateContent, "utf8");
}
this.generateDocumentation(sessionId, true);
send(res, 200, {
success: true,
message: "Documentation generation initiated for a new session",
sessionId
});
} catch (error) {
require_logger.logger.error("Error generating documentation:", error);
send(res, 500, {
error: "Failed to generate documentation",
details: error.message
});
}
}
registerHandlebarsHelpers(Handlebars, context) {
registerTemplatePlaygroundHandlebarsHelpers(Handlebars, context);
}
generateCompodocHtml(data) {
const component = data.component || {};
const navTabs = data.navTabs || [];
const tabsHtml = navTabs.map((tab, index) => {
const activeClass = index === 0 ? "nav-link active" : "nav-link";
const label = {
info: "Info",
api: "API",
readme: "Readme",
source: "Source",
template: "Template",
styles: "Styles",
"dom-tree": "DOM Tree"
}[tab.label] || tab.label;
return ` <li class="nav-item">
<a href="${tab.href}" class="${activeClass}" role="tab" id="${tab.id}-tab" data-bs-toggle="tab" data-link="${tab["data-link"]}">${label}</a>
</li>`;
}).join("\n");
let tabContentHtml = "";
if (navTabs.some((tab) => tab.id === "info")) {
const activeClass = navTabs[0].id === "info" ? "active in" : "";
tabContentHtml += ` <div class="tab-pane fade ${activeClass}" id="info">
<p class="comment">
<h3>File</h3>
</p>
<p class="comment">
<code>${component.file || ""}</code>
</p>
${component.description ? `
<p class="comment">
<h3>Description</h3>
</p>
<p class="comment">
<p>${component.description.replace(/\n/g, "</p>\n<p>")}</p>
</p>
` : ""}
${component.implements && component.implements.length > 0 ? `
<p class="comment">
<h3>Implements</h3>
</p>
<p class="comment">
${component.implements.map((impl) => `<code>${impl}</code>`).join(", ")}
</p>
` : ""}
<section data-compodoc="block-metadata">
<h3>Metadata</h3>
<table class="table table-sm table-hover metadata">
<tbody>
${component.selector ? `
<tr>
<td class="col-md-3">selector</td>
<td class="col-md-9"><code>${component.selector}</code></td>
</tr>` : ""}
${component.templateUrl ? `
<tr>
<td class="col-md-3">templateUrl</td>
<td class="col-md-9"><code>${component.templateUrl}</code></td>
</tr>` : ""}
${component.styleUrls && component.styleUrls.length > 0 ? `
<tr>
<td class="col-md-3">styleUrls</td>
<td class="col-md-9"><code>${component.styleUrls.join(", ")}</code></td>
</tr>` : ""}
</tbody>
</table>
</section>
${component.methods && component.methods.length > 0 ? `
<section data-compodoc="block-index">
<h3 id="index">Index</h3>
<table class="table table-sm table-bordered index-table">
<tbody>
<tr>
<td class="col-md-4">
<h6><b>Methods</b></h6>
</td>
</tr>
<tr>