# 01 - Project Detection and Visual Indicators ## Overview Detect legacy React 17 projects and display clear visual indicators throughout the UI so users understand which projects need migration. ## Detection Logic ### When to Check 1. **On app startup** - Scan recent projects list 2. **On "Open Project"** - Check selected folder 3. **On project list refresh** - Re-scan visible projects ### How to Detect Runtime Version ```typescript // packages/noodl-editor/src/editor/src/models/migration/ProjectScanner.ts interface RuntimeVersionInfo { version: 'react17' | 'react19' | 'unknown'; confidence: 'high' | 'medium' | 'low'; indicators: string[]; } async function detectRuntimeVersion(projectPath: string): Promise { const indicators: string[] = []; // Check 1: Explicit version in project.json (most reliable) const projectJson = await readProjectJson(projectPath); if (projectJson.runtimeVersion) { return { version: projectJson.runtimeVersion, confidence: 'high', indicators: ['Explicit runtimeVersion field in project.json'] }; } // Check 2: Look for migratedFrom field (indicates already migrated) if (projectJson.migratedFrom) { return { version: 'react19', confidence: 'high', indicators: ['Project has migratedFrom metadata'] }; } // Check 3: Check project version number // OpenNoodl 1.2+ = React 19, earlier = React 17 const editorVersion = projectJson.editorVersion || projectJson.version; if (editorVersion) { const [major, minor] = editorVersion.split('.').map(Number); if (major >= 1 && minor >= 2) { indicators.push(`Editor version ${editorVersion} >= 1.2`); return { version: 'react19', confidence: 'high', indicators }; } else { indicators.push(`Editor version ${editorVersion} < 1.2`); return { version: 'react17', confidence: 'high', indicators }; } } // Check 4: Heuristic - scan for React 17 specific patterns in custom code const customCodePatterns = await scanForLegacyPatterns(projectPath); if (customCodePatterns.found) { indicators.push(...customCodePatterns.patterns); return { version: 'react17', confidence: 'medium', indicators }; } // Check 5: If project was created before OpenNoodl fork, assume React 17 const projectCreated = projectJson.createdAt || await getProjectCreationDate(projectPath); if (projectCreated && new Date(projectCreated) < new Date('2024-01-01')) { indicators.push('Project created before OpenNoodl fork'); return { version: 'react17', confidence: 'medium', indicators }; } // Default: Assume React 19 for truly unknown projects return { version: 'unknown', confidence: 'low', indicators: ['No version indicators found'] }; } ``` ### Legacy Pattern Scanner ```typescript // Quick scan for legacy React patterns in JavaScript files interface LegacyPatternScan { found: boolean; patterns: string[]; files: Array<{ path: string; line: number; pattern: string; }>; } async function scanForLegacyPatterns(projectPath: string): Promise { const jsFiles = await glob(`${projectPath}/**/*.{js,jsx,ts,tsx}`, { ignore: ['**/node_modules/**'] }); const legacyPatterns = [ { regex: /componentWillMount\s*\(/, name: 'componentWillMount' }, { regex: /componentWillReceiveProps\s*\(/, name: 'componentWillReceiveProps' }, { regex: /componentWillUpdate\s*\(/, name: 'componentWillUpdate' }, { regex: /UNSAFE_componentWillMount/, name: 'UNSAFE_componentWillMount' }, { regex: /UNSAFE_componentWillReceiveProps/, name: 'UNSAFE_componentWillReceiveProps' }, { regex: /UNSAFE_componentWillUpdate/, name: 'UNSAFE_componentWillUpdate' }, { regex: /ref\s*=\s*["'][^"']+["']/, name: 'String ref' }, { regex: /contextTypes\s*=/, name: 'Legacy contextTypes' }, { regex: /childContextTypes\s*=/, name: 'Legacy childContextTypes' }, { regex: /getChildContext\s*\(/, name: 'getChildContext' }, { regex: /React\.createFactory/, name: 'createFactory' }, ]; const results: LegacyPatternScan = { found: false, patterns: [], files: [] }; for (const file of jsFiles) { const content = await fs.readFile(file, 'utf-8'); const lines = content.split('\n'); for (const pattern of legacyPatterns) { lines.forEach((line, index) => { if (pattern.regex.test(line)) { results.found = true; if (!results.patterns.includes(pattern.name)) { results.patterns.push(pattern.name); } results.files.push({ path: file, line: index + 1, pattern: pattern.name }); } }); } } return results; } ``` ## Visual Indicators ### Projects Panel - Recent Projects List ```tsx // packages/noodl-editor/src/editor/src/views/ProjectsPanel/ProjectCard.tsx interface ProjectCardProps { project: RecentProject; runtimeInfo: RuntimeVersionInfo; } function ProjectCard({ project, runtimeInfo }: ProjectCardProps) { const isLegacy = runtimeInfo.version === 'react17'; const [expanded, setExpanded] = useState(false); return (

