Files
OpenNoodl/dev-docs/guidelines/CODING-STANDARDS.md
2025-12-06 22:37:54 +01:00

10 KiB

OpenNoodl Coding Standards

This document defines the coding style and patterns for OpenNoodl development.

TypeScript Standards

Type Safety

// ✅ DO: Explicit types
function processNode(node: NodeGraphNode): ProcessResult {
  return { success: true, data: node.data };
}

// ❌ DON'T: Any types
function processNode(node: any): any {
  return { success: true, data: node.data };
}

// ❌ DON'T: TSFixme
function processNode(node: TSFixme): TSFixme {
  return { success: true, data: node.data };
}

When Type is Truly Unknown

// ✅ DO: Use unknown and narrow
function handleData(data: unknown): string {
  if (typeof data === 'string') {
    return data;
  }
  if (typeof data === 'object' && data !== null && 'message' in data) {
    return String((data as { message: unknown }).message);
  }
  return String(data);
}

// ✅ DO: Document why if using any (rare)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Any required here because external library doesn't export types
function handleExternalLib(input: any): void {
  externalLib.process(input);
}

Interface Definitions

// ✅ DO: Define interfaces for data structures
interface NodeConfig {
  name: string;
  displayName: string;
  category: string;
  inputs: Record<string, InputDefinition>;
  outputs: Record<string, OutputDefinition>;
}

// ✅ DO: Use type for unions/aliases
type NodeColor = 'data' | 'logic' | 'visual' | 'component';

// ✅ DO: Export types from dedicated files
// types.ts
export interface MyComponentProps {
  value: string;
  onChange: (value: string) => void;
}

React Standards

Functional Components

// ✅ DO: Functional components with typed props
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

export function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

// ❌ DON'T: Class components (unless lifecycle methods required)
class Button extends React.Component<ButtonProps> {
  render() {
    return <button>{this.props.label}</button>;
  }
}

Hooks Usage

// ✅ DO: Proper hook dependencies
const handleChange = useCallback((value: string) => {
  onChange(value);
  onValidate?.(value);
}, [onChange, onValidate]);

