Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@tanstack/react-devtools": "workspace:*",
"@tanstack/preact-devtools": "workspace:*",
"@tanstack/solid-devtools": "workspace:*",
"@tanstack/devtools-vite": "workspace:*"
"@tanstack/devtools-vite": "workspace:*",
"@tanstack/angular-devtools": "workspace:*"
}
}
5 changes: 5 additions & 0 deletions packages/angular-devtools/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @ts-check

import rootConfig from '../../eslint.config.js'

export default [...rootConfig]
61 changes: 61 additions & 0 deletions packages/angular-devtools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@tanstack/angular-devtools",
"version": "0.0.1",
"description": "TanStack Devtools is a set of tools for building advanced devtools for your Angular application.",
"author": "Tanner Linsley",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/devtools.git",
"directory": "packages/angular-devtools"
},
"homepage": "https://tanstack.com/devtools",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"keywords": [
"angular",
"devtools"
],
"type": "module",
"types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"engines": {
"node": ">=18"
},
"files": [
"dist",
"src"
],
"scripts": {
"clean": "premove ./build ./dist",
"test:eslint": "eslint ./src",
"test:lib": "vitest --passWithNoTests",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc",
"test:build": "publint --strict",
"build": "vite build"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, most other TanStack Angular adapters are using Angular CLI (and/or ng-packagr) itself:

https://github.com/TanStack/form/blob/main/packages/angular-form/package.json#L25

Because of incompatibilities of using Vite for builds.

},
"dependencies": {
"@tanstack/devtools": "workspace:*"
},
"devDependencies": {
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0"
},
"peerDependencies": {
"@angular/core": ">=19.0.0",
"@angular/platform-browser": ">=19.0.0"
}
}
150 changes: 150 additions & 0 deletions packages/angular-devtools/src/devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
ApplicationRef,
Component,
DestroyRef,
ElementRef,
EnvironmentInjector,
afterNextRender,
createComponent,
effect,
inject,
input,
} from '@angular/core'
import { PLUGIN_CONTAINER_ID, TanStackDevtoolsCore } from '@tanstack/devtools'
import type { ComponentRef, Type } from '@angular/core'
import type { TanStackDevtoolsPlugin } from '@tanstack/devtools'
import type {
TanStackDevtoolsAngularInit,
TanStackDevtoolsAngularPlugin,
} from './types'

@Component({
selector: 'tanstack-devtools',
standalone: true,
template: '<div #devtoolsHost></div>',
})
export class TanStackDevtoolsComponent {
plugins = input<TanStackDevtoolsAngularInit['plugins']>()
config = input<TanStackDevtoolsAngularInit['config']>()
eventBusConfig = input<TanStackDevtoolsAngularInit['eventBusConfig']>()

private hostRef = inject(ElementRef)
private appRef = inject(ApplicationRef)
private injector = inject(EnvironmentInjector)
private destroyRef = inject(DestroyRef)

private componentRefs: Array<ComponentRef<any>> = []
private devtools: TanStackDevtoolsCore | null = null

constructor() {
afterNextRender(() => {
const hostEl = this.hostRef.nativeElement.querySelector('div')
if (!hostEl) return

const pluginsMap = this.getPluginsMap()

this.devtools = new TanStackDevtoolsCore({
config: this.config(),
eventBusConfig: this.eventBusConfig(),
plugins: pluginsMap,
})

this.devtools.mount(hostEl)
})

effect(() => {
const plugins = this.plugins()
const config = this.config()
const eventBusConfig = this.eventBusConfig()

if (this.devtools) {
this.devtools.setConfig({
config,
eventBusConfig,
plugins: plugins?.map((p) => this.convertPlugin(p)) ?? [],
})
}
})

this.destroyRef.onDestroy(() => {
this.destroyAllComponents()
if (this.devtools) {
this.devtools.unmount()
this.devtools = null
}
})
}

private getPluginsMap(): Array<TanStackDevtoolsPlugin> {
const plugins = this.plugins()
if (!plugins) return []

return plugins.map((plugin) => this.convertPlugin(plugin))
}

private convertPlugin(
plugin: TanStackDevtoolsAngularPlugin,
): TanStackDevtoolsPlugin {
return {
id: plugin.id,
defaultOpen: plugin.defaultOpen,
name:
typeof plugin.name === 'string'
? plugin.name
: (e, theme) => {
this.renderComponent(plugin.name as Type<any>, e, {
theme,
...(plugin.inputs ?? {}),
})
},
render: (e, theme) => {
this.renderComponent(plugin.component, e, {
theme,
...(plugin.inputs ?? {}),
})
},
destroy: (pluginId) => {
this.destroyComponentsInContainer(`${PLUGIN_CONTAINER_ID}-${pluginId}`)
},
}
}

private renderComponent(
component: Type<any>,
container: HTMLElement,
inputs: Record<string, unknown>,
) {
const componentRef = createComponent(component, {
environmentInjector: this.injector,
hostElement: container,
})

for (const [key, value] of Object.entries(inputs)) {
componentRef.setInput(key, value)
}

this.appRef.attachView(componentRef.hostView)
componentRef.changeDetectorRef.detectChanges()
this.componentRefs.push(componentRef)
}

private destroyComponentsInContainer(containerId: string) {
this.componentRefs = this.componentRefs.filter((ref) => {
const el = ref.location.nativeElement as HTMLElement
if (el.parentElement?.id === containerId) {
this.appRef.detachView(ref.hostView)
ref.destroy()
return false
}
return true
})
}

private destroyAllComponents() {
for (const ref of this.componentRefs) {
this.appRef.detachView(ref.hostView)
ref.destroy()
}
this.componentRefs = []
}
}
6 changes: 6 additions & 0 deletions packages/angular-devtools/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { TanStackDevtoolsComponent } from './devtools'