{project.name} {isLegacy && ( )}

Last opened: {formatDate(project.lastOpened)}
{isLegacy && (
Legacy Runtime (React 17)
)} {isLegacy && expanded && (

This project needs migration to work with OpenNoodl 1.2+. Your original project will remain untouched.

)} {!isLegacy && (
)}
); } ``` ### CSS Styles ```scss // packages/noodl-editor/src/editor/src/styles/projects-panel.scss .project-card { background: var(--color-bg-secondary); border-radius: 8px; padding: 16px; border: 1px solid var(--color-border); transition: border-color 0.2s; &:hover { border-color: var(--color-border-hover); } &--legacy { border-color: var(--color-warning-border); &:hover { border-color: var(--color-warning-border-hover); } } } .project-card__warning-icon { color: var(--color-warning); margin-left: 8px; width: 16px; height: 16px; } .project-card__legacy-banner { display: flex; justify-content: space-between; align-items: center; background: var(--color-warning-bg); border-radius: 4px; padding: 8px 12px; margin-top: 12px; } .legacy-banner__content { display: flex; align-items: center; gap: 8px; color: var(--color-warning-text); font-size: 13px; font-weight: 500; } .project-card__legacy-details { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-border); p { color: var(--color-text-secondary); font-size: 13px; margin-bottom: 12px; } } .legacy-details__actions { display: flex; gap: 8px; } ``` ### Open Project Dialog - Legacy Detection ```tsx // packages/noodl-editor/src/editor/src/views/dialogs/OpenProjectDialog.tsx function OpenProjectDialog({ onClose }: { onClose: () => void }) { const [selectedPath, setSelectedPath] = useState(null); const [runtimeInfo, setRuntimeInfo] = useState(null); const [checking, setChecking] = useState(false); const handleFolderSelect = async (path: string) => { setSelectedPath(path); setChecking(true); try { const info = await detectRuntimeVersion(path); setRuntimeInfo(info); } finally { setChecking(false); } }; const isLegacy = runtimeInfo?.version === 'react17'; return ( {checking && (
Checking project version...
)} {runtimeInfo && isLegacy && ( )} {isLegacy ? ( <> ) : ( )}
); } function LegacyProjectNotice({ projectPath, runtimeInfo }: { projectPath: string; runtimeInfo: RuntimeVersionInfo; }) { const projectName = path.basename(projectPath); const defaultTargetPath = `${projectPath}-r19`; const [targetPath, setTargetPath] = useState(defaultTargetPath); return (

Legacy Project Detected

"{projectName}" was created with an older version of Noodl using React 17. OpenNoodl 1.2+ uses React 19.

To open this project, we'll create a migrated copy. Your original project will remain untouched.

{projectPath}
setTargetPath(e.target.value)} />
{runtimeInfo.confidence !== 'high' && (
Detection confidence: {runtimeInfo.confidence}. Indicators: {runtimeInfo.indicators.join(', ')}
)}
); } ``` ## Read-Only Mode When opening a legacy project in read-only mode: ```typescript // packages/noodl-editor/src/editor/src/models/projectmodel.ts interface ProjectOpenOptions { readOnly?: boolean; legacyMode?: boolean; } async function openProject(path: string, options: ProjectOpenOptions = {}) { const project = await ProjectModel.fromDirectory(path); if (options.readOnly || options.legacyMode) { project.setReadOnly(true); // Show banner in editor EditorBanner.show({ type: 'warning', message: 'This project is open in read-only mode. Migrate to make changes.', actions: [ { label: 'Migrate Now', onClick: () => openMigrationWizard(path) }, { label: 'Dismiss', onClick: () => EditorBanner.hide() } ] }); } return project; } ``` ### Read-Only Banner Component ```tsx // packages/noodl-editor/src/editor/src/views/EditorBanner.tsx interface EditorBannerProps { type: 'info' | 'warning' | 'error'; message: string; actions?: Array<{ label: string; onClick: () => void; }>; } function EditorBanner({ type, message, actions }: EditorBannerProps) { return (
{type === 'warning' && } {type === 'info' && } {type === 'error' && } {message}
{actions && (
{actions.map((action, i) => ( ))}
)}
); } ``` ## Testing Checklist - [ ] Legacy project shows warning icon in recent projects - [ ] Clicking legacy project shows expanded details - [ ] "Migrate Project" button opens migration wizard - [ ] "Open Read-Only" opens project without changes - [ ] Opening folder with legacy project shows detection dialog - [ ] Target path can be customized - [ ] Read-only mode shows banner - [ ] Banner "Migrate Now" opens wizard - [ ] New/modern projects open normally without warnings