mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
10 KiB
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 featurefix: Bug fixrefactor: Code change that neither fixes bug nor adds featuredocs: Documentation onlytest: Adding or updating testschore: 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),
[]
);