UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

1,394 lines (1,381 loc) โ€ข 304 kB
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>