refactor(editor): Extract instance-registry into a frontend module package (no-changelog) (#36325)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Alex Grozav
2026-08-18 12:00:24 +00:00
committed by GitHub
parent e1a6ede450
commit 59d4300596
19 changed files with 266 additions and 7 deletions
@@ -42,7 +42,9 @@ export const sourcePackages = [
* A module that aliases the other modules lets a cross-module import resolve in its own test run.
* The module tsconfig base holds that boundary.
*/
export const modulePackages: Array<{ name: string; dir: string; entry?: boolean }> = [];
export const modulePackages: Array<{ name: string; dir: string; entry?: boolean }> = [
{ name: '@n8n/frontend-module-instance-registry', dir: 'modules/instance-registry/frontend' },
];
// The code below makes the Vite aliases from the two tables. Keep this code in the same file as
// the tables. A second file needs an import with a `.ts` specifier. That import causes error
+1
View File
@@ -49,6 +49,7 @@
"@n8n/constants": "workspace:*",
"@n8n/design-system": "workspace:*",
"@n8n/frontend-constants": "workspace:*",
"@n8n/frontend-module-instance-registry": "workspace:*",
"@n8n/frontend-module-sdk": "workspace:*",
"@n8n/frontend-utils": "workspace:*",
"@n8n/i18n": "workspace:*",
@@ -7,7 +7,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
import { useToast } from '@n8n/composables/useToast';
import { useClipboard } from '@n8n/composables/useClipboard';
import { useDebugInfo } from '@/app/composables/useDebugInfo';
import { useInstanceRegistryStore } from '@/features/instanceRegistry/stores/instanceRegistry.store';
import { useInstanceRegistryStore } from '@n8n/frontend-module-instance-registry';
import { useI18n } from '@n8n/i18n';
import { getThirdPartyLicenses } from '@n8n/rest-api-client';
@@ -62,7 +62,7 @@ const { mockClusterInfo } = vi.hoisted(() => ({
mockClusterInfo: { value: null as ClusterInfoResponse | null },
}));
vi.mock('@/features/instanceRegistry/stores/instanceRegistry.store', () => ({
vi.mock('@n8n/frontend-module-instance-registry', () => ({
useInstanceRegistryStore: () => ({
get clusterInfo() {
return mockClusterInfo.value;
@@ -1,6 +1,6 @@
import { useRootStore } from '@n8n/stores/useRootStore';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useInstanceRegistryStore } from '@/features/instanceRegistry/stores/instanceRegistry.store';
import { useInstanceRegistryStore } from '@n8n/frontend-module-instance-registry';
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
import type { WorkflowSettings } from 'n8n-workflow';
@@ -7,6 +7,7 @@ import { InstanceAiModule } from '@/features/ai/instanceAi/module.descriptor';
import { AgentsModule } from '@/features/agents/module.descriptor';
import { OtelModule } from '@/features/settings/otel/module.descriptor';
import { WorkflowReviewsModule } from '@/features/workflow-reviews/module.descriptor';
import { InstanceRegistryModule } from '@n8n/frontend-module-instance-registry';
/**
* Hard-coding modules list until we have a dynamic way to load modules.
@@ -20,4 +21,5 @@ export const modules: FrontendModuleDescription[] = [
AgentsModule,
OtelModule,
WorkflowReviewsModule,
InstanceRegistryModule,
];
@@ -25,6 +25,12 @@
"@n8n/composables*": ["../@n8n/composables/src*"],
"@n8n/constants*": ["../../@n8n/constants/src*"],
"@n8n/frontend-module-sdk": ["../@n8n/frontend-module-sdk/src/index.ts"],
"@n8n/frontend-module-instance-registry": [
"../../modules/instance-registry/frontend/src/index.ts"
],
"@n8n/frontend-module-instance-registry/*": [
"../../modules/instance-registry/frontend/src/*"
],
"@n8n/frontend-utils*": ["../@n8n/frontend-utils/src*"],
"@n8n/frontend-constants*": ["../@n8n/frontend-constants/src*"],
"@n8n/chat*": ["../@n8n/chat/src*"],
@@ -0,0 +1,27 @@
# @n8n/frontend-module-instance-registry
Frontend feature module. Consumed from source by the editor-ui shell through
`src/app/modules.manifest.ts`; there is no build step and no `dist`.
```bash
pnpm turbo typecheck --filter=@n8n/frontend-module-instance-registry
pnpm turbo lint --filter=@n8n/frontend-module-instance-registry
pnpm turbo test --filter=@n8n/frontend-module-instance-registry
```
Go through turbo, not `pnpm --filter <pkg> typecheck`: this package is consumed
from source, and on a cold tree its platform dependencies have not been built
yet. Turbo builds them first; the bare pnpm form does not.
## Import rules
- Depend on foundation and platform packages only (`@n8n/design-system`,
`@n8n/stores`, `@n8n/composables`, `@n8n/i18n`, `@n8n/rest-api-client`,
`@n8n/api-types`, `@n8n/frontend-module-sdk`). Never import another
`@n8n/frontend-module-*`, and never import `@/…` from the shell.
- `@n8n/stores` and `@n8n/composables` are **subpath-only** — import
`@n8n/stores/settings.store`, not `@n8n/stores`.
- The no-cross-module rule is currently a convention: the shared tsconfig base
omits sibling modules from `paths`, which blocks an accidental import but not
a deliberate one (declaring the dependency makes it typecheck clean). The
ESLint rule that actually enforces it is CAT-3692.
@@ -0,0 +1,4 @@
{
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../../../biome.jsonc"]
}
@@ -0,0 +1,4 @@
import { defineConfig } from 'eslint/config';
import { frontendConfig } from '@n8n/eslint-config/frontend';
export default defineConfig(frontendConfig);
@@ -0,0 +1,51 @@
{
"name": "@n8n/frontend-module-instance-registry",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"clean": "rimraf .turbo",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"test:changed": "janitor test-scoped",
"test:dev": "vitest",
"lint": "eslint src --quiet",
"lint:fix": "eslint src --fix",
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
"format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore"
},
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/composables": "workspace:*",
"@n8n/design-system": "workspace:*",
"@n8n/frontend-module-sdk": "workspace:*",
"@n8n/i18n": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/rest-api-client": "workspace:*",
"@n8n/stores": "workspace:*",
"n8n-workflow": "workspace:*",
"pinia": "catalog:frontend",
"vue": "catalog:frontend",
"vue-router": "catalog:frontend"
},
"devDependencies": {
"@n8n/eslint-config": "workspace:*",
"@n8n/frontend-vite-config": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@testing-library/jest-dom": "catalog:frontend",
"@testing-library/vue": "catalog:frontend",
"@vitejs/plugin-vue": "catalog:frontend",
"eslint": "catalog:",
"typescript": "catalog:",
"unplugin-icons": "catalog:frontend",
"vite": "catalog:",
"vitest": "catalog:",
"vue-tsc": "catalog:frontend"
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -0,0 +1,12 @@
// The shared jsdom harness — observers, matchMedia, canvas, timers, teardown guards.
import '@n8n/vitest-config/setup/frontend';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach } from 'vitest';
// Framework boot stays per-package on purpose: `@n8n/i18n` devDepends on
// `@n8n/vitest-config`, so booting i18n from inside the shared harness would
// close a turbo build cycle. Add `useI18n` boot here if this module needs it.
beforeEach(() => {
setActivePinia(createPinia());
});
@@ -0,0 +1,5 @@
// The module's only public entry. The shell imports the descriptor from here via
// `modules.manifest.ts`; anything else the shell (or a test) needs must be exported
// here too — deep paths into `src/` are not part of the contract.
export { InstanceRegistryModule } from './instance-registry.module';
export { useInstanceRegistryStore } from './instance-registry.store';
@@ -0,0 +1,15 @@
import type { FrontendModuleDescription } from '@n8n/frontend-module-sdk';
/**
* Store-only: this module contributes no UI surface, so registering it is a no-op —
* `moduleInitializer` guards every surface it reads. `AboutModal` and `useDebugInfo`
* consume the store directly; the descriptor is what makes this a module the shell
* knows about rather than a library it happens to import.
*/
export const InstanceRegistryModule: FrontendModuleDescription = {
// Must match the backend module id: both gate off `/rest/module-settings`.
id: 'instance-registry',
name: 'Instance Registry',
description: 'Reports which instances are in this deployment and their health',
icon: 'server',
};
@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import type { ClusterInfoResponse } from '@n8n/api-types';
import { useInstanceRegistryStore } from '../instanceRegistry.store';
import { setActivePinia, createPinia } from 'pinia';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useInstanceRegistryStore } from './instance-registry.store';
const mocks = vi.hoisted(() => ({
getClusterInfo: vi.fn(),
@@ -0,0 +1,21 @@
{
"extends": "@n8n/typescript-config/tsconfig.frontend-module.json",
"compilerOptions": {
// `rootDirs`, `types` and `include` cannot be inherited from the base: relative entries in
// them resolve against this file, so a copy in the base would point at the base's directory.
// (`paths` is the exception and does come from the base.)
"rootDirs": [".", "../../../frontend/@n8n/design-system/src"],
// The two `.d.ts` entries are ambient declarations this package never imports, so nothing
// pulls them into the program: `~icons/*` and `markdown-it-task-lists` for design-system's
// source, `window.BASE_PATH` for `@n8n/stores`'s. Consuming those packages from source is
// what makes them the consumer's problem — a built `dist` would have carried them.
"types": [
"vite/client",
"vitest/globals",
"unplugin-icons/types/vue",
"../../../frontend/@n8n/design-system/src/shims-modules.d.ts",
"../../../frontend/@n8n/stores/src/shims.d.ts"
]
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
@@ -0,0 +1,23 @@
import vue from '@vitejs/plugin-vue';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { defineConfig, mergeConfig } from 'vite';
import { frontendAliases } from '@n8n/frontend-vite-config';
import { vitestConfig } from '@n8n/vitest-config/frontend';
const packageDir = fileURLToPath(new URL('.', import.meta.url));
const packagesDir = resolve(packageDir, '..', '..', '..');
export default mergeConfig(
defineConfig({
plugins: [vue()],
resolve: {
// The same platform mapping the editor-ui dev server uses, so a test resolves
// `@n8n/stores/...` from source rather than from a stale `dist` — the two disagreeing is
// what put 1,111 specifiers on the wrong side of the src/dist line. Sibling modules are
// deliberately absent: nothing here should make a cross-module import resolve.
alias: frontendAliases(packagesDir),
},
}),
vitestConfig,
);
+85
View File
@@ -5904,6 +5904,9 @@ importers:
'@n8n/frontend-constants':
specifier: workspace:*
version: link:../@n8n/frontend-constants
'@n8n/frontend-module-instance-registry':
specifier: workspace:*
version: link:../../modules/instance-registry/frontend
'@n8n/frontend-module-sdk':
specifier: workspace:*
version: link:../@n8n/frontend-module-sdk
@@ -6296,6 +6299,88 @@ importers:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
packages/modules/instance-registry/frontend:
dependencies:
'@n8n/api-types':
specifier: workspace:*
version: link:../../../@n8n/api-types
'@n8n/composables':
specifier: workspace:*
version: link:../../../frontend/@n8n/composables
'@n8n/design-system':
specifier: workspace:*
version: link:../../../frontend/@n8n/design-system
'@n8n/frontend-module-sdk':
specifier: workspace:*
version: link:../../../frontend/@n8n/frontend-module-sdk
'@n8n/i18n':
specifier: workspace:*
version: link:../../../frontend/@n8n/i18n
'@n8n/permissions':
specifier: workspace:*
version: link:../../../@n8n/permissions
'@n8n/rest-api-client':
specifier: workspace:*
version: link:../../../frontend/@n8n/rest-api-client
'@n8n/stores':
specifier: workspace:*
version: link:../../../frontend/@n8n/stores
n8n-workflow:
specifier: workspace:*
version: link:../../../workflow
pinia:
specifier: catalog:frontend
version: 2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
vue:
specifier: catalog:frontend
version: 3.5.26(typescript@6.0.2)
vue-router:
specifier: catalog:frontend
version: 4.5.0(vue@3.5.26(typescript@6.0.2))
devDependencies:
'@n8n/eslint-config':
specifier: workspace:*
version: link:../../../@n8n/eslint-config
'@n8n/frontend-vite-config':
specifier: workspace:*
version: link:../../../frontend/@n8n/frontend-vite-config
'@n8n/playwright-janitor':
specifier: workspace:*
version: link:../../../testing/janitor
'@n8n/typescript-config':
specifier: workspace:*
version: link:../../../@n8n/typescript-config
'@n8n/vitest-config':
specifier: workspace:*
version: link:../../../@n8n/vitest-config
'@testing-library/jest-dom':
specifier: catalog:frontend
version: 6.6.3
'@testing-library/vue':
specifier: catalog:frontend
version: 8.1.0(@vue/compiler-sfc@3.5.26)(vue@3.5.26(typescript@6.0.2))
'@vitejs/plugin-vue':
specifier: catalog:frontend
version: 5.2.4(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2))
eslint:
specifier: 'catalog:'
version: 9.29.0(jiti@2.6.1)
typescript:
specifier: 'catalog:'
version: 6.0.2
unplugin-icons:
specifier: catalog:frontend
version: 23.0.1(@vue/compiler-sfc@3.5.26)
vite:
specifier: 'catalog:'
version: 8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
vitest:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
vue-tsc:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
packages/node-dev:
dependencies:
'@n8n/di':