Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/3095-create-domain-registration-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@croco/cli": patch
"@croco/problems-core": patch
---

fix(cli): register create domain controllers in the application controller list
4 changes: 2 additions & 2 deletions docs/problem-code-registry.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "croco.problem-code-registry.v1",
"problemCount": 981,
Expand Down Expand Up @@ -686,7 +686,7 @@
"sources": [
{
"file": "packages/cli/src/commands/generateUsageDashboard.ts",
"line": 30,
"line": 25,
"column": 5,
"kind": "problem-constructor"
}
Expand Down Expand Up @@ -27106,7 +27106,7 @@
"sources": [
{
"file": "packages/transports-http/src/libs/RouteCompiler.ts",
"line": 331,
"line": 332,
"column": 10,
"kind": "problem-factory"
}
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/commands/createDomain.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { defineCommand } from "citty";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { CliError } from "../libs/CliError.js";
import { registerController } from "../libs/codemods/registerController.js";
import type { RegisterControllerResult } from "../libs/codemods/registerController.js";
import { resolveControllerRegistrationTarget } from "../libs/controllerRegistrationTarget.js";
import type { WriteResult } from "../libs/fileWriter.js";
import { write as fileWriterWrite } from "../libs/fileWriter.js";
import { getCrocoCommandRuntime, logWriteResult } from "../libs/cliRuntime.js";
Expand Down Expand Up @@ -105,11 +105,9 @@
);

const apiServerSrc = join(workspace.root, workspace.apiServerDir, "src");
const indexPath = join(apiServerSrc, "index.ts");
const entryPath = existsSync(indexPath) ? indexPath : join(apiServerSrc, "app.ts");
const registration = register
? await registerController({
entryPath,
...resolveControllerRegistrationTarget(apiServerSrc),
importPath: `./domains/${kebab}/${className}Controller`,
className: `${className}Controller`,
dryRun,
Expand Down
58 changes: 1 addition & 57 deletions packages/cli/src/commands/generateUsageDashboard.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { defineCommand } from "citty";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { Problem, ProblemCategory } from "@croco/problems-core";
import { Node, Project } from "ts-morph";
import { registerController } from "../libs/codemods/registerController.js";
import type { RegisterControllerResult } from "../libs/codemods/registerController.js";
import { resolveControllerRegistrationTarget } from "../libs/controllerRegistrationTarget.js";
import { CLI_DIAGNOSTIC_CODES, withLegacyCode } from "../libs/diagnosticCodes.js";
import type { WriteResult } from "../libs/fileWriter.js";
import { write as fileWriterWrite } from "../libs/fileWriter.js";
Expand All @@ -20,10 +19,6 @@ import { GLOBAL_OPTIONS } from "./options.js";
const DEFAULT_API_PATH = "/ops/usage";
const DEFAULT_PAGE_PATH = "/usage";
const CONTROLLER_CLASS_NAME = "UsageDashboardController";
const controllerRegistrationTargetProject = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
});

class InvalidUsageDashboardRoutePathProblem extends Problem {
constructor(label: string, value: string) {
Expand Down Expand Up @@ -268,57 +263,6 @@ function createApiSources(
];
}

function resolveControllerRegistrationTarget(apiServerSrc: string): {
readonly entryPath: string;
readonly registrationArrayName?: string;
} {
const applicationModulePath = join(apiServerSrc, "applicationModule.ts");
if (existsSync(applicationModulePath)) {
const content = readFileSync(applicationModulePath, "utf-8");
if (hasLocalControllerRegistrationArray(content)) {
return {
entryPath: applicationModulePath,
registrationArrayName: "SAAS_APPLICATION_CONTROLLERS",
};
}
}

const appPath = join(apiServerSrc, "app.ts");
if (existsSync(appPath)) {
const content = readFileSync(appPath, "utf-8");
if (hasControllerRegistrationTarget(content)) {
return { entryPath: appPath };
}
}

const indexPath = join(apiServerSrc, "index.ts");
return { entryPath: existsSync(indexPath) ? indexPath : appPath };
}

function hasLocalControllerRegistrationArray(content: string): boolean {
const sourceFile = controllerRegistrationTargetProject.createSourceFile(
"/usage-dashboard/applicationModule.ts",
content,
{ overwrite: true },
);
try {
const initializer = sourceFile
.getVariableDeclaration("SAAS_APPLICATION_CONTROLLERS")
?.getInitializer();
return Node.isArrayLiteralExpression(initializer);
} finally {
controllerRegistrationTargetProject.removeSourceFile(sourceFile);
}
}

function hasControllerRegistrationTarget(content: string): boolean {
return (
content.includes("controllers:") ||
content.includes("createApp(") ||
content.includes(".addControllers(")
);
}

function normalizeRoutePath(value: string, label: string): string {
const trimmed = value.trim();
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
Expand Down
68 changes: 68 additions & 0 deletions packages/cli/src/libs/controllerRegistrationTarget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { Node, Project } from "ts-morph";

export type ControllerRegistrationTarget = {
readonly entryPath: string;
readonly registrationArrayName?: string;
};

const controllerRegistrationTargetProject = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
});

export function resolveControllerRegistrationTarget(
apiServerSrc: string,
): ControllerRegistrationTarget {
const applicationModulePath = join(apiServerSrc, "applicationModule.ts");
if (existsSync(applicationModulePath)) {
const content = readFileSync(applicationModulePath, "utf-8");
if (hasNamedArrayLiteral(content, "SAAS_APPLICATION_CONTROLLERS")) {
return {
entryPath: applicationModulePath,
registrationArrayName: "SAAS_APPLICATION_CONTROLLERS",
};
}
}

const appPath = join(apiServerSrc, "app.ts");
if (existsSync(appPath)) {
const content = readFileSync(appPath, "utf-8");
if (hasNamedArrayLiteral(content, "controllers") && hasApplicationControllerTarget(content)) {
return { entryPath: appPath, registrationArrayName: "controllers" };
}
if (hasControllerRegistrationTarget(content)) {
return { entryPath: appPath };
}
}

const indexPath = join(apiServerSrc, "index.ts");
return { entryPath: existsSync(indexPath) ? indexPath : appPath };
}

function hasNamedArrayLiteral(content: string, name: string): boolean {
const sourceFile = controllerRegistrationTargetProject.createSourceFile(
`/controller-registration-target/${name}.ts`,
content,
{ overwrite: true },
);
try {
const initializer = sourceFile.getVariableDeclaration(name)?.getInitializer();
return Node.isArrayLiteralExpression(initializer);
} finally {
controllerRegistrationTargetProject.removeSourceFile(sourceFile);
}
}

function hasApplicationControllerTarget(content: string): boolean {
return (
content.includes("controllers:") ||
content.includes("createApp(") ||
content.includes(".addControllers(")
);
}

function hasControllerRegistrationTarget(content: string): boolean {
return hasApplicationControllerTarget(content);
}
42 changes: 42 additions & 0 deletions packages/cli/src/tests/createDomain.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { createCrocoCommandRuntime, runWithCrocoCommandRuntime } from "../libs/cliRuntime.js";
import { runCreateDomain } from "../commands/createDomain.js";
Expand Down Expand Up @@ -154,6 +155,19 @@ describe("runCreateDomain", () => {
await expect(fs.access(path.join(domainDir, "UserService.ts"))).rejects.toThrow();
await expect(fs.access(path.join(domainDir, "UserRepository.ts"))).rejects.toThrow();
});

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["']/,
);
});
});

