Priority: P2
Problem
runCreateDomain은 apps/api-server/src/index.ts가 있으면 항상 그 파일을 controller 등록 대상으로 씁니다. production-app/admin-console(spa-be-split 템플릿)에서 index.ts는 createCrocoApp() 결과를 createNodeHost(app.getHono())에 넘기는 host 진입점이고, controller 목록은 app.ts의 const controllers = [UserController]에 있습니다. 그래서 codemod는 index.ts에서 등록 지점을 찾지 못하고 unsupported-pattern으로 끝납니다. CLI는 "Skipped controller registration: Could not identify one Croco app variable and its listen call. ..."을 출력합니다. 도메인 파일은 생성되지만 controller는 앱에 연결되지 않습니다.
codemod 자체는 이 구조를 처리할 수 있습니다. registerController({ entryPath: "<app>/src/app.ts", registrationArrayName: "controllers", ... })로 호출하면 템플릿 app.ts가 const controllers = [UserController, InvoiceController];로 바뀌고 import도 추가됩니다(status: "updated"). 배열 이름 없이 app.ts를 넘기면 unsupported-pattern("Controller registration uses unsupported array syntax.")입니다. 같은 CLI의 croco generate usage-dashboard에는 등록 대상 resolver(resolveControllerRegistrationTarget)가 있지만 create domain에는 없습니다. 이 resolver도 spa-be-split app.ts에는 배열 이름을 넘기지 않습니다.
Evidence
기준: trunk@1e313a1
packages/cli/src/commands/createDomain.ts:101-111
const apiServerSrc = join(workspace.root, "apps", "api-server", "src");
const indexPath = join(apiServerSrc, "index.ts");
const entryPath = existsSync(indexPath) ? indexPath : join(apiServerSrc, "app.ts");
const registration = register
? await registerController({
entryPath,
importPath: `./domains/${kebab}/${className}Controller`,
className: `${className}Controller`,
dryRun,
})
: null;
packages/create-croco-app/templates/spa-be-split/apps/api-server/src/app.ts:23,28: import { UserController } from "./controllers/UserController";, const controllers = [UserController];
packages/create-croco-app/templates/spa-be-split/apps/api-server/src/index.ts:4,66-67: createCrocoApp() 결과를 createNodeHost(app.getHono(), ...)에 넘기고 listen은 호출하지 않습니다.
packages/cli/src/libs/codemods/registerController.ts:47-75: registrationArrayName이 없으면 활성 등록/listen 패턴만 찾고, 찾지 못하면 unsupported-pattern을 반환합니다.
packages/cli/src/commands/generateUsageDashboard.ts:112-117,258-282: usage-dashboard는 resolveControllerRegistrationTarget로 applicationModule.ts의 SAAS_APPLICATION_CONTROLLERS → app.ts → index.ts 순서로 등록 대상을 고릅니다.
packages/cli/src/commands/createDomain.ts:258: Skipped controller registration: ${result.hint}를 출력합니다.
Reproduction
packages/cli/src/tests/createDomain.spec.ts에 추가할 테스트입니다. 실제 spa-be-split 템플릿의 index.ts, app.ts를 복사한 workspace를 씁니다. manifest의 zod는 스캐폴드와 같게 두어, controller 템플릿이 zod를 import하도록 바뀌어도 이 테스트가 의존성 검사에서 막히지 않게 합니다. 기존 import에 fileURLToPath(node:url)를 추가합니다.
const spaBackendTemplateSrc = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../create-croco-app/templates/spa-be-split/apps/api-server/src",
);
async function createProductionAppWorkspace(): Promise<string> {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "croco-cli-domain-spa-be-split-"));
const src = path.join(cwd, "apps", "api-server", "src");
await fs.mkdir(src, { recursive: true });
await fs.writeFile(path.join(cwd, "pnpm-workspace.yaml"), 'packages:\n - "apps/*"\n');
await fs.writeFile(
path.join(cwd, "apps", "api-server", "package.json"),
JSON.stringify({
dependencies: {
"@croco/framework-context": "workspace:*",
"@croco/protocols-rest": "workspace:*",
"@croco/repository-core": "workspace:*",
"@croco/transports-http": "workspace:*",
zod: "^3.23.8",
},
}),
);
for (const file of ["index.ts", "app.ts"]) {
await fs.copyFile(path.join(spaBackendTemplateSrc, file), path.join(src, file));
}
return cwd;
}
it("registers the generated controller in the production-app application controller list", async () => {
const cwd = await createProductionAppWorkspace();
const result = await runCreateDomain("Invoice", { cwd });
const appSource = await fs.readFile(path.join(cwd, "apps/api-server/src/app.ts"), "utf-8");
expect(result?.registration).toMatchObject({ status: "updated" });
expect(appSource).toMatch(/const controllers = \[[^\]]*InvoiceController/);
expect(appSource).toMatch(/import \{ InvoiceController \} from ["']\.\/domains\/invoice\/InvoiceController["']/);
});
pnpm --dir packages/cli exec vitest run src/tests/createDomain.spec.ts
관측:
AssertionError: expected { status: 'unsupported-pattern', …(3) } to match object { status: 'updated' }
- "status": "updated",
+ "status": "unsupported-pattern",
이때 result.registration.hint는 "Could not identify one Croco app variable and its listen call. Use one variable initialized by createApp() or createCrocoApp() and call listen through that variable."이고, app.ts는 바뀌지 않습니다.
Desired outcome
croco create domain이 Croco가 생성하는 스캐폴드 구조(application의 controller 배열)에 controller를 등록합니다. 등록할 수 없는 구조에서는 지금처럼 unsupported-pattern과 hint를 반환합니다.
Implementation path
generateUsageDashboard.ts의 resolveControllerRegistrationTarget(:258-282)을 createDomain과 함께 쓰는 등록 대상 resolver로 옮깁니다. spa-be-split 구조(app.ts에 const controllers = [...])는 { entryPath: app.ts, registrationArrayName: "controllers" }로 판별하는 분기를 추가합니다.
createDomain.ts:101-111에서 index.ts 우선 선택 대신 이 resolver를 씁니다.
- spa-be-split 템플릿 파일을 fixture로 쓰는 위 테스트를 추가합니다. usage-dashboard의 기존 등록 테스트(
GenerateUsageDashboard.spec.ts)가 그대로 통과하는지 확인합니다.
@croco/cli patch changeset을 추가합니다.
Acceptance criteria
- production-app/admin-console 스캐폴드에서
croco create domain invoice를 실행하면 app.ts의 controllers 배열에 InvoiceController가 추가되고 import가 생깁니다(status: "updated").
- 기존
createDomain.spec.ts(app.addControllers 진입점)와 GenerateUsageDashboard.spec.ts 시나리오는 그대로 통과합니다.
- 등록 지점이 없는 진입점은 계속
unsupported-pattern과 hint를 반환합니다.
Validation
pnpm --filter @croco/cli test
pnpm --filter @croco/cli typecheck
Scope boundaries
Priority: P2
Problem
runCreateDomain은apps/api-server/src/index.ts가 있으면 항상 그 파일을 controller 등록 대상으로 씁니다. production-app/admin-console(spa-be-split 템플릿)에서index.ts는createCrocoApp()결과를createNodeHost(app.getHono())에 넘기는 host 진입점이고, controller 목록은app.ts의const controllers = [UserController]에 있습니다. 그래서 codemod는index.ts에서 등록 지점을 찾지 못하고unsupported-pattern으로 끝납니다. CLI는 "Skipped controller registration: Could not identify one Croco app variable and its listen call. ..."을 출력합니다. 도메인 파일은 생성되지만 controller는 앱에 연결되지 않습니다.codemod 자체는 이 구조를 처리할 수 있습니다.
registerController({ entryPath: "<app>/src/app.ts", registrationArrayName: "controllers", ... })로 호출하면 템플릿app.ts가const controllers = [UserController, InvoiceController];로 바뀌고 import도 추가됩니다(status: "updated"). 배열 이름 없이app.ts를 넘기면unsupported-pattern("Controller registration uses unsupported array syntax.")입니다. 같은 CLI의croco generate usage-dashboard에는 등록 대상 resolver(resolveControllerRegistrationTarget)가 있지만create domain에는 없습니다. 이 resolver도 spa-be-splitapp.ts에는 배열 이름을 넘기지 않습니다.Evidence
기준:
trunk@1e313a1packages/cli/src/commands/createDomain.ts:101-111packages/create-croco-app/templates/spa-be-split/apps/api-server/src/app.ts:23,28:import { UserController } from "./controllers/UserController";,const controllers = [UserController];packages/create-croco-app/templates/spa-be-split/apps/api-server/src/index.ts:4,66-67:createCrocoApp()결과를createNodeHost(app.getHono(), ...)에 넘기고listen은 호출하지 않습니다.packages/cli/src/libs/codemods/registerController.ts:47-75:registrationArrayName이 없으면 활성 등록/listen패턴만 찾고, 찾지 못하면unsupported-pattern을 반환합니다.packages/cli/src/commands/generateUsageDashboard.ts:112-117,258-282: usage-dashboard는resolveControllerRegistrationTarget로applicationModule.ts의SAAS_APPLICATION_CONTROLLERS→app.ts→index.ts순서로 등록 대상을 고릅니다.packages/cli/src/commands/createDomain.ts:258:Skipped controller registration: ${result.hint}를 출력합니다.Reproduction
packages/cli/src/tests/createDomain.spec.ts에 추가할 테스트입니다. 실제 spa-be-split 템플릿의index.ts,app.ts를 복사한 workspace를 씁니다. manifest의zod는 스캐폴드와 같게 두어, controller 템플릿이zod를 import하도록 바뀌어도 이 테스트가 의존성 검사에서 막히지 않게 합니다. 기존 import에fileURLToPath(node:url)를 추가합니다.pnpm --dir packages/cli exec vitest run src/tests/createDomain.spec.ts관측:
이때
result.registration.hint는"Could not identify one Croco app variable and its listen call. Use one variable initialized by createApp() or createCrocoApp() and call listen through that variable."이고,app.ts는 바뀌지 않습니다.Desired outcome
croco create domain이 Croco가 생성하는 스캐폴드 구조(application의 controller 배열)에 controller를 등록합니다. 등록할 수 없는 구조에서는 지금처럼unsupported-pattern과 hint를 반환합니다.Implementation path
generateUsageDashboard.ts의resolveControllerRegistrationTarget(:258-282)을createDomain과 함께 쓰는 등록 대상 resolver로 옮깁니다. spa-be-split 구조(app.ts에const controllers = [...])는{ entryPath: app.ts, registrationArrayName: "controllers" }로 판별하는 분기를 추가합니다.createDomain.ts:101-111에서index.ts우선 선택 대신 이 resolver를 씁니다.GenerateUsageDashboard.spec.ts)가 그대로 통과하는지 확인합니다.@croco/clipatch changeset을 추가합니다.Acceptance criteria
croco create domain invoice를 실행하면app.ts의controllers배열에InvoiceController가 추가되고 import가 생깁니다(status: "updated").createDomain.spec.ts(app.addControllers진입점)와GenerateUsageDashboard.spec.ts시나리오는 그대로 통과합니다.unsupported-pattern과 hint를 반환합니다.Validation
pnpm --filter @croco/cli testpnpm --filter @croco/cli typecheckScope boundaries
apps/api-server경로 하드코딩([cli] detect() 및 make 커맨드가 apps/api-server 경로를 하드코딩해 프로젝트 레이아웃이 다를 때 스캐폴딩이 전면 실패한다 #2516)과 생성 controller 본문의 ContractGraph 오류는 범위 밖입니다. 후자는 #3094에서 다룹니다.@croco/repository-core의존성이 없어create domain이 거부되는 동작은 바꾸지 않습니다.