diff --git a/packages/sdks/astro/package.json b/packages/sdks/astro/package.json index 4aec62d0..957b20dd 100644 --- a/packages/sdks/astro/package.json +++ b/packages/sdks/astro/package.json @@ -3,7 +3,8 @@ "version": "1.0.6-local", "config": { "transformPackageJson": false, - "transformEnvs": true + "transformEnvs": true, + "docPath": "apps/public/content/docs/(tracking)/sdks/astro.mdx" }, "exports": { ".": "./index.ts" diff --git a/packages/sdks/express/package.json b/packages/sdks/express/package.json index e3ed3c74..af915f9b 100644 --- a/packages/sdks/express/package.json +++ b/packages/sdks/express/package.json @@ -2,6 +2,9 @@ "name": "@openpanel/express", "version": "1.0.4-local", "module": "index.ts", + "config": { + "docPath": "apps/public/content/docs/(tracking)/sdks/express.mdx" + }, "scripts": { "build": "rm -rf dist && tsup", "typecheck": "tsc --noEmit" diff --git a/packages/sdks/nextjs/package.json b/packages/sdks/nextjs/package.json index 93aa0591..85825517 100644 --- a/packages/sdks/nextjs/package.json +++ b/packages/sdks/nextjs/package.json @@ -2,6 +2,9 @@ "name": "@openpanel/nextjs", "version": "1.1.2-local", "module": "index.ts", + "config": { + "docPath": "apps/public/content/docs/(tracking)/sdks/nextjs.mdx" + }, "scripts": { "build": "rm -rf dist && tsup", "typecheck": "tsc --noEmit" diff --git a/packages/sdks/nuxt/package.json b/packages/sdks/nuxt/package.json index 5a314d2e..ff5ab634 100644 --- a/packages/sdks/nuxt/package.json +++ b/packages/sdks/nuxt/package.json @@ -12,7 +12,8 @@ "files": ["dist"], "config": { "transformPackageJson": false, - "transformEnvs": false + "transformEnvs": false, + "docPath": "apps/public/content/docs/(tracking)/sdks/nuxt.mdx" }, "scripts": { "build": "npx nuxt-module-build build", diff --git a/packages/sdks/react-native/package.json b/packages/sdks/react-native/package.json index 20797b9a..d383de97 100644 --- a/packages/sdks/react-native/package.json +++ b/packages/sdks/react-native/package.json @@ -2,6 +2,9 @@ "name": "@openpanel/react-native", "version": "1.0.4-local", "module": "index.ts", + "config": { + "docPath": "apps/public/content/docs/(tracking)/sdks/react-native.mdx" + }, "scripts": { "build": "rm -rf dist && tsup", "typecheck": "tsc --noEmit" diff --git a/packages/sdks/sdk/package.json b/packages/sdks/sdk/package.json index cd813c0a..9d91eae2 100644 --- a/packages/sdks/sdk/package.json +++ b/packages/sdks/sdk/package.json @@ -2,6 +2,9 @@ "name": "@openpanel/sdk", "version": "1.0.3-local", "module": "index.ts", + "config": { + "docPath": "apps/public/content/docs/(tracking)/sdks/javascript.mdx" + }, "scripts": { "build": "rm -rf dist && tsup", "typecheck": "tsc --noEmit" diff --git a/packages/sdks/web/package.json b/packages/sdks/web/package.json index 805f2c17..aaca88df 100644 --- a/packages/sdks/web/package.json +++ b/packages/sdks/web/package.json @@ -2,6 +2,9 @@ "name": "@openpanel/web", "version": "1.0.6-local", "module": "index.ts", + "config": { + "docPath": "apps/public/content/docs/(tracking)/sdks/web.mdx" + }, "scripts": { "build": "rm -rf dist && tsup", "typecheck": "tsc --noEmit" diff --git a/tooling/publish/generate-readme.ts b/tooling/publish/generate-readme.ts new file mode 100644 index 00000000..4d2332a6 --- /dev/null +++ b/tooling/publish/generate-readme.ts @@ -0,0 +1,234 @@ +import fs from 'node:fs'; +import { join, resolve } from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +import type { PackageInfo } from './publish'; + +const workspacePath = (relativePath: string) => + resolve(__dirname, '../../', relativePath); + +const dedentContent = (text: string): string => { + const lines = text.split('\n'); + if (lines.length === 0) return text; + + // Find the minimum indentation (excluding empty lines) + // We'll dedent code blocks too, so include them in the calculation + let minIndent = Number.POSITIVE_INFINITY; + for (const line of lines) { + const trimmed = line.trim(); + // Skip empty lines + if (trimmed.length === 0) continue; + const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0; + if (indent < minIndent) minIndent = indent; + } + + // If no indentation found, return as-is + if (minIndent === Number.POSITIVE_INFINITY || minIndent === 0) return text; + + // Remove the common indentation from all lines + return lines + .map((line) => { + // For lines shorter than minIndent, just return them as-is (preserves empty lines) + if (line.length < minIndent) return line; + // Remove the common indentation + const dedented = line.slice(minIndent); + // If the line was all whitespace, return empty string to preserve the line + return line.trim().length === 0 ? '' : dedented; + }) + .join('\n'); +}; + +const transformMdxToReadme = ( + mdxContent: string, + packageName: string, +): string => { + let content = mdxContent; + + // Load MDX component content files + const commonSdkConfigPath = workspacePath( + 'apps/public/src/components/common-sdk-config.mdx', + ); + const webSdkConfigPath = workspacePath( + 'apps/public/src/components/web-sdk-config.mdx', + ); + + let commonSdkConfigContent = ''; + let webSdkConfigContent = ''; + + try { + if (fs.existsSync(commonSdkConfigPath)) { + commonSdkConfigContent = fs.readFileSync(commonSdkConfigPath, 'utf-8'); + } + } catch { + // Ignore if file doesn't exist + } + + try { + if (fs.existsSync(webSdkConfigPath)) { + webSdkConfigContent = fs.readFileSync(webSdkConfigPath, 'utf-8'); + } + } catch { + // Ignore if file doesn't exist + } + + // Extract title from frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/); + let title = packageName; + let description = ''; + + if (frontmatterMatch?.[1]) { + const frontmatter = frontmatterMatch[1]; + const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); + const descMatch = frontmatter.match(/^description:\s*(.+)$/m); + if (titleMatch?.[1]) title = titleMatch[1].trim(); + if (descMatch?.[1]) description = descMatch[1].trim(); + + // Remove frontmatter + content = content.replace(/^---\n[\s\S]*?\n---\n/, ''); + } + + // Replace MDX component references with their actual content + // This must happen before code block protection so component content is also protected + if (commonSdkConfigContent) { + content = content.replace( + //g, + `\n${commonSdkConfigContent}\n`, + ); + } + if (webSdkConfigContent) { + content = content.replace( + //g, + `\n${webSdkConfigContent}\n`, + ); + } + + // Protect code blocks from transformation + // Extract code blocks before any transformations to preserve their content + const codeBlockPlaceholders: string[] = []; + // Match code blocks: ```language (optional) followed by content until closing ``` + // Using [\s\S] to match across newlines, non-greedy to stop at first closing ``` + const codeBlockRegex = /```[\s\S]*?```/g; + + // Extract and replace code blocks with placeholders + content = content.replace(codeBlockRegex, (match) => { + const placeholder = `__CODE_BLOCK_${codeBlockPlaceholders.length}__`; + codeBlockPlaceholders.push(match); + return placeholder; + }); + + // Remove import statements (outside code blocks) + content = content.replace(/^import\s+.*$/gm, ''); + + // Handle Tabs component specially - convert to markdown sections + // Extract tabs items from items prop + const tabsItemsMatch = content.match(//); + const tabsItems = tabsItemsMatch?.[1] + ? tabsItemsMatch[1] + .replace(/['"]/g, '') + .split(',') + .map((item) => item.trim()) + : []; + + // Replace Tabs/Tab structure with markdown sections + if (tabsItems.length > 0) { + // Match each Tab and convert to a markdown section + content = content.replace( + /([\s\S]*?)<\/Tab>/g, + (match, value, tabContent) => { + const dedented = dedentContent(tabContent).trim(); + return `\n#### ${value}\n\n${dedented}\n\n`; + }, + ); + // Remove the Tabs wrapper + content = content.replace(/]*>([\s\S]*?)<\/Tabs>/g, '$1'); + } else { + // Fallback: if no items prop, just convert tabs to sections + content = content.replace( + /([\s\S]*?)<\/Tab>/g, + (match, value, tabContent) => { + const dedented = dedentContent(tabContent).trim(); + return `\n#### ${value}\n\n${dedented}\n\n`; + }, + ); + content = content.replace(/]*>([\s\S]*?)<\/Tabs>/g, '$1'); + } + + // Remove self-closing JSX components (like , ) + content = content.replace(/<[A-Z][a-zA-Z]*[^>]*\/>/g, ''); + + // Remove JSX component tags but preserve content between opening/closing tags + // Handle nested components by recursively removing outer tags + // This regex matches opening tag, captures content (including nested tags), and closing tag + let previousContent = ''; + while (content !== previousContent) { + previousContent = content; + // Match JSX components with their content - handles one level of nesting + content = content.replace( + /<([A-Z][a-zA-Z]*)[^>]*>([\s\S]*?)<\/\1>/g, + (match, tagName, innerContent) => { + return dedentContent(innerContent).trim(); + }, + ); + } + + // Remove any remaining JSX tags (self-closing or unmatched) + content = content.replace(/<\/?[A-Z][a-zA-Z]*[^>]*>/g, ''); + + // Restore code blocks + codeBlockPlaceholders.forEach((codeBlock, index) => { + content = content.replace(`__CODE_BLOCK_${index}__`, codeBlock); + }); + + // Convert internal links (starting with /) to absolute URLs + content = content.replace( + /\[([^\]]+)\]\((\/[^\)]+)\)/g, + '[$1](https://openpanel.dev$2)', + ); + + // Clean up extra blank lines + content = content.replace(/\n{3,}/g, '\n\n').trim(); + + // Build the README header + const docUrl = `https://openpanel.dev/docs/sdks/${packageName.replace('@openpanel/', '')}`; + let readme = `# ${title}\n\n`; + + if (description) { + readme += `${description}\n\n`; + } + + readme += `> ๐Ÿ“– **Full documentation:** [${docUrl}](${docUrl})\n\n`; + readme += '---\n\n'; + readme += content; + + return readme; +}; + +export const generateReadme = ( + packages: Record, + dependents: string[], +): string[] => { + const generatedReadmes: string[] = []; + for (const dep of dependents) { + const pkg = packages[dep]; + const docPath = pkg?.config?.docPath; + if (!docPath) { + console.log( + `๐Ÿ“ Skipping README generation for ${dep} (no docPath configured)`, + ); + continue; + } + + const packagePath = workspacePath(pkg.localPath); + const readmePath = join(packagePath, 'README.md'); + console.log(`๐Ÿ“ Generating README for ${dep}`); + const mdxContent = fs.readFileSync(workspacePath(docPath), 'utf-8'); + const readmeContent = transformMdxToReadme(mdxContent, pkg.name); + fs.writeFileSync(readmePath, readmeContent, 'utf-8'); + generatedReadmes.push(readmePath); + } + return generatedReadmes; +}; diff --git a/tooling/publish/publish.ts b/tooling/publish/publish.ts index a2b79b44..3d50c801 100644 --- a/tooling/publish/publish.ts +++ b/tooling/publish/publish.ts @@ -3,12 +3,13 @@ import fs from 'node:fs'; import { join, resolve } from 'node:path'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); import arg from 'arg'; import type { ReleaseType } from 'semver'; import semver, { RELEASE_TYPES } from 'semver'; +import { generateReadme } from './generate-readme'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); // Types interface PackageJson { @@ -20,11 +21,12 @@ interface PackageJson { [key: string]: unknown; config?: { transformPackageJson?: boolean; - transformEnvs: boolean; + transformEnvs?: boolean; + docPath?: string; }; } -interface PackageInfo extends PackageJson { +export interface PackageInfo extends PackageJson { nextVersion: string; localPath: string; } @@ -146,7 +148,7 @@ const updatePackageJsonForRelease = ( main: './dist/index.js', module: './dist/index.js', types: './dist/index.d.ts', - files: ['dist'], + files: ['dist', 'README.md'], exports: restPkgJson.exports ?? { '.': { import: './dist/index.js', @@ -266,6 +268,7 @@ const publishPackages = ( const restoreAndUpdateLocal = ( packages: Record, dependents: string[], + generatedReadmes: string[], ): void => { const filesToRestore = dependents .map((dep) => join(workspacePath(packages[dep]!.localPath), 'package.json')) @@ -273,6 +276,14 @@ const restoreAndUpdateLocal = ( execSync(`git checkout ${filesToRestore}`); + // Clean up auto-generated README files + for (const readmePath of generatedReadmes) { + if (fs.existsSync(readmePath)) { + console.log(`๐Ÿงน Removing auto-generated README: ${readmePath}`); + fs.unlinkSync(readmePath); + } + } + for (const dep of dependents) { const { nextVersion, localPath, ...restPkgJson } = packages[dep]!; console.log(`๐Ÿš€ Updating ${dep} (${nextVersion}-local)`); @@ -356,6 +367,8 @@ function main() { buildPackages(packages, dependents); + const generatedReadmes = generateReadme(packages, dependents); + if (args['--publish']) { const config: PublishConfig = { registry: args['--npm'] @@ -365,7 +378,7 @@ function main() { }; publishPackages(packages, dependents, config); - restoreAndUpdateLocal(originalPackages, dependents); + restoreAndUpdateLocal(originalPackages, dependents, generatedReadmes); } console.log('โœ… All done!');