refactor: simplify widget import boundary
This commit is contained in:
4 files changed
+22
-77
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
Generated
+1
-1
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user