plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
3 changed files with 141 additions and 17 deletions
Showing only changes of commit 8459e1661b - Show all commits
+10 -16
View File
@@ -59,6 +59,11 @@ export default [
boundaries,
},
settings: {
"import/resolver": {
node: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
},
"boundaries/elements": [
{ type: "routes", pattern: "src/routes/*" },
{ type: "mf", pattern: "src/mf/*" },
@@ -107,10 +112,13 @@ export default [
},
},
// --- Restricted imports (master plan §1A-3) ---
// OTel SDK internals: only src/observability/metrics/otel.ts may import them
// OTel SDK internals + react-i18next: merged into one block so ESLint 9
// flat config doesn't override one rule with the other.
// otel.ts is allowed to import OTel SDK; provider.tsx is allowed to import react-i18next.
// Neither file needs the other's exemption, so combining ignores is safe.
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["src/observability/metrics/otel.ts"],
ignores: ["src/observability/metrics/otel.ts", "src/i18n/provider.tsx"],
rules: {
"no-restricted-imports": [
"error",
@@ -124,20 +132,6 @@ export default [
name: "@opentelemetry/sdk-node",
message: "Import from @opentelemetry/api or use getMeter()/getTracer() from @/observability/metrics/otel instead. Direct SDK access is restricted to src/observability/metrics/otel.ts.",
},
],
},
],
},
},
// react-i18next: only src/i18n/provider.tsx may import it
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["src/i18n/provider.tsx"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react-i18next",
message: "Import useTranslation from @/i18n/provider instead. Direct react-i18next imports are restricted to src/i18n/provider.tsx.",
+130
View File
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
const ROOT = path.resolve(import.meta.dirname, "../..");
/** Short random suffix to avoid collisions when tests run in parallel. */
function uid(): string {
return crypto.randomBytes(4).toString("hex");
}
/**
* Removes a file and any empty parent directories up to (but not including) the src/ tree root.
*/
function cleanupProbe(absPath: string): void {
try {
fs.unlinkSync(absPath);
} catch {
/* file may not exist */
}
let dir = path.dirname(absPath);
const srcDir = path.join(ROOT, "src");
while (dir.length > srcDir.length) {
try {
fs.rmdirSync(dir);
} catch {
break;
}
dir = path.dirname(dir);
}
}
/**
* Creates a probe source file, optionally a target file, runs ESLint, and cleans up.
*/
function lintProbe(
sourcePath: string,
sourceContent: string,
targetPath?: string,
targetContent?: string,
): string {
const absSrc = path.join(ROOT, sourcePath);
const absTarget = targetPath ? path.join(ROOT, targetPath) : null;
fs.mkdirSync(path.dirname(absSrc), { recursive: true });
fs.writeFileSync(absSrc, sourceContent, "utf8");
if (absTarget && targetContent) {
fs.mkdirSync(path.dirname(absTarget), { recursive: true });
fs.writeFileSync(absTarget, targetContent, "utf8");
}
try {
execSync(`pnpm exec eslint "${absSrc}" --format json`, {
cwd: ROOT,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
});
return "PASS";
} catch (err: unknown) {
const error = err as { stdout?: string };
return error.stdout ?? "UNKNOWN_ERROR";
} finally {
cleanupProbe(absSrc);
if (absTarget) {
cleanupProbe(absTarget);
}
}
}
const TARGET_CONTENT = "export const something = 1;\n";
describe("boundaries rules", () => {
it("features/ cannot import from routes/", () => {
const id = uid();
const result = lintProbe(
`src/features/online-board/__bnd_probe_${id}__.ts`,
`import { something } from "../../routes/__probe_${id}__/__target_probe__";\nexport const x = something;\n`,
`src/routes/__probe_${id}__/__target_probe__.ts`,
TARGET_CONTENT,
);
expect(result).toContain("boundaries/element-types");
});
it("features/ cannot import from mf/", () => {
const id = uid();
const result = lintProbe(
`src/features/online-board/__bnd_probe_${id}__.ts`,
`import { something } from "../../mf/expose/__target_probe_${id}__";\nexport const x = something;\n`,
`src/mf/expose/__target_probe_${id}__.ts`,
TARGET_CONTENT,
);
expect(result).toContain("boundaries/element-types");
});
it("ui/ cannot import from features/", () => {
const id = uid();
const result = lintProbe(
`src/ui/__probe_${id}__/__bnd_probe__.ts`,
'import { something } from "../../features/online-board/index";\nexport const x = something;\n',
);
expect(result).toContain("boundaries/element-types");
});
it("shared/ cannot import from features/", () => {
const id = uid();
const result = lintProbe(
`src/shared/__probe_${id}__/__bnd_probe__.ts`,
'import { something } from "../../features/online-board/index";\nexport const x = something;\n',
);
expect(result).toContain("boundaries/element-types");
});
it("observability/ cannot import from features/", () => {
const id = uid();
const result = lintProbe(
`src/observability/logger/__bnd_probe_${id}__.ts`,
'import { something } from "../../features/online-board/index";\nexport const x = something;\n',
);
expect(result).toContain("boundaries/element-types");
});
it("features/ CAN import from env/ (allowed direction)", () => {
const id = uid();
const result = lintProbe(
`src/features/online-board/__bnd_probe_${id}__.ts`,
'import { getEnv } from "../../env/index";\nexport const x = getEnv;\n',
);
expect(result).not.toContain("boundaries/element-types");
});
});
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
environment: "node",
globals: true,
passWithNoTests: true,
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
include: ["src/**/*.test.ts", "src/**/*.test.tsx", "tests/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "json-summary", "lcov"],