stackthread 0.1.0.dev2__tar.gz → 0.1.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackthread-0.1.0.dev3/PKG-INFO +532 -0
- stackthread-0.1.0.dev3/README.md +506 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/pyproject.toml +1 -4
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/__init__.py +1 -1
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/extract.py +494 -494
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lineage.py +965 -965
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/static/app.js +1 -1
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_extend.py +25 -25
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_frameworks.py +8 -8
- stackthread-0.1.0.dev2/PKG-INFO +0 -51
- stackthread-0.1.0.dev2/README.md +0 -22
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/.gitignore +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/LICENSE +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/__main__.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/_vendor/THIRD_PARTY_NOTICES.txt +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/_vendor/extract.mjs +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/adapters.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/cli.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/config.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/detect.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/__init__.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/js/__init__.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/js/aliases.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/js/build.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/js/worker.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/__init__.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/build.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/frameworks.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/orm.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/lang/python/resolve.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/link.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/logs.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/model.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/paths.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/project.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/report.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/rules.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/server/__init__.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/server/app.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/server/edit.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/server/security.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/static/THIRD_PARTY_NOTICES.txt +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/static/app.css +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/src/stackthread/static/index.html +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/conftest.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_cli.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_config.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_edit.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_lineage.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_link.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_logs.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_orm.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_paths.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_security.py +0 -0
- {stackthread-0.1.0.dev2 → stackthread-0.1.0.dev3}/tests/test_server.py +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stackthread
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
|
+
Summary: Click any element in your running web app and follow it through your code: component, handler, request, endpoint, database.
|
|
5
|
+
Project-URL: Vite plugin, https://www.npmjs.com/package/@stackthread/vite
|
|
6
|
+
Author: The stackthread authors
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: code-navigation,developer-tools,django,fastapi,react,static-analysis,vite
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# stackthread
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/stackthread/)
|
|
30
|
+
[](https://www.npmjs.com/package/@stackthread/vite)
|
|
31
|
+
[](https://pypi.org/project/stackthread/)
|
|
32
|
+
[](#licence)
|
|
33
|
+
|
|
34
|
+
**Click any element in your running web app and follow it through your code**: the component that drew it, the
|
|
35
|
+
handler it runs, the request it sends, the endpoint that answers, the functions behind it and the tables they
|
|
36
|
+
touch. Open any of them at the right line, edit, save. The map rebuilds as you type.
|
|
37
|
+
|
|
38
|
+
stackthread is for the moment every developer knows: you are looking at a button in the browser and you need the
|
|
39
|
+
code behind it, all of it, across the frontend and the backend.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<button> Mark as paid OrderPage.tsx:22
|
|
43
|
+
payNow() onClick handler OrderPage.tsx:12
|
|
44
|
+
shop.pay() used by payNow shop.ts:25
|
|
45
|
+
POST /api/orders/{}/pay/ sent by shop.pay
|
|
46
|
+
POST /api/orders/{pk}/pay/ drf endpoint urls.py:8
|
|
47
|
+
OrderViewSet.pay() views.py:27
|
|
48
|
+
services.pay() services.py:28
|
|
49
|
+
Order (shop_orders) written by pay models.py:13
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **Works on your machine only.** No account, no telemetry, no network calls beyond `127.0.0.1`.
|
|
53
|
+
- **Reads code, never runs it.** Your backend does not need to be running, and nothing is imported or executed.
|
|
54
|
+
- **Honest about certainty.** Every link is marked *exact*, *inferred* or *heuristic*.
|
|
55
|
+
- **No Python dependencies.** Node.js reads the frontend with a bundled parser.
|
|
56
|
+
|
|
57
|
+
> **Status: early preview (0.1 development releases).** Install with `pip install --pre stackthread` and
|
|
58
|
+
> `npm install --save-dev @stackthread/vite@next` until 0.1.0 is released.
|
|
59
|
+
|
|
60
|
+
## Contents
|
|
61
|
+
|
|
62
|
+
- [Supported stacks](#supported-stacks)
|
|
63
|
+
- [Install](#install)
|
|
64
|
+
- [Quick start](#quick-start)
|
|
65
|
+
- [Using the workbench](#using-the-workbench)
|
|
66
|
+
- [Command line](#command-line)
|
|
67
|
+
- [Configuration](#configuration)
|
|
68
|
+
- [Project layouts](#project-layouts)
|
|
69
|
+
- [Teaching stackthread your domain](#teaching-stackthread-your-domain)
|
|
70
|
+
- [Troubleshooting](#troubleshooting)
|
|
71
|
+
- [Security and privacy](#security-and-privacy)
|
|
72
|
+
- [Limitations](#limitations)
|
|
73
|
+
|
|
74
|
+
## Supported stacks
|
|
75
|
+
|
|
76
|
+
| Layer | Supported |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Backend | FastAPI, Django (URLconf, function and class-based views), Django REST framework, django-ninja |
|
|
79
|
+
| Data | Django ORM, SQLAlchemy 2, SQLModel, Flask-SQLAlchemy queries, MongoDB collections (heuristic) |
|
|
80
|
+
| Frontend | React (JSX and TSX) on Vite 5 or later |
|
|
81
|
+
| Routing | React Router (route objects and `<Routes>`), TanStack Router (code-based and file-based) |
|
|
82
|
+
| HTTP | `fetch`, axios and axios instances, ky, ofetch, openapi-fetch, and your own request wrappers |
|
|
83
|
+
|
|
84
|
+
What each framework detector understands, in detail:
|
|
85
|
+
|
|
86
|
+
- **FastAPI:** `FastAPI()` and `APIRouter(prefix=...)` wherever they are created; `get`, `post`, `put`, `patch`,
|
|
87
|
+
`delete`, `options`, `head` and `api_route`; `include_router` nested to any depth and `add_api_route`;
|
|
88
|
+
`Depends` and `Security`, shown as "injected into"; `response_model=` and parameter and return types.
|
|
89
|
+
- **Django:** `ROOT_URLCONF` followed through `include()`; `path()` with converters (`<int:pk>` becomes `{pk}`)
|
|
90
|
+
and `re_path()` with named groups; function views and class-based views through `as_view()`, one endpoint per
|
|
91
|
+
HTTP method, including the generic views.
|
|
92
|
+
- **Django REST framework:** `SimpleRouter` and `DefaultRouter`; `ViewSet`, `GenericViewSet`, `ModelViewSet`,
|
|
93
|
+
`ReadOnlyModelViewSet` and the mixins; `@action(detail=..., methods=[...], url_path=...)` and `lookup_field`;
|
|
94
|
+
`APIView` methods and `@api_view`.
|
|
95
|
+
- **django-ninja:** `NinjaAPI()` mounted with `api.urls`; `Router()` with `add_router(prefix, router)`, as an
|
|
96
|
+
object or an import string; route decorators and `api_operation`.
|
|
97
|
+
- **Data:** model classes and their tables, and which functions read or write them: `Model.objects` chains,
|
|
98
|
+
`get_object_or_404`, `save()` and `delete()`; SQLAlchemy `select`, `insert`, `update`, `delete` and session
|
|
99
|
+
calls; Flask-SQLAlchemy `Model.query`; MongoDB collection methods.
|
|
100
|
+
- **React:** components (functions, `memo`, `forwardRef`, higher-order components, classes), hooks, props,
|
|
101
|
+
`useState`, `useReducer`, `useMemo`, `useCallback`, `useRef`, context; every JSX element with its handlers,
|
|
102
|
+
shown values, render condition and the list it is rendered from. A submit button follows its form's `onSubmit`;
|
|
103
|
+
a prop is followed to the parent that passed it.
|
|
104
|
+
- **HTTP:** literal, template and concatenated URLs; `axios.create({ baseURL })` instances defined anywhere;
|
|
105
|
+
typed `openapi-fetch` paths; wrapper functions that pass their URL on; import aliases from `tsconfig.json`,
|
|
106
|
+
`jsconfig.json` and `vite.config.*`; Vite `server.proxy` keys tell stackthread which requests reach the backend.
|
|
107
|
+
|
|
108
|
+
Python is parsed with the standard library `ast` (Python 3.14 syntax accepted on older interpreters).
|
|
109
|
+
JavaScript and TypeScript are parsed with a bundled Babel, so your project's compiler versions do not matter.
|
|
110
|
+
|
|
111
|
+
## Install
|
|
112
|
+
|
|
113
|
+
| You need | Version | Why |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| Python | 3.11 or later | runs the indexer and the local server |
|
|
116
|
+
| Node.js | 22.18 or later | reads JavaScript and TypeScript files |
|
|
117
|
+
| Vite | 5 or later | makes the elements of your app clickable |
|
|
118
|
+
|
|
119
|
+
Install the Python package where your backend's tools live: in the backend's virtual environment, as a dev
|
|
120
|
+
dependency, or globally.
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
pip install --pre stackthread # or: uv add --dev --prerelease allow stackthread
|
|
124
|
+
# or: pipx install --pip-args=--pre stackthread
|
|
125
|
+
npm install --save-dev @stackthread/vite@next
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Quick start
|
|
129
|
+
|
|
130
|
+
**1. Add the Vite plugin**, before the React plugin:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// vite.config.ts
|
|
134
|
+
import stackthread from '@stackthread/vite'
|
|
135
|
+
import react from '@vitejs/plugin-react'
|
|
136
|
+
import { defineConfig } from 'vite'
|
|
137
|
+
|
|
138
|
+
export default defineConfig({
|
|
139
|
+
plugins: [stackthread(), react()],
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In `vite dev` it marks every JSX element with its source location and serves the workbench. In `vite build` it
|
|
144
|
+
does nothing: production bundles contain nothing from stackthread.
|
|
145
|
+
|
|
146
|
+
**2. Describe the project.** From the folder that holds both your backend and your frontend:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
stackthread init
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`init` finds Python backends (by `manage.py`, or a `pyproject.toml`, `requirements.txt`, `setup.cfg` or
|
|
153
|
+
`Pipfile` that names a supported framework) and React-on-Vite frontends (a `package.json` with `react` and
|
|
154
|
+
`vite`), up to four folders deep, and writes `stackthread.toml`:
|
|
155
|
+
|
|
156
|
+
```toml
|
|
157
|
+
[project]
|
|
158
|
+
name = "shop"
|
|
159
|
+
|
|
160
|
+
[[sources]]
|
|
161
|
+
name = "api"
|
|
162
|
+
lang = "python"
|
|
163
|
+
root = "backend"
|
|
164
|
+
exclude = ["**/migrations/**"]
|
|
165
|
+
|
|
166
|
+
[[sources]]
|
|
167
|
+
name = "web"
|
|
168
|
+
lang = "typescript"
|
|
169
|
+
root = "web"
|
|
170
|
+
include = ["src/**/*"]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The same settings can live under `[tool.stackthread]` in `pyproject.toml`.
|
|
174
|
+
|
|
175
|
+
**3. Run it**, next to your usual dev servers:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
stackthread serve # reads the code, watches it, serves the workbench on 127.0.0.1
|
|
179
|
+
npm run dev # your app, as usual
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**4. Open the workbench** at `http://localhost:5173/__stackthread/` (use your Vite port), press **Inspect** and
|
|
183
|
+
click anything in your app.
|
|
184
|
+
|
|
185
|
+
## Using the workbench
|
|
186
|
+
|
|
187
|
+
The workbench shows your running app on the left and its code on the right. Its top bar switches between
|
|
188
|
+
**Inspector**, **Map** and **Findings**.
|
|
189
|
+
|
|
190
|
+
### Inspector
|
|
191
|
+
|
|
192
|
+
Turn on **Inspect** (or press **Alt+Shift+S**) and click a button, a field, a heading or any text. The panel
|
|
193
|
+
shows its thread in five layers:
|
|
194
|
+
|
|
195
|
+
| Layer | What it lists |
|
|
196
|
+
|---|---|
|
|
197
|
+
| On screen | the element and the components that render it |
|
|
198
|
+
| In the browser | the handlers and state it uses, and the functions they call |
|
|
199
|
+
| Over the wire | the requests those functions send |
|
|
200
|
+
| On the server | the endpoint that answers, and the functions and dependencies behind it |
|
|
201
|
+
| Data | the models and tables those functions read or write |
|
|
202
|
+
|
|
203
|
+
Each row names a real definition with its file and line, and why it is there ("onClick handler", "sent by
|
|
204
|
+
shop.pay", "written by pay"). Rows marked *reads* are data the element shows; the rest are actions. From the panel:
|
|
205
|
+
|
|
206
|
+
- **Show it in the page** highlights the element again in the app.
|
|
207
|
+
- **Show in the map** opens the focus view around a row.
|
|
208
|
+
- **Copy** puts the whole thread on the clipboard as text, for a ticket, a review or an AI assistant.
|
|
209
|
+
|
|
210
|
+
The left sidebar lists the **pages** found in your router and the **views** you defined (see
|
|
211
|
+
[Configuration](#configuration)); click one to open it in the app.
|
|
212
|
+
|
|
213
|
+
You can also inspect without the workbench: inside your app, **Alt+Shift+S** turns on inspect mode and a panel
|
|
214
|
+
docks on the right side of the page.
|
|
215
|
+
|
|
216
|
+
### Code pane
|
|
217
|
+
|
|
218
|
+
Every row opens its file at the line in the code pane, with syntax highlighting for Python, TypeScript and
|
|
219
|
+
JavaScript. **Ctrl+S** (or **Cmd+S**) saves. stackthread:
|
|
220
|
+
|
|
221
|
+
- refuses the save if the file changed on disk since you opened it, so your editor's work is never overwritten;
|
|
222
|
+
- never writes secrets or lock files (`.env*`, `*.pem`, `*.key`, lock files, `.git/` and more);
|
|
223
|
+
- can run your formatter after each save (`server.format`).
|
|
224
|
+
|
|
225
|
+
**Show the thread of the definition at this line** answers "who reaches this function?" from any line. The
|
|
226
|
+
button named after your editor opens the file there instead (VS Code, Cursor, Windsurf, Zed, IntelliJ IDEA,
|
|
227
|
+
PyCharm or WebStorm; set `server.editor`).
|
|
228
|
+
|
|
229
|
+
### Map
|
|
230
|
+
|
|
231
|
+
| Tab | What it shows |
|
|
232
|
+
|---|---|
|
|
233
|
+
| Overview | counts, and every page, component, endpoint and model grouped by source |
|
|
234
|
+
| Focus | one definition with everything it calls and everything that reaches it |
|
|
235
|
+
| Endpoints | every endpoint with its method, handler and framework, and where the frontend calls it |
|
|
236
|
+
| Pages | every route, the component it renders and where the route is defined |
|
|
237
|
+
|
|
238
|
+
### Search
|
|
239
|
+
|
|
240
|
+
**Ctrl+K** (or **Cmd+K**) finds components, functions, endpoints, pages and files. **Enter** opens the result in
|
|
241
|
+
the code pane, **Alt+Enter** in the map.
|
|
242
|
+
|
|
243
|
+
### Findings
|
|
244
|
+
|
|
245
|
+
The **Findings** tab lists what `stackthread check` reports: requests that reach no endpoint, imports that break
|
|
246
|
+
your layer rules, files that could not be parsed.
|
|
247
|
+
|
|
248
|
+
### Status and problem reports
|
|
249
|
+
|
|
250
|
+
The status in the top bar always says what stackthread is doing:
|
|
251
|
+
|
|
252
|
+
| Status | Meaning |
|
|
253
|
+
|---|---|
|
|
254
|
+
| Indexing 48/132 files | the first full read, with a progress line under the top bar |
|
|
255
|
+
| Re-indexing OrderPage.tsx | a file changed and is being read again |
|
|
256
|
+
| Live · rev 3 | up to date; the number goes up with every change |
|
|
257
|
+
| Offline, reconnecting | the browser lost the stackthread server; it retries by itself |
|
|
258
|
+
|
|
259
|
+
Click it for the activity panel: recent events, and a **Problems** tab with every warning and error.
|
|
260
|
+
**Report a problem**, there or in the status bar, makes a zip of the logs and diagnostics to attach when you ask
|
|
261
|
+
for help. It never contains source code.
|
|
262
|
+
|
|
263
|
+
### Keyboard
|
|
264
|
+
|
|
265
|
+
| Keys | Action |
|
|
266
|
+
|---|---|
|
|
267
|
+
| Alt+Shift+S | inspect mode on or off, in the workbench and inside your app |
|
|
268
|
+
| Ctrl+K / Cmd+K | search |
|
|
269
|
+
| Enter, Alt+Enter | in search: open in the code pane, open in the map |
|
|
270
|
+
| Ctrl+S / Cmd+S | save in the code pane |
|
|
271
|
+
| Esc | close search, a dialog, or inspect mode in your app |
|
|
272
|
+
|
|
273
|
+
The theme button in the top bar switches between your system theme, light and dark.
|
|
274
|
+
|
|
275
|
+
## Command line
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
stackthread [--config PATH] [--log-level LEVEL | --verbose] <command> [options]
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
| Command | What it does |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `init [--force]` | detect backends and frontends and write `stackthread.toml` |
|
|
284
|
+
| `serve` | serve the workbench on `127.0.0.1`, read the code in the background, follow changes |
|
|
285
|
+
| `scan` | index once and print a summary; `--json FILE` writes the whole graph |
|
|
286
|
+
| `explain FILE:LINE` | the thread through a code location, as text or `--json` |
|
|
287
|
+
| `check` | findings for CI; exits with 1 at or above `--fail-on` (default `error`) |
|
|
288
|
+
| `doctor` | check Node.js, the bundled parser, the UI and the config |
|
|
289
|
+
| `logs` | print, filter and follow the log |
|
|
290
|
+
| `report` | make a problem report zip |
|
|
291
|
+
|
|
292
|
+
`serve` options: `--port N` (default 7341; the next nine are tried when busy), `--no-open`, `--no-watch`,
|
|
293
|
+
`--read-only`.
|
|
294
|
+
|
|
295
|
+
`explain` shows who reaches a function from the screen, which requests lead to it and what data it touches:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
$ stackthread explain backend/shop/services.py:21
|
|
299
|
+
reserve() backend/shop/services.py:21
|
|
300
|
+
|
|
301
|
+
In the browser
|
|
302
|
+
shop.placeOrder() web/src/lib/shop.ts:23 calls POST /api/orders/
|
|
303
|
+
|
|
304
|
+
Over the wire
|
|
305
|
+
POST /api/orders/ web/src/lib/shop.ts:24 sent by shop.placeOrder
|
|
306
|
+
|
|
307
|
+
On the server
|
|
308
|
+
reserve() backend/shop/services.py:21 this definition
|
|
309
|
+
POST /api/orders/ backend/shop/urls.py:8 reaches this definition
|
|
310
|
+
|
|
311
|
+
Data
|
|
312
|
+
Product (shop_product) backend/shop/models.py:4 written by reserve
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### In CI
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
pip install --pre stackthread
|
|
319
|
+
stackthread check # --fail-on warning is stricter; --json for machines
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| Rule | Severity | Meaning |
|
|
323
|
+
|---|---|---|
|
|
324
|
+
| `unmatched-request` | warning | a frontend request matches no endpoint |
|
|
325
|
+
| `layers` | error | an import breaks a `[[rules.layers]]` order |
|
|
326
|
+
| `parse-error` | error | a file could not be parsed |
|
|
327
|
+
| `node-unavailable` | error | frontend sources exist but Node.js 22.18+ was not found |
|
|
328
|
+
| `adapter` | error | an adapter failed to load or raised |
|
|
329
|
+
|
|
330
|
+
Exit codes: 0 success, 1 findings at the threshold, 2 invalid configuration, 70 unexpected error (in the log),
|
|
331
|
+
130 interrupted.
|
|
332
|
+
|
|
333
|
+
## Configuration
|
|
334
|
+
|
|
335
|
+
stackthread reads `stackthread.toml` from the current folder or the nearest parent, or `[tool.stackthread]` in
|
|
336
|
+
`pyproject.toml`. Every key is optional. Unknown keys and wrong types are reported together, with suggestions for
|
|
337
|
+
misspellings. Paths are relative to the project root; `*` matches within a folder and `**` across folders.
|
|
338
|
+
`.gitignore`, `node_modules`, virtual environments and build output are always skipped.
|
|
339
|
+
|
|
340
|
+
```toml
|
|
341
|
+
[project]
|
|
342
|
+
name = "shop" # shown in the workbench title
|
|
343
|
+
|
|
344
|
+
[[sources]] # one per backend or frontend
|
|
345
|
+
name = "api"
|
|
346
|
+
lang = "python" # python | typescript | javascript
|
|
347
|
+
root = "backend" # the folder that contains your top-level package
|
|
348
|
+
exclude = ["**/migrations/**"]
|
|
349
|
+
# include = ["**/*.py"] # default: every file of the language
|
|
350
|
+
# frameworks = ["drf", "django-orm"] # default: detect everything
|
|
351
|
+
|
|
352
|
+
[[sources]]
|
|
353
|
+
name = "web"
|
|
354
|
+
lang = "typescript"
|
|
355
|
+
root = "web"
|
|
356
|
+
include = ["src/**/*"]
|
|
357
|
+
# alias = { "@" = "src" } # default: from tsconfig.json and vite.config.*
|
|
358
|
+
|
|
359
|
+
[http]
|
|
360
|
+
# Requests from "web" starting with /api reach routes of "api" with the prefix replaced:
|
|
361
|
+
links = [{ from = "web", to = "api", client_prefix = "/api", server_prefix = "" }]
|
|
362
|
+
clients = ["request"] # extra functions to treat as HTTP clients
|
|
363
|
+
|
|
364
|
+
[pages]
|
|
365
|
+
params = { orderId = "1" } # sample values, so /orders/$orderId opens as /orders/1
|
|
366
|
+
|
|
367
|
+
[[views]] # named states your router does not express
|
|
368
|
+
group = "Checkout"
|
|
369
|
+
name = "Payment step"
|
|
370
|
+
path = "/checkout?step=payment"
|
|
371
|
+
|
|
372
|
+
[server]
|
|
373
|
+
port = 7341
|
|
374
|
+
editor = "vscode" # vscode | cursor | windsurf | zed | idea | pycharm | webstorm | none
|
|
375
|
+
edit = true # allow saving; `serve --read-only` overrides it
|
|
376
|
+
edit_deny = ["config/*.yaml"] # added to the built-in list of files that are never written
|
|
377
|
+
format = { python = ["ruff", "format", "{file}"], typescript = ["npx", "prettier", "--write", "{file}"] }
|
|
378
|
+
open_browser = true
|
|
379
|
+
|
|
380
|
+
[[rules.layers]] # for `stackthread check`: lower layers never import higher ones
|
|
381
|
+
name = "backend layers"
|
|
382
|
+
order = ["shop.core", "shop.services", "shop.api"]
|
|
383
|
+
|
|
384
|
+
[log]
|
|
385
|
+
level = "debug" # what the log file records
|
|
386
|
+
size_kb = 1024 # size of one file before it is rotated
|
|
387
|
+
keep = 5 # rotated files kept
|
|
388
|
+
|
|
389
|
+
[adapters]
|
|
390
|
+
use = ["tools.stackthread_adapter:Orders"]
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Python test files (`test_*.py`, `*_test.py`, `tests/`) are indexed and tagged: they appear in search but never in a
|
|
394
|
+
server thread.
|
|
395
|
+
|
|
396
|
+
## Project layouts
|
|
397
|
+
|
|
398
|
+
- **Monorepo with `backend/` and `web/`:** run `init` and `serve` at the root. The plugin finds the root by
|
|
399
|
+
looking upwards for `stackthread.toml`.
|
|
400
|
+
- **Several frontends:** one `[[sources]]` per frontend, and the plugin in each Vite app.
|
|
401
|
+
- **Backend and frontend in different repositories:** put both under one folder, or run stackthread in the
|
|
402
|
+
frontend repository with a source `root = "../api"`.
|
|
403
|
+
- **Without the Vite plugin:** `http://127.0.0.1:7341/__stackthread/` serves the same workbench; the map, search,
|
|
404
|
+
code pane and reverse threads work, but there is no live app to click. Django projects that render templates
|
|
405
|
+
get the map, endpoints, models, `explain` and `check`.
|
|
406
|
+
|
|
407
|
+
## Teaching stackthread your domain
|
|
408
|
+
|
|
409
|
+
Every application has meaning no framework detector can see: order stages kept in a database, feature flags,
|
|
410
|
+
translation keys. There are two ways to add it.
|
|
411
|
+
|
|
412
|
+
### Concepts: no code
|
|
413
|
+
|
|
414
|
+
A concept links a `data-*` attribute in your markup to where its value is defined:
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
<button data-i18n="pay_button" onClick={pay}>{t('pay_button')}</button>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```toml
|
|
421
|
+
[[concepts]]
|
|
422
|
+
attribute = "data-i18n"
|
|
423
|
+
label = "Text {value}"
|
|
424
|
+
find = [{ file = "web/src/locales/en.json", pattern = '"{value}":' }]
|
|
425
|
+
mentions = true # also list up to 40 source lines that mention the value
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Clicking the button, or anything inside an element with the attribute, adds a layer "Text pay_button" with the
|
|
429
|
+
line that defines it and the lines that use it.
|
|
430
|
+
|
|
431
|
+
### Adapters: a little Python
|
|
432
|
+
|
|
433
|
+
An adapter is a class in your own code; every method is optional.
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
# tools/stackthread_adapter.py
|
|
437
|
+
from stackthread.adapters import Adapter, Context, View
|
|
438
|
+
from stackthread.model import Finding, Graph, Node
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class Orders(Adapter):
|
|
442
|
+
name = "orders"
|
|
443
|
+
|
|
444
|
+
def views(self, ctx: Context) -> list[View]:
|
|
445
|
+
"""Named states for the sidebar: here, one per order stage."""
|
|
446
|
+
stages = ["packing", "shipping", "delivered"]
|
|
447
|
+
return [View(group="Order stages", name=s.title(), path=f"/orders/1?stage={s}") for s in stages]
|
|
448
|
+
|
|
449
|
+
def extend(self, ctx: Context, graph: Graph) -> None:
|
|
450
|
+
"""Add nodes and edges after the built-in detectors ran."""
|
|
451
|
+
graph.add_node(Node(id="stage:packing", kind="store", name="Packing stage", lang="py", source="api"))
|
|
452
|
+
|
|
453
|
+
def lineage(self, ctx: Context, selection: dict, layers: list[dict]) -> None:
|
|
454
|
+
"""Add or change rows of a thread before it is shown."""
|
|
455
|
+
layers.append({"id": "stage", "title": "Stage", "note": "from the stage table",
|
|
456
|
+
"rows": [{"label": "Packing", "kind": "stage"}]})
|
|
457
|
+
|
|
458
|
+
def checks(self, ctx: Context, graph: Graph) -> list[Finding]:
|
|
459
|
+
"""Extra findings for `stackthread check` and the Findings tab."""
|
|
460
|
+
return []
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Enable it with `[adapters] use = ["tools.stackthread_adapter:Orders"]`, or publish it from a package through the
|
|
464
|
+
`stackthread.adapters` entry-point group:
|
|
465
|
+
|
|
466
|
+
```toml
|
|
467
|
+
[project.entry-points."stackthread.adapters"]
|
|
468
|
+
orders = "my_package.stackthread:Orders"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
An adapter receives `ctx.config` (the parsed settings), `ctx.project` (with the graph and the facts of every file),
|
|
472
|
+
`graph` (nodes, edges, and `out`, `into`, `of_kind`, `at(file, line)`), `selection` (what was clicked) and `layers`
|
|
473
|
+
(the thread as JSON). An adapter that fails becomes an `adapter` finding; indexing carries on without it. Adapters
|
|
474
|
+
run with your permissions, like any dev dependency.
|
|
475
|
+
|
|
476
|
+
## Troubleshooting
|
|
477
|
+
|
|
478
|
+
Start with `stackthread doctor`: it checks Node.js, the bundled parser, the UI and the configuration, and says
|
|
479
|
+
where the logs are.
|
|
480
|
+
|
|
481
|
+
Every run writes JSON lines to `.stackthread/logs/stackthread.log` in your project (the folder ignores itself in
|
|
482
|
+
git). Read them with:
|
|
483
|
+
|
|
484
|
+
```sh
|
|
485
|
+
stackthread logs # the last 50 entries at info and above
|
|
486
|
+
stackthread logs --level warning # only problems
|
|
487
|
+
stackthread logs --area http -f # one part, following new entries
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Areas: `cli`, `index`, `worker` (the Node process), `server`, `http`, `edit`, `adapters` and `client` (errors in
|
|
491
|
+
the workbench, the in-app panel and the Vite plugin). Use `--verbose` on any command for more on the console.
|
|
492
|
+
|
|
493
|
+
`stackthread report --note "what went wrong"` (or **Report a problem** in the workbench) makes a zip with the
|
|
494
|
+
logs, versions, `doctor` checks and summaries of the settings and the project. It never contains source code;
|
|
495
|
+
paths under your home folder are shown as `~`. Nothing is sent anywhere; you decide where the file goes.
|
|
496
|
+
|
|
497
|
+
**A request is not linked to its endpoint.** `stackthread check` lists it as `unmatched-request` with the URL it
|
|
498
|
+
saw. Usually the frontend calls `/api/...` while backend routes start at `/`; add an `[http] links` entry.
|
|
499
|
+
|
|
500
|
+
**Frontend files are missing.** Check Node.js 22.18 or later with `stackthread doctor`; the log area `worker`
|
|
501
|
+
shows what Node printed.
|
|
502
|
+
|
|
503
|
+
**The workbench says the server is not running.** Start `stackthread serve` in the project and reload.
|
|
504
|
+
|
|
505
|
+
## Security and privacy
|
|
506
|
+
|
|
507
|
+
stackthread is for one developer, on their own machine, with their own project.
|
|
508
|
+
|
|
509
|
+
- **Nothing leaves your machine.** No telemetry, no update checks, no accounts.
|
|
510
|
+
- **Loopback only.** The server binds `127.0.0.1`, with no option to change it.
|
|
511
|
+
- **Other websites cannot use it.** Requests must carry a loopback `Host`, a loopback `Origin` when present, and a
|
|
512
|
+
random 256-bit session token created at start-up.
|
|
513
|
+
- **The Vite plugin adds no new door.** It only proxies same-origin requests from this machine, only in
|
|
514
|
+
`vite dev`.
|
|
515
|
+
- **Edits are fenced.** Only existing files inside your sources; never secrets or lock files; a save is refused if
|
|
516
|
+
the file changed on disk; files over 2 MB or not UTF-8 are refused; writes are atomic and keep line endings.
|
|
517
|
+
`--read-only` turns saving off.
|
|
518
|
+
- **The workbench is locked down** with a strict Content-Security-Policy.
|
|
519
|
+
- **Logs stay local** and never contain file contents or the session token.
|
|
520
|
+
|
|
521
|
+
## Limitations
|
|
522
|
+
|
|
523
|
+
stackthread reads code; where a value only exists at run time (a URL built from a variable, a router registered
|
|
524
|
+
conditionally) it shows what it can, and `check` lists what it could not link.
|
|
525
|
+
|
|
526
|
+
Not supported yet: Next.js, Remix framework mode and other server-rendered React frameworks; Vue, Svelte,
|
|
527
|
+
Angular; Flask, Starlette and Litestar routes; GraphQL and tRPC; webpack and Rspack.
|
|
528
|
+
|
|
529
|
+
## Licence
|
|
530
|
+
|
|
531
|
+
MIT. The workbench bundles Preact and CodeMirror, and the parser bundles Babel, all under the MIT licence; their
|
|
532
|
+
notices ship in the package.
|