From 3efb719eeeb7537771612a02445206974eed8a19 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 22 Aug 2026 19:47:12 +0200 Subject: [PATCH] refactor: simplify widget import boundary --- .oxlintrc.json | 20 +++++ .../assistant/widget-import-boundary.spec.ts | 75 ------------------- pnpm-lock.yaml | 2 +- pnpm-workspace.yaml | 2 +- 4 files changed, 22 insertions(+), 77 deletions(-) delete mode 100644 apps/nextjs/src/components/assistant/widget-import-boundary.spec.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 8f0096195..37ed7a019 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -30,6 +30,26 @@ "options": { "reportUnusedDisableDirectives": "off" }, + "overrides": [ + { + "files": ["apps/nextjs/src/**/*.ts", "apps/nextjs/src/**/*.tsx"], + "excludeFiles": ["**/*.spec.ts", "**/*.spec.tsx"], + "rules": { + "eslint/no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@homarr/widgets", + "allowTypeImports": true, + "message": "Import a narrow @homarr/widgets export to preserve widget lazy loading." + } + ] + } + ] + } + } + ], "ignorePatterns": [ "**/*.config.js", "**/*.config.mjs", diff --git a/apps/nextjs/src/components/assistant/widget-import-boundary.spec.ts b/apps/nextjs/src/components/assistant/widget-import-boundary.spec.ts deleted file mode 100644 index e4aa2a4ad..000000000 --- a/apps/nextjs/src/components/assistant/widget-import-boundary.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { readdirSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parse } from "@babel/parser"; -import { describe, expect, test } from "vitest"; - -const sourceRoot = join(dirname(fileURLToPath(import.meta.url)), "../.."); - -const collectRuntimeSourceFiles = (directory: string): string[] => - readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { - const path = join(directory, entry.name); - if (entry.isDirectory()) return collectRuntimeSourceFiles(path); - if (!/\.(ts|tsx)$/.test(entry.name) || /\.spec\.(ts|tsx)$/.test(entry.name)) return []; - return [path]; - }); - -const rootWidgetsModule = "@homarr/widgets"; - -const hasRuntimeRootWidgetImport = (source: string, fileName: string): boolean => { - const { program } = parse(source, { - sourceFilename: fileName, - sourceType: "unambiguous", - plugins: ["typescript", "jsx", "decorators-legacy", "importAttributes"], - }); - const hasStaticImport = program.body.some( - (node) => - node.type === "ImportDeclaration" && - node.source.value === rootWidgetsModule && - node.importKind !== "type" && - (node.specifiers.length === 0 || - node.specifiers.some((specifier) => specifier.type !== "ImportSpecifier" || specifier.importKind !== "type")), - ); - return hasStaticImport || hasDynamicRootWidgetImport(program); -}; - -const hasDynamicRootWidgetImport = (value: unknown): boolean => { - if (Array.isArray(value)) return value.some(hasDynamicRootWidgetImport); - if (typeof value !== "object" || value === null) return false; - - const node = value as Record; - if (node.type === "CallExpression") { - const callee = node.callee as Record | undefined; - const argument = Array.isArray(node.arguments) - ? (node.arguments[0] as Record | undefined) - : undefined; - if (callee?.type === "Import" && argument?.type === "StringLiteral" && argument.value === rootWidgetsModule) { - return true; - } - } - - return Object.values(node).some(hasDynamicRootWidgetImport); -}; - -describe("widget import boundary", () => { - test("application runtime code uses narrow widget exports", () => { - const offenders = collectRuntimeSourceFiles(sourceRoot).filter((path) => { - const source = readFileSync(path, "utf8"); - return hasRuntimeRootWidgetImport(source, path); - }); - - expect(offenders).toEqual([]); - }); - - test.each([ - ['import type { WidgetDefinition } from "@homarr/widgets";', false], - ['import { type WidgetDefinition } from "@homarr/widgets";', false], - ["const example = 'import { widgets } from \"@homarr/widgets\";';", false], - ['// import { widgets } from "@homarr/widgets";', false], - ['import { widgets } from "@homarr/widgets";', true], - ['import { type WidgetDefinition, widgets } from "@homarr/widgets";', true], - ['import /* chunk */ ( /* source */ "@homarr/widgets" );', true], - ])("detects runtime root imports in %s", (source, expected) => { - expect(hasRuntimeRootWidgetImport(source, "fixture.ts")).toBe(expected); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a56278e42..43b4000ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,7 +202,7 @@ catalogs: specifier: ^5.101.2 version: 5.101.2 '@tanstack/react-query-persist-client': - specifier: 5.101.2 + specifier: ^5.101.2 version: 5.101.2 '@testcontainers/mysql': specifier: ^12.0.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4f6c9b329..952d1f3a8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -63,7 +63,7 @@ catalog: "@tanstack/react-query": ^5.101.2 "@tanstack/react-query-devtools": ^5.101.2 "@tanstack/react-query-next-experimental": ^5.101.2 - "@tanstack/react-query-persist-client": 5.101.2 + "@tanstack/react-query-persist-client": ^5.101.2 "@testcontainers/mysql": ^12.0.4 "@testcontainers/postgresql": ^12.0.4 "@testcontainers/redis": ^12.0.4