diff --git a/AGENTS.md b/AGENTS.md index 2e859835..43f242d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,10 @@ cli/ │ │ │ ├── api.ts # uploadSite() - reads archive, sends to API │ │ │ ├── deploy.ts # deploySite() - validates, creates tar.gz, uploads │ │ │ └── index.ts +│ │ ├── types/ # TypeScript type generation +│ │ │ ├── generator.ts # generateTypesFile() - creates types.d.ts +│ │ │ ├── update-project.ts # updateProjectConfig() - updates tsconfig.json +│ │ │ └── index.ts │ │ ├── utils/ │ │ │ ├── fs.ts # File system utilities │ │ │ └── index.ts @@ -116,8 +120,11 @@ cli/ │ │ │ └── push.ts │ │ ├── functions/ │ │ │ └── deploy.ts -│ │ └── site/ -│ │ └── deploy.ts +│ │ ├── site/ +│ │ │ └── deploy.ts +│ │ └── types/ +│ │ ├── index.ts # getTypesCommand(context) - parent command +│ │ └── generate.ts # getTypesGenerateCommand(context) factory │ ├── telemetry/ # Error reporting and telemetry │ │ ├── consts.ts # PostHog API key, env var names │ │ ├── posthog.ts # PostHog client singleton @@ -584,6 +591,7 @@ When an error is thrown, the CLI displays: | `FILE_NOT_FOUND` | `FileNotFoundError` | File doesn't exist | | `FILE_READ_ERROR` | `FileReadError` | Can't read/write file | | `INTERNAL_ERROR` | `InternalError` | Unexpected error | +| `TYPE_GENERATION_ERROR` | `TypeGenerationError` | Type generation failed for entity | ### CLIExitError (Special Case) diff --git a/bun.lock b/bun.lock index c076bf66..dc9a1c82 100644 --- a/bun.lock +++ b/bun.lock @@ -8,17 +8,21 @@ "@biomejs/biome": "^2.0.0", "@clack/prompts": "^0.11.0", "@types/bun": "^1.2.15", + "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", + "@types/json-schema": "^7.0.15", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@types/tar": "^6.1.13", "@vercel/detect-agent": "^1.1.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "common-tags": "^1.8.2", "ejs": "^3.1.10", "execa": "^9.6.1", "front-matter": "^4.0.2", "globby": "^16.1.0", + "json-schema-to-typescript": "^15.0.4", "json5": "^2.2.3", "ky": "^1.14.2", "lodash.kebabcase": "^4.1.1", @@ -37,6 +41,8 @@ }, }, "packages": { + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], @@ -75,6 +81,8 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -151,12 +159,16 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/common-tags": ["@types/common-tags@1.8.4", "", {}, "sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], "@types/lodash.kebabcase": ["@types/lodash.kebabcase@4.1.9", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-kPrrmcVOhSsjAVRovN0lRfrbuidfg0wYsrQa5IYuoQO1fpHHGSme66oyiYA/5eQPVl8Z95OA3HG0+d2SvYC85w=="], @@ -219,6 +231,8 @@ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -315,10 +329,14 @@ "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="], + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "ky": ["ky@1.14.2", "", {}, "sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -329,6 +347,8 @@ "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], @@ -367,6 +387,8 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -477,12 +499,16 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "json-schema-to-typescript/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -499,8 +525,12 @@ "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@apidevtools/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "json-schema-to-typescript/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index 7fc0b358..7118d68a 100644 --- a/package.json +++ b/package.json @@ -37,17 +37,21 @@ "@biomejs/biome": "^2.0.0", "@clack/prompts": "^0.11.0", "@types/bun": "^1.2.15", + "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", + "@types/json-schema": "^7.0.15", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@types/tar": "^6.1.13", "@vercel/detect-agent": "^1.1.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "common-tags": "^1.8.2", "ejs": "^3.1.10", "execa": "^9.6.1", "front-matter": "^4.0.2", "globby": "^16.1.0", + "json-schema-to-typescript": "^15.0.4", "json5": "^2.2.3", "ky": "^1.14.2", "lodash.kebabcase": "^4.1.1", diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts new file mode 100644 index 00000000..983ca8ef --- /dev/null +++ b/src/cli/commands/types/generate.ts @@ -0,0 +1,38 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { runCommand, runTask } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { readProjectConfig } from "@/core/index.js"; +import { generateTypesFile, updateProjectConfig } from "@/core/types/index.js"; + +const TYPES_FILE_PATH = "base44/.types/types.d.ts"; + +async function generateTypesAction(): Promise { + const { entities, functions, agents, project } = await readProjectConfig(); + + await runTask("Generating types", async () => { + await generateTypesFile({ entities, functions, agents }); + }); + + const tsconfigUpdated = await updateProjectConfig(project.root); + + return { + outroMessage: tsconfigUpdated + ? `Generated ${TYPES_FILE_PATH} and updated tsconfig.json` + : `Generated ${TYPES_FILE_PATH}`, + }; +} + +export function getTypesGenerateCommand(context: CLIContext): Command { + return new Command("generate") + .description( + "Generate TypeScript declaration file (types.d.ts) from project resources" + ) + .action(async () => { + await runCommand( + () => generateTypesAction(), + { requireAuth: false }, + context + ); + }); +} diff --git a/src/cli/commands/types/index.ts b/src/cli/commands/types/index.ts new file mode 100644 index 00000000..91e86701 --- /dev/null +++ b/src/cli/commands/types/index.ts @@ -0,0 +1,9 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getTypesGenerateCommand } from "./generate.js"; + +export function getTypesCommand(context: CLIContext): Command { + return new Command("types") + .description("Manage TypeScript type generation") + .addCommand(getTypesGenerateCommand(context)); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 8d01d274..15e4f1d7 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -10,6 +10,7 @@ import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; import { getSiteCommand } from "@/cli/commands/site/index.js"; +import { getTypesCommand } from "@/cli/commands/types/index.js"; import packageJson from "../../package.json"; import type { CLIContext } from "./types.js"; @@ -50,5 +51,8 @@ export function createProgram(context: CLIContext): Command { // Register site commands program.addCommand(getSiteCommand(context)); + // Register types command + program.addCommand(getTypesCommand(context), { hidden: true }); + return program; } diff --git a/src/cli/utils/version-check.ts b/src/cli/utils/version-check.ts index fcecb503..65e16b74 100644 --- a/src/cli/utils/version-check.ts +++ b/src/cli/utils/version-check.ts @@ -22,7 +22,7 @@ export async function checkForUpgrade(): Promise { try { const { stdout } = await execa("npm", ["view", "base44", "version"], { - timeout: 500, + timeout: 1000, shell: true, env: { CI: "1" }, }); diff --git a/src/core/config.ts b/src/core/config.ts index 7ec86e19..30f307a4 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,7 +1,11 @@ import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { PROJECT_SUBDIR } from "@/core/consts.js"; +import { + PROJECT_SUBDIR, + TYPES_FILENAME, + TYPES_OUTPUT_SUBDIR, +} from "@/core/consts.js"; import { type TestOverrides, TestOverridesSchema, @@ -31,6 +35,10 @@ export function getAppConfigPath(projectRoot: string): string { return join(projectRoot, PROJECT_SUBDIR, ".app.jsonc"); } +export function getTypesOutputPath(projectRoot: string): string { + return join(projectRoot, PROJECT_SUBDIR, TYPES_OUTPUT_SUBDIR, TYPES_FILENAME); +} + export function getBase44ApiUrl(): string { return process.env.BASE44_API_URL || "https://app.base44.com"; } diff --git a/src/core/consts.ts b/src/core/consts.ts index fc7b28e3..7aec471d 100644 --- a/src/core/consts.ts +++ b/src/core/consts.ts @@ -12,5 +12,9 @@ export const PROJECT_CONFIG_PATTERNS = [ `config.${CONFIG_FILE_EXTENSION_GLOB}`, ]; +// Types generation +export const TYPES_OUTPUT_SUBDIR = ".types"; +export const TYPES_FILENAME = "types.d.ts"; + // Auth export const AUTH_CLIENT_ID = "base44_cli"; diff --git a/src/core/errors.ts b/src/core/errors.ts index 41a4e438..5c63711d 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -370,6 +370,28 @@ export class InternalError extends SystemError { } } +/** + * Thrown when type generation fails for an entity. + */ +export class TypeGenerationError extends SystemError { + readonly code = "TYPE_GENERATION_ERROR"; + readonly entityName?: string; + + constructor(message: string, entityName?: string, cause?: unknown) { + super(message, { + hints: [ + { + message: entityName + ? `Check the schema for entity "${entityName}"` + : "Check your entity schemas for errors", + }, + ], + cause: cause instanceof Error ? cause : undefined, + }); + this.entityName = entityName; + } +} + // ============================================================================ // Type Guards // ============================================================================ diff --git a/src/core/resources/entity/schema.ts b/src/core/resources/entity/schema.ts index 9876b0f0..99391776 100644 --- a/src/core/resources/entity/schema.ts +++ b/src/core/resources/entity/schema.ts @@ -107,7 +107,6 @@ const PropertyTypeSchema = z.enum([ "boolean", "array", "object", - "binary", ]); const StringFormatSchema = z.enum([ @@ -120,8 +119,6 @@ const StringFormatSchema = z.enum([ "ipv4", "ipv6", "uuid", - "file", - "regex", ]); const PropertyDefinitionSchema = z.object({ diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts new file mode 100644 index 00000000..2f9b59eb --- /dev/null +++ b/src/core/types/generator.ts @@ -0,0 +1,124 @@ +import { source, stripIndent } from "common-tags"; +import type { JSONSchema4 } from "json-schema"; +import { compile } from "json-schema-to-typescript"; +import { getTypesOutputPath } from "@/core/config.js"; +import { TypeGenerationError } from "@/core/errors.js"; +import { getAppConfig } from "@/core/project/app-config.js"; +import type { AgentConfig } from "@/core/resources/agent/index.js"; +import type { Entity } from "@/core/resources/entity/index.js"; +import type { BackendFunction } from "@/core/resources/function/index.js"; +import { writeFile } from "@/core/utils/fs.js"; + +export interface GenerateTypesInput { + entities: Entity[]; + functions: BackendFunction[]; + agents: AgentConfig[]; +} + +const HEADER = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types generate +`; + +const EMPTY_TEMPLATE = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types + // + // No entities, functions, or agents found in project. + // Add resources to base44/entities/, base44/functions/, or base44/agents/ + // and run \`base44 types generate\` again. + + declare module '@base44/sdk' { + // No types to augment - add resources and regenerate + } +`; + +/** + * Generate and write types.d.ts file. + */ +export async function generateTypesFile( + input: GenerateTypesInput +): Promise { + const { projectRoot } = getAppConfig(); + const content = await generateContent(input); + await writeFile(getTypesOutputPath(projectRoot), content); +} + +async function generateContent(input: GenerateTypesInput): Promise { + const { entities, functions, agents } = input; + + if (!entities.length && !functions.length && !agents.length) { + return EMPTY_TEMPLATE; + } + + const entityInterfaces = await Promise.all( + entities.map((e) => compileEntity(e)) + ); + + // Build registry entries + const registryEntries: [string, string[]][] = [ + [ + "EntityTypeRegistry", + entities.map((e) => `${e.name}: ${toPascalCase(e.name)};`), + ], + ["FunctionNameRegistry", functions.map((f) => `${f.name}: true;`)], + ["AgentNameRegistry", agents.map((a) => `${a.name}: true;`)], + ]; + + // Generate registries (only for non-empty entries) + const registries = registryEntries + .filter(([, entries]) => entries.length > 0) + .map(([name, entries]) => registry(name, entries)); + + return [ + HEADER, + entityInterfaces.join("\n\n"), + source` + declare module '@base44/sdk' { + ${registries.join("\n\n")} + } + `, + ] + .filter(Boolean) + .join("\n\n"); +} + +async function compileEntity(entity: Entity): Promise { + const { name, ...schema } = entity; + + const jsonSchema = { + ...schema, + title: name, + additionalProperties: false, + } as JSONSchema4; + + try { + const ts = await compile(jsonSchema, name, { + bannerComment: "", + additionalProperties: false, + strictIndexSignatures: true, + }); + return ts.trim(); + } catch (error) { + throw new TypeGenerationError( + `Failed to generate types for entity "${name}"`, + name, + error + ); + } +} + +function registry(name: string, entries: string[]): string { + return source` + interface ${name} { + ${entries.join("\n")} + } + `; +} + +function toPascalCase(name: string): string { + return name + .split(/[-_\s]+/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts new file mode 100644 index 00000000..bb5182be --- /dev/null +++ b/src/core/types/index.ts @@ -0,0 +1,2 @@ +export { type GenerateTypesInput, generateTypesFile } from "./generator.js"; +export { updateProjectConfig } from "./update-project.js"; diff --git a/src/core/types/update-project.ts b/src/core/types/update-project.ts new file mode 100644 index 00000000..e89d2773 --- /dev/null +++ b/src/core/types/update-project.ts @@ -0,0 +1,46 @@ +import { join } from "node:path"; +import { PROJECT_SUBDIR, TYPES_OUTPUT_SUBDIR } from "@/core/consts.js"; +import { pathExists, readJsonFile, writeJsonFile } from "@/core/utils/fs.js"; + +const TYPES_INCLUDE_PATH = `${PROJECT_SUBDIR}/${TYPES_OUTPUT_SUBDIR}/*.d.ts`; + +/** + * Update project configuration files after generating types. + * Currently handles: + * - tsconfig.json: adds base44/.types to the include array + * + * @returns true if tsconfig.json was updated, false otherwise + */ +export async function updateProjectConfig( + projectRoot: string +): Promise { + const tsconfigPath = join(projectRoot, "tsconfig.json"); + + if (!(await pathExists(tsconfigPath))) { + return false; + } + + try { + const tsconfig = (await readJsonFile(tsconfigPath)) as { + include?: string[]; + }; + + // Ensure include array exists + if (!tsconfig.include) { + tsconfig.include = []; + } + + // Check if already included + if (tsconfig.include.includes(TYPES_INCLUDE_PATH)) { + return false; + } + + // Add to include array + tsconfig.include.push(TYPES_INCLUDE_PATH); + await writeJsonFile(tsconfigPath, tsconfig); + return true; + } catch { + // If we can't parse or update, silently fail and let user configure manually + return false; + } +} diff --git a/templates/backend-and-client/gitignore.ejs b/templates/backend-and-client/gitignore.ejs index 959a7a6e..00d4323a 100644 --- a/templates/backend-and-client/gitignore.ejs +++ b/templates/backend-and-client/gitignore.ejs @@ -18,5 +18,6 @@ dist .DS_Store *.swp -# Base44 App Config +# Base44 .app.json* +.types/ diff --git a/templates/backend-only/base44/gitignore.ejs b/templates/backend-only/base44/gitignore.ejs index 0378e42e..b8e4e2f9 100644 --- a/templates/backend-only/base44/gitignore.ejs +++ b/templates/backend-only/base44/gitignore.ejs @@ -6,5 +6,6 @@ outputFileName: .gitignore .env.* *.local -# Base44 App Config +# Base44 .app.json* +.types/ diff --git a/tests/cli/testkit/CLITestkit.ts b/tests/cli/testkit/CLITestkit.ts index d32c8357..7b8ff032 100644 --- a/tests/cli/testkit/CLITestkit.ts +++ b/tests/cli/testkit/CLITestkit.ts @@ -1,4 +1,4 @@ -import { cp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { Command } from "commander"; @@ -36,7 +36,8 @@ export class CLITestkit { private cleanupFn: () => Promise; private env: Record = {}; private projectDir?: string; - private testOverrides: TestOverrides = {}; + // Default latestVersion to null to skip npm version check in tests + private testOverrides: TestOverrides = { latestVersion: null }; /** Typed API mock for Base44 endpoints */ readonly api: Base44APIMock; @@ -90,7 +91,13 @@ export class CLITestkit { await cp(fixturePath, this.projectDir, { recursive: true }); } - givenLatestVersion(version: string | null): void { + /** + * Set the latest version for upgrade check. + * - Pass a version string (e.g., "1.0.0") to simulate an upgrade available + * - Pass null to simulate no upgrade available (default) + * - Pass undefined to test the real npm version check (not recommended, makes network call) + */ + givenLatestVersion(version: string | null | undefined): void { this.testOverrides.latestVersion = version; } @@ -98,9 +105,6 @@ export class CLITestkit { /** Execute CLI command */ async run(...args: string[]): Promise { - // Reset modules to clear any cached state (e.g., refreshPromise) - vi.resetModules(); - // Setup mocks this.setupCwdMock(); this.setupEnvOverrides(); @@ -120,7 +124,10 @@ export class CLITestkit { // Apply all API mocks before running this.api.apply(); - // Dynamic import after vi.resetModules() to get fresh module instances + // Reset module state to ensure test isolation + vi.resetModules(); + + // Import CLI module fresh after reset const { createProgram, CLIExitError } = (await import( DIST_INDEX_PATH )) as ProgramModule; @@ -260,6 +267,31 @@ export class CLITestkit { } } + /** Read a file from the project directory */ + async readProjectFile(relativePath: string): Promise { + if (!this.projectDir) { + throw new Error("No project set up. Call givenProject() first."); + } + try { + return await readFile(join(this.projectDir, relativePath), "utf-8"); + } catch { + return null; + } + } + + /** Check if a file exists in the project directory */ + async fileExists(relativePath: string): Promise { + if (!this.projectDir) { + throw new Error("No project set up. Call givenProject() first."); + } + try { + await access(join(this.projectDir, relativePath)); + return true; + } catch { + return false; + } + } + // ─── CLEANUP ────────────────────────────────────────────────── async cleanup(): Promise { diff --git a/tests/cli/testkit/index.ts b/tests/cli/testkit/index.ts index 10669692..345334ac 100644 --- a/tests/cli/testkit/index.ts +++ b/tests/cli/testkit/index.ts @@ -35,7 +35,7 @@ export interface TestContext { user?: { email: string; name: string } ) => Promise; - givenLatestVersion: (version: string | null) => void; + givenLatestVersion: (version: string | null | undefined) => void; // ─── WHEN METHODS ────────────────────────────────────────── @@ -50,6 +50,12 @@ export interface TestContext { /** Read the auth file (for login tests) */ readAuthFile: () => Promise | null>; + /** Read a file from the project directory */ + readProjectFile: (relativePath: string) => Promise; + + /** Check if a file exists in the project directory */ + fileExists: (relativePath: string) => Promise; + /** Get the temp directory path */ getTempDir: () => string; @@ -129,6 +135,8 @@ export function setupCLITests(): TestContext { // Then methods expectResult: (result) => getKit().expect(result), readAuthFile: () => getKit().readAuthFile(), + readProjectFile: (relativePath) => getKit().readProjectFile(relativePath), + fileExists: (relativePath) => getKit().fileExists(relativePath), getTempDir: () => getKit().getTempDir(), // API mocks diff --git a/tests/cli/types_generate.spec.ts b/tests/cli/types_generate.spec.ts new file mode 100644 index 00000000..3b54d313 --- /dev/null +++ b/tests/cli/types_generate.spec.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("types generate command", () => { + const t = setupCLITests(); + + it("generates types file with all resource types", async () => { + // Given a project with entities, agents, and functions + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Generated"); + + // And the types file is created + const typesFileExists = await t.fileExists("base44/.types/types.d.ts"); + expect(typesFileExists).toBe(true); + + // And the file contains the expected content + const typesContent = await t.readProjectFile("base44/.types/types.d.ts"); + expect(typesContent).not.toBeNull(); + + // Contains the auto-generated header + expect(typesContent).toContain("Auto-generated by Base44 CLI"); + + // Contains the entity interface + expect(typesContent).toContain("interface User"); + expect(typesContent).toContain("email"); + + // Contains the EntityTypeRegistry with the entity mapping + expect(typesContent).toContain("EntityTypeRegistry"); + expect(typesContent).toContain("User: User"); + + // Contains the FunctionNameRegistry with the function name + expect(typesContent).toContain("FunctionNameRegistry"); + expect(typesContent).toContain("hello: true"); + + // Contains the AgentNameRegistry with the agent name + expect(typesContent).toContain("AgentNameRegistry"); + expect(typesContent).toContain("assistant: true"); + }); + + it("updates tsconfig.json to include types path", async () => { + // Given a project with tsconfig.json + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // Verify initial tsconfig doesn't have the types include + const initialTsconfig = await t.readProjectFile("tsconfig.json"); + expect(initialTsconfig).not.toBeNull(); + expect(initialTsconfig).not.toContain("base44/.types"); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("tsconfig.json"); + + // And tsconfig.json is updated with the types include + const updatedTsconfig = await t.readProjectFile("tsconfig.json"); + expect(updatedTsconfig).not.toBeNull(); + const tsconfigObj = JSON.parse(updatedTsconfig!); + expect(tsconfigObj.include).toContain("base44/.types/*.d.ts"); + }); + + it("handles empty project with no resources", async () => { + // Given an empty project (no entities, agents, or functions) + await t.givenLoggedInWithProject(fixture("basic")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + + // And an empty template is generated + const typesContent = await t.readProjectFile("base44/.types/types.d.ts"); + expect(typesContent).not.toBeNull(); + expect(typesContent).toContain("No entities, functions, or agents found"); + }); + + it("skips tsconfig update if types path already included", async () => { + // Given a project with tsconfig.json + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // Run types generate first time + const firstResult = await t.run("types", "generate"); + t.expectResult(firstResult).toSucceed(); + + // Verify tsconfig was updated + const tsconfigAfterFirst = await t.readProjectFile("tsconfig.json"); + const firstTsconfigObj = JSON.parse(tsconfigAfterFirst!); + const includeCountAfterFirst = firstTsconfigObj.include.filter( + (p: string) => p === "base44/.types/*.d.ts" + ).length; + expect(includeCountAfterFirst).toBe(1); + + // Run types generate second time + const secondResult = await t.run("types", "generate"); + t.expectResult(secondResult).toSucceed(); + + // Verify tsconfig still has only one entry + const tsconfigAfterSecond = await t.readProjectFile("tsconfig.json"); + const secondTsconfigObj = JSON.parse(tsconfigAfterSecond!); + const includeCountAfterSecond = secondTsconfigObj.include.filter( + (p: string) => p === "base44/.types/*.d.ts" + ).length; + expect(includeCountAfterSecond).toBe(1); + }); + + it("works without tsconfig.json", async () => { + // Given a project without tsconfig.json (basic fixture doesn't have one) + await t.givenLoggedInWithProject(fixture("basic")); + + // Verify no tsconfig.json exists + const tsconfigExists = await t.fileExists("tsconfig.json"); + expect(tsconfigExists).toBe(false); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + + // And types file is still generated + const typesFileExists = await t.fileExists("base44/.types/types.d.ts"); + expect(typesFileExists).toBe(true); + }); + + it("fails with TypeGenerationError for invalid entity schema", async () => { + // Given a project with an invalid entity schema + await t.givenLoggedInWithProject(fixture("invalid-entity-schema")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command fails + t.expectResult(result).toFail(); + + // And the error message mentions the entity name + t.expectResult(result).toContain("Broken"); + t.expectResult(result).toContain("Failed to generate types"); + }); +}); diff --git a/tests/cli/version-check.spec.ts b/tests/cli/version-check.spec.ts index 964fa30b..90a99a5b 100644 --- a/tests/cli/version-check.spec.ts +++ b/tests/cli/version-check.spec.ts @@ -17,7 +17,7 @@ describe("upgrade notification", () => { }); it("does not display notification when version is current", async () => { - t.givenLatestVersion(null); + // latestVersion defaults to null (no upgrade available) await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); const result = await t.run("whoami"); @@ -27,6 +27,8 @@ describe("upgrade notification", () => { }); it("does not display notification when check is not overridden", async () => { + // Opt into real npm version check (default is null which skips it) + t.givenLatestVersion(undefined); await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); const result = await t.run("whoami"); diff --git a/tests/fixtures/basic/config.jsonc b/tests/fixtures/basic/base44/config.jsonc similarity index 100% rename from tests/fixtures/basic/config.jsonc rename to tests/fixtures/basic/base44/config.jsonc diff --git a/tests/fixtures/duplicate-agent-names/agents/first.json b/tests/fixtures/duplicate-agent-names/base44/agents/first.json similarity index 100% rename from tests/fixtures/duplicate-agent-names/agents/first.json rename to tests/fixtures/duplicate-agent-names/base44/agents/first.json diff --git a/tests/fixtures/duplicate-agent-names/agents/second.json b/tests/fixtures/duplicate-agent-names/base44/agents/second.json similarity index 100% rename from tests/fixtures/duplicate-agent-names/agents/second.json rename to tests/fixtures/duplicate-agent-names/base44/agents/second.json diff --git a/tests/fixtures/duplicate-agent-names/config.jsonc b/tests/fixtures/duplicate-agent-names/base44/config.jsonc similarity index 100% rename from tests/fixtures/duplicate-agent-names/config.jsonc rename to tests/fixtures/duplicate-agent-names/base44/config.jsonc diff --git a/tests/fixtures/full-project/config.jsonc b/tests/fixtures/full-project/base44/config.jsonc similarity index 100% rename from tests/fixtures/full-project/config.jsonc rename to tests/fixtures/full-project/base44/config.jsonc diff --git a/tests/fixtures/full-project/entities/task.json b/tests/fixtures/full-project/base44/entities/task.json similarity index 100% rename from tests/fixtures/full-project/entities/task.json rename to tests/fixtures/full-project/base44/entities/task.json diff --git a/tests/fixtures/full-project/functions/hello/function.jsonc b/tests/fixtures/full-project/base44/functions/hello/function.jsonc similarity index 100% rename from tests/fixtures/full-project/functions/hello/function.jsonc rename to tests/fixtures/full-project/base44/functions/hello/function.jsonc diff --git a/tests/fixtures/full-project/functions/hello/index.ts b/tests/fixtures/full-project/base44/functions/hello/index.ts similarity index 100% rename from tests/fixtures/full-project/functions/hello/index.ts rename to tests/fixtures/full-project/base44/functions/hello/index.ts diff --git a/tests/fixtures/invalid-agent/agents/broken.json b/tests/fixtures/invalid-agent/base44/agents/broken.json similarity index 100% rename from tests/fixtures/invalid-agent/agents/broken.json rename to tests/fixtures/invalid-agent/base44/agents/broken.json diff --git a/tests/fixtures/invalid-agent/config.jsonc b/tests/fixtures/invalid-agent/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-agent/config.jsonc rename to tests/fixtures/invalid-agent/base44/config.jsonc diff --git a/tests/fixtures/invalid-config-schema/config.jsonc b/tests/fixtures/invalid-config-schema/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-config-schema/config.jsonc rename to tests/fixtures/invalid-config-schema/base44/config.jsonc diff --git a/tests/fixtures/invalid-entity-schema/base44/.app.jsonc b/tests/fixtures/invalid-entity-schema/base44/.app.jsonc new file mode 100644 index 00000000..d7852426 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/tests/fixtures/invalid-entity-schema/base44/config.jsonc b/tests/fixtures/invalid-entity-schema/base44/config.jsonc new file mode 100644 index 00000000..3d337ce3 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Invalid Entity Schema Project" +} diff --git a/tests/fixtures/invalid-entity-schema/base44/entities/broken.json b/tests/fixtures/invalid-entity-schema/base44/entities/broken.json new file mode 100644 index 00000000..bb99f155 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/entities/broken.json @@ -0,0 +1,10 @@ +{ + "name": "Broken", + "type": "object", + "properties": { + "circular": { + "type": "object", + "$ref": "#/definitions/invalid" + } + } +} diff --git a/tests/fixtures/invalid-entity/config.jsonc b/tests/fixtures/invalid-entity/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-entity/config.jsonc rename to tests/fixtures/invalid-entity/base44/config.jsonc diff --git a/tests/fixtures/invalid-entity/entities/broken.json b/tests/fixtures/invalid-entity/base44/entities/broken.json similarity index 100% rename from tests/fixtures/invalid-entity/entities/broken.json rename to tests/fixtures/invalid-entity/base44/entities/broken.json diff --git a/tests/fixtures/invalid-json/config.jsonc b/tests/fixtures/invalid-json/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-json/config.jsonc rename to tests/fixtures/invalid-json/base44/config.jsonc diff --git a/tests/fixtures/no-app-config/config.jsonc b/tests/fixtures/no-app-config/base44/config.jsonc similarity index 100% rename from tests/fixtures/no-app-config/config.jsonc rename to tests/fixtures/no-app-config/base44/config.jsonc diff --git a/tests/fixtures/with-agents/agents/customer_support.json b/tests/fixtures/with-agents/base44/agents/customer_support.json similarity index 100% rename from tests/fixtures/with-agents/agents/customer_support.json rename to tests/fixtures/with-agents/base44/agents/customer_support.json diff --git a/tests/fixtures/with-agents/agents/data_analyst.jsonc b/tests/fixtures/with-agents/base44/agents/data_analyst.jsonc similarity index 100% rename from tests/fixtures/with-agents/agents/data_analyst.jsonc rename to tests/fixtures/with-agents/base44/agents/data_analyst.jsonc diff --git a/tests/fixtures/with-agents/agents/order_assistant.json b/tests/fixtures/with-agents/base44/agents/order_assistant.json similarity index 100% rename from tests/fixtures/with-agents/agents/order_assistant.json rename to tests/fixtures/with-agents/base44/agents/order_assistant.json diff --git a/tests/fixtures/with-agents/config.jsonc b/tests/fixtures/with-agents/base44/config.jsonc similarity index 100% rename from tests/fixtures/with-agents/config.jsonc rename to tests/fixtures/with-agents/base44/config.jsonc diff --git a/tests/fixtures/with-entities/config.jsonc b/tests/fixtures/with-entities/base44/config.jsonc similarity index 100% rename from tests/fixtures/with-entities/config.jsonc rename to tests/fixtures/with-entities/base44/config.jsonc diff --git a/tests/fixtures/with-entities/entities/customer.json b/tests/fixtures/with-entities/base44/entities/customer.json similarity index 100% rename from tests/fixtures/with-entities/entities/customer.json rename to tests/fixtures/with-entities/base44/entities/customer.json diff --git a/tests/fixtures/with-entities/entities/product.json b/tests/fixtures/with-entities/base44/entities/product.json similarity index 100% rename from tests/fixtures/with-entities/entities/product.json rename to tests/fixtures/with-entities/base44/entities/product.json diff --git a/tests/fixtures/with-functions-and-entities/config.jsonc b/tests/fixtures/with-functions-and-entities/base44/config.jsonc similarity index 100% rename from tests/fixtures/with-functions-and-entities/config.jsonc rename to tests/fixtures/with-functions-and-entities/base44/config.jsonc diff --git a/tests/fixtures/with-functions-and-entities/entities/order.json b/tests/fixtures/with-functions-and-entities/base44/entities/order.json similarity index 100% rename from tests/fixtures/with-functions-and-entities/entities/order.json rename to tests/fixtures/with-functions-and-entities/base44/entities/order.json diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc similarity index 100% rename from tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc rename to tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts similarity index 100% rename from tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts rename to tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/index.ts b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts similarity index 100% rename from tests/fixtures/with-functions-and-entities/functions/process-order/index.ts rename to tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts diff --git a/tests/fixtures/with-site/config.jsonc b/tests/fixtures/with-site/base44/config.jsonc similarity index 100% rename from tests/fixtures/with-site/config.jsonc rename to tests/fixtures/with-site/base44/config.jsonc diff --git a/tests/fixtures/with-types-resources/base44/.app.jsonc b/tests/fixtures/with-types-resources/base44/.app.jsonc new file mode 100644 index 00000000..d7852426 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/tests/fixtures/with-types-resources/base44/agents/assistant.json b/tests/fixtures/with-types-resources/base44/agents/assistant.json new file mode 100644 index 00000000..39c248a6 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/agents/assistant.json @@ -0,0 +1,5 @@ +{ + "name": "assistant", + "description": "A helpful assistant agent", + "instructions": "You are a helpful assistant. Answer questions clearly and concisely." +} diff --git a/tests/fixtures/with-types-resources/base44/config.jsonc b/tests/fixtures/with-types-resources/base44/config.jsonc new file mode 100644 index 00000000..d37f7c24 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Types Test Project" +} diff --git a/tests/fixtures/with-types-resources/base44/entities/user.json b/tests/fixtures/with-types-resources/base44/entities/user.json new file mode 100644 index 00000000..73065b7d --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/entities/user.json @@ -0,0 +1,15 @@ +{ + "name": "User", + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address" + }, + "age": { + "type": "number", + "description": "User age" + } + }, + "required": ["email"] +} diff --git a/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc b/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc new file mode 100644 index 00000000..a8484502 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc @@ -0,0 +1,4 @@ +{ + "name": "hello", + "entry": "index.ts" +} diff --git a/tests/fixtures/with-types-resources/base44/functions/hello/index.ts b/tests/fixtures/with-types-resources/base44/functions/hello/index.ts new file mode 100644 index 00000000..f3350963 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/functions/hello/index.ts @@ -0,0 +1,3 @@ +export default function hello() { + return "Hello, World!"; +} diff --git a/tests/fixtures/with-types-resources/tsconfig.json b/tests/fixtures/with-types-resources/tsconfig.json new file mode 100644 index 00000000..419c2114 --- /dev/null +++ b/tests/fixtures/with-types-resources/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES2020" + }, + "include": ["src"] +} diff --git a/vitest.config.ts b/vitest.config.ts index 710ad485..46a91b01 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ environment: "node", globals: true, include: ["tests/**/*.spec.ts"], - testTimeout: 10000, + testTimeout: 30000, mockReset: true, silent: true, // Suppress stdout/stderr from tests (CLI output is very noisy) },