export type {
TanStackDevtoolsAngularPlugin,
TanStackDevtoolsAngularInit,
} from './types'
44 changes: 44 additions & 0 deletions packages/angular-devtools/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Type } from '@angular/core'
import type {
ClientEventBusConfig,
TanStackDevtoolsConfig,
} from '@tanstack/devtools'

export type TanStackDevtoolsAngularPlugin = {
id?: string
component: Type<any>
name: string | Type<any>
inputs?: Record<string, any>
defaultOpen?: boolean
}

export interface TanStackDevtoolsAngularInit {
/**
* Array of plugins to be used in the devtools.
* Each plugin should have a `component` that is an Angular component class.
*
* Example:
* ```typescript
* @Component({
* template: `<tanstack-devtools [plugins]="plugins" />`,
* imports: [TanStackDevtoolsComponent],
* })
* class AppComponent {
* plugins: TanStackDevtoolsAngularPlugin[] = [
* { name: 'My Plugin', component: MyPluginComponent }
* ];
* }
* ```
*/
plugins?: Array<TanStackDevtoolsAngularPlugin>
/**
* Configuration for the devtools shell. These configuration options are used to set the
* initial state of the devtools when it is started for the first time. Afterwards,
* the settings are persisted in local storage and changed through the settings panel.
*/
config?: Partial<TanStackDevtoolsConfig>
/**
* Configuration for the TanStack Devtools client event bus.
*/
eventBusConfig?: ClientEventBusConfig
}
7 changes: 7 additions & 0 deletions packages/angular-devtools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"experimentalDecorators": true
},
"include": ["src", "eslint.config.js", "vite.config.ts", "tests"]
}
29 changes: 29 additions & 0 deletions packages/angular-devtools/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import packageJson from './package.json'

const config = defineConfig({
plugins: [],
test: {
name: packageJson.name,
dir: './tests',
watch: false,
environment: 'jsdom',
globals: true,
},
})

export default mergeConfig(
config,
tanstackViteConfig({
entry: ['./src/index.ts'],
srcDir: './src',
externalDeps: [
'@angular/core',
'@angular/platform-browser',
'rxjs',
'zone.js',
],
cjs: false,
}),
)
12 changes: 11 additions & 1 deletion packages/devtools-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
"default": "./dist/vue/esm/index.js"
}
},
"./angular": {
"import": {
"types": "./dist/angular/esm/index.d.ts",
"default": "./dist/angular/esm/index.js"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
Expand All @@ -63,13 +69,17 @@
"@tanstack/devtools-ui": "workspace:^"
},
"peerDependencies": {
"@angular/core": ">=19.0.0",
"@types/react": ">=17.0.0",
"preact": ">=10.0.0",
"react": ">=17.0.0",
"solid-js": ">=1.9.7",
"vue": ">=3.2.0"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
},
"@types/react": {
"optional": true
},
Expand Down Expand Up @@ -98,7 +108,7 @@
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc",
"test:build": "publint --strict",
"build": "vite build && vite build --config vite.config.preact.ts && vite build --config vite.config.vue.ts && tsup "
"build": "vite build && vite build --config vite.config.preact.ts && vite build --config vite.config.vue.ts && vite build --config vite.config.angular.ts && tsup "
},
"devDependencies": {
"tsup": "^8.5.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools-utils/src/angular/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './panel'
export * from './plugin'
Loading
Loading