// ✅ DO: Cleanup in effects
useEffect(() => {
  const handler = (e: Event) => { /* ... */ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

// ❌ DON'T: Missing dependencies
const handleChange = useCallback((value: string) => {
  onChange(value); // onChange not in deps!
}, []);

Component Organization

// Component file structure
import React, { useState, useCallback, useEffect } from 'react';

// External imports
import classNames from 'classnames';

// Internal imports
import { Icon, IconName } from '@noodl-core-ui/components/common/Icon';
import { useModel } from '@noodl-utils/hooks';

// Relative imports
import { ButtonProps } from './types';
import { validateInput } from './utils';

// Styles last
import css from './Button.module.scss';

// Types (if not in separate file)
interface LocalState {
  isHovered: boolean;
}

// Component
export function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
  // Hooks first
  const [state, setState] = useState<LocalState>({ isHovered: false });
  const model = useModel(SomeModel.instance);
  
  // Callbacks
  const handleClick = useCallback(() => {
    onClick();
  }, [onClick]);
  
  // Effects
  useEffect(() => {
    // Setup
    return () => {
      // Cleanup
    };
  }, []);
  
  // Render helpers
  const buttonClass = classNames(css.Button, css[variant]);
  
  // Render
  return (
    <button className={buttonClass} onClick={handleClick}>
      {label}
    </button>
  );
}

File Organization

Directory Structure

feature/
├── index.ts              # Public exports only
├── FeatureName.tsx       # Main component
├── FeatureName.module.scss
├── FeatureName.test.ts
├── types.ts              # Type definitions
├── utils.ts              # Helper functions
└── hooks.ts              # Custom hooks (if any)

Index Files (Barrel Exports)

// index.ts - Export only public API
export { FeatureName } from './FeatureName';
export type { FeatureNameProps } from './types';

// DON'T export internal utilities

Import Order

// 1. React
import React, { useState, useEffect } from 'react';

// 2. External packages (alphabetical)
import classNames from 'classnames';
import { motion } from 'framer-motion';

// 3. Internal packages (alphabetical by alias)
import { Icon } from '@noodl-core-ui/components/common/Icon';
import { NodeGraphModel } from '@noodl-models/nodegraphmodel';
import { guid } from '@noodl-utils/utils';

// 4. Relative imports (parent first, then siblings)
import { ParentComponent } from '../ParentComponent';
import { SiblingComponent } from './SiblingComponent';
import { localHelper } from './utils';

// 5. Types (if separate import needed)
import type { MyComponentProps } from './types';

// 6. Styles
import css from './MyComponent.module.scss';

Documentation Standards

JSDoc for Public APIs

/**
 * Processes a node and returns the computed result.
 * 
 * @param node - The node to process
 * @param options - Processing options
 * @returns The computed result with output values
 * 
 * @example
 * ```typescript
 * const result = processNode(myNode, { validate: true });
 * console.log(result.outputs);
 * ```
 */
export function processNode(
  node: NodeGraphNode,
  options: ProcessOptions = {}
): ProcessResult {
  // Implementation
}

File Headers

/**
 * NodeGraphModel - Manages the structure of a node graph.
 * 
 * This model handles:
 * - Node creation and deletion
 * - Connection management
 * - Graph traversal
 * 
 * @module models/NodeGraphModel
 */

Inline Comments

// ✅ DO: Explain "why", not "what"
// We batch updates here to prevent cascading re-renders
// when multiple inputs change in the same frame
this.scheduleAfterInputsHaveUpdated(() => {
  this.processAllInputs();
});

// ❌ DON'T: State the obvious
// Loop through items
for (const item of items) {
  // Process item
  process(item);
}

Naming Conventions

Files

Type Convention Example
React Component PascalCase NodePicker.tsx
Utility camelCase formatUtils.ts
Types camelCase or PascalCase types.ts or NodeTypes.ts
Test Match source + .test NodePicker.test.ts
Styles Match component + .module NodePicker.module.scss

Code

// Constants: UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const DEFAULT_TIMEOUT_MS = 5000;

// Functions/Methods: camelCase
function processNodeGraph() {}
function calculateOffset() {}

// Classes/Interfaces/Types: PascalCase
class NodeGraphModel {}
interface ProcessOptions {}
type NodeColor = 'data' | 'logic';

// Private members: underscore prefix
class MyClass {
  private _internalState: State;
  private _processInternal(): void {}
}

// Boolean variables: is/has/should prefix
const isEnabled = true;
const hasChildren = node.children.length > 0;
const shouldUpdate = isDirty && isVisible;

Error Handling

// ✅ DO: Specific error types
class NodeNotFoundError extends Error {
  constructor(nodeId: string) {
    super(`Node not found: ${nodeId}`);
    this.name = 'NodeNotFoundError';
  }
}

// ✅ DO: Handle errors gracefully
async function fetchData(): Promise<Result> {
  try {
    const response = await api.fetch();
    return { success: true, data: response };
  } catch (error) {
    console.error('Failed to fetch data:', error);
    return { success: false, error: getErrorMessage(error) };
  }
}

// ✅ DO: Type-safe error messages
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

Testing Standards

Test File Structure

import { render, screen, fireEvent } from '@testing-library/react';
import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
  // Group related tests
  describe('rendering', () => {
    it('should render with default props', () => {
      render(<MyComponent />);
      expect(screen.getByRole('button')).toBeInTheDocument();
    });
  });
  
  describe('interactions', () => {
    it('should call onClick when clicked', () => {
      const onClick = jest.fn();
      render(<MyComponent onClick={onClick} />);
      
      fireEvent.click(screen.getByRole('button'));
      
      expect(onClick).toHaveBeenCalledTimes(1);
    });
  });
});

Test Naming

// ✅ DO: Descriptive test names
it('should display error message when validation fails', () => {});
it('should disable submit button while loading', () => {});

// ❌ DON'T: Vague names
it('works', () => {});
it('test 1', () => {});

Git Commit Messages

Format

type(scope): description

[optional body]

[optional footer]

Types

  • feat: New feature
  • fix: Bug fix
  • refactor: Code change that neither fixes bug nor adds feature
  • docs: Documentation only
  • test: Adding or updating tests
  • chore: Build process or auxiliary tool changes

Examples

feat(editor): add breakpoint support to connections

fix(runtime): resolve memory leak in collection listener

refactor(property-panel): convert to functional component

docs(readme): update installation instructions

test(nodes): add unit tests for REST node

Performance Guidelines

React Performance

// ✅ DO: Memoize expensive computations
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// ✅ DO: Memoize callbacks passed to children
const handleChange = useCallback((value: string) => {
  onChange(value);
}, [onChange]);

// ✅ DO: Use React.memo for pure components
export const ListItem = React.memo(function ListItem({ item }: Props) {
  return <div>{item.name}</div>;
});

General Performance

// ✅ DO: Batch DOM operations
function updateNodes(nodes: Node[]) {
  // Collect all changes first
  const changes = nodes.map(calculateChange);
  
  // Apply in single batch
  requestAnimationFrame(() => {
    changes.forEach(applyChange);
  });
}

// ✅ DO: Debounce frequent events
const debouncedSearch = useMemo(
  () => debounce((query: string) => performSearch(query), 300),
  []
);