From c746971c1822d1f669e0358568b98633d30ba922 Mon Sep 17 00:00:00 2001
From: "Anthony Fu (via agent)"
Date: Fri, 24 Jul 2026 07:12:56 +0000
Subject: [PATCH 1/5] feat(inspector): export current filters and settings as a
config file
Add a "Save as config file" button to the settings panel that opens a
dialog with the generated node-modules-inspector.config.ts source for the
current filters and settings, with copy and download actions.
---
.../src/app/components/panel/Settings.vue | 17 +++
.../components/panel/SettingsConfigDialog.vue | 125 +++++++++++++++++
.../src/app/state/settings.ts | 46 ++++---
.../src/app/utils/config.ts | 126 ++++++++++++++++++
4 files changed, 292 insertions(+), 22 deletions(-)
create mode 100644 packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
create mode 100644 packages/node-modules-inspector/src/app/utils/config.ts
diff --git a/packages/node-modules-inspector/src/app/components/panel/Settings.vue b/packages/node-modules-inspector/src/app/components/panel/Settings.vue
index 454b71b9..81009339 100644
--- a/packages/node-modules-inspector/src/app/components/panel/Settings.vue
+++ b/packages/node-modules-inspector/src/app/components/panel/Settings.vue
@@ -1,9 +1,12 @@
@@ -61,5 +64,19 @@ const backend = getBackend()
Refetch Data
+
+
+
+
+ Export the current filters and settings as a
+ node-modules-inspector.config.ts
+ file.
+
+
+
+
diff --git a/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue b/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
new file mode 100644
index 00000000..c48075a5
--- /dev/null
+++ b/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save as config file
+
+
+ {{ CONFIG_FILENAME }}
+
+
+
+
+
+
+
+ Save the following as
+ {{ CONFIG_FILENAME }}
+ in your project root to reuse the current filters and settings as defaults.
+
+
{{ content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/node-modules-inspector/src/app/state/settings.ts b/packages/node-modules-inspector/src/app/state/settings.ts
index 8c41a1c5..59b02e3d 100644
--- a/packages/node-modules-inspector/src/app/state/settings.ts
+++ b/packages/node-modules-inspector/src/app/state/settings.ts
@@ -1,30 +1,32 @@
import type { SettingsOptions } from '../../shared/types'
import { useLocalStorage } from '@vueuse/core'
+export const SETTINGS_DEFAULT: SettingsOptions = {
+ graphRender: 'normal',
+ moduleTypeSimple: false,
+ moduleTypeRender: 'badge',
+ deepDependenciesTree: true,
+ dependenciesGroupBy: 'none',
+ packageDetailsTab: 'dependents',
+ colorizePackageSize: true,
+ showInstallSizeBadge: true,
+ showPublishTimeBadge: false,
+ showProvenanceBadge: 'present',
+ showFileComposition: false,
+ showDependencySourceBadge: 'dev',
+ showPublintMessages: false,
+ showMaintainerActions: false,
+ showThirdPartyServices: false,
+ treatFauxAsESM: false,
+ chartColoringMode: 'spectrum',
+ collapseSidepanel: false,
+ chartAnimation: true,
+ preferNpmx: true,
+}
+
export const settings = useLocalStorage(
'node-modules-inspector-settings',
- {
- graphRender: 'normal',
- moduleTypeSimple: false,
- moduleTypeRender: 'badge',
- deepDependenciesTree: true,
- dependenciesGroupBy: 'none',
- packageDetailsTab: 'dependents',
- colorizePackageSize: true,
- showInstallSizeBadge: true,
- showPublishTimeBadge: false,
- showProvenanceBadge: 'present',
- showFileComposition: false,
- showDependencySourceBadge: 'dev',
- showPublintMessages: false,
- showMaintainerActions: false,
- showThirdPartyServices: false,
- treatFauxAsESM: false,
- chartColoringMode: 'spectrum',
- collapseSidepanel: false,
- chartAnimation: true,
- preferNpmx: true,
- },
+ { ...SETTINGS_DEFAULT },
{
deep: true,
mergeDefaults: true,
diff --git a/packages/node-modules-inspector/src/app/utils/config.ts b/packages/node-modules-inspector/src/app/utils/config.ts
new file mode 100644
index 00000000..e37a9399
--- /dev/null
+++ b/packages/node-modules-inspector/src/app/utils/config.ts
@@ -0,0 +1,126 @@
+import type { FilterOptions } from '../../shared/filters'
+import type { NodeModulesInspectorConfig, SettingsOptions } from '../../shared/types'
+import { objectMap } from '@antfu/utils'
+import { toRaw } from 'vue'
+import { FILTERS_SCHEMA } from '../../shared/filters'
+import { filters } from '../state/filters'
+import { settings, SETTINGS_DEFAULT } from '../state/settings'
+
+/**
+ * The hard schema defaults, independent of any loaded config file. Diffing
+ * against these makes the generated config reproduce the current UI state
+ * exactly, regardless of an existing `node-modules-inspector.config.ts`.
+ */
+const FILTERS_DEFAULT = objectMap(FILTERS_SCHEMA, (k, v) => [k, v.default]) as FilterOptions
+
+/**
+ * Filter keys that represent transient navigation state rather than
+ * persistent configuration, and therefore should not be serialized.
+ */
+const TRANSIENT_FILTER_KEYS = new Set([
+ 'search',
+ 'focus',
+ 'why',
+ 'compareA',
+ 'compareB',
+])
+
+function isDeepEqual(a: unknown, b: unknown): boolean {
+ if (a === b)
+ return true
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length)
+ return false
+ return a.every((v, i) => isDeepEqual(v, b[i]))
+ }
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
+ const ak = Object.keys(a)
+ const bk = Object.keys(b)
+ if (ak.length !== bk.length)
+ return false
+ return ak.every(k => isDeepEqual((a as any)[k], (b as any)[k]))
+ }
+ return false
+}
+
+/**
+ * Collect the current filters that differ from their defaults, omitting
+ * transient navigation state.
+ */
+export function getNonDefaultFilters(): Partial {
+ const state = toRaw(filters.state) as FilterOptions
+ const result: Partial = {}
+ for (const key of Object.keys(state) as (keyof FilterOptions)[]) {
+ if (TRANSIENT_FILTER_KEYS.has(key))
+ continue
+ if (!isDeepEqual(state[key], FILTERS_DEFAULT[key]))
+ result[key] = structuredClone(toRaw(state[key])) as any
+ }
+ return result
+}
+
+/**
+ * Collect the current settings that differ from their defaults.
+ */
+export function getNonDefaultSettings(): Partial {
+ const current = settings.value
+ const result: Partial = {}
+ for (const key of Object.keys(SETTINGS_DEFAULT) as (keyof SettingsOptions)[]) {
+ if (!isDeepEqual(current[key], SETTINGS_DEFAULT[key]))
+ result[key] = structuredClone(toRaw(current[key])) as any
+ }
+ return result
+}
+
+const IDENTIFIER_RE = /^[a-z_$][\w$]*$/i
+
+function serialize(value: unknown, indent: number): string {
+ const pad = ' '.repeat(indent)
+ const padInner = ' '.repeat(indent + 1)
+
+ if (Array.isArray(value)) {
+ if (value.length === 0)
+ return '[]'
+ const items = value.map(v => `${padInner}${serialize(v, indent + 1)},`).join('\n')
+ return `[\n${items}\n${pad}]`
+ }
+
+ if (value && typeof value === 'object') {
+ const keys = Object.keys(value)
+ if (keys.length === 0)
+ return '{}'
+ const items = keys.map((k) => {
+ const key = IDENTIFIER_RE.test(k) ? k : JSON.stringify(k)
+ return `${padInner}${key}: ${serialize((value as any)[k], indent + 1)},`
+ }).join('\n')
+ return `{\n${items}\n${pad}}`
+ }
+
+ if (typeof value === 'string')
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`
+
+ return String(value)
+}
+
+/**
+ * Generate the source of a `node-modules-inspector.config.ts` file that
+ * captures the current filters and settings as defaults.
+ */
+export function generateConfigFile(): string {
+ const config: NodeModulesInspectorConfig = {}
+
+ const defaultFilters = getNonDefaultFilters()
+ if (Object.keys(defaultFilters).length)
+ config.defaultFilters = defaultFilters
+
+ const defaultSettings = getNonDefaultSettings()
+ if (Object.keys(defaultSettings).length)
+ config.defaultSettings = defaultSettings
+
+ return [
+ `import { defineConfig } from 'node-modules-inspector'`,
+ '',
+ `export default defineConfig(${serialize(config, 0)})`,
+ '',
+ ].join('\n')
+}
From 1b0f54fd5f8b38f4a7741689a2830d89490bbfbd Mon Sep 17 00:00:00 2001
From: Anthony Fu
Date: Fri, 24 Jul 2026 16:18:15 +0900
Subject: [PATCH 2/5] chore: update ui
---
.../src/app/components/panel/Settings.vue | 8 --------
.../src/app/components/panel/SettingsConfigDialog.vue | 6 +++---
2 files changed, 3 insertions(+), 11 deletions(-)
diff --git a/packages/node-modules-inspector/src/app/components/panel/Settings.vue b/packages/node-modules-inspector/src/app/components/panel/Settings.vue
index 81009339..8f9ccb06 100644
--- a/packages/node-modules-inspector/src/app/components/panel/Settings.vue
+++ b/packages/node-modules-inspector/src/app/components/panel/Settings.vue
@@ -63,18 +63,10 @@ const showConfigDialog = ref(false)
Refetch Data
-
-
-
-
- Export the current filters and settings as a
- node-modules-inspector.config.ts
- file.
-
diff --git a/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue b/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
index c48075a5..2db938cc 100644
--- a/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
+++ b/packages/node-modules-inspector/src/app/components/panel/SettingsConfigDialog.vue
@@ -83,14 +83,14 @@ function download() {