plan/react-rewrite #1
+10
-16
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user