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.
Files changed (47) hide show
  1. package/.claude/launch.json +11 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +87 -0
  4. package/devtools/CHANGELOG.md +26 -0
  5. package/devtools/LICENSE +15 -0
  6. package/devtools/README.md +164 -0
  7. package/devtools/client/BeastDevtools.btsx +185 -0
  8. package/devtools/client/CodeView.btsx +61 -0
  9. package/devtools/client/ComponentsPanel.btsx +212 -0
  10. package/devtools/client/FileList.btsx +35 -0
  11. package/devtools/client/InspectorPanel.btsx +131 -0
  12. package/devtools/client/RefactorPanel.btsx +304 -0
  13. package/devtools/client/api.ts +69 -0
  14. package/devtools/client/compile.test.ts +22 -0
  15. package/devtools/client/devtools.css +1279 -0
  16. package/devtools/client/highlight.ts +210 -0
  17. package/devtools/client/mount.ts +16 -0
  18. package/devtools/client/runtime.ts +168 -0
  19. package/devtools/client/util.ts +136 -0
  20. package/devtools/package.json +73 -0
  21. package/devtools/server/analyze.test.ts +148 -0
  22. package/devtools/server/analyze.ts +737 -0
  23. package/devtools/server/diff.ts +82 -0
  24. package/devtools/server/line-map.ts +63 -0
  25. package/devtools/server/octane-bundler.d.ts +17 -0
  26. package/devtools/server/project.ts +369 -0
  27. package/devtools/server/refactor.test.ts +224 -0
  28. package/devtools/server/refactor.ts +224 -0
  29. package/devtools/server/source-scan.ts +377 -0
  30. package/devtools/shared/types.ts +208 -0
  31. package/devtools/test/fixtures/App.btsx +193 -0
  32. package/devtools/tsconfig.build.json +17 -0
  33. package/devtools/vite.ts +159 -0
  34. package/favicon.ico +0 -0
  35. package/index.html +14 -0
  36. package/package.json +34 -0
  37. package/public/beast.svg +1 -0
  38. package/src/App.btsx +144 -0
  39. package/src/AppHeader.btsx +24 -0
  40. package/src/LeftArticle.btsx +22 -0
  41. package/src/RightArticle.btsx +19 -0
  42. package/src/env.d.ts +8 -0
  43. package/src/lib/utils.ts +6 -0
  44. package/src/main.ts +8 -0
  45. package/src/style.css +44 -0
  46. package/tsconfig.json +39 -0
  47. package/vite.config.ts +19 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "dev",
6
+ "runtimeExecutable": "bun",
7
+ "runtimeArgs": ["run", "dev", "--port", "5173", "--strictPort"],
8
+ "port": 5173
9
+ }
10
+ ]
11
+ }
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`).
@@ -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}