beast-devtools 0.0.1
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.
- package/.claude/launch.json +11 -0
- package/CHANGELOG.md +37 -0
- package/README.md +87 -0
- package/devtools/CHANGELOG.md +26 -0
- package/devtools/LICENSE +15 -0
- package/devtools/README.md +164 -0
- package/devtools/client/BeastDevtools.btsx +185 -0
- package/devtools/client/CodeView.btsx +61 -0
- package/devtools/client/ComponentsPanel.btsx +212 -0
- package/devtools/client/FileList.btsx +35 -0
- package/devtools/client/InspectorPanel.btsx +131 -0
- package/devtools/client/RefactorPanel.btsx +304 -0
- package/devtools/client/api.ts +69 -0
- package/devtools/client/compile.test.ts +22 -0
- package/devtools/client/devtools.css +1279 -0
- package/devtools/client/highlight.ts +210 -0
- package/devtools/client/mount.ts +16 -0
- package/devtools/client/runtime.ts +168 -0
- package/devtools/client/util.ts +136 -0
- package/devtools/package.json +73 -0
- package/devtools/server/analyze.test.ts +148 -0
- package/devtools/server/analyze.ts +737 -0
- package/devtools/server/diff.ts +82 -0
- package/devtools/server/line-map.ts +63 -0
- package/devtools/server/octane-bundler.d.ts +17 -0
- package/devtools/server/project.ts +369 -0
- package/devtools/server/refactor.test.ts +224 -0
- package/devtools/server/refactor.ts +224 -0
- package/devtools/server/source-scan.ts +377 -0
- package/devtools/shared/types.ts +208 -0
- package/devtools/test/fixtures/App.btsx +193 -0
- package/devtools/tsconfig.build.json +17 -0
- package/devtools/vite.ts +159 -0
- package/favicon.ico +0 -0
- package/index.html +14 -0
- package/package.json +34 -0
- package/public/beast.svg +1 -0
- package/src/App.btsx +144 -0
- package/src/AppHeader.btsx +24 -0
- package/src/LeftArticle.btsx +22 -0
- package/src/RightArticle.btsx +19 -0
- package/src/env.d.ts +8 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.ts +8 -0
- package/src/style.css +44 -0
- package/tsconfig.json +39 -0
- package/vite.config.ts +19 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `beast-devtools` will be recorded here.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Beast DevTools, an in-page overlay for `vite dev` (toggle with Alt+Shift+D):
|
|
10
|
+
- **Components**: live Octane component tree and hook values named after
|
|
11
|
+
their `setup` bindings, with context and the declaring `.btsx` line.
|
|
12
|
+
- **BTSX → TSRX**: source next to the generated TSRX, with line linking
|
|
13
|
+
through Beast's source map and compile diagnostics.
|
|
14
|
+
- **Refactor**: nesting depth per line, plus extraction suggestions for deep
|
|
15
|
+
sections (typed props, ready-to-paste `component` snippets) and detection
|
|
16
|
+
of structurally identical blocks.
|
|
17
|
+
- Automatic refactors from the Refactor panel. A section can be hoisted into a
|
|
18
|
+
local `component` in the same file, or moved to its own `Name.btsx` with its
|
|
19
|
+
imports and exported module types and values. Sections of at least
|
|
20
|
+
`fileLines` lines (default 30) default to their own file. Every change is
|
|
21
|
+
previewed as a diff, compiled through Beast and Octane before writing, and
|
|
22
|
+
can be undone.
|
|
23
|
+
- `bun run test`, which runs the analyzer suite and compiles every overlay
|
|
24
|
+
component through Beast and Octane.
|
|
25
|
+
- The devtools are packaged as `@beastjs/devtools` (version 0.1.0, ISC) in a
|
|
26
|
+
Bun workspace. `devtools/` has its own manifest, build to `dist/`, README,
|
|
27
|
+
CHANGELOG and LICENSE; the playground app consumes it by name.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- The playground builds the devtools plugin before `dev` and `check`.
|
|
32
|
+
`bun run devtools:pack` previews the published files.
|
|
33
|
+
- `beastOctane()` now uses `profile: "auto"` so dev builds expose Octane's
|
|
34
|
+
runtime inspection hook; production builds are unchanged.
|
|
35
|
+
- Tailwind no longer scans `devtools/`, since the overlay ships its own
|
|
36
|
+
prefixed stylesheet.
|
|
37
|
+
- `bun run check` now also runs the tests.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# beast-devtools
|
|
2
|
+
|
|
3
|
+
A [Beast](https://www.npmjs.com/package/beast-tsrx) project powered by
|
|
4
|
+
[TSRX](https://tsrx.dev/) and [Octane](https://octanejs.dev/).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
bun install
|
|
8
|
+
bun run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Edit `src/App.btsx` to get started. Declare typed props at the top of the BTSX
|
|
12
|
+
file; the Beast bundler adapter compiles it into native TSRX and then lets Octane
|
|
13
|
+
produce the browser module.
|
|
14
|
+
|
|
15
|
+
The starter pins the tested `octane@0.4.3` toolchain. Run the complete local
|
|
16
|
+
verification before shipping:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun run check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `scope` when setup belongs to an exact child position instead of the whole
|
|
23
|
+
component:
|
|
24
|
+
|
|
25
|
+
```btsx
|
|
26
|
+
scope
|
|
27
|
+
setup const label = "Owned by this child";
|
|
28
|
+
p #{label}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Octane signals need no build option. Import `octane/signals` in a module to
|
|
32
|
+
enable native signal reads there:
|
|
33
|
+
|
|
34
|
+
```btsx
|
|
35
|
+
import { createScope } from "octane/signals"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Record application changes in [CHANGELOG.md](CHANGELOG.md).
|
|
39
|
+
|
|
40
|
+
## Beast DevTools
|
|
41
|
+
|
|
42
|
+
This repository develops [`@beastjs/devtools`](devtools/README.md), an in-page
|
|
43
|
+
devtools overlay for Beast and Octane apps. The package lives in
|
|
44
|
+
[`devtools/`](devtools/) as a Bun workspace. The app at the root is its
|
|
45
|
+
playground and uses the package by name, the same way a real app would:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// vite.config.ts
|
|
49
|
+
beastOctane({ octane: { profile: "auto" } }), // runtime hook in dev only
|
|
50
|
+
beastDevtools({ include: ["src"], analyzer: { depthLimit: 5, minLines: 8, fileLines: 30 } }),
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`bun run dev` builds the plugin and starts the playground. Open the overlay
|
|
54
|
+
from the **Beast** button or with <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>.
|
|
55
|
+
See the [package README](devtools/README.md) for what each panel does.
|
|
56
|
+
|
|
57
|
+
| Path | Contents |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `devtools/vite.ts` | The Vite plugin: injects the overlay, serves its JSON API, and pushes an HMR event when `.btsx` files change. |
|
|
60
|
+
| `devtools/server/` | Node side: compiles, source-maps, analyzes and refactors `.btsx` files. Built to `devtools/dist/`. |
|
|
61
|
+
| `devtools/client/` | The overlay, written in BTSX. It ships as source and is compiled by the host app's toolchain. |
|
|
62
|
+
| `devtools/shared/` | Wire types shared by both sides. |
|
|
63
|
+
| `devtools/test/fixtures/` | A snapshot of the deeply nested starter `App.btsx` that the analyzer tests run against. |
|
|
64
|
+
|
|
65
|
+
Edits under `devtools/client/` hot-reload in the playground. Changes to
|
|
66
|
+
`devtools/vite.ts` or `devtools/server/` need `bun run --cwd devtools build`
|
|
67
|
+
and a dev-server restart.
|
|
68
|
+
|
|
69
|
+
### Publishing
|
|
70
|
+
|
|
71
|
+
1. Update `version` in [`devtools/package.json`](devtools/package.json) and add
|
|
72
|
+
an entry to [`devtools/CHANGELOG.md`](devtools/CHANGELOG.md).
|
|
73
|
+
2. Run `bun run check` (type check, tests, plugin build, app build).
|
|
74
|
+
3. Run `bun run devtools:pack` to review exactly which files will be published.
|
|
75
|
+
4. Publish from the package directory: `cd devtools && npm publish`.
|
|
76
|
+
`prepublishOnly` rebuilds and retests, and `publishConfig.access` is `public`.
|
|
77
|
+
You must be logged in to npm with publish rights on the `@beastjs` scope.
|
|
78
|
+
|
|
79
|
+
## Selected stack
|
|
80
|
+
|
|
81
|
+
- Bundler: vite
|
|
82
|
+
- UI: base-ui (@octanejs/base-ui)
|
|
83
|
+
- Styling: Tailwind CSS v4
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Button } from "@octanejs/base-ui/button";
|
|
87
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@beastjs/devtools` are recorded here.
|
|
4
|
+
|
|
5
|
+
## 0.1.0
|
|
6
|
+
|
|
7
|
+
First release: an in-page overlay for Beast (BTSX) and Octane apps during
|
|
8
|
+
`vite dev`. Toggle it with Alt+Shift+D.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Components**: the live Octane component tree, including `each`/`if`
|
|
13
|
+
scopes. Hook values are named after their `setup` bindings, alongside
|
|
14
|
+
context values and the `.btsx` line that declares the component.
|
|
15
|
+
- **BTSX → TSRX**: each `.btsx` file next to the TSRX Beast generates for it,
|
|
16
|
+
with line linking through Beast's source map and `BEAST####` compile
|
|
17
|
+
diagnostics.
|
|
18
|
+
- **Refactor**: nesting depth for every template line, suggestions for
|
|
19
|
+
extracting deep sections into components with inferred, typed props, and
|
|
20
|
+
detection of structurally identical blocks.
|
|
21
|
+
- **Automatic refactors**: hoist a section into a local `component`, or move it
|
|
22
|
+
to its own `Name.btsx` with the imports and exported module types and values
|
|
23
|
+
it needs. Every change is previewed as a diff, compiled through Beast and
|
|
24
|
+
Octane before anything is written, and can be undone.
|
|
25
|
+
- `beastDevtools({ include, analyzer })` options for source directories and
|
|
26
|
+
analyzer defaults (`depthLimit`, `minLines`, `fileLines`).
|
package/devtools/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, phtn
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @beastjs/devtools
|
|
2
|
+
|
|
3
|
+
In-page devtools for [Beast](https://www.npmjs.com/package/beast-tsrx) (BTSX)
|
|
4
|
+
and [Octane](https://octanejs.dev/) apps. It adds a panel to your app during
|
|
5
|
+
`vite dev` that shows live component state, puts each `.btsx` file next to the
|
|
6
|
+
TSRX it compiles to, and finds deeply nested templates. It can then extract
|
|
7
|
+
those sections into components for you.
|
|
8
|
+
|
|
9
|
+
Production builds are untouched: the plugin only runs on the dev server.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
| Package | Version |
|
|
14
|
+
| ------------ | ----------- |
|
|
15
|
+
| `beast-tsrx` | `^0.4.3` |
|
|
16
|
+
| `octane` | `^0.4.3` |
|
|
17
|
+
| `vite` | `^8.0.16` |
|
|
18
|
+
| Node.js | `>=22.22.2` |
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add -d @beastjs/devtools
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or with npm: `npm install -D @beastjs/devtools`.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
Add the plugin next to `beastOctane()` in `vite.config.ts`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { beastOctane } from 'beast-tsrx/vite'
|
|
34
|
+
import { beastDevtools } from '@beastjs/devtools'
|
|
35
|
+
import { defineConfig } from 'vite'
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
plugins: [
|
|
39
|
+
// `profile: 'auto'` compiles Octane's runtime inspection hook into dev
|
|
40
|
+
// builds only. Without it, the Components panel explains how to enable it.
|
|
41
|
+
beastOctane({ octane: { profile: 'auto' } }),
|
|
42
|
+
beastDevtools(),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run `vite` (or `bun run dev`), then open the panel from the **Beast** button in
|
|
48
|
+
the bottom-right corner or with <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>.
|
|
49
|
+
The panel remembers its size, tab and settings per browser.
|
|
50
|
+
|
|
51
|
+
## Panels
|
|
52
|
+
|
|
53
|
+
### Components
|
|
54
|
+
|
|
55
|
+
The live Octane component tree, including the `each`/`if` scopes Beast
|
|
56
|
+
templates create. Selecting a component shows:
|
|
57
|
+
|
|
58
|
+
- its hook values, named after the `setup` bindings that declare them (for
|
|
59
|
+
example `activeId: "language"`);
|
|
60
|
+
- context values and the number of effect slots;
|
|
61
|
+
- the `.btsx` line that declares it, with buttons to open it in your editor or
|
|
62
|
+
view its compiled TSRX.
|
|
63
|
+
|
|
64
|
+
### BTSX → TSRX
|
|
65
|
+
|
|
66
|
+
Any `.btsx` file next to the TSRX that Beast generates for it. Hovering a line
|
|
67
|
+
highlights its counterpart through Beast's source map; clicking pins it. A file
|
|
68
|
+
that fails to compile shows its `BEAST####` diagnostic and the failing line.
|
|
69
|
+
Both panes update when you save.
|
|
70
|
+
|
|
71
|
+
### Refactor
|
|
72
|
+
|
|
73
|
+
The structural nesting depth of every template line, with totals and a
|
|
74
|
+
per-depth chart, plus two kinds of suggestions:
|
|
75
|
+
|
|
76
|
+
- **Extract**: a section nested deeper than the depth limit becomes a component.
|
|
77
|
+
Its props and their types are inferred from the bindings the section uses,
|
|
78
|
+
and a loop's `key` stays at the call site.
|
|
79
|
+
- **Shared shape**: structurally identical blocks that one component could
|
|
80
|
+
replace.
|
|
81
|
+
|
|
82
|
+
The toolbar adjusts the depth limit, the smallest section worth extracting,
|
|
83
|
+
and the size at which a section defaults to its own file.
|
|
84
|
+
|
|
85
|
+
## Automatic refactors
|
|
86
|
+
|
|
87
|
+
Each suggestion card can apply itself in one of two ways:
|
|
88
|
+
|
|
89
|
+
- **Hoist in file** adds a local `component` above the host component's
|
|
90
|
+
`props`/`setup` and replaces the section with a call.
|
|
91
|
+
- **Move to `Name.btsx`** writes the section to a new file next to the source.
|
|
92
|
+
The new file gets the imports it needs. Module-level types and values the
|
|
93
|
+
section uses are exported from the source and imported back (type-only where
|
|
94
|
+
possible), and the source imports the new component.
|
|
95
|
+
|
|
96
|
+
Sections of at least **New file at** lines (default 30) default to their own
|
|
97
|
+
file. Clicking a target first shows a diff of every file it will touch, and
|
|
98
|
+
nothing is written until you confirm. After applying, **Undo** restores the
|
|
99
|
+
files, unless they have been edited since. Undo history lives in the dev
|
|
100
|
+
server's memory, so it is lost when the server restarts.
|
|
101
|
+
|
|
102
|
+
Automatic refactors are conservative:
|
|
103
|
+
|
|
104
|
+
- The dev server recomputes the change from the file on disk. The browser only
|
|
105
|
+
names the suggestion, and changes to a file that changed since it was
|
|
106
|
+
analyzed are refused.
|
|
107
|
+
- Every resulting file must compile through Beast and Octane before anything
|
|
108
|
+
is written.
|
|
109
|
+
- The write endpoints accept only same-origin JSON requests, and only for
|
|
110
|
+
`.btsx` files in the configured directories.
|
|
111
|
+
- Automatic refactoring is refused when it cannot be done safely:
|
|
112
|
+
- copies that differ from each other, or that live in different components;
|
|
113
|
+
- components with scoped `style` blocks;
|
|
114
|
+
- moving a section that uses a file-local `component` into a new file.
|
|
115
|
+
|
|
116
|
+
The card explains why, and the code can still be copied by hand.
|
|
117
|
+
|
|
118
|
+
## Options
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
beastDevtools({
|
|
122
|
+
include: ['src'],
|
|
123
|
+
analyzer: { depthLimit: 5, minLines: 8, fileLines: 30 },
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Option | Default | Description |
|
|
128
|
+
| --------------------- | --------- | ----------------------------------------------------------------------- |
|
|
129
|
+
| `include` | `['src']` | Directories, relative to the Vite root, scanned for `.btsx` files. |
|
|
130
|
+
| `analyzer.depthLimit` | `5` | Template nesting depth (0 = component root) above which lines are deep. |
|
|
131
|
+
| `analyzer.minLines` | `8` | Smallest section, in lines, worth extracting. |
|
|
132
|
+
| `analyzer.fileLines` | `30` | Sections at least this long move to their own file by default. |
|
|
133
|
+
|
|
134
|
+
The overlay's own settings override the analyzer defaults for that browser.
|
|
135
|
+
|
|
136
|
+
## How it works
|
|
137
|
+
|
|
138
|
+
- The plugin injects the overlay into `index.html` and serves a small JSON API
|
|
139
|
+
under `/__beast-devtools/api`. That API compiles, source-maps, analyzes and
|
|
140
|
+
refactors `.btsx` files with `beast-tsrx`. It also sends an HMR event when a
|
|
141
|
+
`.btsx` file changes.
|
|
142
|
+
- The overlay is itself written in BTSX and ships as source, so your app's own
|
|
143
|
+
Beast and Octane versions compile it. It reads the component tree from
|
|
144
|
+
Octane's `__OCTANE_DEVTOOLS__` hook, which `profile: 'auto'` enables in dev
|
|
145
|
+
builds. The plugin dedupes `octane` so the overlay and the app share one
|
|
146
|
+
runtime.
|
|
147
|
+
- Opening files in your editor uses Vite's built-in `/__open-in-editor`
|
|
148
|
+
endpoint, which honors the `LAUNCH_EDITOR` environment variable.
|
|
149
|
+
|
|
150
|
+
## Limitations
|
|
151
|
+
|
|
152
|
+
- Hook values are matched to `setup` bindings by position and kind. When they
|
|
153
|
+
don't line up (custom hooks, for example), the panel shows positions such as
|
|
154
|
+
`#0` instead of guessing.
|
|
155
|
+
- Props whose type cannot be read from the source are typed `any` in extracted
|
|
156
|
+
components.
|
|
157
|
+
- Moving a section that uses module-level values makes the two files import
|
|
158
|
+
each other. This is safe because the values are read at render time, but the
|
|
159
|
+
import cycle is worth knowing about.
|
|
160
|
+
- Imports that only the moved section used are left in the source file.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
ISC
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore } from 'octane'
|
|
2
|
+
import type { AnalyzerSettings, FileReport, ProjectReport } from '../shared/types.ts'
|
|
3
|
+
import ComponentsPanel from './ComponentsPanel.btsx'
|
|
4
|
+
import InspectorPanel from './InspectorPanel.btsx'
|
|
5
|
+
import RefactorPanel from './RefactorPanel.btsx'
|
|
6
|
+
import { fetchFile, fetchProject, onSourceChanged } from './api.ts'
|
|
7
|
+
import { getRuntimeSnapshot, subscribeRuntime } from './runtime.ts'
|
|
8
|
+
import { clampHeight, cx, loadPreferences, savePreferences, scrollTarget, startResize, type RecentRefactor, type ScrollTarget, type TabId } from './util.ts'
|
|
9
|
+
|
|
10
|
+
module
|
|
11
|
+
const TABS: ReadonlyArray<{ id: TabId; label: string }> = [
|
|
12
|
+
{ id: 'components', label: 'Components' },
|
|
13
|
+
{ id: 'inspector', label: 'BTSX → TSRX' },
|
|
14
|
+
{ id: 'refactor', label: 'Refactor' },
|
|
15
|
+
]
|
|
16
|
+
const LOGO_PATH = 'm8.393 1.002-.205.006a7.3 7.3 0 0 0-3.024.742 7.43 7.43 0 0 0-3.786 4.317 7.3 7.3 0 0 0-.375 2.145 7.34 7.34 0 0 0 .941 3.808 7.5 7.5 0 0 0 2.653 2.731 7.37 7.37 0 0 0 4.231 1.042 7 7 0 0 0 .917-.11 7.4 7.4 0 0 0 4.446-2.673 7.37 7.37 0 0 0 1.597-4.109c.012-.157.012-.828 0-.994a7.3 7.3 0 0 0-.747-2.768 7.3 7.3 0 0 0-1.128-1.67 11 11 0 0 0-.577-.577 7.4 7.4 0 0 0-2.612-1.516 7.5 7.5 0 0 0-2.099-.368 6 6 0 0 1-.232-.006M8.11 3.488a5.4 5.4 0 0 0-.808.114 5 5 0 0 0-1.074.379l-.047.024 1.11 1.11 1.11 1.109 1.109-1.11 1.11-1.11-.048-.023a4.9 4.9 0 0 0-1.811-.487 8 8 0 0 0-.651-.006M3.955 6.281a4.9 4.9 0 0 0-.48 1.854 4.9 4.9 0 0 0 .503 2.441l.024.047 1.11-1.11L6.22 8.405l-1.11-1.11L4 6.184zm7.736 1.014-1.11 1.11 1.11 1.109 1.11 1.11.023-.048a4.94 4.94 0 0 0 .503-2.433 5 5 0 0 0-.497-1.898l-.03-.06zM7.29 11.697l-1.108 1.108.097.046c.562.27 1.164.424 1.817.468.165.01.606.005.761-.01a5 5 0 0 0 1.668-.458l.097-.046-1.108-1.108c-.61-.61-1.11-1.109-1.112-1.109s-.502.499-1.112 1.109'
|
|
17
|
+
|
|
18
|
+
const initial = loadPreferences()
|
|
19
|
+
|
|
20
|
+
/** Alt+Shift+D toggles the panel. `code` is used because Alt changes `key` on macOS. */
|
|
21
|
+
function isToggleShortcut(event: KeyboardEvent): boolean {
|
|
22
|
+
return event.altKey && event.shiftKey && !event.metaKey && !event.ctrlKey && event.code === 'KeyD'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
component BeastLogo
|
|
26
|
+
svg.bdt-logo(viewBox="0 0 16 16" fill="none" aria-hidden="true")
|
|
27
|
+
path(fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d={LOGO_PATH})
|
|
28
|
+
|
|
29
|
+
setup
|
|
30
|
+
const runtime = useSyncExternalStore(subscribeRuntime, getRuntimeSnapshot, getRuntimeSnapshot);
|
|
31
|
+
const [open, setOpen] = useState(initial.open);
|
|
32
|
+
const [tab, setTab] = useState<TabId>(initial.tab);
|
|
33
|
+
const [height, setHeight] = useState(initial.height);
|
|
34
|
+
const [file, setFile] = useState<string | null>(initial.file);
|
|
35
|
+
const [showControlFlow, setShowControlFlow] = useState(initial.showControlFlow);
|
|
36
|
+
const [settings, setSettings] = useState<AnalyzerSettings>(initial.settings);
|
|
37
|
+
const [focus, setFocus] = useState<ScrollTarget | null>(null);
|
|
38
|
+
const [revision, setRevision] = useState(0);
|
|
39
|
+
const [project, setProject] = useState<ProjectReport | null>(null);
|
|
40
|
+
const [report, setReport] = useState<FileReport | null>(null);
|
|
41
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
42
|
+
const [recent, setRecent] = useState<RecentRefactor | null>(null);
|
|
43
|
+
const suggestionCount = project?.files.reduce((sum, entry) => sum + entry.suggestions, 0) ?? 0;
|
|
44
|
+
const status = runtime.status === 'connected'
|
|
45
|
+
? `${runtime.componentCount} component${runtime.componentCount === 1 ? '' : 's'} live`
|
|
46
|
+
: runtime.status === 'connecting' ? 'Connecting…' : 'Runtime off';
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
savePreferences({ open, tab, height, file, showControlFlow, settings });
|
|
50
|
+
}, [open, tab, height, file, showControlFlow, settings]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => onSourceChanged(() => setRevision((value) => value + 1)), []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
56
|
+
if (!isToggleShortcut(event)) return;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
setOpen((value) => !value);
|
|
59
|
+
};
|
|
60
|
+
const onResize = () => setHeight((value) => clampHeight(value));
|
|
61
|
+
window.addEventListener('keydown', onKeyDown);
|
|
62
|
+
window.addEventListener('resize', onResize);
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
65
|
+
window.removeEventListener('resize', onResize);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// The project list refreshes whenever a .btsx file changes on disk.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) return;
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
fetchProject(settings).then(
|
|
74
|
+
(next) => {
|
|
75
|
+
if (cancelled) return;
|
|
76
|
+
setProject(next);
|
|
77
|
+
setLoadError(null);
|
|
78
|
+
setFile((current) => (current !== null && next.files.some((entry) => entry.path === current) ? current : (next.files[0]?.path ?? null)));
|
|
79
|
+
},
|
|
80
|
+
(error: Error) => {
|
|
81
|
+
if (!cancelled) setLoadError(error.message);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
};
|
|
87
|
+
}, [open, revision, settings]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!open || file === null) return;
|
|
91
|
+
let cancelled = false;
|
|
92
|
+
fetchFile(file, settings).then(
|
|
93
|
+
(next) => {
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
setReport(next);
|
|
96
|
+
setLoadError(null);
|
|
97
|
+
},
|
|
98
|
+
(error: Error) => {
|
|
99
|
+
if (!cancelled) setLoadError(error.message);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, [open, file, revision, settings]);
|
|
106
|
+
|
|
107
|
+
const viewSource = (path: string, line: number) => {
|
|
108
|
+
setFile(path);
|
|
109
|
+
setTab('inspector');
|
|
110
|
+
setFocus(scrollTarget(line));
|
|
111
|
+
};
|
|
112
|
+
const analyze = (path: string) => {
|
|
113
|
+
setFile(path);
|
|
114
|
+
setTab('refactor');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if open
|
|
118
|
+
section.bdt-root.bdt-dock(aria-label="Beast DevTools" style={{ height: `${height}px` }})
|
|
119
|
+
div.bdt-resize(
|
|
120
|
+
~ role="separator"
|
|
121
|
+
~ aria-orientation="horizontal"
|
|
122
|
+
~ aria-label="Resize Beast DevTools"
|
|
123
|
+
~ onPointerDown={(event) => startResize(event, height, setHeight)}
|
|
124
|
+
~ )
|
|
125
|
+
header.bdt-topbar
|
|
126
|
+
.bdt-brand
|
|
127
|
+
BeastLogo
|
|
128
|
+
div(className="skew-4") Beast
|
|
129
|
+
span.bdt-brand-sub DevTools
|
|
130
|
+
nav.bdt-tabs(role="tablist" aria-label="Beast DevTools panels" className="has[:focus]:rounded-sm! rounded-full")
|
|
131
|
+
each item in TABS key item.id
|
|
132
|
+
button(
|
|
133
|
+
~ type="button"
|
|
134
|
+
~ role="tab"
|
|
135
|
+
~ aria-selected={tab === item.id}
|
|
136
|
+
~ className={cx('bdt-tab', tab === item.id && 'is-active')}
|
|
137
|
+
~ onClick={() => setTab(item.id)}
|
|
138
|
+
~ )
|
|
139
|
+
| #{item.label}
|
|
140
|
+
if item.id === 'refactor' && suggestionCount > 0
|
|
141
|
+
span.bdt-tab-count(title={`${suggestionCount} refactor suggestion${suggestionCount === 1 ? '' : 's'}`}) #{suggestionCount}
|
|
142
|
+
.bdt-spacer
|
|
143
|
+
span.bdt-status
|
|
144
|
+
span(className={cx('bdt-launcher-dot', runtime.status === 'connected' && 'is-live')})
|
|
145
|
+
| #{status}
|
|
146
|
+
button.bdt-icon-button(type="button" aria-label="Close Beast DevTools" title="Close (Alt+Shift+D)" onClick={() => setOpen(false)})
|
|
147
|
+
svg(viewBox="0 0 16 16" fill="none" aria-hidden="true")
|
|
148
|
+
path(d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round")
|
|
149
|
+
div.bdt-body(role="tabpanel" aria-label={TABS.find((item) => item.id === tab)?.label})
|
|
150
|
+
switch tab
|
|
151
|
+
case 'components'
|
|
152
|
+
ComponentsPanel(
|
|
153
|
+
~ components={project?.components ?? []}
|
|
154
|
+
~ showControlFlow={showControlFlow}
|
|
155
|
+
~ onShowControlFlowChange={setShowControlFlow}
|
|
156
|
+
~ onViewSource={viewSource}
|
|
157
|
+
~ onAnalyze={analyze}
|
|
158
|
+
~ )
|
|
159
|
+
case 'inspector'
|
|
160
|
+
InspectorPanel(
|
|
161
|
+
~ files={project?.files ?? []}
|
|
162
|
+
~ file={file}
|
|
163
|
+
~ report={report}
|
|
164
|
+
~ loadError={loadError}
|
|
165
|
+
~ focus={focus}
|
|
166
|
+
~ onSelectFile={setFile}
|
|
167
|
+
~ )
|
|
168
|
+
default
|
|
169
|
+
RefactorPanel(
|
|
170
|
+
~ files={project?.files ?? []}
|
|
171
|
+
~ file={file}
|
|
172
|
+
~ report={report}
|
|
173
|
+
~ loadError={loadError}
|
|
174
|
+
~ settings={settings}
|
|
175
|
+
~ recent={recent}
|
|
176
|
+
~ onSelectFile={setFile}
|
|
177
|
+
~ onSettingsChange={setSettings}
|
|
178
|
+
~ onRecentChange={setRecent}
|
|
179
|
+
~ )
|
|
180
|
+
else
|
|
181
|
+
div.bdt-root.bdt-launcher-host
|
|
182
|
+
button.bdt-launcher(type="button" title="Open Beast DevTools (Alt+Shift+D)" onClick={() => setOpen(true)})
|
|
183
|
+
BeastLogo
|
|
184
|
+
span Beast
|
|
185
|
+
span(className={cx('bdt-launcher-dot', runtime.status === 'connected' && 'is-live')} aria-label={status})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'octane'
|
|
2
|
+
import type { LineRange } from '../shared/types.ts'
|
|
3
|
+
import { highlight, type Language } from './highlight.ts'
|
|
4
|
+
import { cx, scrollToLine, type ScrollTarget } from './util.ts'
|
|
5
|
+
|
|
6
|
+
module
|
|
7
|
+
interface CodeViewProps {
|
|
8
|
+
source: string
|
|
9
|
+
language: Language
|
|
10
|
+
/** Lines linked from the other pane or a hover. */
|
|
11
|
+
linked?: readonly number[]
|
|
12
|
+
activeLine?: number | null
|
|
13
|
+
/** Softly tinted regions, such as refactor suggestion occurrences. */
|
|
14
|
+
ranges?: readonly LineRange[]
|
|
15
|
+
errorLine?: number | null
|
|
16
|
+
/** Per-line nesting depth; renders the depth gutter when present. */
|
|
17
|
+
depths?: ReadonlyArray<number | null> | null
|
|
18
|
+
depthLimit?: number
|
|
19
|
+
scrollTo?: ScrollTarget | null
|
|
20
|
+
label?: string
|
|
21
|
+
onLineEnter?: (line: number) => void
|
|
22
|
+
onLineClick?: (line: number) => void
|
|
23
|
+
onLeave?: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMPTY: readonly number[] = []
|
|
27
|
+
const NO_RANGES: readonly LineRange[] = []
|
|
28
|
+
|
|
29
|
+
function depthTone(depth: number, limit: number): string {
|
|
30
|
+
return depth > limit ? 'is-over' : depth === limit ? 'is-near' : ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
props { source, language, linked = EMPTY, activeLine = null, ranges = NO_RANGES, errorLine = null, depths = null, depthLimit = 5, scrollTo = null, label = 'Source code', onLineEnter, onLineClick, onLeave }: CodeViewProps
|
|
34
|
+
setup
|
|
35
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
36
|
+
const rows = useMemo(() => highlight(source, language).map((tokens, index) => ({ number: index + 1, tokens })), [source, language]);
|
|
37
|
+
const linkedLines = useMemo(() => new Set(linked), [linked]);
|
|
38
|
+
const inRange = (line: number) => ranges.some((range) => range.startLine <= line && line <= range.endLine);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (scrollTo !== null) scrollToLine(containerRef.current, scrollTo.line);
|
|
41
|
+
}, [scrollTo, rows]);
|
|
42
|
+
|
|
43
|
+
div.bdt-code(ref={containerRef} role="region" aria-label={label} tabIndex={0} onMouseLeave={() => onLeave?.()})
|
|
44
|
+
.bdt-code-inner
|
|
45
|
+
each row in rows key row.number
|
|
46
|
+
div(
|
|
47
|
+
~ data-line={row.number}
|
|
48
|
+
~ className={cx('bdt-line', inRange(row.number) && 'is-range', linkedLines.has(row.number) && 'is-linked', row.number === activeLine && 'is-active', row.number === errorLine && 'is-error')}
|
|
49
|
+
~ onMouseEnter={() => onLineEnter?.(row.number)}
|
|
50
|
+
~ onClick={() => onLineClick?.(row.number)}
|
|
51
|
+
~ )
|
|
52
|
+
span.bdt-ln #{row.number}
|
|
53
|
+
if depths !== null
|
|
54
|
+
span.bdt-depth
|
|
55
|
+
if depths[row.number - 1] != null
|
|
56
|
+
fragment
|
|
57
|
+
span(className={cx('bdt-depth-bar', depthTone(depths[row.number - 1]!, depthLimit))} style={{ width: `${4 + depths[row.number - 1]! * 5}px` }})
|
|
58
|
+
span.bdt-depth-num #{depths[row.number - 1]}
|
|
59
|
+
span.bdt-src
|
|
60
|
+
each token, index in row.tokens key index
|
|
61
|
+
span(className={`tk-${token.type}`}) #{token.value}
|