Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,364 changes: 1,363 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
"private": true,
"description": "PostHog example projects",
"scripts": {
"build:docs": "node scripts/build-examples-mcp-resources.js"
"build:docs": "node scripts/build-examples-mcp-resources.js",
"test:plugins": "vitest run scripts/plugins/tests",
"test:plugins:watch": "vitest scripts/plugins/tests"
},
"devDependencies": {
"archiver": "^7.0.1"
"archiver": "^7.0.1",
"vitest": "^2.0.0"
}
}
55 changes: 49 additions & 6 deletions scripts/build-examples-mcp-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const { composePlugins, ignoreLinePlugin, ignoreFilePlugin, ignoreBlockPlugin } = require('./plugins/index');

/**
* Build configuration
*/
const defaultConfig = {
// Global plugins applied to all examples
plugins: [ignoreFilePlugin, ignoreBlockPlugin, ignoreLinePlugin],
examples: [
{
path: 'basics/next-app-router',
Expand All @@ -23,6 +26,8 @@ const defaultConfig = {
includes: [],
regex: [],
},
// Example-specific plugins (optional)
plugins: [],
},
{
path: 'basics/next-pages-router',
Expand All @@ -33,6 +38,8 @@ const defaultConfig = {
includes: [],
regex: [],
},
// Example-specific plugins (optional)
plugins: [],
},
],
globalSkipPatterns: {
Expand Down Expand Up @@ -100,14 +107,38 @@ function buildMarkdownHeader(frameworkName, repoUrl, relativePath) {

/**
* Convert a file to a markdown code block
* Now supports plugins for content transformation
*
* @param {string} relativePath - The relative path of the file
* @param {string} content - The file content
* @param {string} extension - The file extension
* @param {Array} plugins - Optional array of plugins to transform content
* @returns {string|null} - The markdown representation of the file, or null if content is empty after transformation
*/
function fileToMarkdown(relativePath, content, extension) {
function fileToMarkdown(relativePath, content, extension, plugins = []) {
// Create context object for plugins
const context = {
relativePath,
extension,
};

// Apply plugins to content if provided
const transformedContent = plugins.length > 0
? composePlugins(plugins)(content, context)
: content;

// If content is empty after transformation, return null to skip the file
if (!transformedContent || transformedContent.trim() === '') {
return null;
}

// Build markdown output
let markdown = `## ${relativePath}\n\n`;
if (extension === 'md') {
markdown += content;
markdown += transformedContent;
} else {
markdown += `\`\`\`${extension}\n`;
markdown += content;
markdown += transformedContent;
markdown += '\n```\n\n';
}
markdown += '\n---\n\n';
Expand Down Expand Up @@ -181,7 +212,7 @@ function getAllFiles(dirPath, arrayOfFiles = [], baseDir = dirPath, skipPatterns
/**
* Convert an example project directory to markdown
*/
function convertProjectToMarkdown(absolutePath, frameworkInfo, relativePath, skipPatterns) {
function convertProjectToMarkdown(absolutePath, frameworkInfo, relativePath, skipPatterns, plugins = []) {
const repoUrl = 'https://github.com/PostHog/examples';
let markdown = buildMarkdownHeader(frameworkInfo.displayName, repoUrl, relativePath);

Expand All @@ -203,7 +234,12 @@ function convertProjectToMarkdown(absolutePath, frameworkInfo, relativePath, ski
try {
const content = fs.readFileSync(file.fullPath, 'utf8');
const extension = path.extname(file.fullPath).slice(1) || '';
markdown += fileToMarkdown(file.relativePath, content, extension);
const fileMarkdown = fileToMarkdown(file.relativePath, content, extension, plugins);

// Skip file if plugins returned empty content
if (fileMarkdown !== null) {
markdown += fileMarkdown;
}
} catch (e) {
// Skip files that can't be read as text
console.warn(`Skipping ${file.relativePath}: ${e.message}`);
Expand Down Expand Up @@ -243,12 +279,19 @@ async function build() {
example.skipPatterns
);

// Merge global and example-specific plugins
const plugins = [
...(defaultConfig.plugins || []),
...(example.plugins || [])
];

console.log(`Processing ${example.displayName}...`);
const markdown = convertProjectToMarkdown(
absolutePath,
example,
example.path,
skipPatterns
skipPatterns,
plugins
);

const outputFilename = `${example.id}.md`;
Expand Down
38 changes: 38 additions & 0 deletions scripts/plugins/ignore-block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Plugin to remove blocks of code marked with @ignoreBlock
* Removes all lines between @ignoreBlockStart and @ignoreBlockEnd (inclusive)
* Supports both line comments and block comments
*/
const ignoreBlockPlugin = {
name: 'ignore-block',
transform: (content) => {
const lines = content.split('\n');
const result = [];
let insideIgnoreBlock = false;

for (const line of lines) {
// Check for block start marker (must be at start of comment)
if (line.match(/(?:\/\/|#|\/\*|<!--)\s*@ignoreBlockStart(?:\s|$|\*\/|-->)/)) {
insideIgnoreBlock = true;
continue;
}

// Check for block end marker (must be at start of comment)
if (line.match(/(?:\/\/|#|\/\*|<!--)\s*@ignoreBlockEnd(?:\s|$|\*\/|-->)/)) {
insideIgnoreBlock = false;
continue;
}

// Skip lines inside ignore block
if (insideIgnoreBlock) {
continue;
}

result.push(line);
}

return result.join('\n');
},
};

module.exports = ignoreBlockPlugin;
24 changes: 24 additions & 0 deletions scripts/plugins/ignore-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Plugin to ignore entire files marked with @ignoreFile at the start
* Returns empty string if @ignoreFile is found in the first few lines
* Supports both line comments and block comments
*/
const ignoreFilePlugin = {
name: 'ignore-file',
transform: (content) => {
// Check first 10 lines for @ignoreFile marker
const lines = content.split('\n');
const checkLines = lines.slice(0, 10);

for (const line of checkLines) {
// Must be at start of comment: // @ignoreFile is valid, // text @ignoreFile is not
if (line.match(/(?:\/\/|#|\/\*|<!--)\s*@ignoreFile(?:\s|$|\*\/|-->)/)) {
return ''; // Return empty to skip entire file
}
}

return content;
},
};

module.exports = ignoreFilePlugin;
37 changes: 37 additions & 0 deletions scripts/plugins/ignore-line.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Plugin to remove lines marked with @ignoreLine comment
* Removes both the @ignoreLine comment and the following line
* Supports both line comments and block comments
*/
const ignoreLinePlugin = {
name: 'ignore-line',
transform: (content) => {
const lines = content.split('\n');
const result = [];
let skipNext = false;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];

// Check if this line should be skipped due to previous @ignoreLine
if (skipNext) {
skipNext = false;
continue;
}

// Check if line ends with comment and @ignoreLine directive
// Valid: code // @ignoreLine, code # @ignoreLine
// Invalid: code // text @ignoreLine (must be at start of comment)
if (line.match(/(?:\/\/|#|\/\*|<!--)\s*@ignoreLine(?:\s|$|\*\/|-->)/)) {
skipNext = true;
continue;
}

result.push(line);
}

return result.join('\n');
},
};

module.exports = ignoreLinePlugin;
41 changes: 41 additions & 0 deletions scripts/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Plugin system for content transformation
*/

const ignoreLinePlugin = require('./ignore-line');
const ignoreFilePlugin = require('./ignore-file');
const ignoreBlockPlugin = require('./ignore-block');

/**
* Compose multiple plugins into a single transformation function
* Plugins are applied in order (left to right)
* Short-circuits and returns empty string if content becomes empty
*
* @param {Array} plugins - Array of plugins to compose
* @returns {function(string, Object): string} - Composed transformation function
*/
function composePlugins(plugins = []) {
return (content, context) => {
return plugins.reduce((transformedContent, plugin) => {
// Short-circuit if content is already empty
if (!transformedContent || transformedContent.trim() === '') {
return transformedContent;
}

try {
return plugin.transform(transformedContent, context);
} catch (error) {
console.error(`Error in plugin '${plugin.name}':`, error.message);
// Return content as-is if plugin fails
return transformedContent;
}
}, content);
};
}

module.exports = {
composePlugins,
ignoreLinePlugin,
ignoreFilePlugin,
ignoreBlockPlugin,
};
84 changes: 84 additions & 0 deletions scripts/plugins/tests/ignore-block.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, it, expect } from 'vitest';
import ignoreBlockPlugin from '../ignore-block.js';

describe('ignore-block plugin', () => {
it('removes block with // comments', () => {
const input = `line 1
// @ignoreBlockStart
line 2
line 3
// @ignoreBlockEnd
line 4`;
const expected = `line 1
line 4`;
expect(ignoreBlockPlugin.transform(input)).toBe(expected);
});

it('removes block with # comments', () => {
const input = `line 1
# @ignoreBlockStart
line 2
line 3
# @ignoreBlockEnd
line 4`;
const expected = `line 1
line 4`;
expect(ignoreBlockPlugin.transform(input)).toBe(expected);
});

it('removes block with /* */ comments', () => {
const input = `line 1
/* @ignoreBlockStart */
line 2
line 3
/* @ignoreBlockEnd */
line 4`;
const expected = `line 1
line 4`;
expect(ignoreBlockPlugin.transform(input)).toBe(expected);
});

it('removes block with HTML comments', () => {
const input = `line 1
<!-- @ignoreBlockStart -->
line 2
line 3
<!-- @ignoreBlockEnd -->
line 4`;
const expected = `line 1
line 4`;
expect(ignoreBlockPlugin.transform(input)).toBe(expected);
});

it('handles multiple ignore blocks', () => {
const input = `line 1
// @ignoreBlockStart
line 2
// @ignoreBlockEnd
line 3
# @ignoreBlockStart
line 4
# @ignoreBlockEnd
line 5`;
const expected = `line 1
line 3
line 5`;
expect(ignoreBlockPlugin.transform(input)).toBe(expected);
});

it('does not modify content without ignore blocks', () => {
const input = `line 1
line 2
line 3`;
expect(ignoreBlockPlugin.transform(input)).toBe(input);
});

it('does NOT remove when markers are not at start of comment', () => {
const input = `line 1
// some text @ignoreBlockStart
line 2
// some text @ignoreBlockEnd
line 3`;
expect(ignoreBlockPlugin.transform(input)).toBe(input);
});
});
Loading