mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-07-28 04:07:18 +00:00
Condense style guide for coding agents (890→183 lines, 79% reduction)
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
This commit is contained in:
co-authored by
thomasnordquist
parent
7600748306
commit
b7c6223e89
+87
-794
@@ -1,890 +1,183 @@
|
||||
# MQTT Explorer Style Guide
|
||||
|
||||
This document defines the coding standards, design patterns, and best practices for the MQTT Explorer project. Following these guidelines ensures consistency, maintainability, and quality across the codebase.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Code Formatting](#code-formatting)
|
||||
2. [TypeScript Conventions](#typescript-conventions)
|
||||
3. [React Component Patterns](#react-component-patterns)
|
||||
4. [State Management](#state-management)
|
||||
5. [File Organization](#file-organization)
|
||||
6. [Naming Conventions](#naming-conventions)
|
||||
7. [Testing](#testing)
|
||||
8. [Security](#security)
|
||||
9. [Documentation](#documentation)
|
||||
10. [Build and Development](#build-and-development)
|
||||
Coding standards and patterns for the MQTT Explorer project, optimized for coding agents.
|
||||
|
||||
## Code Formatting
|
||||
|
||||
### Prettier Configuration
|
||||
**Prettier config:** No semicolons, single quotes, 2 spaces, 120 char max, ES5 trailing commas
|
||||
**TSLint:** Airbnb base, generic arrays `Array<T>`, explicit access modifiers (`public`/`private`/`protected`)
|
||||
|
||||
The project uses Prettier for automatic code formatting with the following configuration:
|
||||
|
||||
```javascript
|
||||
{
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 2,
|
||||
semi: false, // No semicolons
|
||||
singleQuote: true, // Single quotes for strings
|
||||
printWidth: 120, // Max line length 120 characters
|
||||
arrowParens: 'avoid', // Omit parens when possible
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules:**
|
||||
- **No semicolons** at the end of statements
|
||||
- **Single quotes** for strings (except in JSX where double quotes are used)
|
||||
- **2 spaces** for indentation (never tabs)
|
||||
- **120 characters** maximum line length
|
||||
- **ES5 trailing commas** in objects and arrays
|
||||
- **Arrow function parens** only when necessary
|
||||
|
||||
**Examples:**
|
||||
```typescript
|
||||
// ✅ Good
|
||||
// Format example
|
||||
const name = 'MQTT Explorer'
|
||||
const config = {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
}
|
||||
const items: Array<string> = []
|
||||
|
||||
// ❌ Bad
|
||||
const name = "MQTT Explorer";
|
||||
const config = {
|
||||
port: 3000,
|
||||
host: 'localhost' // Missing trailing comma
|
||||
};
|
||||
```
|
||||
|
||||
### TSLint Rules
|
||||
|
||||
Based on Airbnb style guide with customizations:
|
||||
|
||||
- **Array types:** Use generic syntax `Array<T>` instead of `T[]`
|
||||
- **Member access:** Always specify access modifiers (`public`, `private`, `protected`)
|
||||
- **Import organization:** Group and order imports logically
|
||||
- **No implicit dependencies:** Explicitly declare all dependencies
|
||||
|
||||
**Examples:**
|
||||
```typescript
|
||||
// ✅ Good - Generic array syntax
|
||||
const topics: Array<string> = []
|
||||
const nodes: Array<TreeNode<ViewModel>> = []
|
||||
|
||||
// ❌ Bad - Array bracket syntax
|
||||
const topics: string[] = []
|
||||
|
||||
// ✅ Good - Explicit access modifiers
|
||||
class MqttSource {
|
||||
class Example {
|
||||
private client: MqttClient
|
||||
public connect(options: MqttOptions) {}
|
||||
}
|
||||
|
||||
// ❌ Bad - Missing access modifiers
|
||||
class MqttSource {
|
||||
client: MqttClient
|
||||
connect(options: MqttOptions) {}
|
||||
}
|
||||
```
|
||||
|
||||
### Linting Commands
|
||||
**Commands:** `yarn lint`, `yarn lint:fix`
|
||||
|
||||
```bash
|
||||
# Check all linting issues
|
||||
yarn lint
|
||||
## TypeScript
|
||||
|
||||
# Auto-fix linting issues
|
||||
yarn lint:fix
|
||||
- Strict mode enabled (`noImplicitAny`, `strictNullChecks`)
|
||||
- Explicit function types: `function name(param: Type): ReturnType {}`
|
||||
- Avoid `any`, use interfaces for objects, enums for constants
|
||||
- Type imports: `import type { Type } from './module'`
|
||||
|
||||
# Individual linters
|
||||
yarn lint:prettier # Check formatting
|
||||
yarn lint:prettier:fix # Fix formatting
|
||||
yarn lint:tslint # Check TypeScript
|
||||
yarn lint:tslint:fix # Fix TypeScript issues
|
||||
yarn lint:spellcheck # Check spelling
|
||||
```
|
||||
|
||||
## TypeScript Conventions
|
||||
|
||||
### Strict Type Checking
|
||||
|
||||
The project uses strict TypeScript configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"target": "ES2020",
|
||||
"module": "commonjs"
|
||||
}
|
||||
```
|
||||
|
||||
**Always:**
|
||||
- Explicitly type function parameters and return types
|
||||
- Use strict null checks (`value: Type | undefined`)
|
||||
- Avoid `any` type unless absolutely necessary
|
||||
- Use interfaces for object shapes
|
||||
- Use enums for action types and constants
|
||||
|
||||
**Examples:**
|
||||
```typescript
|
||||
// ✅ Good - Explicit types
|
||||
enum ActionTypes {
|
||||
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
|
||||
}
|
||||
|
||||
interface ConnectionState {
|
||||
host?: string
|
||||
connected: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
function connect(options: MqttOptions): DataSourceStateMachine {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// ❌ Bad - Implicit any
|
||||
function connect(options) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// ✅ Good - Null checks
|
||||
function processNode(node: TreeNode | undefined) {
|
||||
if (!node) return
|
||||
// Use node safely
|
||||
}
|
||||
```
|
||||
|
||||
### Type Imports
|
||||
## React Components
|
||||
|
||||
Use `import type` for type-only imports when possible to improve tree-shaking:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
import type { MqttOptions } from './DataSource'
|
||||
import { connect } from './mqtt'
|
||||
|
||||
// Also acceptable for mixed imports
|
||||
import { type MqttOptions, connect } from './mqtt'
|
||||
```
|
||||
|
||||
### Enums for Constants
|
||||
|
||||
Use enums for action types, states, and other constant values:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
export enum ActionTypes {
|
||||
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
|
||||
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
|
||||
CONNECTION_SET_DISCONNECTED = 'CONNECTION_SET_DISCONNECTED',
|
||||
}
|
||||
|
||||
export type ConnectionHealth = 'offline' | 'online' | 'connecting'
|
||||
```
|
||||
|
||||
## React Component Patterns
|
||||
|
||||
### Component Types
|
||||
|
||||
#### Class Components
|
||||
|
||||
Use `React.PureComponent` for class components to enable shallow prop comparison:
|
||||
**Class components:** Use `React.PureComponent`, type props/state, mark methods `public`/`private`
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
connectionId: string
|
||||
settingsVisible: boolean
|
||||
actions: typeof globalActions
|
||||
}
|
||||
|
||||
class App extends React.PureComponent<Props, {}> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// Initialization
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div>{/* content */}</div>
|
||||
return <div />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use `React.PureComponent` instead of `React.Component`
|
||||
- Always type props interface
|
||||
- Use empty object `{}` for state if no local state
|
||||
- Mark lifecycle methods as `public`
|
||||
- Mark helper methods as `private`
|
||||
|
||||
#### Functional Components
|
||||
|
||||
Prefer functional components with hooks for new code:
|
||||
**Functional components:** Destructure props, use custom hooks, keep under 200 lines
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
nodePath?: string
|
||||
tree?: q.Tree<TopicViewModel>
|
||||
connectionId?: string
|
||||
}
|
||||
|
||||
function Sidebar(props: Props) {
|
||||
const { tree, nodePath } = props
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath || '')
|
||||
|
||||
return <div>{/* content */}</div>
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath)
|
||||
return <div />
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Destructure props early for clarity
|
||||
- Use custom hooks for reusable logic
|
||||
- Keep components focused and small (<200 lines)
|
||||
**Hooks:** Prefix with `use`, place in `hooks/` or `helper/`, always specify `useEffect` dependencies
|
||||
|
||||
### Hooks Patterns
|
||||
**Lazy loading:** `const Component = React.lazy(() => import('./Component'))`
|
||||
|
||||
#### Custom Hooks
|
||||
**Material-UI:** Use both `ThemeProvider` and `LegacyThemeProvider`, `withStyles` for class components
|
||||
|
||||
Place custom hooks in `app/src/components/hooks/` or `app/src/components/helper/`:
|
||||
## State Management (Redux)
|
||||
|
||||
**Reducers:** Define state interface, use enums for action types, use `createReducer` helper, return new objects
|
||||
|
||||
```typescript
|
||||
// usePollingToFetchTreeNode.tsx
|
||||
export function usePollingToFetchTreeNode(
|
||||
tree?: q.Tree<TopicViewModel>,
|
||||
nodePath?: string
|
||||
): q.TreeNode<TopicViewModel> | undefined {
|
||||
const [node, setNode] = useState<q.TreeNode<TopicViewModel> | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
// Implementation
|
||||
}, [tree, nodePath])
|
||||
|
||||
return node
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Prefix hook names with `use`
|
||||
- Place hooks in separate files
|
||||
- Document complex hooks with JSDoc comments
|
||||
- Always specify dependencies array in `useEffect`
|
||||
|
||||
#### Effect Hooks
|
||||
|
||||
Use throttling for performance-critical updates:
|
||||
|
||||
```typescript
|
||||
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
|
||||
const [, setLastUpdate] = useState(0)
|
||||
const updateNode = useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate(node ? node.lastUpdate : 0)
|
||||
}, 300),
|
||||
[node]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
node?.onMerge.subscribe(updateNode)
|
||||
node?.onMessage.subscribe(updateNode)
|
||||
|
||||
return () => {
|
||||
node?.onMerge.unsubscribe(updateNode)
|
||||
node?.onMessage.unsubscribe(updateNode)
|
||||
}
|
||||
}, [node])
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Use `React.lazy` for code splitting large components:
|
||||
|
||||
```typescript
|
||||
const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
|
||||
const ContentView = React.lazy(() => import('./Layout/ContentView'))
|
||||
|
||||
// In render:
|
||||
<React.Suspense fallback={<div></div>}>
|
||||
<Settings />
|
||||
</React.Suspense>
|
||||
```
|
||||
|
||||
### Material-UI Integration
|
||||
|
||||
#### Theming
|
||||
|
||||
Use both legacy and new theme providers for compatibility:
|
||||
|
||||
```typescript
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import { ThemeProvider as LegacyThemeProvider } from '@mui/styles'
|
||||
|
||||
<ThemeProvider theme={theme}>
|
||||
<LegacyThemeProvider theme={theme}>
|
||||
<App />
|
||||
</LegacyThemeProvider>
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
||||
#### Styling
|
||||
|
||||
Use Material-UI `withStyles` for class-based components:
|
||||
|
||||
```typescript
|
||||
const styles = (theme: Theme) => ({
|
||||
drawer: {
|
||||
display: 'block' as 'block',
|
||||
},
|
||||
details: {
|
||||
padding: '0px 16px 8px 8px',
|
||||
display: 'block',
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(Sidebar)
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Define styles as a function taking `theme` parameter
|
||||
- Use type assertions for specific CSS values: `'block' as 'block'`
|
||||
- Keep styles close to component usage
|
||||
|
||||
## State Management
|
||||
|
||||
### Redux Pattern
|
||||
|
||||
The project uses Redux with thunk middleware for async actions.
|
||||
|
||||
#### Reducer Pattern
|
||||
|
||||
```typescript
|
||||
import { createReducer } from './lib'
|
||||
|
||||
export interface ConnectionState {
|
||||
connected: boolean
|
||||
connecting: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export enum ActionTypes {
|
||||
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
|
||||
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
|
||||
}
|
||||
|
||||
export type Action = SetConnecting | SetConnected
|
||||
|
||||
const initialState: ConnectionState = {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
}
|
||||
|
||||
export const connectionReducer = createReducer<ConnectionState>(initialState, {
|
||||
[ActionTypes.CONNECTION_SET_CONNECTING]: (state, action) => ({
|
||||
...state,
|
||||
connecting: true,
|
||||
}),
|
||||
[ActionTypes.CONNECTION_SET_CONNECTED]: (state, action) => ({
|
||||
...state,
|
||||
connected: true,
|
||||
connecting: false,
|
||||
}),
|
||||
export const reducer = createReducer<State>(initialState, {
|
||||
[ActionTypes.ACTION]: (state, action) => ({ ...state, field: value }),
|
||||
})
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Define state interface
|
||||
- Use enums for action types
|
||||
- Create union type for all actions
|
||||
- Use `createReducer` helper
|
||||
- Return new state objects (immutability)
|
||||
|
||||
#### Action Creators
|
||||
**Actions:** Use thunk for async, export action creators
|
||||
|
||||
```typescript
|
||||
export const connect =
|
||||
(options: MqttOptions, connectionId: string) =>
|
||||
export const connect = (options: MqttOptions, id: string) =>
|
||||
(dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch(connecting(connectionId))
|
||||
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
|
||||
|
||||
const event = makeConnectionStateEvent(connectionId)
|
||||
rendererEvents.subscribe(event, dataSourceState => {
|
||||
if (dataSourceState.connected) {
|
||||
dispatch(connected(tree, host))
|
||||
}
|
||||
})
|
||||
dispatch(connecting(id))
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Export thunk action creators for async operations
|
||||
- Use `dispatch` and `getState` from thunk middleware
|
||||
- Chain multiple dispatches as needed
|
||||
- Handle subscriptions in action creators
|
||||
|
||||
#### Connecting Components
|
||||
|
||||
```typescript
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
tree: state.connection.tree,
|
||||
nodePath: state.tree.get('selectedTopic')?.path(),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
actions: bindActionCreators(sidebarActions, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar)
|
||||
```
|
||||
**Connect:** `mapStateToProps`, `mapDispatchToProps` with `bindActionCreators`
|
||||
|
||||
## File Organization
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
MQTT-Explorer/
|
||||
├── app/ # Frontend React application
|
||||
│ ├── src/
|
||||
│ │ ├── actions/ # Redux actions
|
||||
│ │ ├── components/ # React components
|
||||
│ │ │ ├── ComponentName/
|
||||
│ │ │ │ ├── index.tsx # Main component
|
||||
│ │ │ │ ├── SubComponent.tsx # Sub-components
|
||||
│ │ │ │ └── styles.ts # Component styles (if needed)
|
||||
│ │ ├── contexts/ # React contexts
|
||||
│ │ ├── decoders/ # Message decoders
|
||||
│ │ ├── effects/ # Side effects
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── model/ # Frontend models
|
||||
│ │ ├── reducers/ # Redux reducers
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── test/ # Frontend tests
|
||||
│ └── webpack.config.js
|
||||
├── backend/ # Backend data models and logic
|
||||
│ ├── src/
|
||||
│ │ ├── DataSource/ # MQTT data sources
|
||||
│ │ ├── Model/ # Core data models
|
||||
│ │ └── spec/ # Backend tests
|
||||
├── events/ # Event system (IPC, Socket.IO)
|
||||
├── src/ # Electron and server entry points
|
||||
│ ├── electron.ts # Electron main process
|
||||
│ ├── server.ts # Express server for browser mode
|
||||
│ └── spec/ # Integration tests
|
||||
├── scripts/ # Build and utility scripts
|
||||
└── docs/ # Documentation
|
||||
app/src/
|
||||
actions/ # Redux actions
|
||||
components/ # React components (PascalCase folders)
|
||||
reducers/ # Redux reducers
|
||||
hooks/ # Custom hooks
|
||||
model/ # Frontend models
|
||||
utils/ # Utilities
|
||||
|
||||
backend/src/
|
||||
Model/ # Core data models
|
||||
DataSource/ # MQTT sources
|
||||
|
||||
src/
|
||||
electron.ts # Electron main
|
||||
server.ts # Express server
|
||||
```
|
||||
|
||||
### Component Organization
|
||||
|
||||
Group related components in folders:
|
||||
|
||||
**Component structure:**
|
||||
```
|
||||
components/
|
||||
├── Sidebar/
|
||||
│ ├── index.ts # Re-exports
|
||||
│ ├── Sidebar.tsx # Main component
|
||||
│ ├── Panel.tsx # Sub-component
|
||||
│ ├── TopicPanel/ # Nested component group
|
||||
│ │ └── TopicPanel.tsx
|
||||
│ └── ValueRenderer/
|
||||
│ └── ValuePanel.tsx
|
||||
ComponentName/
|
||||
index.ts # Re-exports
|
||||
ComponentName.tsx
|
||||
SubComponent.tsx
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- One component per file
|
||||
- Use `index.ts` for re-exports
|
||||
- Group related components in folders
|
||||
- Keep component files under 200 lines
|
||||
- Split large components into smaller ones
|
||||
|
||||
### File Naming
|
||||
|
||||
- **Components:** PascalCase (e.g., `Sidebar.tsx`, `ConnectionSetup.tsx`)
|
||||
- **Utilities:** camelCase (e.g., `browserMode.ts`, `tracking.ts`)
|
||||
- **Types/Interfaces:** PascalCase files (e.g., `ConnectionOptions.ts`)
|
||||
- **Tests:** `*.spec.ts` or `*.spec.tsx` (e.g., `Tree.spec.ts`)
|
||||
- **Hooks:** camelCase with `use` prefix (e.g., `usePollingToFetchTreeNode.tsx`)
|
||||
**File naming:** Components = PascalCase, utilities = camelCase, tests = `*.spec.ts`, hooks = `useHookName.tsx`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Variables and Functions
|
||||
|
||||
```typescript
|
||||
// ✅ Good - camelCase
|
||||
const connectionId = 'abc123'
|
||||
const mqttOptions = { url: 'mqtt://localhost' }
|
||||
|
||||
function handleNewData(msg: MqttMessage) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// ❌ Bad
|
||||
const ConnectionId = 'abc123'
|
||||
const mqtt_options = { url: 'mqtt://localhost' }
|
||||
```
|
||||
|
||||
### Classes and Interfaces
|
||||
|
||||
```typescript
|
||||
// ✅ Good - PascalCase
|
||||
class MqttSource implements DataSource<MqttOptions> {}
|
||||
|
||||
interface ConnectionState {
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
// ❌ Bad
|
||||
class mqttSource {}
|
||||
interface connectionState {}
|
||||
```
|
||||
|
||||
### Constants and Enums
|
||||
|
||||
```typescript
|
||||
// ✅ Good - SCREAMING_SNAKE_CASE for constants
|
||||
const MAX_FILE_SIZE = 16 * 1024 * 1024
|
||||
const DEFAULT_PORT = 3000
|
||||
|
||||
// ✅ Good - PascalCase for enums
|
||||
enum ActionTypes {
|
||||
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
|
||||
}
|
||||
```
|
||||
|
||||
### React Props and State
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Interfaces named Props and State
|
||||
interface Props {
|
||||
connectionId: string
|
||||
tree?: q.Tree<TopicViewModel>
|
||||
}
|
||||
|
||||
interface State {
|
||||
isExpanded: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Boolean Variables
|
||||
|
||||
Prefix boolean variables with `is`, `has`, `should`, or `can`:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
const isConnected = true
|
||||
const hasError = false
|
||||
const shouldUpdate = true
|
||||
const canPublish = false
|
||||
|
||||
// ❌ Bad
|
||||
const connected = true
|
||||
const error = false
|
||||
```
|
||||
- Variables/functions: `camelCase`
|
||||
- Classes/interfaces: `PascalCase`
|
||||
- Constants: `SCREAMING_SNAKE_CASE`
|
||||
- Booleans: `isX`, `hasX`, `shouldX`, `canX`
|
||||
- Props/State interfaces: `Props`, `State`
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Organization
|
||||
|
||||
- **Frontend tests:** `app/test/` or co-located `*.spec.tsx`
|
||||
- **Backend tests:** `backend/src/Model/spec/` or `backend/src/spec/`
|
||||
- **Integration tests:** `src/spec/`
|
||||
|
||||
### Testing Framework
|
||||
|
||||
Uses Mocha with Chai assertions:
|
||||
**Framework:** Mocha + Chai
|
||||
**Location:** `app/test/`, `backend/src/spec/`, `src/spec/`, co-located `*.spec.tsx`
|
||||
|
||||
```typescript
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { Tree } from '../'
|
||||
import { makeTreeNode } from './makeTreeNode'
|
||||
|
||||
describe('Tree', () => {
|
||||
it('node can be merged into a tree', () => {
|
||||
const tree = new Tree()
|
||||
const leaf = makeTreeNode('foo/bar')
|
||||
|
||||
tree.updateWithNode(leaf.firstNode())
|
||||
const expectedNode = tree.findNode('foo/bar')
|
||||
|
||||
expect(expectedNode).to.eq(leaf)
|
||||
})
|
||||
|
||||
it('handles empty nodes', () => {
|
||||
const tree = new Tree()
|
||||
const result = tree.findNode('nonexistent')
|
||||
|
||||
expect(result).to.be.undefined
|
||||
describe('Component', () => {
|
||||
it('does something', () => {
|
||||
expect(result).to.eq(expected)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use `describe` for test suites
|
||||
- Use `it` for individual tests
|
||||
- Write descriptive test names
|
||||
- Test both success and error cases
|
||||
- Use `expect` from Chai for assertions
|
||||
- Keep tests focused and isolated
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
yarn test
|
||||
|
||||
# Frontend tests only
|
||||
yarn test:app
|
||||
|
||||
# Backend tests only
|
||||
yarn test:backend
|
||||
|
||||
# UI tests (requires build)
|
||||
yarn build
|
||||
yarn test:ui
|
||||
|
||||
# All tests including demo video
|
||||
yarn build
|
||||
yarn test:all
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Aim for high coverage of business logic
|
||||
- Focus on critical paths
|
||||
- Test edge cases and error handling
|
||||
- Mock external dependencies (MQTT, file system, etc.)
|
||||
**Commands:** `yarn test` (all), `yarn test:app`, `yarn test:backend`, `yarn test:ui` (needs build)
|
||||
|
||||
## Security
|
||||
|
||||
### Authentication
|
||||
|
||||
**Never** hardcode credentials:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
const username = process.env.MQTT_EXPLORER_USERNAME
|
||||
const password = process.env.MQTT_EXPLORER_PASSWORD
|
||||
|
||||
// ❌ Bad
|
||||
const username = 'admin'
|
||||
const password = 'password123'
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
Always validate and sanitize user input:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
function sanitizeFilename(filename: string): string {
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
throw new Error('Invalid filename')
|
||||
}
|
||||
|
||||
const sanitized = filename.replace(/[/\\]/g, '').replace(/\0/g, '')
|
||||
|
||||
if (sanitized.includes('..') || sanitized.startsWith('.')) {
|
||||
throw new Error('Invalid filename: directory traversal not allowed')
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// ❌ Bad
|
||||
function processFile(filename: string) {
|
||||
// Directly using user input without validation
|
||||
fs.readFile(filename, ...)
|
||||
}
|
||||
```
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
1. **Use helmet.js** for HTTP security headers
|
||||
2. **Implement rate limiting** for authentication endpoints
|
||||
3. **Validate all file paths** against traversal attacks
|
||||
4. **Use bcrypt** for password hashing
|
||||
5. **Implement constant-time comparison** for credentials
|
||||
6. **Set proper CORS policies**
|
||||
7. **Use HTTPS in production**
|
||||
8. **Keep dependencies updated** (`yarn audit`)
|
||||
|
||||
See [SECURITY.md](SECURITY.md) for comprehensive security guidelines.
|
||||
- Never hardcode credentials, use `process.env`
|
||||
- Validate/sanitize all user input
|
||||
- Use helmet.js, rate limiting, bcrypt, HTTPS in production
|
||||
- See [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
### Code Comments
|
||||
- JSDoc for public APIs
|
||||
- Comments only for complex logic, workarounds, TODOs
|
||||
- No obvious/redundant comments
|
||||
|
||||
Use comments sparingly and only when necessary:
|
||||
## Development
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Explaining complex logic
|
||||
/**
|
||||
* Validates and sanitizes file paths to prevent path traversal attacks
|
||||
* @param filename The filename to validate
|
||||
* @returns Sanitized filename or throws error if invalid
|
||||
*/
|
||||
function sanitizeFilename(filename: string): string {
|
||||
// Implementation
|
||||
}
|
||||
**Workflow:**
|
||||
- `yarn dev` - Electron dev mode
|
||||
- `yarn dev:server` - Browser mode with hot reload (port 8080)
|
||||
- `yarn build` / `yarn build:server`
|
||||
- `yarn lint` / `yarn lint:fix`
|
||||
|
||||
// ❌ Bad - Stating the obvious
|
||||
// Set connected to true
|
||||
this.connected = true
|
||||
```
|
||||
**Git:** Feature branches from `master`, conventional commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`)
|
||||
|
||||
**When to comment:**
|
||||
- Complex algorithms or business logic
|
||||
- Non-obvious workarounds or hacks
|
||||
- API documentation (JSDoc)
|
||||
- TODOs for future improvements
|
||||
**Performance:** Use `PureComponent`, throttle/debounce, `React.lazy`, `useCallback`/`useMemo`, avoid inline functions in render
|
||||
|
||||
**When NOT to comment:**
|
||||
- Self-explanatory code
|
||||
- Redundant information
|
||||
- Outdated or incorrect comments (remove them!)
|
||||
## Architecture
|
||||
|
||||
### JSDoc for Public APIs
|
||||
**Event system:** `rendererEvents.emit()`, `rendererEvents.subscribe()`, `rendererEvents.unsubscribe()`
|
||||
|
||||
Document public APIs with JSDoc:
|
||||
**Data flow:** User Action → Redux → Event System → Backend → MQTT → State Update → Re-render
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Connects to an MQTT broker with the specified options
|
||||
* @param options Connection configuration including URL, credentials, and TLS settings
|
||||
* @returns DataSourceStateMachine for tracking connection state
|
||||
* @throws Error if URL is invalid or connection fails
|
||||
*/
|
||||
public connect(options: MqttOptions): DataSourceStateMachine {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
**MVVM:** Model (`backend/src/Model/`), View (components), ViewModel (`app/src/model/`)
|
||||
|
||||
### README and Documentation
|
||||
---
|
||||
|
||||
- Keep README.md up-to-date with setup instructions
|
||||
- Document new features in Changelog.md
|
||||
- Add usage examples for complex features
|
||||
- Link to relevant documentation files
|
||||
|
||||
## Build and Development
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# Desktop app development
|
||||
yarn dev # Start Electron in dev mode
|
||||
|
||||
# Browser mode development
|
||||
yarn dev:server # Start with hot reload (localhost:8080)
|
||||
|
||||
# Building
|
||||
yarn build # Build desktop app
|
||||
yarn build:server # Build browser mode
|
||||
|
||||
# Testing
|
||||
yarn test # Run unit tests
|
||||
yarn lint # Check linting
|
||||
yarn lint:fix # Auto-fix linting issues
|
||||
```
|
||||
|
||||
### Code Quality Checklist
|
||||
|
||||
Before committing:
|
||||
|
||||
- [ ] Run `yarn lint` - No linting errors
|
||||
- [ ] Run `yarn test` - All tests pass
|
||||
- [ ] Run `yarn build` - Build succeeds
|
||||
- [ ] Code follows style guide
|
||||
- [ ] Added tests for new features
|
||||
- [ ] Updated documentation if needed
|
||||
- [ ] No console.log() statements in production code
|
||||
- [ ] No commented-out code
|
||||
- [ ] Security considerations addressed
|
||||
|
||||
### Git Workflow
|
||||
|
||||
- Create feature branches from `master`
|
||||
- Use conventional commit messages:
|
||||
- `feat:` for new features
|
||||
- `fix:` for bug fixes
|
||||
- `docs:` for documentation
|
||||
- `refactor:` for code refactoring
|
||||
- `test:` for test changes
|
||||
- `chore:` for build/tooling changes
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Use `React.PureComponent` for class components
|
||||
- Implement `shouldComponentUpdate` when necessary
|
||||
- Use `React.memo` for functional components if needed
|
||||
- Throttle/debounce expensive operations
|
||||
- Use `React.lazy` for code splitting
|
||||
- Avoid inline function definitions in render
|
||||
- Use `useCallback` and `useMemo` appropriately
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Throttled updates
|
||||
const updateNode = useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate(node ? node.lastUpdate : 0)
|
||||
}, 300),
|
||||
[node]
|
||||
)
|
||||
|
||||
// ❌ Bad - Updates on every render
|
||||
const updateNode = () => {
|
||||
setLastUpdate(node ? node.lastUpdate : 0)
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Event System
|
||||
|
||||
The project uses a custom event system for IPC and WebSocket communication:
|
||||
|
||||
```typescript
|
||||
import { rendererEvents, addMqttConnectionEvent } from '../../../events'
|
||||
|
||||
// Emit events
|
||||
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
|
||||
|
||||
// Subscribe to events
|
||||
rendererEvents.subscribe(event, callback)
|
||||
|
||||
// Unsubscribe
|
||||
rendererEvents.unsubscribe(event, callback)
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Action → Redux Action → Event System → Backend → MQTT Broker
|
||||
↓
|
||||
Redux State Update
|
||||
↓
|
||||
Component Re-render
|
||||
```
|
||||
|
||||
### Model-View-ViewModel (MVVM)
|
||||
|
||||
- **Model:** `backend/src/Model/` - Tree data structure, nodes
|
||||
- **View:** React components in `app/src/components/`
|
||||
- **ViewModel:** `app/src/model/TopicViewModel.ts` - View-specific data
|
||||
|
||||
## Summary
|
||||
|
||||
This style guide ensures:
|
||||
|
||||
- **Consistency:** All code follows the same patterns
|
||||
- **Maintainability:** Easy to understand and modify
|
||||
- **Quality:** High standards for testing and security
|
||||
- **Performance:** Optimized rendering and data flow
|
||||
- **Developer Experience:** Clear conventions and tooling
|
||||
|
||||
When in doubt, look at existing code in the project for examples, and always prioritize clarity and maintainability over cleverness.
|
||||
|
||||
For questions or suggestions about this style guide, please open an issue on GitHub.
|
||||
**Key principle:** Prioritize clarity and maintainability. When in doubt, follow existing code patterns.
|
||||
|
||||
Reference in New Issue
Block a user