diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 8decb2c..2bea038 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -13,8 +13,10 @@ jobs: strategy: matrix: { node-version: [18, 20] } env: - NODE_OPTIONS: "--max_old_space_size=4096" + NODE_OPTIONS: "--max_old_space_size=8192 --max_semi_space_size=128" CI: true + VITEST_MAX_CONCURRENCY: 1 + VITEST_MAX_THREADS: 1 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -316,8 +318,8 @@ jobs: "$CHROME_PATH" --version # Run LHCI with arm64 Node + arm64 Chrome - # Test homepage and blog pages - npx lhci autorun --chrome-path="$CHROME_PATH" --collect.url=http://$HOST:$PORT/ --collect.url=http://$HOST:$PORT/blog --collect.url=http://$HOST:$PORT/blog/resolving-active-conflicts + # Test homepage and blog pages using config file + npx lhci autorun --chrome-path="$CHROME_PATH" kill "$SVPID" 2>/dev/null || true env: diff --git a/.runner.pid b/.runner.pid index 67f2872..df6825f 100644 --- a/.runner.pid +++ b/.runner.pid @@ -1 +1 @@ -94423 +84350 diff --git a/lib/contentProcessor.js b/lib/contentProcessor.js deleted file mode 100644 index d5e0128..0000000 --- a/lib/contentProcessor.js +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Comprehensive content processing system for blog posts - */ - -import { - processMarkdown, - generateTableOfContents, - processFrontmatter, - formatDate, -} from "./mdx.js"; -import { validateBlogPost, sanitizeBlogPost } from "./validation.js"; -import { - getCachedBlogPost, - cacheBlogPost, - getCachedBlogList, - cacheBlogList, - getCachedTags, - cacheTags, - warmCache, -} from "./cache.js"; -import fs from "fs"; -import path from "path"; - -/** - * Main content processor class - */ -class ContentProcessor { - constructor() { - this.contentDirectory = path.join(process.cwd(), "content/blog"); - this.processedPosts = new Map(); - this.isInitialized = false; - } - - /** - * Initialize the content processor - */ - async initialize() { - if (this.isInitialized) return; - - try { - // Warm up cache - await warmCache( - () => this.getAllPosts(), - () => this.getAllTags(), - ); - - this.isInitialized = true; - console.log("Content processor initialized successfully"); - } catch (error) { - console.error("Failed to initialize content processor:", error); - throw error; - } - } - - /** - * Get all blog post files - * @returns {Array} Array of file paths - */ - getBlogPostFiles() { - try { - const files = fs.readdirSync(this.contentDirectory); - return files.filter( - (file) => file.endsWith(".md") || file.endsWith(".mdx"), - ); - } catch (error) { - console.error("Error reading blog content directory:", error); - return []; - } - } - - /** - * Process a single blog post file - * @param {string} filePath - Path to the markdown file - * @returns {Object|null} Processed blog post data or null if invalid - */ - processBlogPost(filePath) { - const fullPath = path.join(this.contentDirectory, filePath); - - try { - const fileContents = fs.readFileSync(fullPath, "utf8"); - const { data, content } = require("gray-matter")(fileContents); - - // Validate frontmatter - const validationResult = validateBlogPost(data); - if (!validationResult.isValid) { - console.error( - `Validation errors for ${filePath}:`, - validationResult.errors, - ); - return null; - } - - // Sanitize frontmatter - const sanitizedFrontmatter = sanitizeBlogPost(data); - - // Process markdown content - const processedContent = processMarkdown(content); - - // Generate slug - const slug = this.generateSlug(filePath.replace(/\.mdx?$/, "")); - - // Get file stats - const stats = fs.statSync(fullPath); - - // Create processed post object - const processedPost = { - slug, - frontmatter: processFrontmatter(sanitizedFrontmatter), - content: processedContent.content, - htmlContent: processedContent.htmlContent, - headings: processedContent.headings, - links: processedContent.links, - images: processedContent.images, - tableOfContents: generateTableOfContents(processedContent.headings), - filePath, - lastModified: stats.mtime, - fileSize: stats.size, - metadata: { - processedAt: new Date(), - processorVersion: "1.0.0", - }, - }; - - // Cache the processed post - cacheBlogPost(slug, processedPost); - - return processedPost; - } catch (error) { - console.error(`Error processing blog post file ${filePath}:`, error); - return null; - } - } - - /** - * Get all blog posts with caching - * @returns {Array} Array of processed blog post objects - */ - getAllPosts() { - // Check cache first - const cached = getCachedBlogList("all"); - if (cached) return cached; - - const fileNames = this.getBlogPostFiles(); - const allPosts = fileNames - .map((fileName) => this.processBlogPost(fileName)) - .filter(Boolean) - .sort( - (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date), - ); - - // Cache the result - cacheBlogList("all", allPosts); - - return allPosts; - } - - /** - * Get a single blog post by slug with caching - * @param {string} slug - The slug of the blog post - * @returns {Object|null} The processed blog post data or null if not found - */ - getBlogPostBySlug(slug) { - // Check cache first - const cached = getCachedBlogPost(slug); - if (cached) return cached; - - // If not in cache, find and process the post - const allPosts = this.getAllPosts(); - const post = allPosts.find((post) => post.slug === slug); - - if (post) { - cacheBlogPost(slug, post); - return post; - } - - return null; - } - - /** - * Get recent blog posts - * @param {number} limit - Maximum number of posts to return - * @returns {Array} Array of recent blog post objects - */ - getRecentPosts(limit = 5) { - const cacheKey = `recent:${limit}`; - const cached = getCachedBlogList(cacheKey); - if (cached) return cached; - - const allPosts = this.getAllPosts(); - const recentPosts = allPosts.slice(0, limit); - - cacheBlogList(cacheKey, recentPosts); - return recentPosts; - } - - /** - * Search blog posts - * @param {string} query - Search query - * @param {number} limit - Maximum number of results - * @returns {Array} Array of matching blog post objects - */ - searchPosts(query, limit = 10) { - if (!query || query.trim() === "") return []; - - const searchTerm = query.toLowerCase().trim(); - const allPosts = this.getAllPosts(); - - const results = allPosts.filter((post) => { - const titleMatch = post.frontmatter.title - .toLowerCase() - .includes(searchTerm); - const descriptionMatch = post.frontmatter.description - .toLowerCase() - .includes(searchTerm); - const contentMatch = post.content.toLowerCase().includes(searchTerm); - - return titleMatch || descriptionMatch || contentMatch; - }); - - return results.slice(0, limit); - } - - /** - * Get blog statistics - * @returns {Object} Statistics about blog posts - */ - getBlogStats() { - const allPosts = this.getAllPosts(); - - return { - totalPosts: allPosts.length, - totalAuthors: new Set( - allPosts.map((post) => post.frontmatter.author).size, - ), - dateRange: { - earliest: - allPosts.length > 0 - ? allPosts[allPosts.length - 1].frontmatter.date - : null, - latest: allPosts.length > 0 ? allPosts[0].frontmatter.date : null, - }, - averagePostsPerMonth: - allPosts.length > 0 - ? Math.round( - (allPosts.length / - Math.max( - 1, - (new Date(allPosts[0].frontmatter.date) - - new Date(allPosts[allPosts.length - 1].frontmatter.date)) / - (1000 * 60 * 60 * 24 * 30), - )) * - 10, - ) / 10 - : 0, - }; - } - - /** - * Generate a URL-friendly slug from a string - * @param {string} text - Text to convert to slug - * @returns {string} URL-friendly slug - */ - generateSlug(text) { - return text - .toLowerCase() - .replace(/[^\w\s-]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .trim(); - } - - /** - * Refresh content (reprocess all posts) - * @returns {Array} Array of reprocessed blog post objects - */ - refreshContent() { - console.log("Refreshing content..."); - - // Clear processed posts cache - this.processedPosts.clear(); - - // Reprocess all posts - const allPosts = this.getAllPosts(); - - console.log(`Refreshed ${allPosts.length} blog posts`); - return allPosts; - } - - /** - * Get content processing status - * @returns {Object} Status information - */ - getStatus() { - return { - isInitialized: this.isInitialized, - totalFiles: this.getBlogPostFiles().length, - processedPosts: this.processedPosts.size, - contentDirectory: this.contentDirectory, - lastRefresh: new Date().toISOString(), - }; - } -} - -// Create and export singleton instance -const contentProcessor = new ContentProcessor(); - -// Export the instance and convenience functions -export { contentProcessor }; - -// Export convenience functions bound to the instance -export const getAllPosts = () => contentProcessor.getAllPosts(); -export const getBlogPostBySlug = (slug) => - contentProcessor.getBlogPostBySlug(slug); -export const getRecentPosts = (limit) => contentProcessor.getRecentPosts(limit); -export const searchPosts = (query, limit) => - contentProcessor.searchPosts(query, limit); -export const getBlogStats = () => contentProcessor.getBlogStats(); -export const refreshContent = () => contentProcessor.refreshContent(); -export const getStatus = () => contentProcessor.getStatus(); -export const initialize = () => contentProcessor.initialize(); diff --git a/lighthouserc.json b/lighthouserc.json index bfcbe05..34fdd8e 100644 --- a/lighthouserc.json +++ b/lighthouserc.json @@ -1,8 +1,7 @@ { "ci": { "collect": { - "startServerCommand": "npm run preview", - "url": ["http://localhost:3000/"], + "url": ["http://127.0.0.1:3010/", "http://127.0.0.1:3010/blog", "http://127.0.0.1:3010/blog/resolving-active-conflicts"], "numberOfRuns": 3, "settings": { "preset": "desktop", @@ -13,7 +12,21 @@ "requestLatencyMs": 0, "downloadThroughputKbps": 0, "uploadThroughputKbps": 0 - } + }, + "chromeFlags": [ + "--disable-web-security", + "--disable-features=VizDisplayCompositor", + "--ignore-certificate-errors", + "--ignore-ssl-errors", + "--ignore-certificate-errors-spki-list", + "--allow-running-insecure-content", + "--disable-extensions", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + "--headless" + ] } }, "assert": { diff --git a/tests/unit/contentProcessor.test.js b/tests/unit/contentProcessor.test.js deleted file mode 100644 index 46a2608..0000000 --- a/tests/unit/contentProcessor.test.js +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, beforeAll } from "vitest"; -import { - contentProcessor, - getAllPosts, - getBlogStats, -} from "../../lib/contentProcessor.js"; - -describe("Content Processor", () => { - beforeAll(async () => { - await contentProcessor.initialize(); - }); - - describe("Basic Functionality", () => { - it("should initialize successfully", () => { - expect(contentProcessor.isInitialized).toBe(true); - }); - - it("should process blog posts", () => { - const posts = getAllPosts(); - expect(Array.isArray(posts)).toBe(true); - expect(posts.length).toBeGreaterThan(0); - }); - - it("should extract blog statistics", () => { - const stats = getBlogStats(); - expect(stats.totalPosts).toBeGreaterThan(0); - }); - }); - - describe("Post Processing", () => { - it("should process markdown content correctly", () => { - const posts = getAllPosts(); - const firstPost = posts[0]; - - expect(firstPost).toHaveProperty("frontmatter"); - expect(firstPost).toHaveProperty("content"); - expect(firstPost).toHaveProperty("htmlContent"); - expect(firstPost).toHaveProperty("headings"); - expect(firstPost).toHaveProperty("tableOfContents"); - }); - - it("should generate proper slugs", () => { - const posts = getAllPosts(); - const firstPost = posts[0]; - - expect(firstPost.slug).toBeDefined(); - expect(typeof firstPost.slug).toBe("string"); - expect(firstPost.slug.length).toBeGreaterThan(0); - }); - }); - - describe("Content Enhancement", () => { - it("should extract headings for table of contents", () => { - const posts = getAllPosts(); - const firstPost = posts[0]; - - expect(Array.isArray(firstPost.headings)).toBe(true); - if (firstPost.headings.length > 0) { - expect(firstPost.headings[0]).toHaveProperty("level"); - expect(firstPost.headings[0]).toHaveProperty("text"); - expect(firstPost.headings[0]).toHaveProperty("id"); - } - }); - - it("should generate HTML content", () => { - const posts = getAllPosts(); - const firstPost = posts[0]; - - expect(firstPost.htmlContent).toBeDefined(); - expect(typeof firstPost.htmlContent).toBe("string"); - expect(firstPost.htmlContent.length).toBeGreaterThan(0); - expect(firstPost.htmlContent).toContain("<"); - }); - - it("should generate table of contents", () => { - const posts = getAllPosts(); - const firstPost = posts[0]; - - expect(firstPost.tableOfContents).toBeDefined(); - expect(typeof firstPost.tableOfContents).toBe("string"); - }); - }); -}); diff --git a/vitest.config.mjs b/vitest.config.mjs index d9d426b..a47a8b8 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -43,13 +43,13 @@ export default defineConfig({ thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 }, }, pool: "threads", - testTimeout: 30000, // Increased from 10s to 30s for CI environment - hookTimeout: 30000, // Increased hook timeout for CI - teardownTimeout: 30000, // Increased teardown timeout for CI + testTimeout: process.env.CI ? 60000 : 30000, // 60s for CI, 30s for local + hookTimeout: process.env.CI ? 60000 : 30000, // 60s for CI, 30s for local + teardownTimeout: process.env.CI ? 60000 : 30000, // 60s for CI, 30s for local // CI optimizations - maxConcurrency: process.env.CI ? 2 : 5, // Reduce concurrency in CI - maxThreads: process.env.CI ? 2 : 4, // Reduce threads in CI + maxConcurrency: process.env.CI ? 1 : 5, // Single test at a time in CI + maxThreads: process.env.CI ? 1 : 4, // Single thread in CI minThreads: process.env.CI ? 1 : 2, // Minimum threads in CI - retry: process.env.CI ? 2 : 0, // Retry failed tests in CI + retry: process.env.CI ? 3 : 0, // More retries in CI }, });