|
| 1 | +--- |
| 2 | +name: fractal-file-structuring |
| 3 | +description: "Use when creating, moving, splitting, or organizing TypeScript files and folders. Applies fractal tree file-structuring rules which reduce the cognitive overhead of choosing where to put files and ultimately navigating a codebase (once the structure is established and understood)." |
| 4 | +license: MIT |
| 5 | +metadata: |
| 6 | + triggers: |
| 7 | + type: domain |
| 8 | + enforcement: suggest |
| 9 | + priority: high |
| 10 | + keywords: |
| 11 | + - TypeScript |
| 12 | + - JavaScript |
| 13 | + - file structure |
| 14 | + - folder structure |
| 15 | + - create file |
| 16 | + - create folder |
| 17 | + - split file |
| 18 | + - shared folder |
| 19 | + intent-patterns: |
| 20 | + - "\\b(create|add|move|split|organize|refactor)\\b.*?\\b(file|folder|directory|module|component|hook|type|helper)\\b" |
| 21 | + - "\\b(file|folder|directory)\\b.*?\\b(structure|layout|organization|placement)\\b" |
| 22 | +--- |
| 23 | + |
| 24 | +# Fractal File Structuring |
| 25 | + |
| 26 | +TypeScript and JavaScript files should be organised in a fractal tree structure. Use this skill when deciding where to create, move, split, or organize files and folders in a TypeScript or JavaScript workspace. |
| 27 | + |
| 28 | +This guidance is based on HASH's file-structuring approach: https://hash.dev/blog/file-structuring |
| 29 | + |
| 30 | +## Scope |
| 31 | + |
| 32 | +Apply this skill to TypeScript and JavaScript source files, including modules, components, hooks, helpers, types, tests, scripts, and entry points. |
| 33 | + |
| 34 | +## Core Rules |
| 35 | + |
| 36 | +### Use kebab-case names |
| 37 | + |
| 38 | +Use kebab-case for all TypeScript and JavaScript file and folder names. |
| 39 | + |
| 40 | +```text |
| 41 | +create-worker-factory.ts |
| 42 | +playback-settings-menu.tsx |
| 43 | +button.tsx |
| 44 | +``` |
| 45 | + |
| 46 | +Avoid PascalCase, camelCase, and mixed-case file names, even for React components. |
| 47 | + |
| 48 | +### Do not create index files |
| 49 | + |
| 50 | +Do not add `index.ts`, `index.tsx`, `index.js`, or `index.jsx` files for folder imports. Prefer explicit file entry points with meaningful names. |
| 51 | + |
| 52 | +If a subtree needs a public entry point, name that file after the concept it exposes (e.g. `schema.ts`) |
| 53 | + |
| 54 | +### Treat each file as a mini-library |
| 55 | + |
| 56 | +A file should expose one or more named exports with a shared semantic purpose. The file name should summarize that purpose (e.g. `users.ts`) |
| 57 | + |
| 58 | +If a file contains only one main export, prefer naming the file after that export in kebab-case (e.g. `create-user.ts`) |
| 59 | + |
| 60 | +Avoid default exports unless a framework or external API requires them. |
| 61 | + |
| 62 | +### Split outgrown files into private subtrees |
| 63 | + |
| 64 | +When a file becomes too large or contains implementation details worth extracting, create a same-named folder next to it and move private pieces there. |
| 65 | + |
| 66 | +```text |
| 67 | +editor-view.tsx # public mini-library: the component other files import |
| 68 | +editor-view/ |
| 69 | + panels.tsx # private entry point imported by editor-view.tsx |
| 70 | + panels/ |
| 71 | + simulate-view.tsx # private to panels.tsx |
| 72 | + calculate-timeline-range.ts # private helper used only by editor-view.tsx |
| 73 | + create-panel-state.ts # private helper used only by editor-view.tsx |
| 74 | +``` |
| 75 | + |
| 76 | +Only `editor-view.tsx` should import from direct child mini-libraries such as `editor-view/panels.tsx` and `editor-view/calculate-timeline-range.ts`. Only `editor-view/panels.tsx` should import from `editor-view/panels/*.tsx`. Other files should import from `editor-view.tsx`, not from its private subtree. This keeps `editor-view.tsx` as the API boundary and makes `editor-view/` read as its implementation. |
| 77 | + |
| 78 | +If `editor-view/calculate-timeline-range.ts` grows and needs its own private implementation files, create `editor-view/calculate-timeline-range/`. Only `editor-view/calculate-timeline-range.ts` should import from that deeper subtree. |
| 79 | + |
| 80 | +```text |
| 81 | +editor-view/ |
| 82 | + calculate-timeline-range.ts |
| 83 | + calculate-timeline-range/ |
| 84 | + clamp-time.ts # private to calculate-timeline-range.ts |
| 85 | + get-visible-duration.ts # private to calculate-timeline-range.ts |
| 86 | +``` |
| 87 | + |
| 88 | +### Keep private subtrees private |
| 89 | + |
| 90 | +Do not import directly from another file's implementation folder. |
| 91 | + |
| 92 | +```typescript |
| 93 | +// Avoid: reaches into another file's private subtree |
| 94 | +import { SimulateView } from "../editor-view/panels/simulate-view"; |
| 95 | + |
| 96 | +// Prefer (1): import from a public mini-library (if it is conceptually part of editor-view) |
| 97 | +import { EditorView } from "../editor-view"; |
| 98 | + |
| 99 | +// Prefer (2): move shared code to a shared folder (if it is NOT conceptually part of editor-view) |
| 100 | +import { Button } from "../shared/button"; |
| 101 | +``` |
| 102 | + |
| 103 | +If a resource must be available outside the subtree, re-export it from the subtree root only when it is part of that root's public concept. If it is independently useful to sibling branches, move it to an appropriate `shared/` folder instead. |
| 104 | + |
| 105 | +### Put shared resources at the closest fork |
| 106 | + |
| 107 | +When multiple sibling branches need the same helper, type, component, constant, or hook, place it in the nearest applicable `shared/` folder. |
| 108 | + |
| 109 | +```text |
| 110 | +editor-view.tsx |
| 111 | +editor-view/ |
| 112 | + shared/ |
| 113 | + duration-label.tsx # used by both panels.tsx and bottom-section.tsx |
| 114 | + playback-time.ts # shared formatting/parsing logic for this subtree |
| 115 | + panels.tsx # imports from panels/ |
| 116 | + panels/ |
| 117 | + simulate-view.tsx # private to panels.tsx |
| 118 | + bottom-section.tsx # imports from bottom-section/ |
| 119 | + bottom-section/ |
| 120 | + bottom-bar.tsx # private to bottom-section.tsx |
| 121 | +``` |
| 122 | + |
| 123 | +Place shared files as deep as possible while still covering all current consumers. Do not move something to a high-level shared folder just because it might be reused later. |
| 124 | + |
| 125 | +Here `editor-view.tsx` imports `./editor-view/panels` and `./editor-view/bottom-section`. `panels.tsx` may import `./panels/simulate-view` and `./shared/duration-label`; `bottom-section.tsx` may import `./bottom-section/bottom-bar` and `./shared/duration-label`. Nothing else should import from `panels/` or `bottom-section/` directly. |
| 126 | + |
| 127 | +Shared files are mini-libraries too. A shared file can have its own private same-named subtree, and those internals should remain private to that shared file. |
| 128 | + |
| 129 | +```text |
| 130 | +editor-view/ |
| 131 | + shared/ |
| 132 | + playback-time.ts # public to editor-view/* branches |
| 133 | + playback-time/ |
| 134 | + parse-playback-time.ts # private to playback-time.ts |
| 135 | + format-playback-time.ts # private to playback-time.ts |
| 136 | +``` |
| 137 | + |
| 138 | +If later only `bottom-bar.tsx` uses `duration-label.tsx`, move it beside `bottom-bar.tsx` or under `bottom-bar/`. The folder structure should describe current consumers, not preserve old sharing. |
| 139 | + |
| 140 | +### Use relative imports within a workspace |
| 141 | + |
| 142 | +For imports inside the same workspace, use relative paths. Do not introduce workspace-local aliases just to shorten paths. |
| 143 | + |
| 144 | +Imports from other workspaces should use the package name. |
| 145 | + |
| 146 | +### Co-locate unit tests |
| 147 | + |
| 148 | +Place unit tests next to the file they cover. |
| 149 | + |
| 150 | +```text |
| 151 | +foo.ts |
| 152 | +foo.test.ts |
| 153 | +``` |
| 154 | + |
| 155 | +If a private extracted file needs direct tests, place those tests next to that extracted file. |
| 156 | + |
| 157 | +```text |
| 158 | +editor-view.tsx |
| 159 | +editor-view.test.tsx |
| 160 | +editor-view/ |
| 161 | + calculate-timeline-range.ts |
| 162 | + calculate-timeline-range.test.ts |
| 163 | +``` |
| 164 | + |
| 165 | +Prefer testing through the public mini-library when that gives enough coverage. Add direct tests for private extracted files when the logic is complex enough that tests through the owner would be indirect or brittle. |
| 166 | + |
| 167 | +### Match the current shape |
| 168 | + |
| 169 | +Organize files for the code's current relationships, not speculative future reuse. Moving files later is expected and cheaper than adding premature structure now. |
| 170 | + |
| 171 | +## Decision Checklist |
| 172 | + |
| 173 | +Before creating a TypeScript or JavaScript file or folder: |
| 174 | + |
| 175 | +1. Identify the semantic concept the file represents. |
| 176 | +2. Name the file or folder in kebab-case. |
| 177 | +3. If extracting from an existing file, put private implementation files under a same-named folder. |
| 178 | +4. If multiple current branches need the resource, put it in the nearest `shared/` folder. |
| 179 | +5. Avoid `index` files and implicit folder imports. |
| 180 | +6. Use relative imports within the workspace. |
| 181 | +7. Co-locate tests with the file under test. |
| 182 | + |
| 183 | +## When Unsure |
| 184 | + |
| 185 | +Choose the location that communicates the file's current consumers and API boundary most clearly: |
| 186 | + |
| 187 | +- Private implementation detail: place it under the owning file's same-named folder, and import it only from that owner. |
| 188 | +- Named mini-library: create a normal named file when the concept has its own purpose and exports a small API for nearby consumers. |
| 189 | +- Shared mini-library: place the named file in the closest `shared/` folder when multiple branches need that API. |
| 190 | +- Subtree entry point: expose the public API from a named root file, and keep any deeper implementation files private to that root. |
| 191 | + |
| 192 | +Do not add broad `components`, `hooks`, `utils`, `types`, or `services` folders unless absolutely necessary. If they exist, these folders MUST only be imported from by files called `components.ts`, `hooks.ts`, etc. |
0 commit comments