diff --git a/cli/shared/deployment/deploy-project.test.ts b/cli/shared/deployment/deploy-project.test.ts index a4e486e0cf..f81d0697fa 100644 --- a/cli/shared/deployment/deploy-project.test.ts +++ b/cli/shared/deployment/deploy-project.test.ts @@ -163,6 +163,80 @@ describe("DeployProject", () => { }); }); + it("refuses a config the hosted runtime can never evaluate", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + await Deno.writeTextFile( + `${projectDir}/veryfront.config.ts`, + `import { defineConfig } from "veryfront";\n` + + `import extCssLightning from "@veryfront/ext-css-lightning";\n\n` + + `export default defineConfig({\n extensions: [extCssLightning()],\n});\n`, + ); + const controlPlane = new InMemoryDeployControlPlane(); + try { + const error = await expectDeployError(() => executeApply(projectDir, controlPlane)); + + const message = (error as Error).message; + assertStringIncludes(message, "veryfront.config.ts"); + assertStringIncludes(message, "@veryfront/ext-css-lightning"); + assertEquals(controlPlane.createdReleases, [], "no release for an undeployable config"); + assertEquals( + controlPlane.createdDeployments, + [], + "no deployment for an undeployable config", + ); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + + it("refuses a literal config the hosted result policy always rejects", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + // Every construct here is one the hosted evaluator parses happily. It + // refuses the record afterwards, on every request, so a deploy that let + // this through would report success over an environment answering 500. + await Deno.writeTextFile( + `${projectDir}/veryfront.config.ts`, + `export default { cache: { dir: ".tenant-cache" } };\n`, + ); + const controlPlane = new InMemoryDeployControlPlane(); + try { + const error = await expectDeployError(() => executeApply(projectDir, controlPlane)); + + assertStringIncludes((error as Error).message, "cache.dir"); + assertEquals(controlPlane.createdReleases, [], "no release for an undeployable config"); + assertEquals( + controlPlane.createdDeployments, + [], + "no deployment for an undeployable config", + ); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + + it("deploys a config that only uses the hosted configuration helpers", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + await Deno.writeTextFile( + `${projectDir}/veryfront.config.ts`, + `import { defineConfig } from "veryfront";\n\n` + + `export default defineConfig({ title: "Demo" });\n`, + ); + const controlPlane = new InMemoryDeployControlPlane(); + try { + const outcome = await executeApply(projectDir, controlPlane); + + assertEquals(outcome.kind, "deployed", "a hosted-compatible config still deploys"); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + it("deploys a request-scoped project without inferring or persisting a local link", async () => { await withDeployEnv(async () => { const { projectDir, files } = await createUnlinkedPushedProject(); diff --git a/cli/shared/deployment/deploy-project.ts b/cli/shared/deployment/deploy-project.ts index b9bcfe2a83..c706c3d801 100644 --- a/cli/shared/deployment/deploy-project.ts +++ b/cli/shared/deployment/deploy-project.ts @@ -1,4 +1,9 @@ import { type EnvironmentConfig, getConfig, getEnvironmentConfig } from "veryfront/config"; +import { findVeryfrontConfigFile } from "#veryfront/config/config-files.ts"; +import { + findHostedConfigIncompatibility, + formatHostedConfigIncompatibility, +} from "#veryfront/config/hosted-compatibility.ts"; import { createFileSystem, isNotFoundError, runtime } from "veryfront/platform"; import { join, relative, resolve } from "veryfront/platform/path"; import { isWithinDirectory, normalizePath } from "veryfront/utils"; @@ -13,6 +18,7 @@ import { routeForPage, } from "veryfront/release-assets"; import { + CONFIG_NOT_DEPLOYABLE, DEPLOYMENT_ERROR, ENVIRONMENT_NOT_FOUND, RELEASE_MISSING_VERSION, @@ -547,6 +553,46 @@ function resolveProjectRouteDirectory( return routeRoot; } +/** + * Refuse a configuration file Veryfront Cloud can never read. + * + * A hosted project's config is evaluated as data, not imported, so a config + * that imports an extension is rejected on every request: the deploy reports + * success and the environment answers 500 to all traffic. Deciding it here + * costs one parse and turns that into a message before anything is created. + * + * Only rejections the source alone decides reach this far (see + * `findHostedConfigIncompatibility`), so a config whose values depend on + * deployment environment variables is never blocked by a difference between + * that environment and the developer's. + */ +async function assertConfigIsDeployable(projectDir: string): Promise { + const fs = createFileSystem(); + const configFile = await findVeryfrontConfigFile(projectDir, (path) => fs.exists(path)); + if (!configFile) return; + + let source: string; + try { + source = await fs.readTextFile(configFile.path); + } catch (error) { + if (isNotFoundError(error)) return; + throw error; + } + + const incompatibility = await findHostedConfigIncompatibility(source, configFile.fileName); + if (!incompatibility) return; + + throw CONFIG_NOT_DEPLOYABLE.create({ + detail: formatHostedConfigIncompatibility(incompatibility, configFile.fileName), + context: { + configFile: configFile.fileName, + code: incompatibility.code, + reason: incompatibility.reason, + ...(incompatibility.line === undefined ? {} : { line: incompatibility.line }), + }, + }); +} + async function collectProjectPageRoutes(projectDir: string): Promise { const fs = createFileSystem(); const directories = await getProjectRouteDirectories(projectDir); @@ -1164,7 +1210,15 @@ export function createDeployProject(options: { const environmentConfig = await step( observer, "resolve-config", - async () => getEnvironmentConfig(), + async () => { + // Naming a project deploys what that project already has, so the + // working directory is not the source under review and its config + // must not decide this deploy. + if (request.projectSlug === undefined) { + await assertConfigIsDeployable(request.projectDir); + } + return await getEnvironmentConfig(); + }, ); const receipt = await readPushReceipt(request.projectDir); const branch = request.branch ?? "main"; diff --git a/docs/api-reference/veryfront/errors.md b/docs/api-reference/veryfront/errors.md index 8a5bfc9a76..1241f0f288 100644 --- a/docs/api-reference/veryfront/errors.md +++ b/docs/api-reference/veryfront/errors.md @@ -37,129 +37,130 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); ### Components -| Name | Description | Source | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | -| `AGENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L3) | -| `AGENT_INTENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L27) | -| `AGENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L11) | -| `AGENT_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L19) | -| `API_CLIENT_ERROR` | API client request/response errors (replaces VeryfrontAPIError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L93) | -| `API_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L43) | -| `API_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L43) | -| `ASSET_OPTIMIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L35) | -| `AUTHENTICATION_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L11) | -| `BRANCH_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L91) | -| `BUILD_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/build-errors.ts#L4) | -| `BUILD_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L3) | -| `BUNDLE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L11) | -| `CACHE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L20) | -| `CACHE_INVARIANT_VIOLATION` | Cache path invariant violations (replaces CacheInvariantError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L111) | -| `CACHE_PATH_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L76) | -| `CIRCUIT_BREAKER_OPEN` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L68) | -| `CIRCULAR_DEPENDENCY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L19) | -| `CLIENT_BOUNDARY_VIOLATION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L3) | -| `CLIENT_ONLY_IN_SERVER` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L19) | -| `COMPILATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L59) | -| `COMPONENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L19) | -| `CONFIG_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/config-errors.ts#L4) | -| `CONFIG_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L12) | -| `CONFIG_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L3) | -| `CONFIG_PARSE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L20) | -| `CONFIG_TYPE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L37) | -| `CONFIG_VALIDATION_ERROR` | Schema-level config validation (e.g. Zod schema mismatch at runtime) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L29) | -| `CONFIG_VALIDATION_FAILED` | Config file validation failures (replaces ConfigValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L62) | -| `CORS_CONFIG_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L53) | -| `COST_LIMIT_EXCEEDED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L43) | -| `DEPENDENCY_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L35) | -| `DEPLOYMENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L3) | -| `DEPLOYMENT_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/deployment-errors.ts#L4) | -| `DEPLOYMENT_VERIFICATION_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L59) | -| `DEV_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/dev-errors.ts#L4) | -| `DEV_SERVER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L11) | -| `DURABLE_RUN_EVENT_PERSISTENCE_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L59) | -| `DYNAMIC_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L27) | -| `ENV_VAR_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L19) | -| `ENVIRONMENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L35) | -| `ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/index.ts#L31) | -| `ERROR_OVERLAY_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L27) | -| `ERROR_REGISTRY` | Central registry mapping every error slug to its definition. Assembled from the per-category registry fragments. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry.ts#L39) | -| `ERROR_SOLUTIONS` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/user-friendly/error-catalog.ts#L6) | -| `FALLBACK_EXHAUSTED` | Both primary and fallback operations failed (replaces FallbackExecutionError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L129) | -| `FAST_REFRESH_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L19) | -| `FILE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L27) | -| `FILE_WATCH_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L28) | -| `GENERAL_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/general-errors.ts#L4) | -| `HMR_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L3) | -| `HYDRATION_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L3) | -| `IMPORT_MAP_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L45) | -| `IMPORT_RESOLUTION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L11) | -| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L60) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | -| `INVALID_ARGUMENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L43) | -| `INVALID_IMPORT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L27) | -| `INVALID_ROUTE_FILE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L11) | -| `INVALID_USE_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L27) | -| `INVALID_USE_SERVER` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L35) | -| `LAYOUT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L27) | -| `LOCKFILE_FORMAT_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L51) | -| `LOCKFILE_READ_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L59) | -| `MDX_COMPILE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L27) | -| `MIDDLEWARE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L51) | -| `MODULE_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/module-errors.ts#L4) | -| `MODULE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L3) | -| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L103) | -| `NETWORK_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L84) | -| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L68) | -| `ORCHESTRATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L35) | -| `PAGE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L35) | -| `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | -| `PLATFORM_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L11) | -| `PORT_IN_USE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L3) | -| `PREVIEW_HOSTNAME_TOO_LONG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L83) | -| `PROBLEM_JSON_CONTENT_TYPE` | Content-Type header for RFC 9457 responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/http-error.ts#L32) | -| `PRODUCTION_BUILD_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L27) | -| `PROJECT_EXECUTION_UNAVAILABLE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L52) | -| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L94) | -| `PUSH_RECEIPT_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L67) | -| `RAG_STORE_CORRUPT` | Persisted RAG index is malformed or failed structural validation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L138) | -| `RAG_STORE_UNAVAILABLE` | A persisted RAG index operation could not be completed safely. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L147) | -| `RELEASE_BUILD_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L51) | -| `RELEASE_MISSING_VERSION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L43) | -| `RELEASE_NOT_FOUND` | Production domain resolved but no active release found | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L120) | -| `RENDER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L11) | -| `REQUEST_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L36) | -| `RESOURCE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L35) | -| `ROUTE_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L3) | -| `ROUTE_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/route-errors.ts#L4) | -| `ROUTE_HANDLER_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L19) | -| `ROUTE_PARAMS_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L35) | -| `RSC_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/rsc-errors.ts#L4) | -| `RSC_PAYLOAD_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L43) | -| `RUNTIME_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/runtime-errors.ts#L4) | -| `SCHEDULE_CONFIG_INVALID` | Schedule definition validation failures (required fields, cron, concurrencyPolicy, target) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L80) | -| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L77) | -| `SEMAPHORE_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L60) | -| `SERVER_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/server-errors.ts#L4) | -| `SERVER_ONLY_IN_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L11) | -| `SERVER_START_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L12) | -| `SERVICE_OVERLOADED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L44) | -| `SOURCE_DIGEST_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L75) | -| `SOURCE_MAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L35) | -| `SOURCEMAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L51) | -| `SSG_GENERATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L43) | -| `SSR_OUTPUT_LIMIT_EXCEEDED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L51) | -| `TEMPLATE_NOT_FOUND` | `veryfront init --template ` (and `npm create veryfront -- --template`) was given a name that is not in the starter catalog. The detail carries the list of valid names so a wrong guess is self-correcting. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L104) | -| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L52) | -| `TOKEN_STORAGE_ERROR` | Token storage adapter failures (replaces TokenStorageError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L102) | -| `TOOL_ID_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L51) | -| `TRIGGER_CONFIG_INVALID` | Trigger ID format and input serialization validation failures | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L90) | -| `TRIGGER_EXECUTION_FAILED` | Trigger target task or workflow failed during local run | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L69) | -| `TRIGGER_NOT_SUPPORTED` | Trigger target type is not supported in the current runtime context | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L78) | -| `TRIGGER_TARGET_NOT_FOUND` | Trigger target (task or workflow) not found during local run | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L60) | -| `TYPESCRIPT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L19) | -| `UNKNOWN_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L3) | -| `VERSION_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L43) | -| `WEBHOOK_CONFIG_INVALID` | Webhook definition validation failures (required fields, target, eventFilter) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L71) | +| Name | Description | Source | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `AGENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L3) | +| `AGENT_INTENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L27) | +| `AGENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L11) | +| `AGENT_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L19) | +| `API_CLIENT_ERROR` | API client request/response errors (replaces VeryfrontAPIError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L93) | +| `API_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L43) | +| `API_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L43) | +| `ASSET_OPTIMIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L35) | +| `AUTHENTICATION_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L11) | +| `BRANCH_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L91) | +| `BUILD_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/build-errors.ts#L4) | +| `BUILD_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L3) | +| `BUNDLE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L11) | +| `CACHE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L20) | +| `CACHE_INVARIANT_VIOLATION` | Cache path invariant violations (replaces CacheInvariantError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L111) | +| `CACHE_PATH_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L76) | +| `CIRCUIT_BREAKER_OPEN` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L68) | +| `CIRCULAR_DEPENDENCY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L19) | +| `CLIENT_BOUNDARY_VIOLATION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L3) | +| `CLIENT_ONLY_IN_SERVER` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L19) | +| `COMPILATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L59) | +| `COMPONENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L19) | +| `CONFIG_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/config-errors.ts#L4) | +| `CONFIG_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L12) | +| `CONFIG_NOT_DEPLOYABLE` | The project's configuration file uses a construct Veryfront Cloud's configuration evaluator can never accept, so the release would answer 500 to every request. Raised before a release is created; the detail names the file, the change that makes the project deployable, and the line when the evaluator located the construct it refused. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L106) | +| `CONFIG_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L3) | +| `CONFIG_PARSE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L20) | +| `CONFIG_TYPE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L37) | +| `CONFIG_VALIDATION_ERROR` | Schema-level config validation (e.g. Zod schema mismatch at runtime) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L29) | +| `CONFIG_VALIDATION_FAILED` | Config file validation failures (replaces ConfigValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L62) | +| `CORS_CONFIG_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L53) | +| `COST_LIMIT_EXCEEDED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L43) | +| `DEPENDENCY_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L35) | +| `DEPLOYMENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L3) | +| `DEPLOYMENT_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/deployment-errors.ts#L4) | +| `DEPLOYMENT_VERIFICATION_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L59) | +| `DEV_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/dev-errors.ts#L4) | +| `DEV_SERVER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L11) | +| `DURABLE_RUN_EVENT_PERSISTENCE_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L59) | +| `DYNAMIC_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L27) | +| `ENV_VAR_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L19) | +| `ENVIRONMENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L35) | +| `ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/index.ts#L31) | +| `ERROR_OVERLAY_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L27) | +| `ERROR_REGISTRY` | Central registry mapping every error slug to its definition. Assembled from the per-category registry fragments. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry.ts#L39) | +| `ERROR_SOLUTIONS` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/user-friendly/error-catalog.ts#L6) | +| `FALLBACK_EXHAUSTED` | Both primary and fallback operations failed (replaces FallbackExecutionError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L129) | +| `FAST_REFRESH_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L19) | +| `FILE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L27) | +| `FILE_WATCH_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L28) | +| `GENERAL_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/general-errors.ts#L4) | +| `HMR_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L3) | +| `HYDRATION_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L3) | +| `IMPORT_MAP_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L45) | +| `IMPORT_RESOLUTION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L11) | +| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L60) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | +| `INVALID_ARGUMENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L43) | +| `INVALID_IMPORT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L27) | +| `INVALID_ROUTE_FILE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L11) | +| `INVALID_USE_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L27) | +| `INVALID_USE_SERVER` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L35) | +| `LAYOUT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L27) | +| `LOCKFILE_FORMAT_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L51) | +| `LOCKFILE_READ_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L59) | +| `MDX_COMPILE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L27) | +| `MIDDLEWARE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L51) | +| `MODULE_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/module-errors.ts#L4) | +| `MODULE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L3) | +| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L103) | +| `NETWORK_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L84) | +| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L68) | +| `ORCHESTRATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L35) | +| `PAGE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L35) | +| `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | +| `PLATFORM_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L11) | +| `PORT_IN_USE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L3) | +| `PREVIEW_HOSTNAME_TOO_LONG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L83) | +| `PROBLEM_JSON_CONTENT_TYPE` | Content-Type header for RFC 9457 responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/http-error.ts#L32) | +| `PRODUCTION_BUILD_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L27) | +| `PROJECT_EXECUTION_UNAVAILABLE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L52) | +| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L94) | +| `PUSH_RECEIPT_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L67) | +| `RAG_STORE_CORRUPT` | Persisted RAG index is malformed or failed structural validation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L138) | +| `RAG_STORE_UNAVAILABLE` | A persisted RAG index operation could not be completed safely. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L147) | +| `RELEASE_BUILD_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L51) | +| `RELEASE_MISSING_VERSION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L43) | +| `RELEASE_NOT_FOUND` | Production domain resolved but no active release found | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L120) | +| `RENDER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L11) | +| `REQUEST_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L36) | +| `RESOURCE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L35) | +| `ROUTE_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L3) | +| `ROUTE_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/route-errors.ts#L4) | +| `ROUTE_HANDLER_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L19) | +| `ROUTE_PARAMS_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L35) | +| `RSC_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/rsc-errors.ts#L4) | +| `RSC_PAYLOAD_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L43) | +| `RUNTIME_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/runtime-errors.ts#L4) | +| `SCHEDULE_CONFIG_INVALID` | Schedule definition validation failures (required fields, cron, concurrencyPolicy, target) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L80) | +| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L77) | +| `SEMAPHORE_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L60) | +| `SERVER_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/server-errors.ts#L4) | +| `SERVER_ONLY_IN_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L11) | +| `SERVER_START_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L12) | +| `SERVICE_OVERLOADED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L44) | +| `SOURCE_DIGEST_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L75) | +| `SOURCE_MAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L35) | +| `SOURCEMAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L51) | +| `SSG_GENERATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L43) | +| `SSR_OUTPUT_LIMIT_EXCEEDED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L51) | +| `TEMPLATE_NOT_FOUND` | `veryfront init --template ` (and `npm create veryfront -- --template`) was given a name that is not in the starter catalog. The detail carries the list of valid names so a wrong guess is self-correcting. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L104) | +| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L52) | +| `TOKEN_STORAGE_ERROR` | Token storage adapter failures (replaces TokenStorageError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L102) | +| `TOOL_ID_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L51) | +| `TRIGGER_CONFIG_INVALID` | Trigger ID format and input serialization validation failures | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L90) | +| `TRIGGER_EXECUTION_FAILED` | Trigger target task or workflow failed during local run | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L69) | +| `TRIGGER_NOT_SUPPORTED` | Trigger target type is not supported in the current runtime context | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L78) | +| `TRIGGER_TARGET_NOT_FOUND` | Trigger target (task or workflow) not found during local run | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L60) | +| `TYPESCRIPT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L19) | +| `UNKNOWN_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L3) | +| `VERSION_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L43) | +| `WEBHOOK_CONFIG_INVALID` | Webhook definition validation failures (required fields, target, eventFilter) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L71) | ### Functions diff --git a/docs/guides/errors.md b/docs/guides/errors.md index 6f8321a7a5..04c487e299 100644 --- a/docs/guides/errors.md +++ b/docs/guides/errors.md @@ -569,6 +569,14 @@ Source map loading error. Raised while building, uploading, or activating a deployment. +### config-not-deployable + +Configuration cannot be deployed to Veryfront Cloud. + +- **HTTP status:** 400 +- **CLI exit code:** 2 +- **What to do:** Veryfront Cloud reads veryfront.config.ts as data: keep it to literals and the veryfront configuration helpers + ### deployment-error Deployment process failed. diff --git a/docs/guides/extensions.md b/docs/guides/extensions.md index 5815d137b6..01ec0717e6 100644 --- a/docs/guides/extensions.md +++ b/docs/guides/extensions.md @@ -18,6 +18,18 @@ see [Framework extensions](../concepts/framework-extensions.md). - For a local extension: a folder under `extensions/` with a default-exported factory (see [Extension authoring](./extension-authoring.md)). +## Where extensions run + +Extensions run wherever you run the project: `veryfront dev`, `veryfront +start`, and any runtime you host yourself. + +Veryfront Cloud is the exception. It reads a project's configuration file as +data rather than importing it, so a configuration file that imports an +extension factory cannot be evaluated there. `veryfront deploy` refuses such a +configuration before it creates a release, and names the line it refused. Keep +a configuration file that Veryfront Cloud serves to literals and the +`defineConfig`, `defineConfigWithEnv`, `getEnv` and `mergeConfigs` helpers. + ## Enable an extension Add extension factories to `veryfront.config.ts`: diff --git a/src/config/hosted-compatibility.test.ts b/src/config/hosted-compatibility.test.ts new file mode 100644 index 0000000000..da87f88561 --- /dev/null +++ b/src/config/hosted-compatibility.test.ts @@ -0,0 +1,162 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { + findHostedConfigIncompatibility, + formatHostedConfigIncompatibility, +} from "./hosted-compatibility.ts"; + +const EXTENSION_CONFIG = `import { defineConfig } from "veryfront"; +import extCssLightning from "@veryfront/ext-css-lightning"; + +export default defineConfig({ + extensions: [extCssLightning()], +}); +`; + +describe("hosted config compatibility", () => { + it("names the extension import that makes a project undeployable", async () => { + const incompatibility = await findHostedConfigIncompatibility(EXTENSION_CONFIG); + + assertEquals(incompatibility?.reason, "unsupported-import"); + assertEquals(incompatibility?.line, 2); + assertEquals( + incompatibility?.excerpt, + `import extCssLightning from "@veryfront/ext-css-lightning";`, + ); + }); + + it("names an extension import that stands on its own", async () => { + const incompatibility = await findHostedConfigIncompatibility( + `import extCssLightning from "@veryfront/ext-css-lightning";\n` + + `export default { extensions: [extCssLightning()] };\n`, + ); + + assertEquals(incompatibility?.code, "unsupported-syntax"); + assertEquals(incompatibility?.reason, "unsupported-import"); + assertEquals(incompatibility?.line, 1); + }); + + it("formats a message that says where, what, and what to do", () => { + const message = formatHostedConfigIncompatibility({ + code: "unsupported-syntax", + reason: "unsupported-import", + line: 2, + excerpt: `import extCssLightning from "@veryfront/ext-css-lightning";`, + summary: "Summary sentence.", + remedy: "Remedy sentence.", + }, "veryfront.config.ts"); + + assertStringIncludes(message, "veryfront.config.ts:2"); + assertStringIncludes(message, "@veryfront/ext-css-lightning"); + assertStringIncludes(message, "Summary sentence."); + assertStringIncludes(message, "Remedy sentence."); + }); + + it("keeps a credential out of the excerpt it prints", async () => { + const incompatibility = await findHostedConfigIncompatibility( + `const client = connect("postgres://admin:hunter2@db.internal.test/app");\n` + + `export default { title: "Demo" };\n`, + ); + + assertEquals(incompatibility?.line, 1); + assertEquals(incompatibility?.excerpt?.includes("hunter2"), false); + assertStringIncludes(incompatibility?.excerpt ?? "", "connect("); + }); + + it("accepts the configuration shapes the hosted evaluator supports", async () => { + for ( + const source of [ + `export default { title: "Demo" };`, + `import { defineConfig } from "veryfront";\nexport default defineConfig({ title: "Demo" });`, + `import { getEnv } from "veryfront";\n` + + `export default { title: getEnv("TITLE") ?? "Demo" };`, + `export default { extensions: [{ name: "ext-css-lightning", enabled: false }] };`, + ] + ) { + assertEquals( + await findHostedConfigIncompatibility(source), + null, + `expected no incompatibility for: ${source}`, + ); + } + }); + + it("refuses a literal config the hosted result policy always rejects", async () => { + // Nothing here reads the deployment environment, so this evaluation and + // the hosted one see the same record: the deploy would ship a release that + // answers 500 to every request. + for ( + const [source, reason] of [ + [`export default { cache: { dir: ".tenant-cache" } };`, "hosted-cache-directory"], + [ + `export default { extensions: [{ name: "ext-css-lightning" }] };`, + "hosted-extensions", + ], + [ + `export default { cache: { render: { type: "filesystem" } } };`, + "hosted-render-cache-backend", + ], + ] as const + ) { + const incompatibility = await findHostedConfigIncompatibility(source); + + assertEquals(incompatibility?.code, "unsupported-hosted-feature", source); + assertEquals(incompatibility?.reason, reason, source); + // The evaluator reports a result rejection against the program, not the + // key, so no line is claimed for one. + assertEquals(incompatibility?.line, undefined, source); + assertEquals(incompatibility?.excerpt, undefined, source); + } + }); + + it("names the hosted limit rather than the generic literal remedy", async () => { + const incompatibility = await findHostedConfigIncompatibility( + `export default { cache: { dir: ".tenant-cache" } };`, + ); + + assertStringIncludes(incompatibility?.summary ?? "", "cache.dir"); + assertStringIncludes(incompatibility?.remedy ?? "", "cache.dir"); + }); + + it("is not silenced by a helper name that is only text", async () => { + // "getEnv" here is a title, not a binding. Nothing in this config reads + // the environment, so the cache.dir verdict is still the hosted one. + const incompatibility = await findHostedConfigIncompatibility( + `export default { cache: { dir: ".tenant-cache" }, title: "getEnv" };\n`, + ); + + assertEquals(incompatibility?.reason, "hosted-cache-directory"); + }); + + it("defers on a literal rejection standing beside a real environment read", async () => { + // A known limit, and the safe direction: the evaluator names the reason it + // refused, not the path of the value it refused, so nothing here can tell + // whether ORIGINS reaches cache.dir. Reporting would risk blocking a deploy + // over a local difference; staying silent leaves the verdict where it was + // before this check existed, with the hosted runtime. + assertEquals( + await findHostedConfigIncompatibility( + `import { getEnv } from "veryfront";\n` + + `export default {\n` + + ` cache: { dir: ".tenant-cache" },\n` + + ` security: { cors: { origin: getEnv("ORIGINS") } },\n` + + `};\n`, + ), + null, + ); + }); + + it("stays silent about rejections that depend on evaluated values", async () => { + // The hosted result policy refuses an origin this evaluation cannot + // produce: ORIGINS is set in the deployment environment, not this one. A + // caller cannot reach that verdict honestly, so it reports nothing. + assertEquals( + await findHostedConfigIncompatibility( + `import { getEnv } from "veryfront";\n` + + `export default { security: { cors: { origin: getEnv("ORIGINS") } } };\n`, + ), + null, + ); + }); +}); diff --git a/src/config/hosted-compatibility.ts b/src/config/hosted-compatibility.ts new file mode 100644 index 0000000000..9e1f7d3b68 --- /dev/null +++ b/src/config/hosted-compatibility.ts @@ -0,0 +1,291 @@ +/** + * Static answer to "will Veryfront Cloud be able to read this config file?". + * + * A hosted project's `veryfront.config.ts` is never imported: the shared + * multi-project runtime evaluates it as data through the bounded declarative + * evaluator, which accepts only literals and the four `veryfront` helpers. + * Anything else is rejected on every request, so the deploy itself looks + * healthy while the environment answers 500 to all traffic. + * + * This module lets a deploy make that verdict before it creates a release. It + * reports a rejection only when this evaluation and the hosted one are bound to + * agree: + * + * - `validate`-phase rejections are decided by the parsed program alone, so a + * caller without the deployment environment's variables reaches exactly the + * verdict the hosted evaluator will. + * - `result`-phase rejections are decided by the evaluated configuration. When + * nothing in the source can read deployment environment data, that record is + * the source's own literals and the verdict is equally fixed. `cache.dir` is + * the plain case: a literal config that sets it is refused on every hosted + * request, and the deploy that shipped it reported success. + * + * A source that can read the environment is left alone in the `result` phase: a + * config whose `security.cors.origin` comes from `getEnv("ORIGINS")` evaluates + * to nothing against an empty local environment, and a deploy must never be + * blocked by a difference the developer cannot see. + * + * @module config/hosted-compatibility + */ + +import { + type DeclarativeConfigErrorCode, + type DeclarativeConfigErrorReason, + DeclarativeConfigEvaluationError, + type DeclarativeConfigFileName, + evaluateDeclarativeConfig, +} from "./declarative-evaluator.ts"; +import { sanitizeUrlCredentials } from "#veryfront/utils/logger/redact.ts"; + +/** Longest source excerpt echoed back to the developer. */ +const MAX_SOURCE_EXCERPT_CHARACTERS = 160; + +/** + * The names through which a configuration file can reach environment data. + * + * `getEnv` reads a deployment variable and `defineConfigWithEnv` hands the + * environment name to a callback; the hosted evaluator binds nothing else that + * can. An import names the helper it takes even when it renames it locally + * (`import { getEnv as env }`), so a file that imports neither evaluates to + * what its own literals say, here and in production alike. + */ +const ENVIRONMENT_READING_HELPERS = /\b(?:getEnv|defineConfigWithEnv)\b/; + +/** + * An `import ... from "veryfront"` statement, up to its specifier. + * + * Only an import binds a helper, so only an import can make a configuration + * environment-dependent — and by the time a result rejection exists the program + * has validated, which means every import it has is this one. Reading the + * statements rather than the whole file keeps a string or a comment that + * happens to spell `getEnv` from silencing a verdict the config's own literals + * decide. + */ +const VERYFRONT_IMPORT_STATEMENTS = /\bimport\b[^;]*?\bfrom\s*["']veryfront["']/g; + +// deno-lint-ignore no-control-regex +const CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g; + +/** A statically decided reason a config cannot run on Veryfront Cloud. */ +export interface HostedConfigIncompatibility { + readonly code: DeclarativeConfigErrorCode; + readonly reason: DeclarativeConfigErrorReason; + /** One-based line of the offending construct, when the evaluator located it. */ + readonly line?: number; + /** The offending source line, trimmed and bounded, when one was located. */ + readonly excerpt?: string; + /** What the hosted runtime cannot do, in one sentence. */ + readonly summary: string; + /** The change that makes the project deployable. */ + readonly remedy: string; +} + +/** + * Describe why `source` cannot be evaluated by the hosted runtime, or return + * `null` when nothing statically rules it out. + * + * Never throws: an evaluator that cannot run (no parser installed, for + * example) reports "nothing statically ruled out" rather than blocking a + * deploy on this check's own unavailability. + */ +export async function findHostedConfigIncompatibility( + source: string, + fileName: DeclarativeConfigFileName = "veryfront.config.ts", +): Promise { + let error: unknown; + try { + await evaluateDeclarativeConfig({ + source, + fileName, + environmentName: "production", + environment: {}, + }); + return null; + } catch (caught) { + error = caught; + } + + if (!(error instanceof DeclarativeConfigEvaluationError)) return null; + if (!isDecidedByTheSourceAlone(error, source)) return null; + + // Only a validate-phase rejection is located at the construct it refused. A + // result-phase one is reported against the program, so pointing at a line + // would send the reader to the top of their file for a key further down. + const line = error.phase === "validate" ? error.location?.line : undefined; + const excerpt = line === undefined ? undefined : sourceExcerpt(source, line); + return { + code: error.code, + reason: error.reason, + ...(line === undefined ? {} : { line }), + ...(excerpt === undefined ? {} : { excerpt }), + ...describeReason(error.reason), + }; +} + +/** + * Render an incompatibility as the message a developer reads in their + * terminal: what was found, where, and what to do about it. + */ +export function formatHostedConfigIncompatibility( + incompatibility: HostedConfigIncompatibility, + fileName: string, +): string { + const at = incompatibility.line === undefined ? fileName : `${fileName}:${incompatibility.line}`; + const found = incompatibility.excerpt === undefined ? "" : `\n ${incompatibility.excerpt}`; + return `${at} cannot be deployed to Veryfront Cloud. ${incompatibility.summary}${found}\n` + + `${incompatibility.remedy}`; +} + +/** + * The same explanation, for a rejection that was only discovered once the + * hosted runtime tried to serve the project. Keeps the terminal message and + * the served error saying the same thing about the same config. + */ +export function describeHostedConfigRejection( + reason: DeclarativeConfigErrorReason, +): string { + const { summary, remedy } = describeReason(reason); + return `${summary} ${remedy}`; +} + +/** + * Would the hosted evaluator reject `source` for the same reason, whatever the + * deployment environment holds? + * + * Anything this answers `false` for is left to the hosted runtime: an + * unavailable parser, a rejection this caller's empty environment produced, and + * every other verdict a deploy must not make on a difference it cannot see. + */ +function isDecidedByTheSourceAlone( + error: DeclarativeConfigEvaluationError, + source: string, +): boolean { + if (error.phase === "validate") return true; + return error.phase === "result" && + error.code === "unsupported-hosted-feature" && + !readsDeploymentEnvironment(source); +} + +/** Does `source` import a helper that reads deployment environment data? */ +function readsDeploymentEnvironment(source: string): boolean { + const statements = source.match(VERYFRONT_IMPORT_STATEMENTS); + if (statements === null) return false; + for (let index = 0; index < statements.length; index += 1) { + if (ENVIRONMENT_READING_HELPERS.test(statements[index]!)) return true; + } + return false; +} + +function describeReason( + reason: DeclarativeConfigErrorReason, +): { summary: string; remedy: string } { + if (reason === "unsupported-import" || reason === "import-form") { + return { + summary: + `The hosted runtime reads the configuration file as data and never imports project ` + + `modules: it accepts one import statement, naming any of defineConfig, ` + + `defineConfigWithEnv, getEnv and mergeConfigs from "veryfront", and no other import at ` + + `all. An imported extension is a function call that runtime cannot make, so a project ` + + `that declares one answers 500 on every request.`, + remedy: + `Remove the import and the value it provides from the configuration file. Extensions ` + + `declared this way are supported when you run or self-host the project yourself; they ` + + `cannot be declared in a configuration file deployed to Veryfront Cloud.`, + }; + } + if (reason === "hosted-extensions") { + return { + summary: + `The hosted runtime does not run project-declared extensions: the only entry it accepts ` + + `under "extensions" is { name, enabled: false }, which turns an extension off.`, + remedy: + `Remove the extension entries from the configuration file. Extensions are supported when ` + + `you run or self-host the project yourself.`, + }; + } + if (reason === "hosted-cache-directory") { + return { + summary: + `The hosted runtime has no project-writable cache directory: it serves every project from ` + + `a shared runtime whose caches live in memory, so "cache.dir" names a location that does ` + + `not exist there.`, + remedy: + `Remove "cache.dir" from the configuration file. It applies when you run or self-host the ` + + `project yourself.`, + }; + } + if (reason === "hosted-cache-option") { + return { + summary: + `The hosted runtime accepts only "bundleManifest", "render" and "queryParams" under ` + + `"cache". Every other cache option belongs to a backend it does not run.`, + remedy: `Remove the other "cache" options from the configuration file.`, + }; + } + if ( + reason === "hosted-bundle-manifest-backend" || + reason === "hosted-render-cache-backend" + ) { + return { + summary: + `The hosted runtime keeps render and bundle-manifest caches in memory: it selects no other ` + + `cache backend and accepts no backend-specific option.`, + remedy: + `Remove the backend selection and its options, or set type: "memory". Other backends are ` + + `supported when you run or self-host the project yourself.`, + }; + } + if (reason === "hosted-render-cache-capacity") { + return { + summary: + `The hosted runtime bounds a project's render cache: "cache.render.maxEntries" asks for ` + + `more entries than a shared environment gives one project.`, + remedy: + `Lower "cache.render.maxEntries", or leave it unset and let the hosted runtime size the ` + + `cache.`, + }; + } + if (reason === "hosted-custom-middleware") { + return { + summary: + `The hosted runtime does not run project-supplied middleware: "middleware.custom" is a ` + + `list of functions it can neither read nor call, so it accepts only an empty one.`, + remedy: + `Remove the "middleware.custom" entries. Custom middleware is supported when you run or ` + + `self-host the project yourself.`, + }; + } + if (reason === "hosted-cors-origin") { + return { + summary: + `The hosted runtime accepts "security.cors.origin" only as a plain origin string or a ` + + `list of them.`, + remedy: `Give "security.cors.origin" a string or an array of strings.`, + }; + } + return { + summary: + `The hosted runtime reads the configuration file as data: it accepts literals, the four ` + + `veryfront configuration helpers, and nothing that has to be executed.`, + remedy: `Replace the reported construct with a literal value.`, + }; +} + +/** + * The offending line, in the form it is safe to print. + * + * The line is the project's own source and travels into a terminal and a CI + * log, so credential-shaped content is masked before anything is cut away — + * the order `sanitizeUrlCredentials` needs, since truncating first can split a + * `scheme://user:password@host` before the `@host` it matches on. What remains + * is the construct's shape, which is what the reader came for. + */ +function sourceExcerpt(source: string, line: number): string | undefined { + const text = source.split("\n")[line - 1]; + if (text === undefined) return undefined; + const normalized = sanitizeUrlCredentials(text).replace(CONTROL_CHARACTERS, " ").trim(); + if (normalized.length === 0) return undefined; + return normalized.length > MAX_SOURCE_EXCERPT_CHARACTERS + ? `${normalized.slice(0, MAX_SOURCE_EXCERPT_CHARACTERS)}…` + : normalized; +} diff --git a/src/config/loader.test.ts b/src/config/loader.test.ts index f5291341de..1868c82999 100644 --- a/src/config/loader.test.ts +++ b/src/config/loader.test.ts @@ -4,6 +4,7 @@ import { assertEquals, assertRejects, assertStrictEquals, + assertStringIncludes, assertThrows, } from "#veryfront/testing/assert.ts"; import { afterAll, afterEach, describe, it } from "#veryfront/testing/bdd.ts"; @@ -978,6 +979,35 @@ export default config as const; } }); + it("explains a hosted rejection after the code and reason operators correlate on", async () => { + clearConfigCache(); + + const error = await assertRejects( + () => + evaluateHostedConfigSource({ + cacheKey: "exact-hosted-rejection-detail", + source: { + source: `import extCssLightning from "@veryfront/ext-css-lightning";\n` + + `export default { extensions: [extCssLightning()] };\n`, + fileName: "veryfront.config.ts", + }, + environmentName: "release", + environment: {}, + }), + VeryfrontError, + ) as VeryfrontError; + + assertEquals(error.slug, "config-parse-error"); + // The pair stays first and unchanged: it is what an operator matches on. + assertStringIncludes( + error.detail ?? "", + "Hosted configuration rejected (unsupported-syntax: unsupported-import)", + ); + // The sentences after it are for the developer whose project this is. + assertStringIncludes(error.detail ?? "", "never imports project modules"); + assertStringIncludes(error.detail ?? "", "Remove the import"); + }); + it("binds exact release evaluation to an empty tenant environment", async () => { clearConfigCache(); const envKey = "VERYFRONT_EXACT_CONFIG_HOST_SECRET_TEST"; diff --git a/src/config/loader.ts b/src/config/loader.ts index eeeea0d2b9..0b57ed1aec 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -48,6 +48,7 @@ import { evaluatePreparedDeclarativeConfigInWorker, } from "./declarative-evaluator-worker-runner.ts"; import { createDeclarativeConfigWorkerInfrastructureError } from "./declarative-evaluator-worker-protocol.ts"; +import { describeHostedConfigRejection } from "./hosted-compatibility.ts"; // Capture the collection and reflection intrinsics before trusted executable // project configuration can mutate the shared host realm. Hosted configuration @@ -1485,8 +1486,12 @@ function translateHostedConfigEvaluationError( }); } + // The code/reason pair is what operators correlate on, so it stays first + // and unchanged. The sentence after it is for the developer whose project + // this is: without it the only signal a rejected config gives is a 500. return CONFIG_PARSE_ERROR.create({ - detail: `Hosted configuration rejected (${error.code}: ${error.reason})`, + detail: `Hosted configuration rejected (${error.code}: ${error.reason}). ` + + describeHostedConfigRejection(error.reason), cause: error, context, }); diff --git a/src/errors/error-registry.test.ts b/src/errors/error-registry.test.ts index bfa5458cc7..eb701dccbf 100644 --- a/src/errors/error-registry.test.ts +++ b/src/errors/error-registry.test.ts @@ -29,9 +29,9 @@ describe("error-registry", () => { assertEquals(slugs.length, uniqueSlugs.size, "Duplicate slugs detected"); }); - it("should have 107 registered errors", () => { + it("should have 108 registered errors", () => { const slugs = getAllSlugs(); - assertEquals(slugs.length, 107); + assertEquals(slugs.length, 108); }); }); @@ -325,7 +325,7 @@ describe("error-registry", () => { SERVER: 18, BOUNDARY: 7, DEV: 5, - DEPLOY: 12, + DEPLOY: 13, AGENT: 8, GENERAL: 13, }; diff --git a/src/errors/error-registry/deploy.ts b/src/errors/error-registry/deploy.ts index 9d2945a2e2..c2dd9a852e 100644 --- a/src/errors/error-registry/deploy.ts +++ b/src/errors/error-registry/deploy.ts @@ -96,8 +96,26 @@ export const BRANCH_NOT_FOUND = defineError({ suggestion: "List branches in Studio or push a new one with: veryfront push --branch ", }); +/** + * The project's configuration file uses a construct Veryfront Cloud's + * configuration evaluator can never accept, so the release would answer 500 to + * every request. Raised before a release is created; the detail names the file, + * the change that makes the project deployable, and the line when the evaluator + * located the construct it refused. + */ +export const CONFIG_NOT_DEPLOYABLE = defineError({ + slug: "config-not-deployable", + category: "DEPLOY", + status: 400, + title: "Configuration cannot be deployed to Veryfront Cloud", + suggestion: + "Veryfront Cloud reads veryfront.config.ts as data: keep it to literals and the veryfront configuration helpers", + exitCode: 2, +}); + /** Registry fragment for DEPLOY errors (slug → definition). */ export const DEPLOY_REGISTRY = { + "config-not-deployable": CONFIG_NOT_DEPLOYABLE, "deployment-error": DEPLOYMENT_ERROR, "platform-error": PLATFORM_ERROR, "env-var-missing": ENV_VAR_MISSING, diff --git a/src/errors/index.ts b/src/errors/index.ts index 110718c30b..9a6b7944dc 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -55,6 +55,7 @@ export { COMPILATION_ERROR, COMPONENT_ERROR, CONFIG_INVALID, + CONFIG_NOT_DEPLOYABLE, // CONFIG CONFIG_NOT_FOUND, CONFIG_PARSE_ERROR, diff --git a/src/extensions/setup-hint.test.ts b/src/extensions/setup-hint.test.ts index 686f9dd828..683ee0d935 100644 --- a/src/extensions/setup-hint.test.ts +++ b/src/extensions/setup-hint.test.ts @@ -43,6 +43,32 @@ describe("extensions/setup-hint", () => { }); }); + it("says where the composition it recommends stops working", async () => { + // The recommended config imports the extension, and Veryfront Cloud + // evaluates a project config as data rather than importing it. A hint that + // stays silent here hands the reader a project that deploys and then + // answers 500 on every request. + await withTempDir(async (directory) => { + await writeTextFile(join(directory, "package.json"), `{"name":"scaffold"}`); + + const created = formatExtensionSetupHint("@veryfront/ext-css-lightning", { + projectDirectory: directory, + }); + await writeTextFile(join(directory, "veryfront.config.ts"), "export default {};\n"); + const existing = formatExtensionSetupHint("@veryfront/ext-css-lightning", { + projectDirectory: directory, + }); + + for (const hint of [created, existing]) { + assertEquals( + hint.includes("cannot be deployed to Veryfront Cloud"), + true, + `hint must say the composition is not deployable, got: ${hint}`, + ); + } + }); + }); + it("never names `veryfront/config`, which is not an exported subpath", async () => { // The obvious guess when the hint stays silent. `veryfront`'s package // exports map has no `./config` entry, so Node rejects it with diff --git a/src/extensions/setup-hint.ts b/src/extensions/setup-hint.ts index 8ae6029a35..2044934ad0 100644 --- a/src/extensions/setup-hint.ts +++ b/src/extensions/setup-hint.ts @@ -45,6 +45,19 @@ const NPM_SPECIFIER_PREFIX = "npm:"; /** Config file a project is told to create when it has none. */ const DEFAULT_CONFIG_FILE: VeryfrontConfigFileName = "veryfront.config.ts"; +/** + * Where the recommended composition stops working. + * + * Veryfront Cloud evaluates a project's configuration file as data and never + * imports it, so the import this hint asks for is rejected there. Saying so + * here is the earliest the reader can learn it; `veryfront deploy` refuses the + * same config, and without this line the hint reads as advice that quietly + * costs the reader a deployable project. + */ +const HOSTED_CAVEAT = + "Extensions run where you run the project; a configuration file that imports one cannot be " + + "deployed to Veryfront Cloud."; + export interface ExtensionSetupHintOptions { /** Project root to inspect; defaults to the working directory. */ readonly projectDirectory?: string; @@ -129,8 +142,10 @@ export function formatExtensionSetupHint( if (existingConfigFile === undefined) { return `Install one with: ${install}, then create ${DEFAULT_CONFIG_FILE} containing: ` + `import { defineConfig } from "veryfront"; ${importLine} ` + - `export default defineConfig({ extensions: [${binding}()] });`; + `export default defineConfig({ extensions: [${binding}()] });` + + ` ${HOSTED_CAVEAT}`; } return `Install one with: ${install}, then activate it in ${existingConfigFile}: ` + - `add ${importLine} and list ${binding}() in "extensions".`; + `add ${importLine} and list ${binding}() in "extensions".` + + ` ${HOSTED_CAVEAT}`; }