From 0a8d4b4ac3081ae1a83157a906ba1c25844b7b79 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 25 Sep 2026 16:24:29 +0300 Subject: [PATCH] feat(overlay): add manual overlay initialization and support for Capacitor apps --- README.md | 46 +++++++++++++++++++++--- packages/ng-devtools/package.json | 6 ++-- packages/ng-devtools/src/overlay-auto.ts | 12 +++++++ packages/ng-devtools/src/overlay.ts | 23 ++++++------ packages/ng-devtools/tsdown.config.ts | 2 +- 5 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 packages/ng-devtools/src/overlay-auto.ts diff --git a/README.md b/README.md index 1914b3a..40c82e5 100644 --- a/README.md +++ b/README.md @@ -158,17 +158,53 @@ import '@santoshyadavdev/ng-devtools/overlay'; It looks for the devframe connection next to the page and then at `/__ng-devtools/`. -`initOverlay` is exported for a devtools mounted somewhere else. Importing the -module has already started an overlay on the default URLs by then, so dispose of -that one before starting another, or the page ends up with two connections and -two polling intervals: +For a devtools mounted somewhere else, import `initOverlay` from +`/overlay/manual`, which starts nothing on import and adds no popup: ```ts -import { initOverlay } from '@santoshyadavdev/ng-devtools/overlay'; +import { initOverlay } from '@santoshyadavdev/ng-devtools/overlay/manual'; const dispose = await initOverlay({ baseURL: '/__my-devtools/' }); ``` +### Capacitor and Ionic + +An Ionic or Capacitor app runs in a WebView, so the overlay works there as it +does in a browser, forms included. The devtools server runs on your machine and +the app connects to it: + +```ts +// main.ts, after bootstrapApplication(...) +if (isDevMode()) { + import('@santoshyadavdev/ng-devtools/overlay/manual').then(({ initOverlay }) => + initOverlay({ + baseURL: 'http://localhost:4000/__ng-devtools/', + connectionMeta: { backend: 'sse', sse: { path: '__sse' } }, + }), + ); +} +``` + +`connectionMeta` describes the server, so the app does not have to fetch +`__connection.json`, which the WebView blocks as a cross-origin request. Use +`{ backend: 'sse', sse: { path: '__sse' } }` for the Express middleware with +`ws: false`, and open `__connection.json` in a browser to see what another +server uses. + +| Where the app runs | How it reaches the server | +| -------------------------- | ------------------------------------------------------------- | +| Android emulator or device | `adb reverse tcp:4000 tcp:4000` (USB), then `localhost` works | +| iOS simulator | `localhost` works as is | + +Android blocks plain `http` unless you allow it for development in +`capacitor.config.ts`: + +```ts +server: { cleartext: true }, +``` + +Open the devtools UI in your desktop browser at the server's `/__ng-devtools/`. + ### In-Page Popup The devtools can appear as a floating popup directly on your page — no browser extension needed: diff --git a/packages/ng-devtools/package.json b/packages/ng-devtools/package.json index 28706d1..07fc919 100644 --- a/packages/ng-devtools/package.json +++ b/packages/ng-devtools/package.json @@ -16,7 +16,8 @@ "exports": { ".": "./src/devframe.ts", "./devframe": "./src/devframe.ts", - "./overlay": "./src/overlay.ts", + "./overlay": "./src/overlay-auto.ts", + "./overlay/manual": "./src/overlay.ts", "./popup": "./src/popup.ts", "./package.json": "./package.json" }, @@ -24,7 +25,8 @@ "exports": { ".": "./dist/devframe.mjs", "./devframe": "./dist/devframe.mjs", - "./overlay": "./dist/overlay.mjs", + "./overlay": "./dist/overlay-auto.mjs", + "./overlay/manual": "./dist/overlay.mjs", "./popup": "./dist/popup.mjs", "./package.json": "./package.json" } diff --git a/packages/ng-devtools/src/overlay-auto.ts b/packages/ng-devtools/src/overlay-auto.ts new file mode 100644 index 0000000..c4c9e2b --- /dev/null +++ b/packages/ng-devtools/src/overlay-auto.ts @@ -0,0 +1,12 @@ +import { initOverlay } from './overlay.ts'; + +export * from './overlay.ts'; + +// Auto-init when loaded as a script (skip during test environment) +if ( + typeof document !== 'undefined' && + !(typeof process !== 'undefined' && process.env?.['VITEST']) +) { + initOverlay().catch(console.error); + import('./popup.ts').then((m) => m.createDevtoolsPopup()).catch(console.error); +} diff --git a/packages/ng-devtools/src/overlay.ts b/packages/ng-devtools/src/overlay.ts index 143146d..0465ec6 100644 --- a/packages/ng-devtools/src/overlay.ts +++ b/packages/ng-devtools/src/overlay.ts @@ -1,4 +1,4 @@ -import { connectDevframe } from 'devframe/client'; +import { connectDevframe, type SetupDevframeConnectionOptions } from 'devframe/client'; import { collectForms, diffForms, @@ -83,10 +83,18 @@ function isFieldTarget(target: unknown): target is { formId: string; path: strin ); } -export async function initOverlay(options: { baseURL?: string | string[] } = {}) { +export interface OverlayOptions { + baseURL?: string | string[]; + connectionMeta?: SetupDevframeConnectionOptions['connectionMeta']; +} + +export async function initOverlay(options: OverlayOptions = {}) { // `connectDevframe()` alone looks for the connection next to the page, which // misses the documented `/__ng-devtools/` mount in a host app. - const rpc = await connectDevframe({ baseURL: options.baseURL ?? ['./', '/__ng-devtools/'] }); + const rpc = await connectDevframe({ + baseURL: options.baseURL ?? ['./', '/__ng-devtools/'], + connectionMeta: options.connectionMeta, + }); const my = rpc.scope('ng-devtools'); async function pushTree() { @@ -806,12 +814,3 @@ function safeSerialize(val: unknown): unknown { return String(val); } } - -// Auto-init when loaded as a script (skip during test environment) -if ( - typeof document !== 'undefined' && - !(typeof process !== 'undefined' && process.env?.['VITEST']) -) { - initOverlay().catch(console.error); - import('./popup.ts').then((m) => m.createDevtoolsPopup()).catch(console.error); -} diff --git a/packages/ng-devtools/tsdown.config.ts b/packages/ng-devtools/tsdown.config.ts index 2205823..a444c09 100644 --- a/packages/ng-devtools/tsdown.config.ts +++ b/packages/ng-devtools/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ - entry: ['src/devframe.ts', 'src/popup.ts', 'src/overlay.ts'], + entry: ['src/devframe.ts', 'src/popup.ts', 'src/overlay.ts', 'src/overlay-auto.ts'], format: 'esm', platform: 'node', dts: true,