mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
🔨 chore: Add CI to Check console.log (#10333)
* lint: Clean breakpoints * build: Add CI to check * build: Add `next` branch * build: Remove markdown files * fix: CI hang out * fix: Show warning on GitHub * feat: Send comment * fix: CI error * fix: show file list
This commit is contained in:
parent
4c7ebd5b39
commit
c0542e80a3
7 changed files with 280 additions and 41 deletions
14
.console-log-whitelist.json
Normal file
14
.console-log-whitelist.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"files": ["drizzle.config.ts"],
|
||||||
|
"patterns": [
|
||||||
|
"scripts/**",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/examples/**",
|
||||||
|
"e2e/**",
|
||||||
|
".github/scripts/**",
|
||||||
|
"apps/desktop/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
117
.github/workflows/check-console-log.yml
vendored
Normal file
117
.github/workflows/check-console-log.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
name: Check Console Log (Warning)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- next
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-console-log:
|
||||||
|
name: Check for console.log statements (non-blocking)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Install bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Run console.log check
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
OUTPUT=$(bunx tsx scripts/checkConsoleLog.mts 2>&1)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
|
||||||
|
# Save output to file for later use
|
||||||
|
echo "$OUTPUT" > /tmp/console-log-output.txt
|
||||||
|
|
||||||
|
# Check if violations were found
|
||||||
|
if echo "$OUTPUT" | grep -q "Total violations:"; then
|
||||||
|
echo "has_violations=true" >> $GITHUB_OUTPUT
|
||||||
|
TOTAL=$(echo "$OUTPUT" | grep -oP "Total violations: \K\d+")
|
||||||
|
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_violations=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Comment on PR
|
||||||
|
if: steps.check.outputs.has_violations == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
VIOLATION_COUNT: ${{ steps.check.outputs.total }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const output = fs.readFileSync('/tmp/console-log-output.txt', 'utf8');
|
||||||
|
const total = process.env.VIOLATION_COUNT || '0';
|
||||||
|
|
||||||
|
// Parse violations from output (format: " file:line" followed by " content")
|
||||||
|
const lines = output.split('\n');
|
||||||
|
const violations = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
// Match lines like " packages/database/src/client/db.ts:258"
|
||||||
|
const fileMatch = line.match(/^\s{2}(\S+:\d+)\s*$/);
|
||||||
|
if (fileMatch) {
|
||||||
|
const file = fileMatch[1];
|
||||||
|
const content = lines[i + 1]?.trim() || '';
|
||||||
|
violations.push({ file, content });
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build comment body
|
||||||
|
const maxDisplay = 30;
|
||||||
|
let body = `## ⚠️ Console.log Check Warning\n\n`;
|
||||||
|
body += `Found **${total}** \`console.log\` statement(s) in this PR.\n\n`;
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
body += `<details>\n<summary>📋 Click to see violations (${Math.min(violations.length, maxDisplay)} of ${total} shown)</summary>\n\n`;
|
||||||
|
body += `| File | Code |\n|------|------|\n`;
|
||||||
|
violations.slice(0, maxDisplay).forEach(v => {
|
||||||
|
const escapedContent = v.content
|
||||||
|
.substring(0, 60)
|
||||||
|
.replace(/\|/g, '\\|')
|
||||||
|
.replace(/`/g, "'");
|
||||||
|
body += `| \`${v.file}\` | \`${escapedContent}${v.content.length > 60 ? '...' : ''}\` |\n`;
|
||||||
|
});
|
||||||
|
if (parseInt(total) > maxDisplay) {
|
||||||
|
body += `\n*...and ${parseInt(total) - maxDisplay} more violations*\n`;
|
||||||
|
}
|
||||||
|
body += `\n</details>\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
body += `> 💡 **Tip:** Remove \`console.log\` or add files to \`.console-log-whitelist.json\`\n`;
|
||||||
|
body += `> ✅ This check is **non-blocking** and won't prevent merging.`;
|
||||||
|
|
||||||
|
// Find existing comment to update instead of creating duplicates
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
});
|
||||||
|
|
||||||
|
const botComment = comments.find(c =>
|
||||||
|
c.user.type === 'Bot' && c.body.includes('Console.log Check Warning')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (botComment) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: botComment.id,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
"lint:circular": "npm run lint:circular:main && npm run lint:circular:packages",
|
"lint:circular": "npm run lint:circular:main && npm run lint:circular:packages",
|
||||||
"lint:circular:main": "dpdm src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
|
"lint:circular:main": "dpdm src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
|
||||||
"lint:circular:packages": "dpdm packages/**/src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
|
"lint:circular:packages": "dpdm packages/**/src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
|
||||||
|
"lint:console": "tsx scripts/checkConsoleLog.mts",
|
||||||
"lint:md": "remark . --silent --output",
|
"lint:md": "remark . --silent --output",
|
||||||
"lint:mdx": "npm run workflow:mdx && remark \"docs/**/*.mdx\" -r ./.remarkrc.mdx.js --silent --output && eslint \"docs/**/*.mdx\" --quiet --fix",
|
"lint:mdx": "npm run workflow:mdx && remark \"docs/**/*.mdx\" -r ./.remarkrc.mdx.js --silent --output && eslint \"docs/**/*.mdx\" --quiet --fix",
|
||||||
"lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
|
"lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
|
||||||
|
|
|
||||||
148
scripts/checkConsoleLog.mts
Normal file
148
scripts/checkConsoleLog.mts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
interface WhitelistConfig {
|
||||||
|
files?: string[];
|
||||||
|
patterns?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const WHITELIST_PATH = '.console-log-whitelist.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load whitelist configuration
|
||||||
|
*/
|
||||||
|
const loadWhitelist = (): WhitelistConfig => {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(WHITELIST_PATH, 'utf8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
return { files: [], patterns: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is whitelisted
|
||||||
|
*/
|
||||||
|
const isWhitelisted = (filePath: string, whitelist: WhitelistConfig): boolean => {
|
||||||
|
const normalizedPath = filePath.replaceAll('\\', '/');
|
||||||
|
|
||||||
|
// Check exact file matches
|
||||||
|
if (whitelist.files?.some((f) => normalizedPath.includes(f.replaceAll('\\', '/')))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pattern matches (simple glob-like patterns)
|
||||||
|
if (whitelist.patterns) {
|
||||||
|
for (const pattern of whitelist.patterns) {
|
||||||
|
// Escape dots and replace glob patterns
|
||||||
|
// Use a placeholder for ** to avoid conflicts with single *
|
||||||
|
let regexPattern = pattern
|
||||||
|
.replaceAll('.', '\\.')
|
||||||
|
.replaceAll('**', '\u0000DOUBLESTAR\u0000')
|
||||||
|
.replaceAll('*', '[^/]*')
|
||||||
|
.replaceAll('\u0000DOUBLESTAR\u0000', '.*');
|
||||||
|
|
||||||
|
// If pattern ends with /**, match everything under that directory
|
||||||
|
// If pattern ends with **, just match everything from that point
|
||||||
|
const regex = new RegExp(`^${regexPattern}`);
|
||||||
|
if (regex.test(normalizedPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main check function
|
||||||
|
*/
|
||||||
|
const checkConsoleLogs = () => {
|
||||||
|
const whitelist = loadWhitelist();
|
||||||
|
|
||||||
|
console.log('🔍 Checking for console.log statements...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Search for console.log in TypeScript and JavaScript files
|
||||||
|
const output = execSync(
|
||||||
|
`git grep -n "console\\.log" -- "*.ts" "*.tsx" "*.js" "*.jsx" "*.mts" "*.cts" || true`,
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!output.trim()) {
|
||||||
|
console.log('✅ No console.log statements found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = output.trim().split('\n');
|
||||||
|
const violations: Array<{ content: string, file: string; line: string; }> = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Parse git grep output: filename:lineNumber:content
|
||||||
|
const match = line.match(/^([^:]+):(\d+):(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const [, filePath, lineNumber, content] = match;
|
||||||
|
|
||||||
|
// Skip if whitelisted
|
||||||
|
if (isWhitelisted(filePath, whitelist)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip comments
|
||||||
|
const trimmedContent = content.trim();
|
||||||
|
if (trimmedContent.startsWith('//') || trimmedContent.startsWith('*')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.push({
|
||||||
|
content: content.trim(),
|
||||||
|
file: filePath,
|
||||||
|
line: lineNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length === 0) {
|
||||||
|
console.log('✅ No console.log violations found (all matches are whitelisted or in comments)!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report violations as warnings
|
||||||
|
console.log('⚠️ Found console.log statements in the following files:\n');
|
||||||
|
|
||||||
|
// Use GitHub Actions annotation format for better visibility in CI
|
||||||
|
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
||||||
|
|
||||||
|
for (const violation of violations) {
|
||||||
|
if (isCI) {
|
||||||
|
// GitHub Actions warning annotation format
|
||||||
|
console.log(`::warning file=${violation.file},line=${violation.line}::console.log found: ${violation.content}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${violation.file}:${violation.line}`);
|
||||||
|
console.log(` ${violation.content}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n💡 Total violations: ${violations.length}`);
|
||||||
|
console.log(
|
||||||
|
`\n📝 To whitelist files, add them to ${WHITELIST_PATH} in the following format:`,
|
||||||
|
);
|
||||||
|
console.log(`{
|
||||||
|
"files": ["path/to/file.ts"],
|
||||||
|
"patterns": ["scripts/**/*.mts", "**/*.test.ts"]
|
||||||
|
}\n`);
|
||||||
|
|
||||||
|
// Exit with 0 to not block CI, but warnings will still be visible
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error && 'status' in error && error.status !== 0) {
|
||||||
|
console.error('❌ Error running git grep:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkConsoleLogs();
|
||||||
|
|
@ -143,40 +143,20 @@ const NoteEditorModal = memo<NoteEditorModalProps>(
|
||||||
const handleOpenChange = (isOpen: boolean) => {
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
// When modal opens, load document content if in edit mode
|
// When modal opens, load document content if in edit mode
|
||||||
if (isOpen && documentId && editor) {
|
if (isOpen && documentId && editor) {
|
||||||
console.log('[NoteEditorModal] Loading content:', {
|
|
||||||
cachedEditorDataPreview: cachedEditorData
|
|
||||||
? JSON.stringify(cachedEditorData).slice(0, 100)
|
|
||||||
: null,
|
|
||||||
cachedEditorDataType: typeof cachedEditorData,
|
|
||||||
documentId,
|
|
||||||
documentTitle,
|
|
||||||
hasCachedEditorData: !!cachedEditorData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If editorData is already cached (from list), use it directly
|
// If editorData is already cached (from list), use it directly
|
||||||
if (cachedEditorData) {
|
if (cachedEditorData) {
|
||||||
console.log('[NoteEditorModal] Using cached editorData', cachedEditorData);
|
|
||||||
setNoteTitle(documentTitle || '');
|
setNoteTitle(documentTitle || '');
|
||||||
editor.setDocument('json', JSON.stringify(cachedEditorData));
|
editor.setDocument('json', JSON.stringify(cachedEditorData));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, fetch full content from API
|
// Otherwise, fetch full content from API
|
||||||
console.log('[NoteEditorModal] Fetching from API');
|
|
||||||
documentService
|
documentService
|
||||||
.getDocumentById(documentId)
|
.getDocumentById(documentId)
|
||||||
.then((doc) => {
|
.then((doc) => {
|
||||||
if (doc && doc.content) {
|
if (doc && doc.content) {
|
||||||
setNoteTitle(doc.title || doc.filename || '');
|
setNoteTitle(doc.title || doc.filename || '');
|
||||||
|
|
||||||
console.log('[NoteEditorModal] Fetched doc.editorData:', {
|
|
||||||
editorDataPreview: doc.editorData
|
|
||||||
? JSON.stringify(doc.editorData).slice(0, 100)
|
|
||||||
: null,
|
|
||||||
editorDataType: typeof doc.editorData,
|
|
||||||
hasEditorData: !!doc.editorData,
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.setDocument('json', doc.editorData);
|
editor.setDocument('json', doc.editorData);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -177,19 +177,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
||||||
const isMarkdown = isMarkdownFile(name, fileType);
|
const isMarkdown = isMarkdownFile(name, fileType);
|
||||||
const isNote = isCustomNote(fileType);
|
const isNote = isCustomNote(fileType);
|
||||||
|
|
||||||
// Debug: Log editorData for notes
|
|
||||||
useEffect(() => {
|
|
||||||
if (isNote) {
|
|
||||||
console.log('[MasonryFileItem] Note item:', {
|
|
||||||
editorDataPreview: editorData ? JSON.stringify(editorData).slice(0, 100) : null,
|
|
||||||
editorDataType: typeof editorData,
|
|
||||||
hasEditorData: !!editorData,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isNote, id, name, editorData]);
|
|
||||||
|
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
const [isInView, setIsInView] = useState(false);
|
const [isInView, setIsInView] = useState(false);
|
||||||
|
|
||||||
|
|
@ -288,12 +275,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isNote) {
|
if (isNote) {
|
||||||
console.log('[MasonryFileItem] Opening note modal with:', {
|
|
||||||
editorDataType: typeof editorData,
|
|
||||||
hasEditorData: !!editorData,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
setIsNoteModalOpen(true);
|
setIsNoteModalOpen(true);
|
||||||
} else {
|
} else {
|
||||||
onOpen(id);
|
onOpen(id);
|
||||||
|
|
@ -384,7 +365,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
||||||
editorData={editorData}
|
editorData={editorData}
|
||||||
knowledgeBaseId={knowledgeBaseId}
|
knowledgeBaseId={knowledgeBaseId}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
console.log('[MasonryFileItem] Closing note modal');
|
|
||||||
setIsNoteModalOpen(false);
|
setIsNoteModalOpen(false);
|
||||||
}}
|
}}
|
||||||
open={isNoteModalOpen}
|
open={isNoteModalOpen}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ const AddButton = ({ knowledgeBaseId }: { knowledgeBaseId?: string }) => {
|
||||||
|
|
||||||
const handleCreateFolder = () => {
|
const handleCreateFolder = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
console.log('create folder');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = useMemo<MenuProps['items']>(
|
const items = useMemo<MenuProps['items']>(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue