create-bridgething 0.11.0 → 0.12.0
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/README.md +16 -12
- package/dist/index.mjs +86 -21
- package/package.json +9 -3
- package/template/CLAUDE.md +60 -84
- package/template/README.md +31 -44
- package/template/_claude/skills/bridgething/SKILL.md +12 -17
- package/template/_claude/skills/bridgething/reference/develop.md +84 -65
- package/template/_claude/skills/bridgething/reference/extension.md +183 -0
- package/template/_claude/skills/bridgething/reference/sdk.md +74 -106
- package/template/_claude/skills/bridgething/reference/ship.md +19 -38
- package/template/_gitignore +1 -0
- package/template/package.json +6 -5
- package/template/scripts/bridgething.ts +1086 -0
- package/template/scripts/push.ts +212 -193
- package/template/src/App.tsx +2 -5
- package/template/src/daemon.ts +14 -0
- package/template/vite.config.ts +5 -3
- package/template-extension/_claude_append.md +23 -0
- package/template-extension/extension/main.ts +20 -0
- package/template-launcher/_claude_append.md +14 -30
- package/template-launcher/src/App.tsx +2 -5
- package/template-overlay/_claude_append.md +31 -45
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
# Running and driving the app
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
To develop effectively you (1) run the app, (2) see it at the real size, and
|
|
5
|
-
(3) press the buttons. The fast loop drives a browser you control on your own
|
|
6
|
-
machine; you only need the physical Car Thing for final validation.
|
|
3
|
+
## Start the dev server
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
```bash
|
|
6
|
+
bun run dev
|
|
7
|
+
```
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
Vite serves at `http://localhost:5173/` with hot reload. The page reaches the
|
|
10
|
+
daemon on the Car Thing plugged in over USB through the dev server's
|
|
11
|
+
`/__bridgething` proxy, so now-playing, config, and geo are the real thing rather
|
|
12
|
+
than fixtures. `SUPERBIRD_HOST=bridgething-<serial>.local` targets another
|
|
13
|
+
device, and `BRIDGETHING_DAEMON_URL=ws://host:8891` targets a daemon anywhere
|
|
14
|
+
else.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Show it on the Car Thing with hot reload
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
bun run dev
|
|
19
|
+
bun run dev:device
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
On start it installs the app once if the daemon has never seen it, makes it the
|
|
23
|
+
active webapp so config, permissions, and forwards are yours, then points the
|
|
24
|
+
kiosk at `http://<your usb address>:5173/`. Every save from then on hot-reloads
|
|
25
|
+
on the real display. Ctrl-C hands the screen back to the installed build.
|
|
23
26
|
|
|
24
|
-
##
|
|
27
|
+
## The extension half
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
When `public/manifest.json` declares `extension`, both dev commands also bundle
|
|
30
|
+
`extension/main.ts`, run it under Deno with the manifest's permissions, and link
|
|
31
|
+
it to the same Car Thing. Every save rebuilds and restarts it, and `ctx.log.*`
|
|
32
|
+
prints in the vite terminal. The contract is in
|
|
33
|
+
[reference/extension.md](extension.md).
|
|
34
|
+
|
|
35
|
+
## Drive it with Playwright
|
|
36
|
+
|
|
37
|
+
To screenshot and drive the app from a script, add Playwright:
|
|
28
38
|
|
|
29
39
|
```bash
|
|
30
40
|
bun add -d playwright
|
|
31
41
|
bunx playwright install chromium
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
Then a short script gives you a locked 800x480 window you can screenshot and send
|
|
35
|
-
input to:
|
|
36
|
-
|
|
37
44
|
```ts
|
|
38
45
|
import { chromium } from 'playwright';
|
|
39
46
|
|
|
@@ -44,50 +51,46 @@ await page.goto('http://localhost:5173/');
|
|
|
44
51
|
await page.screenshot({ path: 'screen.png' }); // look at the result
|
|
45
52
|
|
|
46
53
|
// physical controls -> the exact events the device sends:
|
|
47
|
-
await page.keyboard.press('1');
|
|
48
|
-
await page.keyboard.press('m');
|
|
49
|
-
await page.keyboard.press('Escape');
|
|
50
|
-
await page.mouse.wheel(120, 0);
|
|
54
|
+
await page.keyboard.press('1'); // preset 1 (also '2' '3' '4')
|
|
55
|
+
await page.keyboard.press('m'); // Mode button
|
|
56
|
+
await page.keyboard.press('Escape'); // Back button
|
|
57
|
+
await page.mouse.wheel(120, 0); // rotary wheel: HORIZONTAL scroll (deltaX)
|
|
51
58
|
// touch: page.tap('selector') or page.mouse.click(x, y)
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
Loop: edit code
|
|
55
|
-
|
|
61
|
+
Loop: edit code, let vite hot-reload, screenshot, adjust. Keep the viewport at
|
|
62
|
+
`{ width: 800, height: 480 }` so what you see matches the device.
|
|
56
63
|
|
|
57
|
-
##
|
|
64
|
+
## Your own browser and DevTools
|
|
58
65
|
|
|
59
|
-
Open `http://localhost:5173
|
|
60
|
-
toolbar
|
|
61
|
-
|
|
62
|
-
scroll). This is the recommended manual path.
|
|
66
|
+
Open `http://localhost:5173/`, then set a custom device of 800 x 480 in DevTools'
|
|
67
|
+
device toolbar. Type `1` through `4`, `m`, and `Escape`, and hold shift while
|
|
68
|
+
scrolling for the rotary's horizontal wheel.
|
|
63
69
|
|
|
64
70
|
## Drive the real device over CDP
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
launcher gesture and on-device browser-nav side effects never interfere.
|
|
72
|
+
CDP input goes into the page, so the launcher gesture and browser-nav side
|
|
73
|
+
effects stay out of the way.
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
Get the app on the kiosk first with `bun run dev:device` or `bun run push`. CDP
|
|
76
|
+
is served at `bridgething.local:9222`, reachable over USB:
|
|
72
77
|
|
|
73
78
|
```bash
|
|
74
|
-
# device answers at bridgething.local over USB; root has no password
|
|
75
79
|
curl -s http://bridgething.local:9222/json/version >/dev/null && echo cdp-up
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
Use the `webSocketDebuggerUrl` exactly as returned. It comes back as an IP rather
|
|
79
|
-
than the hostname you asked for, and
|
|
83
|
+
than the hostname you asked for, and that address is the correct one.
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
instead. Connect to the `page` target from `/json` (the page-level endpoint, not
|
|
85
|
-
the browser-level one). This script screenshots the live device and presses a
|
|
86
|
-
button; it is the whole loop:
|
|
85
|
+
Playwright's `connectOverCDP` never finishes the websocket upgrade against the
|
|
86
|
+
device's embedded chromium, so drive it with a raw CDP websocket. Connect to the
|
|
87
|
+
`page` target from `/json`, not the browser-level endpoint.
|
|
87
88
|
|
|
88
89
|
```ts
|
|
89
90
|
// drive.ts -> run with: bun drive.ts
|
|
90
|
-
const targets = (await fetch('http://bridgething.local:9222/json').then(r =>
|
|
91
|
+
const targets = (await fetch('http://bridgething.local:9222/json').then(r =>
|
|
92
|
+
r.json(),
|
|
93
|
+
)) as any[];
|
|
91
94
|
const page = targets.find(t => t.type === 'page');
|
|
92
95
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
93
96
|
await new Promise<void>((res, rej) => {
|
|
@@ -99,45 +102,61 @@ let id = 0;
|
|
|
99
102
|
const pending = new Map<number, (v: any) => void>();
|
|
100
103
|
ws.onmessage = ev => {
|
|
101
104
|
const m = JSON.parse(ev.data as string);
|
|
102
|
-
if (m.id && pending.has(m.id))
|
|
105
|
+
if (m.id && pending.has(m.id))
|
|
106
|
+
(pending.get(m.id)!(m.result), pending.delete(m.id));
|
|
103
107
|
};
|
|
104
108
|
const send = (method: string, params: any = {}) =>
|
|
105
|
-
new Promise<any>(
|
|
109
|
+
new Promise<any>(
|
|
110
|
+
res => (
|
|
111
|
+
pending.set(++id, res),
|
|
112
|
+
ws.send(JSON.stringify({ id, method, params }))
|
|
113
|
+
),
|
|
114
|
+
);
|
|
106
115
|
|
|
107
116
|
// screenshot
|
|
108
117
|
const { data } = await send('Page.captureScreenshot', { format: 'png' });
|
|
109
118
|
await Bun.write('device.png', Buffer.from(data, 'base64'));
|
|
110
119
|
|
|
111
|
-
// read what is on screen
|
|
112
|
-
const text = await send('Runtime.evaluate', {
|
|
120
|
+
// read what is on screen, to assert a change happened
|
|
121
|
+
const text = await send('Runtime.evaluate', {
|
|
122
|
+
expression: 'document.body.innerText',
|
|
123
|
+
returnByValue: true,
|
|
124
|
+
});
|
|
113
125
|
console.log(text.result.value);
|
|
114
126
|
|
|
115
127
|
// press a button: keyDown + keyUp. key/code/text must match a real key.
|
|
116
128
|
const key = (type: string, k: string, code: string, vk: number) =>
|
|
117
|
-
send('Input.dispatchKeyEvent', {
|
|
118
|
-
|
|
129
|
+
send('Input.dispatchKeyEvent', {
|
|
130
|
+
type,
|
|
131
|
+
key: k,
|
|
132
|
+
code,
|
|
133
|
+
text: k.length === 1 ? k : '',
|
|
134
|
+
windowsVirtualKeyCode: vk,
|
|
135
|
+
});
|
|
136
|
+
await key('keyDown', '3', 'Digit3', 51); // preset 3
|
|
119
137
|
await key('keyUp', '3', 'Digit3', 51);
|
|
120
138
|
|
|
121
139
|
// rotary wheel = horizontal scroll:
|
|
122
|
-
await send('Input.dispatchMouseEvent', {
|
|
140
|
+
await send('Input.dispatchMouseEvent', {
|
|
141
|
+
type: 'mouseWheel',
|
|
142
|
+
x: 400,
|
|
143
|
+
y: 240,
|
|
144
|
+
deltaX: 120,
|
|
145
|
+
deltaY: 0,
|
|
146
|
+
});
|
|
123
147
|
|
|
124
148
|
process.exit(0);
|
|
125
149
|
```
|
|
126
150
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
now-playing, real artwork, the actual screen).
|
|
151
|
+
Iterate in the local loop, then confirm real now-playing, real artwork, and the
|
|
152
|
+
actual screen on the device.
|
|
130
153
|
|
|
131
154
|
## Input reference
|
|
132
155
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
| Mode (M) | `keydown` key `"m"` | `KeyM` / 77 |
|
|
141
|
-
| Back | `keydown` key `"Escape"` | `Escape` / 27 |
|
|
142
|
-
| Rotary wheel | `wheel` with `deltaX` (horizontal) | mouseWheel `deltaX` |
|
|
143
|
-
| Touch | pointer / touch events | `Input.dispatchTouchEvent` |
|
|
156
|
+
| Control | Browser event | CDP `key`/`code`/vk |
|
|
157
|
+
| ------------ | ------------------------------------- | -------------------------- |
|
|
158
|
+
| Preset 1-4 | `keydown` key `"1"` `"2"` `"3"` `"4"` | `Digit1`-`Digit4` / 49-52 |
|
|
159
|
+
| Mode (M) | `keydown` key `"m"` | `KeyM` / 77 |
|
|
160
|
+
| Back | `keydown` key `"Escape"` | `Escape` / 27 |
|
|
161
|
+
| Rotary wheel | `wheel` with `deltaX` (horizontal) | mouseWheel `deltaX` |
|
|
162
|
+
| Touch | pointer / touch events | `Input.dispatchTouchEvent` |
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Extensions (`@bridgething/extension`)
|
|
2
|
+
|
|
3
|
+
An extension is the desktop-side half of a webapp: a Deno process the bridgething
|
|
4
|
+
desktop app spawns, one per installed and enabled app. It has whatever host
|
|
5
|
+
access the manifest asks for, and reaches every connected Car Thing through the
|
|
6
|
+
daemon's forward surface.
|
|
7
|
+
|
|
8
|
+
## Shape of an extension
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { asJson, defineExtension, json } from '@bridgething/extension';
|
|
12
|
+
|
|
13
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
14
|
+
|
|
15
|
+
defineExtension({
|
|
16
|
+
start(ctx) {
|
|
17
|
+
ctx.on('device', event => ctx.log.info(event.type, event.device.name));
|
|
18
|
+
ctx.on('message', (device, message) =>
|
|
19
|
+
device.send(json({ echo: asJson(message) })),
|
|
20
|
+
);
|
|
21
|
+
timer = setInterval(() => ctx.broadcast(json({ tick: Date.now() })), 5_000);
|
|
22
|
+
},
|
|
23
|
+
stop() {
|
|
24
|
+
clearInterval(timer);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`defineExtension` runs `start` after the host's `hello`, sends `ready` when the
|
|
30
|
+
returned promise settles, and on `stop`, or when the host closes stdin, runs
|
|
31
|
+
`stop` and exits. The extension runs device or no device.
|
|
32
|
+
|
|
33
|
+
- Stdout is the protocol. Log with `ctx.log.*` and keep `console.error` on stderr
|
|
34
|
+
for scratch debugging.
|
|
35
|
+
- Register listeners synchronously at the top of `start`. The host replays every
|
|
36
|
+
live device immediately after it calls `start`, so a listener added after an
|
|
37
|
+
`await` misses them.
|
|
38
|
+
|
|
39
|
+
## The `ctx` contract
|
|
40
|
+
|
|
41
|
+
Read `node_modules/@bridgething/extension/dist/*.d.ts` for the exact signatures.
|
|
42
|
+
|
|
43
|
+
Identity:
|
|
44
|
+
|
|
45
|
+
- `ctx.api` is the host protocol revision, `1`.
|
|
46
|
+
- `ctx.webapp` is `{ id, name, version }` from `manifest.json`.
|
|
47
|
+
- `ctx.dataDir` is a directory this extension owns. The kv store lives there.
|
|
48
|
+
|
|
49
|
+
Devices:
|
|
50
|
+
|
|
51
|
+
- `ctx.devices` is the connected devices, recomputed on read.
|
|
52
|
+
- `ctx.device(id)` returns a handle for any id, connected or not. It never
|
|
53
|
+
throws.
|
|
54
|
+
- `ctx.on('device', event => ...)` streams `event.type` of `connected`,
|
|
55
|
+
`disconnected`, or `active`, with `event.device` as the handle.
|
|
56
|
+
- A handle carries `id`, `name`, `active`, `connected`, `config`, and `send`, and
|
|
57
|
+
survives a disconnect with `connected` false.
|
|
58
|
+
|
|
59
|
+
Messages:
|
|
60
|
+
|
|
61
|
+
- `ctx.device(id).send(message)` and `ctx.broadcast(message)`. Broadcast reaches
|
|
62
|
+
every connected device where this webapp is active.
|
|
63
|
+
- `ctx.on('message', (device, message) => ...)` takes forwards coming back.
|
|
64
|
+
- Both directions are fire and forget. Build your own correlation for
|
|
65
|
+
request and reply.
|
|
66
|
+
- The daemon delivers a forward to the active webapp, so check `device.active`. A
|
|
67
|
+
send to a backgrounded app is dropped silently.
|
|
68
|
+
|
|
69
|
+
Config:
|
|
70
|
+
|
|
71
|
+
- `ctx.config(device)` is a snapshot of that device's settings, keyed by
|
|
72
|
+
`manifest.json` config key. Values are strings.
|
|
73
|
+
- `ctx.on('config', (device, key, value) => ...)` fires when a settings page
|
|
74
|
+
writes, from any companion. `value` is `null` when the user cleared the key,
|
|
75
|
+
and the key leaves the snapshot. An empty string is a real empty value.
|
|
76
|
+
|
|
77
|
+
Storage, auth, logging:
|
|
78
|
+
|
|
79
|
+
- `ctx.kv.get(key)`, `.set(key, value)`, `.delete(key)`, and `.list()` are a
|
|
80
|
+
persistent JSON store scoped to this extension. `get` resolves `undefined` for
|
|
81
|
+
a missing key.
|
|
82
|
+
- `ctx.auth.authorize(url)` opens the system browser and resolves with the full
|
|
83
|
+
callback URL, query string included. It rejects when the user backs out.
|
|
84
|
+
- `ctx.log.debug/info/warn/error(...args)` writes into the host's log: the vite
|
|
85
|
+
terminal in development, the desktop app's log once installed.
|
|
86
|
+
- `kv` and `auth` failures reject with an `ExtensionError` carrying a `kind` of
|
|
87
|
+
`host-error`, `disconnected`, or `write-failed`.
|
|
88
|
+
|
|
89
|
+
Persist eagerly as state changes. The host drops your stdin the moment it sends
|
|
90
|
+
`stop`, so a `ctx.kv` or `ctx.auth` call made from `stop` rejects with
|
|
91
|
+
`kind: 'disconnected'`. Use `stop` to release timers, sockets, and children.
|
|
92
|
+
|
|
93
|
+
## Forward messages
|
|
94
|
+
|
|
95
|
+
A forward is `{ encoding, data }`, the same type the webapp SDK uses:
|
|
96
|
+
|
|
97
|
+
| encoding | reading | writing |
|
|
98
|
+
| -------- | ----------------------------------- | -------------------------------------- |
|
|
99
|
+
| `text` | `asText(message)` -> `string` | `send('hi')` or `send(text('hi'))` |
|
|
100
|
+
| `json` | `asJson<T>(message)` -> `T` | `send(json(value))` |
|
|
101
|
+
| `binary` | `asBinary(message)` -> `Uint8Array` | `send(bytes)` or `send(binary(bytes))` |
|
|
102
|
+
|
|
103
|
+
The narrowing helpers return `undefined` when the encoding does not match, so
|
|
104
|
+
`asJson(message) ?? fallback` is the normal shape. Wrap a plain object in
|
|
105
|
+
`json(...)` before sending, because `send` cannot tell one apart from a
|
|
106
|
+
`ForwardMessage`. Binary always arrives as a `Uint8Array`.
|
|
107
|
+
|
|
108
|
+
## Manifest block and permissions
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
"extension": {
|
|
112
|
+
"entry": "extension/desktop.mjs",
|
|
113
|
+
"permissions": ["all"],
|
|
114
|
+
"api": 1
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `entry` is the path inside the built bundle. It must exist, or the daemon
|
|
119
|
+
refuses the install.
|
|
120
|
+
- `api` is the host protocol revision. `1` is frozen.
|
|
121
|
+
- `permissions` are Deno permission descriptors, handed to the runtime as flags.
|
|
122
|
+
|
|
123
|
+
Descriptor grammar, one string per entry: `all`, `net`, `net:<host[:port]>`,
|
|
124
|
+
`read`, `read:<path>`, `write`, `write:<path>`, `run`, `run:<binary>`, `env`,
|
|
125
|
+
`env:<VAR>`, `sys`, `sys:<kind>`, `ffi`, `ffi:<path>`. A path may start with `~`.
|
|
126
|
+
|
|
127
|
+
Narrow `["all"]` before you publish. The dev loop runs the extension with exactly
|
|
128
|
+
the declared flags, so it fails on your machine the moment you use something you
|
|
129
|
+
did not declare. The store card lists the permissions and the desktop app makes
|
|
130
|
+
the user confirm them at install. A version declaring `extension` must publish
|
|
131
|
+
from a public GitHub repo.
|
|
132
|
+
|
|
133
|
+
## The dev loop
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bun run dev # page in your browser + extension under deno, both on the connected Car Thing
|
|
137
|
+
bun run dev:device # the same, with the page on the Car Thing's own screen
|
|
138
|
+
bun run build # webapp + settings page + dist/extension/desktop.mjs
|
|
139
|
+
bun run share # zips dist/, extension included
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Both dev commands:
|
|
143
|
+
|
|
144
|
+
1. Make this app the active webapp on the Car Thing, installing it once if the
|
|
145
|
+
device has never seen it, so forwards route.
|
|
146
|
+
2. Bundle `extension/main.ts` with esbuild and run it under Deno with the
|
|
147
|
+
manifest's permissions, from the gitignored `.dev-extension/`, which also
|
|
148
|
+
holds the kv store. Every save rebuilds and restarts it. A build error prints
|
|
149
|
+
and leaves the previous build running.
|
|
150
|
+
3. Link to the daemon over USB as an extension host, so the extension gets
|
|
151
|
+
`device.connected`, `device.active`, `config.changed`, and every forward the
|
|
152
|
+
page sends. With the Car Thing unplugged it links up when one appears.
|
|
153
|
+
|
|
154
|
+
`ctx.log.*` and stderr both print in the vite terminal. A crash restarts with
|
|
155
|
+
backoff, or immediately on the next save; a tight loop shows as `crashed` in the
|
|
156
|
+
desktop app's row. Ctrl-C stops the extension and, under `dev:device`, hands the
|
|
157
|
+
screen back. Disable this app's extension in the desktop app while you develop,
|
|
158
|
+
or the page hears both copies.
|
|
159
|
+
|
|
160
|
+
Under `bun run dev`, `ctx.auth.authorize(url)` ends on bridgething.com's callback
|
|
161
|
+
page. Copy that page's address and paste it at
|
|
162
|
+
`http://localhost:5173/__extension/authorize` to deliver it to the extension. The
|
|
163
|
+
desktop app captures the callback itself once the app is installed there.
|
|
164
|
+
|
|
165
|
+
The runtime is the `deno` npm package, pinned to the version the desktop app
|
|
166
|
+
downloads and fetched into `node_modules` on first run. `BRIDGETHING_DENO=/path`
|
|
167
|
+
overrides it.
|
|
168
|
+
|
|
169
|
+
## Shipping
|
|
170
|
+
|
|
171
|
+
`bun run push` installs the webapp on a Car Thing, and the extension stays on the
|
|
172
|
+
desktop. Once someone installs the app through the desktop app, that app runs the
|
|
173
|
+
extension from the zip. Phones have no extension host, and the desktop app can be
|
|
174
|
+
closed while the device is on a phone, so make the webapp useful with nothing
|
|
175
|
+
attached.
|
|
176
|
+
|
|
177
|
+
`npm:`, `jsr:`, and `node:` specifiers work in the bundle, as do `Deno.Command`
|
|
178
|
+
and unix sockets, subject to the permissions you declared. Deno resolves them at
|
|
179
|
+
runtime only when it is not standing in a project: a `package.json` or
|
|
180
|
+
`deno.json` anywhere above the bundle switches it to node_modules resolution and
|
|
181
|
+
it refuses `npm:` with _"Could not find a matching package"_. The dev loop and
|
|
182
|
+
the desktop app run the bundle from a data directory with
|
|
183
|
+
`DENO_NO_PACKAGE_JSON=1`. Set that variable when you run the bundle by hand.
|