Overview
SubWallet Extension enforces code quality through automated linting and formatting. All code must pass ESLint validation before being committed.
Linting Setup
ESLint Configuration
SubWallet extends the @polkadot/dev ESLint configuration with custom rules.
Configuration File
The project uses .eslintrc.js at the root level:
const base = require('@polkadot/dev/config/eslint.cjs');
module.exports = {
...base,
ignorePatterns: [
...base.ignorePatterns,
"i18next-scanner.config.js",
"koni-*.mjs",
"packages/extension-web-ui/**/*",
],
parserOptions: {
...base.parserOptions,
project: [
'./tsconfig.eslint.json'
]
},
rules: {
...base.rules,
'header/header': [2, 'line', [
{ pattern: ' Copyright 20(17|18|19|20|21|22)(-2022)? (@polkadot|@subwallet)/' },
' SPDX-License-Identifier: Apache-2.0'
], 2],
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'sort-keys': 'off'
}
};
Custom Rules
All source files must include a copyright header:
// Copyright 2019-2022 @subwallet/extension authors & contributors
// SPDX-License-Identifier: Apache-2.0
This is enforced by the header/header rule.
Disabled Rules
The following rules are disabled:
@typescript-eslint/unbound-method: Turned off due to false positives
@typescript-eslint/ban-ts-comment: Allows TypeScript comments like @ts-ignore
sort-keys: Object key sorting is not enforced
Running the Linter
Before committing any code, run:
This command will:
- Check all TypeScript files for linting errors
- Report any violations of the coding standards
- Suggest fixes where possible
Editor Integration
Set up ESLint in your editor for real-time validation and automatic fixes on save.
VS Code:
Install the ESLint extension and add to your .vscode/settings.json:
{
"eslint.validate": [
"javascript",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
Other Editors:
- WebStorm/IntelliJ: ESLint is supported natively
- Vim/Neovim: Use ALE or coc-eslint
- Sublime Text: Use SublimeLinter-eslint
Prettier Configuration
SubWallet uses Prettier for code formatting, extending the Polkadot configuration:
// .prettierrc.cjs
module.exports = require('@polkadot/dev/config/prettier.cjs');
This includes:
- Print Width: 80 characters (from @polkadot/dev)
- Tab Width: 2 spaces
- Semicolons: Required
- Quotes: Single quotes
- Trailing Commas: ES5 style
- Arrow Function Parens: Always
Format your code before committing:
# Format all files
yarn format
# Check formatting without making changes
yarn format:check
TypeScript Guidelines
Type Safety
Always use explicit types where possible:
// Good
function getPrice(tokenId: string): Promise<PriceJson> {
return this.priceStore.get(tokenId);
}
// Avoid
function getPrice(tokenId) {
return this.priceStore.get(tokenId);
}
Interfaces vs Types
Use interfaces for object shapes that may be extended:
// For message signatures
export interface KoniRequestSignatures {
'pri(price.getPrice)': [RequestPrice, PriceJson];
'pri(price.getSubscription)': [RequestSubscribePrice, boolean, PriceJson];
}
// For data structures
export interface PriceJson {
tokenId: string;
price: number;
currency: string;
}
Async/Await
Prefer async/await over Promise chains:
// Good
async function fetchData(): Promise<void> {
const response = await api.getData();
const processed = await processData(response);
return processed;
}
// Avoid
function fetchData(): Promise<void> {
return api.getData()
.then(response => processData(response))
.then(processed => processed);
}
Naming Conventions
Files and Folders
- React Components: PascalCase (e.g.,
AccountList.tsx)
- Utilities: camelCase (e.g.,
formatBalance.ts)
- Stores: PascalCase (e.g.,
Price.ts)
- Tests: Match source file with
.spec.ts suffix (e.g., Price.spec.ts)
Code Elements
Classes: PascalCase
export default class KoniState extends State {
// ...
}
Functions/Methods: camelCase
public setPrice(priceData: PriceJson): void {
// ...
}
Constants: UPPER_SNAKE_CASE
const EXTENSION_PREFIX = 'subwallet';
const MAX_RETRY_COUNT = 3;
Interfaces: PascalCase with descriptive names
interface RequestPrice {
tokenId: string;
}
interface PriceJson {
price: number;
}
Message Types: Use predefined patterns
// Extension messages start with 'pri'
'pri(price.getPrice)'
'pri(accounts.subscribe)'
// Tab messages start with 'pub'
'pub(utils.getRandom)'
'pub(accounts.list)'
Feature Branch Names
Use the format: <initials>-<feature-description>
# Good
git checkout -b jd-add-staking-api
git checkout -b sm-fix-balance-display
git checkout -b ab-refactor-message-handler
# Avoid
git checkout -b new-feature
git checkout -b fix
git checkout -b john-doe-branch
Store Patterns
Extending BaseStore
export default class Price extends SubscribableStore<PriceJson> {
constructor() {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}price` : null);
}
}
Usage in KoniState
export default class KoniState extends State {
private readonly priceStore = new Price();
private priceStoreReady = false;
public setPrice(priceData: PriceJson, callback?: (priceData: PriceJson) => void): void {
// Implementation
}
public getPrice(update: (value: PriceJson) => void): void {
// Implementation
}
public subscribePrice() {
return this.priceStore.getSubject();
}
}
React and UI Conventions
Component Structure
import React, { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
interface Props {
accountId: string;
onSelect?: (id: string) => void;
}
export const AccountItem: React.FC<Props> = ({ accountId, onSelect }) => {
// Hooks first
const account = useSelector(selectAccount(accountId));
// Event handlers
const handleClick = useCallback(() => {
onSelect?.(accountId);
}, [accountId, onSelect]);
// Effects
useEffect(() => {
// Side effects
}, []);
// Render
return (
<div onClick={handleClick}>
{account.name}
</div>
);
};
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface AccountState {
accounts: Account[];
selectedId: string | null;
}
const accountSlice = createSlice({
name: 'accounts',
initialState: {
accounts: [],
selectedId: null
} as AccountState,
reducers: {
setAccounts(state, action: PayloadAction<Account[]>) {
state.accounts = action.payload;
},
selectAccount(state, action: PayloadAction<string>) {
state.selectedId = action.payload;
}
}
});
Best Practices
Always run yarn lint and yarn test before committing code.
Additional Resources