refactor: simplify widget import boundary

This commit is contained in:
Thomas Camlong committed 2026-08-23 09:00:34 +02:00
1 parent 382cffc015
commit 3efb719eee
4 files changed
+22 -77

No files matched your search

+20
View File
@@ -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",
@@ -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<string, unknown>;
if (node.type === "CallExpression") {
const callee = node.callee as Record<string, unknown> | undefined;
const argument = Array.isArray(node.arguments)
? (node.arguments[0] as Record<string, unknown> | 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);
});
});
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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