Skip to content

Instantly share code, notes, and snippets.

@sujan-s
Last active July 21, 2025 01:31
Show Gist options
  • Select an option

  • Save sujan-s/ca6925e897a462bcdd2ac966c66d191a to your computer and use it in GitHub Desktop.

Select an option

Save sujan-s/ca6925e897a462bcdd2ac966c66d191a to your computer and use it in GitHub Desktop.
I like to format my files juuuust the way I want it, and most of it is handled by a custom code style setup in Webstorm. This script handles the rest—organise imports by customisable categories, column-indent type and interface blocks, and align the `?` character for easy skimming.
const fs = require("fs");
const path = require("path");
const os = require("os");
/**
* Loads configuration by searching upwards from a starting directory for a .seriseirc file.
* Falls back to default values if no config file is found.
* @param {string} startPath The path of the file being processed.
* @returns {object} The resolved configuration object.
*/
const loadConfig = (startPath) => {
const defaultConfig = {
TO_COLUMN_WIDTH: 120,
HEADER_CHAR: "=",
groups: [
{ name: "// EXTERNAL ", matchers: ["\"react\"", "'react'", "next/", "dnd-kit/", "zustand", "framer-motion", "tiptap", "axios", "tanstack", "vite", "path", "tauri", "react-router-dom", "@react-oauth/google"] },
{ name: "// FICTOAN ", matchers: ["fictoan-react"] },
{ name: "// CONTEXTS ", matchers: ["contexts/"] },
{ name: "// COMPONENTS ", matchers: ["components/"] },
{ name: "// CONFIGS ", matchers: ["configs/"] },
{ name: "// LIB ", matchers: ["lib/"] },
{ name: "// LOGIC ", matchers: ["logic/"] },
{ name: "// SERVICES ", matchers: ["services/"] },
{ name: "// DATA ", matchers: ["mock-data/"] },
{ name: "// HOOKS ", matchers: ["hooks/"] },
{ name: "// ASSETS ", matchers: ["assets/"] },
{ name: "// STORES ", matchers: ["store/"] },
{ name: "// STYLES ", matchers: ["styles/", ".css"] },
{ name: "// TYPES ", matchers: ["types", "typings"] },
{ name: "// UTILS ", matchers: ["utils/"] },
{ name: "// OTHER ", matchers: [] },
],
};
let currentDir = path.dirname(startPath);
let configPath = null;
// Search upwards from the current directory to the root
while (currentDir !== path.parse(currentDir).root) {
const potentialPath = path.join(currentDir, ".seriseirc");
if (fs.existsSync(potentialPath)) {
configPath = potentialPath;
break;
}
currentDir = path.dirname(currentDir);
}
if (!configPath) {
const rootPath = path.join(path.parse(startPath).root, ".seriseirc");
if (fs.existsSync(rootPath)) {
configPath = rootPath;
}
}
if (!configPath) {
return defaultConfig;
}
let config = JSON.parse(JSON.stringify(defaultConfig));
try {
const fileContent = fs.readFileSync(configPath, "utf8");
const lines = fileContent.split("\n");
let inGroupsSection = false;
let customGroups = [];
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith("#")) {
continue;
}
if (/^\[\s*groups\s*\]$/.test(trimmedLine)) {
inGroupsSection = true;
continue;
}
const parts = trimmedLine.split("=");
const key = parts[0].trim();
const value = parts.slice(1).join("=").trim();
if (!key) continue;
if (inGroupsSection) {
const name = key;
// The script now respects quotes in the config file for more precise matching.
const matchers = value
? value.split(",").map(m => m.trim()).filter(Boolean)
: [];
customGroups.push({ name, matchers });
} else {
if (key === "TO_COLUMN_WIDTH") {
config.TO_COLUMN_WIDTH = parseInt(value, 10) || defaultConfig.TO_COLUMN_WIDTH;
} else if (key === "HEADER_CHAR") {
config.HEADER_CHAR = value || defaultConfig.HEADER_CHAR;
}
}
}
if (customGroups.length > 0) {
config.groups = customGroups;
}
} catch (e) {
console.error(`Error reading or parsing ${configPath}:`, e);
return defaultConfig;
}
return config;
};
/**
* Group all import statements found in the file based on the provided configuration.
*
* @param {string} fileContent The full file text.
* @param {object} config The configuration object.
* @returns {string} The file text with the import block replaced by grouped imports.
*/
const groupImports = (fileContent, config) => {
const { TO_COLUMN_WIDTH, HEADER_CHAR, groups: groupDefinitions } = config;
const lines = fileContent.split("\n");
const headerRegex = new RegExp(`^\\s*//.*\\s[${HEADER_CHAR}]`);
const importStart = lines.findIndex(line => {
const trimmed = line.trim();
return trimmed !== '' && (headerRegex.test(trimmed) || trimmed.startsWith("import ") || trimmed.startsWith("//") || trimmed.startsWith("/*"));
});
if (importStart === -1) return fileContent;
const imports = [];
let commentBuffer = [];
let importEnd = -1;
for (let i = importStart; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine === '') continue;
if (headerRegex.test(trimmedLine)) continue;
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*')) {
commentBuffer.push(line);
continue;
}
if (trimmedLine.startsWith('import ')) {
let currentImportLines = [...commentBuffer, line];
commentBuffer = [];
let braceDepth = (trimmedLine.match(/\{/g) || []).length - (trimmedLine.match(/\}/g) || []).length;
if (!trimmedLine.endsWith(';') || braceDepth > 0) {
for (let j = i + 1; j < lines.length; j++) {
const nextLine = lines[j];
currentImportLines.push(nextLine);
braceDepth += (nextLine.match(/\{/g) || []).length;
braceDepth -= (nextLine.match(/\}/g) || []).length;
if (nextLine.includes(';') && braceDepth === 0) {
i = j;
break;
}
}
}
imports.push(currentImportLines.join('\n'));
} else {
// This is the first line of code after the imports.
importEnd = i;
// If there were comments before this line, they belong to this code,
// so the import block ended before them.
if (commentBuffer.length > 0) {
const firstCommentLine = commentBuffer[0];
const firstCommentIndex = lines.indexOf(firstCommentLine, importStart);
if (firstCommentIndex !== -1) {
importEnd = firstCommentIndex;
}
}
break;
}
}
if (importEnd === -1) {
importEnd = lines.length;
}
const createHeader = (text) => {
if (text.length >= TO_COLUMN_WIDTH) return text;
return (text.trim() + " ").padEnd(TO_COLUMN_WIDTH, HEADER_CHAR);
};
const groups = groupDefinitions.map(group => {
const isOtherGroup = group.name.includes("OTHER") || group.matchers.length === 0;
const matcher = isOtherGroup ? () => true : (line) => group.matchers.some(pkg => line.includes(pkg));
return [createHeader(group.name), matcher];
});
const result = [];
const processed = new Set();
for (const [header, matcher] of groups) {
const matches = imports.filter(imp => {
if (processed.has(imp)) return false;
const fullImport = imp.replace(/\n\s*/g, " ");
return matcher(fullImport);
});
if (matches.length) {
result.push(header);
result.push(...matches.sort());
result.push("");
matches.forEach(imp => processed.add(imp));
}
}
const preImports = lines.slice(0, importStart);
const postImports = lines.slice(importEnd);
const newLines = [...preImports, ...result, ...postImports];
return newLines.join("\n");
};
/**
* Recursively formats the content of a type or interface block, handling nested objects.
* @param {string[]} blockContentLines - The lines of code inside the { ... } block.
* @param {string} baseIndent - The base indentation of the parent block.
* @returns {string[]} The formatted lines of code.
*/
const formatBlockContent = (blockContentLines, baseIndent) => {
const properties = [];
let currentPropLines = [];
let braceDepth = 0;
// Delimit properties, correctly handling multi-line nested objects.
for (const line of blockContentLines) {
if (line.trim() === '' && currentPropLines.length === 0) continue;
currentPropLines.push(line);
braceDepth += (line.match(/\{/g) || []).length;
braceDepth -= (line.match(/\}/g) || []).length;
if (braceDepth === 0) {
properties.push(currentPropLines);
currentPropLines = [];
}
}
if (currentPropLines.length > 0) properties.push(currentPropLines);
// Calculate alignment for all direct child properties in this block
let maxKeyLength = 0;
let hasOptional = false;
for (const prop of properties) {
const firstLine = prop[0].trim();
if (firstLine.startsWith('//') || firstLine.startsWith('/*')) continue;
const match = firstLine.match(/^(\[.+?\]|[\w$]+)(\s*\?)?\s*:/);
if (match) {
maxKeyLength = Math.max(maxKeyLength, match[1].length);
if (match[2]) hasOptional = true;
}
}
const propertyIndent = baseIndent + " ";
const formattedLines = [];
// Format each property
for (const prop of properties) {
const firstLine = prop[0];
const trimmedFirstLine = firstLine.trim();
if (trimmedFirstLine.startsWith('//') || trimmedFirstLine.startsWith('/*')) {
formattedLines.push(propertyIndent + trimmedFirstLine);
continue;
}
const match = trimmedFirstLine.match(/^(\[.+?\]|[\w$]+)(\s*\?)?\s*:\s*(.*)$/);
if (!match) {
formattedLines.push(...prop.map(l => l));
continue;
}
const [, key, optional, value] = match;
const paddedKey = key.padEnd(maxKeyLength);
let alignedFirstLine;
if (hasOptional) {
alignedFirstLine = `${propertyIndent}${paddedKey}${optional ? ' ?' : ' '} : ${value}`;
} else {
alignedFirstLine = `${propertyIndent}${paddedKey} : ${value}`;
}
if (prop.length === 1) {
formattedLines.push(alignedFirstLine);
} else {
const firstLineHeader = alignedFirstLine.substring(0, alignedFirstLine.lastIndexOf('{') + 1);
formattedLines.push(firstLineHeader);
const nestedContent = prop.slice(1, -1);
if (nestedContent.length > 0) {
const formattedNestedContent = formatBlockContent(nestedContent, propertyIndent);
formattedLines.push(...formattedNestedContent);
}
const lastLine = prop[prop.length - 1];
formattedLines.push(propertyIndent + lastLine.trim());
}
}
return formattedLines;
};
/**
* Finds and formats all type and interface blocks in the file.
* @param {string} fileContent The full file text.
* @returns {string} The file text with formatted interfaces/types.
*/
const formatTypesAndInterfaces = (fileContent) => {
const lines = fileContent.split("\n");
const newLines = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
const trimmed = line.trim();
const isBlockStart = /^\s*(export\s+)?(interface|type)\s+[\w$]+.*\{/.test(trimmed);
if (isBlockStart) {
const baseIndent = (line.match(/^\s*/) || [""])[0];
let blockStartIndex = i;
let braceDepth = 0;
let blockEndIndex = -1;
for (let j = i; j < lines.length; j++) {
braceDepth += (lines[j].match(/\{/g) || []).length;
braceDepth -= (lines[j].match(/\}/g) || []).length;
if (braceDepth === 0) {
blockEndIndex = j;
break;
}
}
if (blockEndIndex !== -1) {
const blockHeader = lines[blockStartIndex];
const blockFooter = lines[blockEndIndex];
const blockContent = lines.slice(blockStartIndex + 1, blockEndIndex);
newLines.push(blockHeader);
if (blockContent.length > 0) {
newLines.push(...formatBlockContent(blockContent, baseIndent));
}
newLines.push(blockFooter);
i = blockEndIndex + 1;
continue;
}
}
newLines.push(line);
i++;
}
return newLines.join("\n");
};
/**
* Write file atomically to avoid conflicts with IDEs
* @param {string} filePath - Path to write to
* @param {string} content - Content to write
*/
const writeFileAtomic = (filePath, content) => {
const tempFile = path.join(os.tmpdir(), `${path.basename(filePath)}.${Date.now()}.tmp`);
try {
fs.writeFileSync(tempFile, content, "utf8");
fs.renameSync(tempFile, filePath);
} catch (error) {
try {
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
} catch (e) {
// Ignore cleanup errors
}
throw error;
}
};
/**
* Read the file, apply the import grouping and type/interface formatting,
* then write the formatted code back to the file.
*
* @param {string} filePath The path of the file to process.
*/
const processFile = (filePath) => {
try {
// The loadConfig function now takes the full file path to start its search.
const config = loadConfig(filePath);
let code = fs.readFileSync(filePath, "utf8");
const originalCode = code;
code = groupImports(code, config);
code = formatTypesAndInterfaces(code);
if (code !== originalCode) {
writeFileAtomic(filePath, code);
}
} catch (error) {
if (error.code === "EBUSY") {
setTimeout(() => processFile(filePath), 100);
} else {
console.error(`Error processing ${filePath}:`, error);
}
}
};
const filePath = process.argv[2];
if (!filePath) {
console.error("Please provide a file path to format.");
process.exit(1);
}
processFile(filePath);
@sujan-s

sujan-s commented Jul 20, 2025

Copy link
Copy Markdown
Author

It looks for a .seriseirc file at the root of your project, where you can add your configs. Here’s a good start:

# .seriseirc
TO_COLUMN_WIDTH = 120
HEADER_CHAR = =

[groups]
# Use quotes to match the exact package name
// REACT CORE = "react", "react-router-dom", "next/", tanstack, tauri, "react-use", dnd-kit, react-dom

// UI = "fictoan-react", framer
// LOCAL COMPONENTS = components/, Page
// CONFIGS = configs/,
// STATE MANAGEMENT = zustand, store/
// LIB = lib/
// HOOKS = hooks/,
// UTILS = utils/,
// ASSETS = assets
// TYPES = types, typings
// SERVICES = services/
// STYLES = .css, styles/
// OTHER =

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment