166 lines
5.3 KiB
JavaScript
166 lines
5.3 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const ts = require("typescript");
|
|
|
|
// 2~4차가 아직 사용하는 기존 기반 helper만 파일·모듈 쌍으로 좁게 허용한다.
|
|
const LEGACY_DEPENDENCIES = {
|
|
"src/features/dredging/api/dredging-api.ts": ["@/components/lib/api"],
|
|
"src/features/dredging/api/photo-upload.ts": ["@/components/lib/utils"],
|
|
"src/features/dredging/lib/use-dredging-common-codes.ts": [
|
|
"@/components/lib/common/apiHelpers",
|
|
],
|
|
};
|
|
const LEGACY_DIRECTORY = "app/(with-base-layout)/oper/dredging/lib/";
|
|
const LEGACY_FILES = [
|
|
"client",
|
|
"types",
|
|
"uuid",
|
|
"date",
|
|
"exif",
|
|
"registration-request",
|
|
"useDredgingCommonCodes",
|
|
].map((name) => `${LEGACY_DIRECTORY}${name}.ts`);
|
|
|
|
/** 정적·동적 import, export-from, require의 문자열 모듈 참조를 수집한다. */
|
|
function moduleSpecifiers(sourceFile) {
|
|
const modules = [];
|
|
function visit(node) {
|
|
if (
|
|
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
node.moduleSpecifier &&
|
|
ts.isStringLiteral(node.moduleSpecifier)
|
|
) {
|
|
modules.push(node.moduleSpecifier.text);
|
|
}
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
node.arguments.length &&
|
|
ts.isStringLiteral(node.arguments[0]) &&
|
|
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
(ts.isIdentifier(node.expression) &&
|
|
node.expression.text === "require"))
|
|
) {
|
|
modules.push(node.arguments[0].text);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(sourceFile);
|
|
return modules;
|
|
}
|
|
|
|
/** 새 feature의 모델·기반과 legacy 진입점에만 검증 범위를 한정한다. */
|
|
function validateSource(relativePath, content) {
|
|
const sourceFile = ts.createSourceFile(
|
|
relativePath,
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
);
|
|
const errors = [];
|
|
if (LEGACY_FILES.includes(relativePath)) {
|
|
if (
|
|
!sourceFile.statements.length ||
|
|
sourceFile.statements.some(
|
|
(statement) =>
|
|
!ts.isExportDeclaration(statement) ||
|
|
!statement.moduleSpecifier ||
|
|
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
!statement.moduleSpecifier.text.startsWith("@/features/dredging/"),
|
|
)
|
|
) {
|
|
errors.push(
|
|
`${relativePath}: legacy 파일은 준설 feature 재수출만 허용합니다.`,
|
|
);
|
|
}
|
|
}
|
|
for (const specifier of moduleSpecifiers(sourceFile)) {
|
|
const resolved = specifier.startsWith("@/")
|
|
? `src/${specifier.slice(2)}`
|
|
: specifier.startsWith(".")
|
|
? path.posix.normalize(
|
|
path.posix.join(path.posix.dirname(relativePath), specifier),
|
|
)
|
|
: specifier;
|
|
if (
|
|
/^src\/features\/(dredging|mapLayers|gisManagement|gisEdit)\//.test(relativePath)
|
|
) {
|
|
if (specifier.startsWith("@/app/") || resolved.startsWith("app/")) {
|
|
errors.push(`${relativePath}: app 라우트 역참조 금지 (${specifier})`);
|
|
}
|
|
if (
|
|
specifier.startsWith("@/components/") &&
|
|
!(LEGACY_DEPENDENCIES[relativePath] ?? []).includes(specifier)
|
|
) {
|
|
errors.push(
|
|
`${relativePath}: 승인되지 않은 legacy 기반 의존 (${specifier})`,
|
|
);
|
|
}
|
|
if (
|
|
relativePath.includes("/model/") &&
|
|
/(?:^|\/)(?:api|ui|app|components)(?:\/|$)/.test(resolved)
|
|
) {
|
|
errors.push(
|
|
`${relativePath}: model이 API/UI/라우트에 의존할 수 없습니다 (${specifier})`,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
relativePath === "src/shared/api/http-transport.ts" &&
|
|
/^(?:src\/)?(?:features|entities|app|components)\//.test(resolved)
|
|
) {
|
|
errors.push(
|
|
`${relativePath}: transport의 상위 계층 역참조 금지 (${specifier})`,
|
|
);
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
/** 신규 feature만 재귀 검사한다. 타 앱·공용 패키지의 부채는 이 게이트에 혼합하지 않는다. */
|
|
function sourceFiles(directory) {
|
|
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const filename = path.join(directory, entry.name);
|
|
return entry.isDirectory()
|
|
? sourceFiles(filename)
|
|
: /\.tsx?$/.test(entry.name)
|
|
? [filename]
|
|
: [];
|
|
});
|
|
}
|
|
|
|
function checkWorkspace(root) {
|
|
const files = [
|
|
...LEGACY_FILES.map((relative) => path.join(root, relative)),
|
|
...sourceFiles(path.join(root, "src/features/dredging")),
|
|
...sourceFiles(path.join(root, "src/features/mapLayers")),
|
|
...sourceFiles(path.join(root, "src/features/gisManagement")),
|
|
...sourceFiles(path.join(root, "src/features/gisEdit")),
|
|
path.join(root, "src/shared/api/http-transport.ts"),
|
|
];
|
|
const errors = files.flatMap((filename) =>
|
|
validateSource(
|
|
path.relative(root, filename).split(path.sep).join("/"),
|
|
fs.readFileSync(filename, "utf8"),
|
|
),
|
|
);
|
|
return {
|
|
checkedFiles: files.length,
|
|
legacyDependencies: LEGACY_DEPENDENCIES,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const result = checkWorkspace(path.resolve(__dirname, ".."));
|
|
console.log(
|
|
`SN module boundaries: ${result.checkedFiles} files, ${result.errors.length} violations`,
|
|
);
|
|
console.log(
|
|
"Retained legacy bridges: dredging-api -> API utility; photo-upload -> TUS utility; common-code hook -> CMS helper",
|
|
);
|
|
for (const error of result.errors) console.error(error);
|
|
if (result.errors.length) process.exitCode = 1;
|
|
}
|
|
|
|
module.exports = { validateSource, checkWorkspace };
|