async function createWorkspace(options: { apiServerManifest?: string } = {}): Promise<string> {
Expand Down Expand Up @@ -194,3 +208,31 @@ function apiServerManifest(packageNames: readonly string[]): string {
2,
);
}

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;
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1409,7 +1409,7 @@ Sources:

Sources:

- `packages/cli/src/commands/generateUsageDashboard.ts:30:5` (problem-constructor)
- `packages/cli/src/commands/generateUsageDashboard.ts:25:5` (problem-constructor)

<a id="croco-executable-assurance-contract-invalid"></a>

Expand Down Expand Up @@ -17249,7 +17249,7 @@ Sources:

Sources:

- `packages/transports-http/src/libs/RouteCompiler.ts:331:10` (problem-factory)
- `packages/transports-http/src/libs/RouteCompiler.ts:332:10` (problem-factory)

<a id="transports-http-graceful-shutdown-configuration"></a>

Expand Down
4 changes: 2 additions & 2 deletions packages/problems-core/src/generated/problem-code-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ export const CROCO_PROBLEM_CODE_REGISTRY = {
sources: [
{
file: "packages/cli/src/commands/generateUsageDashboard.ts",
line: 30,
line: 25,
column: 5,
kind: "problem-constructor",
},
Expand Down Expand Up @@ -28292,7 +28292,7 @@ export const CROCO_PROBLEM_CODE_REGISTRY = {
sources: [
{
file: "packages/transports-http/src/libs/RouteCompiler.ts",
line: 331,
line: 332,
column: 10,
kind: "problem-factory",
},
Expand Down
4 changes: 2 additions & 2 deletions tsconfig/contract-strict.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,7 @@
{
"packageName": "@croco/cli",
"file": "packages/cli/src/commands/createDomain.ts",
"line": 146,
"line": 144,
"column": 67,
"code": "TS2379",
"message": "Argument of type '{ dryRun: boolean; overwrite: boolean; cwd: string | undefined; register: boolean; }' is not assignable to parameter of type 'RunCreateDomainOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties."
Expand Down Expand Up @@ -2653,7 +2653,7 @@
{
"packageName": "@croco/cli",
"file": "packages/cli/src/commands/generateUsageDashboard.ts",
"line": 173,
"line": 168,
"column": 52,
"code": "TS2379",
"message": "Argument of type '{ dryRun: boolean; overwrite: boolean; cwd: string | undefined; apiPath: string | undefined; pagePath: string | undefined; page: boolean; }' is not assignable to parameter of type 'RunGenerateUsageDashboardOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties."
Expand Down
Loading