diff --git a/cli/templates/files/saas-starter/app/page.tsx b/cli/templates/files/saas-starter/app/page.tsx
index fd8670874a..825340dc29 100644
--- a/cli/templates/files/saas-starter/app/page.tsx
+++ b/cli/templates/files/saas-starter/app/page.tsx
@@ -31,8 +31,7 @@ export default function LandingPage(): React.JSX.Element {
Your AI-powered platform
- Built with Veryfront. Agents, tools, and memory are ready for
- production.
+ Built with Veryfront. Agents, tools, and memory are ready for production.
\n {[\n {\n title: \"AI Agents\",\n desc: \"Define agents with tools, memory, and streaming. Veryfront auto-discovers them from your project.\",\n },\n {\n title: \"Per-User Memory\",\n desc: \"Each user gets their own conversation history, persisted across sessions.\",\n },\n {\n title: \"Production Ready\",\n desc: \"Use auth, rate limiting, and deployment to ship to production with one command.\",\n },\n ].map(({ title, desc }) => (\n
\n {[\n {\n title: \"AI Agents\",\n desc:\n \"Define agents with tools, memory, and streaming. Veryfront auto-discovers them from your project.\",\n },\n {\n title: \"Per-User Memory\",\n desc: \"Each user gets their own conversation history, persisted across sessions.\",\n },\n {\n title: \"Production Ready\",\n desc:\n \"Use auth, rate limiting, and deployment to ship to production with one command.\",\n },\n ].map(({ title, desc }) => (\n
\n
\n {title}\n
\n
\n {desc}\n
\n
\n ))}\n
\n \n
\n );\n}\n",
"globals.css": "@import \"tailwindcss\";\n",
"public/favicon.svg": "\n",
"README.md": "# SaaS Starter\n\nA SaaS-shaped starter with authentication, conversation memory, and a full UI.\n\n## What's included\n\n- Landing page with feature highlights\n- OAuth login (Google and GitHub)\n- Dashboard with conversation sidebar\n- Per-user conversation memory persisted across sessions\n\n## Structure\n\n```\nagents/assistant.ts Agent with conversation memory\ntools/search.ts Placeholder domain search\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Landing page\n login/page.tsx OAuth login\n dashboard/page.tsx Chat with sidebar\n```\n\nThis starter is not production-ready.\n",
- "tools/search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"search\",\n description: \"Search your knowledge base\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: async ({ query }) => {\n // Replace with your domain-specific search logic\n return {\n results: [],\n query,\n message: \"Connect your data source for real results.\",\n };\n },\n});\n",
+ "tools/search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"search\",\n description: \"Search your knowledge base\",\n inputSchema: defineSchema((v) =>\n v.object({\n query: v.string().describe(\"Search query\"),\n })\n )(),\n execute: async ({ query }) => {\n // Replace with your domain-specific search logic\n return {\n results: [],\n query,\n message: \"Connect your data source for real results.\",\n };\n },\n});\n",
"tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"
}
},
diff --git a/docs/architecture/12-extension-system.md b/docs/architecture/12-extension-system.md
index 1d6f6f5625..1b83bbab8c 100644
--- a/docs/architecture/12-extension-system.md
+++ b/docs/architecture/12-extension-system.md
@@ -36,16 +36,17 @@ request classification, application authorization, request-to-socket
correlation, cancellation, and shutdown ownership. Those responsibilities do
not require a WebSocket protocol package and remain in the runtime adapter.
-The wire-protocol implementation is supplied by the explicitly activated
+The wire-protocol implementation is supplied by the default
`@veryfront/ext-node-websocket-ws` package through the dependency-free
`NodeWebSocketServerProvider` contract. Bootstrap snapshots one immutable
provider generation before it starts a listener, so later mutation or extension
reload cannot change the implementation underneath a running server.
-The extension is deliberately neither built in nor auto-loaded. Core does not
-probe for `ws` or substitute another implementation. A Node HTTP server can run
-without the provider, but an authorized WebSocket upgrade fails closed and
-identifies the extension needed to enable the feature.
+The standard npm/CLI distribution installs and auto-activates the extension;
+custom service distributions install the package for Node WebSocket support.
+Core does not import `ws`, probe for it, or substitute another implementation.
+A Node HTTP server can run without the provider, but an authorized WebSocket
+upgrade fails closed and identifies the extension needed to restore the feature.
## Boundaries
diff --git a/docs/architecture/20-support-matrix.md b/docs/architecture/20-support-matrix.md
index 0d22d32dfe..72fef25b01 100644
--- a/docs/architecture/20-support-matrix.md
+++ b/docs/architecture/20-support-matrix.md
@@ -60,7 +60,7 @@ project.
| ----------------------------- | --------------------------------------------------------- | ------------------------------------- | --------------------------------------- | ---------------------------- |
| `SchemaValidator` | `@veryfront/ext-schema-zod` | Built-in | Schema-backed runtime validation | None (pure JS) |
| `Bundler`, `ModuleLexer` | `@veryfront/ext-bundler-esbuild` | Built-in | Build, import analysis, module bundling | esbuild binary |
-| `CSSProcessor` | `@veryfront/ext-css-tailwind` | Built-in | Tailwind CSS processing | Network (esm.sh for plugins) |
+| `CSSProcessor` | `@veryfront/ext-css-tailwind` | Built-in | Tailwind CSS processing | Filesystem read (pinned CSS) |
| `ContentProcessor` | `@veryfront/ext-content-mdx` | Built-in | MDX or Markdown content compilation | None (unified ecosystem) |
| `CodeParser` | `@veryfront/ext-parser-babel` | Built-in | AST parsing or build-time code analysis | None (Babel) |
| `DocumentExtractor` | `@veryfront/ext-document-kreuzberg` | Built-in | Document text extraction | FS (WASM/native extraction) |
@@ -73,7 +73,7 @@ project.
| `NodeTelemetryProvider` | `@veryfront/ext-observability-opentelemetry` | Built-in | Node OpenTelemetry SDK bootstrap | Network (OTLP endpoint) |
| `EvalReportExporterRegistry` | Core registry and future `@veryfront/ext-eval-*` packages | Built-in registry, optional exporters | Eval report export | Vendor-specific |
| `TokenCacheStore` | `@veryfront/ext-cache-redis` | Optional | Redis-backed token cache | Network (Redis) |
-| `NodeWebSocketServerProvider` | `@veryfront/ext-node-websocket-ws` | Explicit | Node.js WebSocket upgrades | Node.js and scoped env reads |
+| `NodeWebSocketServerProvider` | `@veryfront/ext-node-websocket-ws` | Built-in | Node.js WebSocket upgrades and HMR | Node.js and scoped env reads |
## Dependency boundaries
diff --git a/docs/guides/extensions.md b/docs/guides/extensions.md
index 01f8316dc6..5815d137b6 100644
--- a/docs/guides/extensions.md
+++ b/docs/guides/extensions.md
@@ -161,26 +161,27 @@ fallback.
## Enable Node.js WebSocket upgrades
-Install the explicit Node.js transport extension:
+The standard `veryfront` npm/CLI distribution installs and auto-activates the
+Node.js transport extension, including for local HMR. Custom Node service
+distributions must install it alongside `veryfront`:
```bash
deno add npm:@veryfront/ext-node-websocket-ws
```
-Add it to `veryfront.config.ts`:
+No `veryfront.config.ts` entry is required. To disable the builtin when
+WebSocket support is intentionally unavailable, use:
```ts
import { defineConfig } from "veryfront";
-import extNodeWebSocketWs from "@veryfront/ext-node-websocket-ws";
export default defineConfig({
- extensions: [extNodeWebSocketWs()],
+ extensions: [{ name: "ext-node-websocket-ws", enabled: false }],
});
```
-Restart the Node.js server after changing the configuration. HTTP serving does
-not require this extension. Without it, Node.js WebSocket upgrades fail closed
-with an error that names the required package.
+HTTP serving does not require the provider. Without it, Node.js WebSocket
+upgrades fail closed with an error that names the required package.
## First-party extension areas
@@ -207,7 +208,7 @@ replacement passes preflight.
## Verify it worked
-Restart `veryfront dev` after editing `veryfront.config.ts`:
+Restart `veryfront dev` after changing extension configuration:
- The dev log should print a setup line for each loaded extension.
- Any contract the extension provides should now be resolvable through the
diff --git a/extensions/README.md b/extensions/README.md
index 7d5dafdc74..e2da58c180 100644
--- a/extensions/README.md
+++ b/extensions/README.md
@@ -38,20 +38,29 @@ Extension availability is separate from contract requirement:
### Build
-| Package | Contract | Description |
-| --------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
-| [`@veryfront/ext-bundler-esbuild`](./ext-bundler-esbuild) | `Bundler`, `ModuleLexer` | ESM bundling and module analysis via `esbuild` and `es-module-lexer` |
-| [`@veryfront/ext-css-lightning`](./ext-css-lightning) | `CSSOptimizationEngine` | Explicit CSS compilation, minification, browser targets, and source maps |
-| [`@veryfront/ext-css-purgecss`](./ext-css-purgecss) | `CSSPurgingEngine` | Explicit parser-backed unused and critical CSS extraction via PurgeCSS |
-| [`@veryfront/ext-css-tailwind`](./ext-css-tailwind) | `CSSProcessor` | Tailwind CSS v4 compilation with pinned local plugins |
-| [`@veryfront/ext-image-sharp`](./ext-image-sharp) | `ImageOptimizationEngine` | Explicit bounded native image transformation via Sharp |
-| [`@veryfront/ext-parser-babel`](./ext-parser-babel) | `CodeParser` | JS/TS AST parsing, traversal, and JSX source-position injection via Babel |
+| Package | Contract | Description |
+| --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------- |
+| [`@veryfront/ext-bundler-esbuild`](./ext-bundler-esbuild) | `Bundler`, `ModuleLexer` | ESM bundling and module analysis via `esbuild` and `es-module-lexer` |
+| [`@veryfront/ext-css-lightning`](./ext-css-lightning) | `CSSOptimizationEngine` | Explicit CSS compilation, minification, browser targets, and source maps |
+| [`@veryfront/ext-css-purgecss`](./ext-css-purgecss) | `CSSPurgingEngine` | Explicit parser-backed unused and critical CSS extraction via PurgeCSS |
+| [`@veryfront/ext-css-tailwind`](./ext-css-tailwind) | `CSSProcessor` | Tailwind CSS v4 compilation with pinned local plugins |
+| [`@veryfront/ext-image-sharp`](./ext-image-sharp) | `ImageOptimizationEngine` | Explicit bounded native image transformation via Sharp |
+| [`@veryfront/ext-parser-babel`](./ext-parser-babel) | `CodeParser` | JS/TS AST parsing, traversal, and JSX source-position injection via Babel |
### Content
-| Package | Contract | Description |
-| ------------------------------------------------- | ------------------ | ----------------------------------------------------- |
-| [`@veryfront/ext-content-mdx`](./ext-content-mdx) | `ContentProcessor` | MDX and Markdown processing via unified/remark/rehype |
+| Package | Contract | Description |
+| ------------------------------------------------- | ----------------------------- | ----------------------------------------------------- |
+| [`@veryfront/ext-content-mdx`](./ext-content-mdx) | `ContentProcessor` | MDX and Markdown processing via unified/remark/rehype |
+| [`@veryfront/ext-yaml`](./ext-yaml) | `SkillDocumentParserProvider` | YAML parsing for skill and agent documents |
+
+### Development and rendering
+
+| Package | Contract | Description |
+| ------------------------------------------------------------- | ----------------------------- | ---------------------------------------------- |
+| [`@veryfront/ext-dev-ui-react`](./ext-dev-ui-react) | `DevUiAssetProvider` | Offline assets for local development UI |
+| [`@veryfront/ext-node-websocket-ws`](./ext-node-websocket-ws) | `NodeWebSocketServerProvider` | Node.js WebSocket upgrades and local HMR |
+| [`@veryfront/ext-react-ssr`](./ext-react-ssr) | `IsolatedSsrRendererProvider` | Explicit renderer for isolated project workers |
### Document extraction
@@ -102,26 +111,60 @@ the same contract.
| ----------------------------------------------------------------- | --------------------------- | ------------------------------------------- |
| [`@veryfront/ext-sandbox-shell-tools`](./ext-sandbox-shell-tools) | `SandboxShellToolsProvider` | Sandbox shell tool creation via `bash-tool` |
-## Auto-enabled core extensions
-
-These extensions are loaded by `createBuiltinExtensions()` during app bootstrap
-unless a project disables or overrides them by name. In npm installs, the root
-`veryfront` package lazy-loads the matching `@veryfront/ext-*` package for
-feature-specific implementations instead of shipping those dependencies in the
-root package.
-
-| Package | Contracts |
-| ------------------------------------ | --------------------------- |
-| `@veryfront/ext-schema-zod` | `SchemaValidator` |
-| `@veryfront/ext-bundler-esbuild` | `Bundler`, `ModuleLexer` |
-| `@veryfront/ext-parser-babel` | `CodeParser` |
-| `@veryfront/ext-content-mdx` | `ContentProcessor` |
-| `@veryfront/ext-document-kreuzberg` | `DocumentExtractor` |
-| `@veryfront/ext-db-sqlite` | `SqliteStore` |
-| `@veryfront/ext-sandbox-shell-tools` | `SandboxShellToolsProvider` |
-| `@veryfront/ext-llm-openai` | `LLMProvider:openai` |
-| `@veryfront/ext-llm-anthropic` | `LLMProvider:anthropic` |
-| `@veryfront/ext-llm-google` | `LLMProvider:google` |
+## Built-in first-party selection
+
+These packages are known to `createBuiltinExtensions()`, but that does not make
+every contract an unconditional global default. Direct built-ins are provided
+by their owning source or service distribution. Deferred candidates load only
+when the matching feature selects their contract and the executing distribution
+contains the package. The standard `veryfront` npm/CLI package installs the
+baseline subset used by ordinary apps and local development.
+
+| Package | Contract | Selection and availability |
+| -------------------------------------------- | ----------------------------- | ----------------------------------------- |
+| `@veryfront/ext-schema-zod` | `SchemaValidator` | Direct built-in |
+| `@veryfront/ext-auth-jwt` | `AuthProvider` | Deferred; install when auth is configured |
+| `@veryfront/ext-bundler-esbuild` | `Bundler`, `ModuleLexer` | Deferred; standard npm baseline |
+| `@veryfront/ext-parser-babel` | `CodeParser` | Deferred; standard npm baseline |
+| `@veryfront/ext-yaml` | `SkillDocumentParserProvider` | Deferred; standard npm baseline |
+| `@veryfront/ext-content-mdx` | `ContentProcessor` | Deferred; standard npm baseline |
+| `@veryfront/ext-css-tailwind` | `CSSProcessor` | Deferred; standard npm baseline |
+| `@veryfront/ext-node-websocket-ws` | `NodeWebSocketServerProvider` | Deferred; standard npm baseline |
+| `@veryfront/ext-dev-ui-react` | `DevUiAssetProvider` | Deferred; standard npm baseline |
+| `@veryfront/ext-document-kreuzberg` | `DocumentExtractor` | Deferred; source/service distribution |
+| `@veryfront/ext-db-sqlite` | `SqliteStore` | Deferred; source/service distribution |
+| `@veryfront/ext-sandbox-shell-tools` | `SandboxShellToolsProvider` | Deferred; source/service distribution |
+| `@veryfront/ext-observability-opentelemetry` | `TracingExporter` | Deferred; install when OTLP is configured |
+| `@veryfront/ext-observability-opentelemetry` | `NodeTelemetryProvider` | Built into the owning agent service |
+| `@veryfront/ext-eval-report-http` | Generic HTTP eval exporter | Deferred; source/service distribution |
+| `@veryfront/ext-eval-report-mlflow` | MLflow eval exporter | Deferred; source/service distribution |
+| `@veryfront/ext-llm-openai` | `LLMProvider:openai` | Direct service/source built-in |
+| `@veryfront/ext-llm-anthropic` | `LLMProvider:anthropic` | Direct service/source built-in |
+| `@veryfront/ext-llm-google` | `LLMProvider:google` | Direct service/source built-in |
+
+## Explicit opt-in extensions
+
+These packages must not become global defaults because they choose mutually
+exclusive infrastructure, change build output, load native processors, or
+select a specialized isolation implementation.
+
+| Packages | Why activation stays explicit |
+| ------------------------------------------------------------- | ----------------------------------------------------- |
+| `@veryfront/ext-blob-s3`, `@veryfront/ext-blob-gcs` | Competing credentialed `BlobStorage` implementations |
+| `@veryfront/ext-cache-redis`, `@veryfront/ext-redis` | External Redis topology and credentials |
+| `@veryfront/ext-css-lightning`, `@veryfront/ext-css-purgecss` | Build-output policy and native/parser cost |
+| `@veryfront/ext-image-sharp` | Native image processing and output policy |
+| `@veryfront/ext-react-ssr` | Isolated-worker renderer selected by hosting topology |
+
+## Service-conditional extensions
+
+`@veryfront/ext-observability-sentry` is loaded by the owning server or agent
+service only after its Sentry enablement, reporter-selection, and DSN policy
+passes. It is not a normal application contract extension: automatically
+running its package factory would register no useful contract, while eagerly
+initializing the SDK would change process-wide error reporting and network
+egress. Compiled services may embed the dormant adapter; npm services install
+the runtime-specific package they execute.
## npm service installs
@@ -130,23 +173,24 @@ raw transitive dependencies such as `bash-tool`, `just-bash`, `jose`,
`better-sqlite3`, `@aws-sdk/client-s3`, `@kreuzberg/node`, `@mdx-js/mdx`, or
`tailwindcss` directly to satisfy Veryfront runtime features.
-| Runtime or service role | Install these extension packages |
-| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
-| CLI, build image, or project server runtime | `@veryfront/ext-bundler-esbuild`, `@veryfront/ext-content-mdx`, `@veryfront/ext-css-tailwind`, `@veryfront/ext-parser-babel` |
-| Build with CSS optimization | `@veryfront/ext-css-lightning` (register explicitly) |
-| Build with CSS purging or critical CSS | `@veryfront/ext-css-purgecss` (register explicitly) |
-| Build with image optimization | `@veryfront/ext-image-sharp` (register explicitly) |
-| Proxy or JWT-authenticated service | `@veryfront/ext-auth-jwt` |
-| Document upload or knowledge ingestion | `@veryfront/ext-document-kreuzberg` |
-| Redis-backed cache or token store | `@veryfront/ext-cache-redis` |
-| Redis-backed distributed runtime or Pub/Sub | `@veryfront/ext-redis` |
-| S3-compatible blob persistence | `@veryfront/ext-blob-s3` |
-| Google Cloud Storage blob persistence | `@veryfront/ext-blob-gcs` |
-| SQLite-backed persistence | `@veryfront/ext-db-sqlite` |
-| OpenTelemetry export or Node telemetry | `@veryfront/ext-observability-opentelemetry` |
-| Sentry application error capture | `@veryfront/ext-observability-sentry` |
-| Local shell-tool agent runtime | `@veryfront/ext-sandbox-shell-tools` |
-| Eval report export to MLflow | `@veryfront/ext-eval-report-mlflow` |
+| Runtime or service role | Install these extension packages |
+| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| CLI, build image, or project server runtime | `@veryfront/ext-bundler-esbuild`, `@veryfront/ext-content-mdx`, `@veryfront/ext-css-tailwind`, `@veryfront/ext-dev-ui-react`, `@veryfront/ext-node-websocket-ws`, `@veryfront/ext-parser-babel`, `@veryfront/ext-yaml` |
+| Build with CSS optimization | `@veryfront/ext-css-lightning` (register explicitly) |
+| Build with CSS purging or critical CSS | `@veryfront/ext-css-purgecss` (register explicitly) |
+| Build with image optimization | `@veryfront/ext-image-sharp` (register explicitly) |
+| Proxy or JWT-authenticated service | `@veryfront/ext-auth-jwt` |
+| Document upload or knowledge ingestion | `@veryfront/ext-document-kreuzberg` |
+| Redis-backed cache or token store | `@veryfront/ext-cache-redis` |
+| Redis-backed distributed runtime or Pub/Sub | `@veryfront/ext-redis` |
+| S3-compatible blob persistence | `@veryfront/ext-blob-s3` |
+| Google Cloud Storage blob persistence | `@veryfront/ext-blob-gcs` |
+| SQLite-backed persistence | `@veryfront/ext-db-sqlite` |
+| OpenTelemetry export or Node telemetry | `@veryfront/ext-observability-opentelemetry` |
+| Sentry application error capture | `@veryfront/ext-observability-sentry` |
+| Local shell-tool agent runtime | `@veryfront/ext-sandbox-shell-tools` |
+| Eval report export to a generic HTTP gateway | `@veryfront/ext-eval-report-http` |
+| Eval report export to MLflow | `@veryfront/ext-eval-report-mlflow` |
An agent runtime needs `@veryfront/ext-sandbox-shell-tools` only when it creates
local bash or shell tools. MCP-only remote tool execution does not need that
@@ -157,27 +201,28 @@ package unless the service also provides local shell tools.
Veryfront treats contracts as required at the call site, not at the package list
level.
-| Contract | Required when | Default source |
-| ---------------------------- | ----------------------------------------------- | ------------------------------------- |
-| `SchemaValidator` | Schema-backed runtime validation runs | Auto-enabled core extension |
-| `Bundler`, `ModuleLexer` | Build, import analysis, or module bundling runs | Auto-enabled core extension |
-| `CodeParser` | AST parsing or build-time code analysis runs | Auto-enabled core extension |
-| `ContentProcessor` | MDX or Markdown content compilation runs | Auto-enabled core extension |
-| `CSSProcessor` | Class-candidate CSS processing runs | Explicit user-installed extension |
-| `CSSOptimizationEngine` | CSS compilation or minification runs | Explicit user-installed extension |
-| `CSSPurgingEngine` | CSS purging or critical-CSS extraction runs | Explicit user-installed extension |
-| `ImageOptimizationEngine` | Image optimization runs | Explicit user-installed extension |
-| `DocumentExtractor` | Document text extraction runs | Auto-enabled native service extension |
-| `SqliteStore` | SQLite-backed persistence runs | Auto-enabled native service extension |
-| `SandboxShellToolsProvider` | Sandbox shell tools are created | Auto-enabled core extension |
-| `LLMProvider:*` | A matching model provider is selected | Auto-enabled core extension |
-| `BlobStorage` | S3 or GCS object persistence is configured | Explicitly configured extension |
-| `AuthProvider` | Auth signing or verification is configured | User-installed extension |
-| `TokenCacheStore` | Redis-backed token cache is configured | User-installed extension |
-| `RedisRuntimeProvider` | A core Redis facade or Pub/Sub is used | Explicitly configured extension |
-| `EvalReportExporterRegistry` | Eval report exporters are registered | Auto-enabled core extension |
-| `TracingExporter` | OTLP tracing export is configured | User-installed extension |
-| `NodeTelemetryProvider` | Node agent service telemetry is enabled | Auto-enabled agent service extension |
+| Contract | Required when | Default source |
+| ----------------------------- | ----------------------------------------------- | ------------------------------------- |
+| `SchemaValidator` | Schema-backed runtime validation runs | Auto-enabled core extension |
+| `Bundler`, `ModuleLexer` | Build, import analysis, or module bundling runs | Auto-enabled core extension |
+| `CodeParser` | AST parsing or build-time code analysis runs | Auto-enabled core extension |
+| `ContentProcessor` | MDX or Markdown content compilation runs | Auto-enabled core extension |
+| `CSSProcessor` | Class-candidate CSS processing runs | Auto-enabled core extension |
+| `CSSOptimizationEngine` | CSS compilation or minification runs | Explicit user-installed extension |
+| `CSSPurgingEngine` | CSS purging or critical-CSS extraction runs | Explicit user-installed extension |
+| `ImageOptimizationEngine` | Image optimization runs | Explicit user-installed extension |
+| `DocumentExtractor` | Document text extraction runs | Auto-enabled native service extension |
+| `SqliteStore` | SQLite-backed persistence runs | Auto-enabled native service extension |
+| `SandboxShellToolsProvider` | Sandbox shell tools are created | Auto-enabled core extension |
+| `LLMProvider:*` | A matching model provider is selected | Auto-enabled core extension |
+| `BlobStorage` | S3 or GCS object persistence is configured | Explicitly configured extension |
+| `AuthProvider` | Auth signing or verification is configured | User-installed extension |
+| `TokenCacheStore` | Redis-backed token cache is configured | User-installed extension |
+| `RedisRuntimeProvider` | A core Redis facade or Pub/Sub is used | Explicitly configured extension |
+| `EvalReportExporterRegistry` | Eval report exporters are registered | Auto-enabled core extension |
+| `TracingExporter` | OTLP tracing export is configured | User-installed extension |
+| `NodeTelemetryProvider` | Node agent service telemetry is enabled | Auto-enabled agent service extension |
+| `NodeWebSocketServerProvider` | Node.js WebSocket upgrades or HMR run | Auto-enabled core extension |
## Architecture
diff --git a/extensions/ext-css-tailwind/README.md b/extensions/ext-css-tailwind/README.md
index b31679064e..47ca795547 100644
--- a/extensions/ext-css-tailwind/README.md
+++ b/extensions/ext-css-tailwind/README.md
@@ -1,25 +1,19 @@
# @veryfront/ext-css-tailwind
-> **Category:** Build | **Contract:** `CSSProcessor` | **Explicit**
+> **Category:** Build | **Contract:** `CSSProcessor` | **Default**
Provides Tailwind CSS v4 compilation for Veryfront. The extension owns the
pinned compiler, local base stylesheet, plugin policy, plugin module loading,
and every third-party import; framework core sees only `CSSProcessor`.
-## Registration
+## Activation
-Install and compose the extension explicitly:
+The standard `veryfront` npm/CLI distribution installs and auto-activates this
+extension. Source and custom service distributions must make the package
+available alongside `veryfront`; the builtin composition then activates it.
+Projects do not need a Tailwind entry in `veryfront.config.ts`.
-```ts
-import extTailwind from "@veryfront/ext-css-tailwind";
-
-export default defineConfig({
- extensions: [extTailwind()],
-});
-```
-
-Core never discovers or auto-registers this provider. Production pipelines
-that request CSS minification must also explicitly compose a
+Production pipelines that request CSS minification must still explicitly compose a
`CSSOptimizationEngine` provider such as `@veryfront/ext-css-lightning`.
If either requested provider is absent, compilation fails instead of returning
empty or regex-rewritten CSS.
diff --git a/extensions/ext-css-tailwind/deno.json b/extensions/ext-css-tailwind/deno.json
index 9c36287ef7..da327464a3 100644
--- a/extensions/ext-css-tailwind/deno.json
+++ b/extensions/ext-css-tailwind/deno.json
@@ -4,7 +4,7 @@
"exports": "./src/index.ts",
"veryfront": {
"extension": true,
- "activation": "explicit",
+ "activation": "auto",
"contracts": {
"provides": ["CSSProcessor"]
},
diff --git a/extensions/ext-css-tailwind/src/index.test.ts b/extensions/ext-css-tailwind/src/index.test.ts
index bc857ff705..50ef8edfaa 100644
--- a/extensions/ext-css-tailwind/src/index.test.ts
+++ b/extensions/ext-css-tailwind/src/index.test.ts
@@ -12,7 +12,7 @@ const noopLogger = { debug() {}, info() {}, warn() {}, error() {} };
describe("ext-css-tailwind", () => {
it("aligns package, factory, contract, and capability metadata", () => {
const extension = factory();
- assertEquals(extensionPackage.veryfront.activation, "explicit");
+ assertEquals(extensionPackage.veryfront.activation, "auto");
assertEquals(extension.name, "ext-css-tailwind");
assertEquals(extension.version, extensionPackage.version);
assertEquals(extension.contracts?.provides, ["CSSProcessor"]);
diff --git a/extensions/ext-eval-report-http/README.md b/extensions/ext-eval-report-http/README.md
index 2b005b12a3..3d112bfad8 100644
--- a/extensions/ext-eval-report-http/README.md
+++ b/extensions/ext-eval-report-http/README.md
@@ -18,9 +18,23 @@ scores, redaction policy, and optional trace correlation. Do not use OTLP
runtime telemetry env vars to route eval reports; `OTEL_*` settings only control
runtime trace and metric export.
-## Installation
+## Activation
-Add the extension to your project's `veryfront.config.ts`:
+Source and compiled Veryfront runtimes select the extension automatically, but
+it remains dormant until an HTTP exporter URL is configured and an eval run
+selects its exporter id. npm services that need HTTP eval export install
+`@veryfront/ext-eval-report-http`; package discovery then selects it
+automatically. The standard `veryfront` npm package does not install this
+network-export integration for every application.
+
+Set `VERYFRONT_EVAL_HTTP_EXPORTER_URL` for the default `http` exporter, then run:
+
+```bash
+veryfront eval deep-research --export http
+```
+
+Register the factory explicitly only when configuration must define multiple
+exporters, inject a fetch implementation, or avoid environment configuration:
```ts
import extEvalReportHttp from "@veryfront/ext-eval-report-http";
diff --git a/extensions/ext-llm-anthropic/src/anthropic-request-builder.ts b/extensions/ext-llm-anthropic/src/anthropic-request-builder.ts
index 4d9d75f6f4..6158b89e21 100644
--- a/extensions/ext-llm-anthropic/src/anthropic-request-builder.ts
+++ b/extensions/ext-llm-anthropic/src/anthropic-request-builder.ts
@@ -2,7 +2,6 @@ import {
jsonValuesEqual,
readProviderOptions,
readRecord,
- stringifyJsonValue,
stringifyToolResultValue,
unwrapToolInputSchema,
} from "veryfront/provider/shared";
diff --git a/extensions/ext-llm-openai/src/openai-responses-request-builder.ts b/extensions/ext-llm-openai/src/openai-responses-request-builder.ts
index e4d358c5bb..093fa001f2 100644
--- a/extensions/ext-llm-openai/src/openai-responses-request-builder.ts
+++ b/extensions/ext-llm-openai/src/openai-responses-request-builder.ts
@@ -1,7 +1,6 @@
import {
jsonValuesEqual,
readProviderOptions,
- stringifyJsonValue,
stringifyToolArguments,
stringifyToolResultValue,
unwrapToolInputSchema,
diff --git a/extensions/ext-node-websocket-ws/README.md b/extensions/ext-node-websocket-ws/README.md
index 9c99d4bcb5..c4b3d9cea9 100644
--- a/extensions/ext-node-websocket-ws/README.md
+++ b/extensions/ext-node-websocket-ws/README.md
@@ -1,25 +1,20 @@
# `@veryfront/ext-node-websocket-ws`
-Explicit Node.js WebSocket transport for Veryfront, backed by the `ws` package.
+Default Node.js WebSocket transport for Veryfront, backed by the `ws` package.
Core owns request authorization, upgrade correlation, and shutdown. This
extension owns the third-party protocol implementation and publishes the
dependency-free `NodeWebSocketServerProvider` contract.
-Install the package and compose it explicitly:
+The standard `veryfront` npm/CLI distribution installs and auto-activates the
+extension, so local HMR works without starter-specific configuration. A custom
+Node service distribution must install the package alongside `veryfront`:
```bash
deno add npm:@veryfront/ext-node-websocket-ws
```
-```ts
-import { defineConfig } from "veryfront";
-import extNodeWebSocketWs from "@veryfront/ext-node-websocket-ws";
-
-export default defineConfig({
- extensions: [extNodeWebSocketWs()],
-});
-```
-
-Veryfront never auto-loads this package and has no built-in WebSocket fallback.
-Without an explicitly registered provider, Node HTTP requests remain available
-but Node WebSocket upgrades fail closed with an actionable diagnostic.
+Projects can disable the builtin with
+`{ name: "ext-node-websocket-ws", enabled: false }`. Without an available
+provider, Node HTTP requests remain available but Node WebSocket upgrades fail
+closed with an actionable diagnostic. Core never imports `ws` or substitutes a
+hidden protocol fallback.
diff --git a/extensions/ext-node-websocket-ws/deno.json b/extensions/ext-node-websocket-ws/deno.json
index c0923b3db3..838b59b8b4 100644
--- a/extensions/ext-node-websocket-ws/deno.json
+++ b/extensions/ext-node-websocket-ws/deno.json
@@ -4,7 +4,7 @@
"exports": "./src/index.ts",
"veryfront": {
"extension": true,
- "activation": "explicit",
+ "activation": "auto",
"contracts": {
"provides": [
"NodeWebSocketServerProvider"
diff --git a/extensions/ext-node-websocket-ws/src/index.test.ts b/extensions/ext-node-websocket-ws/src/index.test.ts
index e87e2a8cda..f9f2d93a2e 100644
--- a/extensions/ext-node-websocket-ws/src/index.test.ts
+++ b/extensions/ext-node-websocket-ws/src/index.test.ts
@@ -1,8 +1,10 @@
-import { assertEquals, assertStrictEquals, assertThrows } from "@std/assert";
+import { assertEquals, assertStrictEquals, assertThrows } from "#veryfront/testing/assert.ts";
+import { describe, it } from "#veryfront/testing/bdd.ts";
import {
type NodeWebSocketServerProvider,
NodeWebSocketServerProviderName,
} from "veryfront/extensions/websocket";
+import extensionPackage from "../deno.json" with { type: "json" };
import extNodeWebSocketWs, { WsNodeWebSocketServerProvider } from "./index.ts";
const logger = {
@@ -27,52 +29,55 @@ function createContext(provided: Map, signal?: AbortSignal) {
};
}
-Deno.test("ws extension explicitly publishes one immutable provider", () => {
- const extension = extNodeWebSocketWs();
- const provided = new Map();
+describe("extNodeWebSocketWs", () => {
+ it("auto-activates one immutable provider", () => {
+ const extension = extNodeWebSocketWs();
+ const provided = new Map();
- extension.setup?.(createContext(provided));
- assertStrictEquals(
- provided.get(NodeWebSocketServerProviderName),
- WsNodeWebSocketServerProvider,
- );
- assertEquals(Object.isFrozen(WsNodeWebSocketServerProvider), true);
- assertEquals(extension.contracts?.provides, [NodeWebSocketServerProviderName]);
- assertEquals(extension.capabilities, [{
- type: "env:read",
- keys: ["WS_NO_BUFFER_UTIL", "WS_NO_UTF_8_VALIDATE"],
- }]);
- assertThrows(() => extension.setup?.(createContext(new Map())), Error, "already set up");
+ assertEquals(extensionPackage.veryfront.activation, "auto");
+ extension.setup?.(createContext(provided));
+ assertStrictEquals(
+ provided.get(NodeWebSocketServerProviderName),
+ WsNodeWebSocketServerProvider,
+ );
+ assertEquals(Object.isFrozen(WsNodeWebSocketServerProvider), true);
+ assertEquals(extension.contracts?.provides, [NodeWebSocketServerProviderName]);
+ assertEquals(extension.capabilities, [{
+ type: "env:read",
+ keys: ["WS_NO_BUFFER_UTIL", "WS_NO_UTF_8_VALIDATE"],
+ }]);
+ assertThrows(() => extension.setup?.(createContext(new Map())), Error, "already set up");
- extension.teardown?.();
- extension.setup?.(createContext(new Map()));
- extension.teardown?.();
-});
-
-Deno.test("ws extension creates a no-server transport", () => {
- const provider = WsNodeWebSocketServerProvider as NodeWebSocketServerProvider;
- const server = provider.createServer({
- noServer: true,
- handleProtocols: () => false,
+ extension.teardown?.();
+ extension.setup?.(createContext(new Map()));
+ extension.teardown?.();
});
- try {
- assertEquals(typeof server.handleUpgrade, "function");
- assertEquals(typeof server.close, "function");
- assertEquals(typeof server.on, "function");
- } finally {
- server.close();
- }
-});
+ it("creates a no-server transport", () => {
+ const provider = WsNodeWebSocketServerProvider as NodeWebSocketServerProvider;
+ const server = provider.createServer({
+ noServer: true,
+ handleProtocols: () => false,
+ });
-Deno.test("ws extension refuses a revoked setup context", () => {
- const extension = extNodeWebSocketWs();
- const controller = new AbortController();
- controller.abort(new DOMException("context retired", "AbortError"));
+ try {
+ assertEquals(typeof server.handleUpgrade, "function");
+ assertEquals(typeof server.close, "function");
+ assertEquals(typeof server.on, "function");
+ } finally {
+ server.close();
+ }
+ });
+
+ it("refuses a revoked setup context", () => {
+ const extension = extNodeWebSocketWs();
+ const controller = new AbortController();
+ controller.abort(new DOMException("context retired", "AbortError"));
- assertThrows(
- () => extension.setup?.(createContext(new Map(), controller.signal)),
- DOMException,
- "context retired",
- );
+ assertThrows(
+ () => extension.setup?.(createContext(new Map(), controller.signal)),
+ DOMException,
+ "context retired",
+ );
+ });
});
diff --git a/extensions/ext-node-websocket-ws/src/index.ts b/extensions/ext-node-websocket-ws/src/index.ts
index 799143aea7..1db7c23263 100644
--- a/extensions/ext-node-websocket-ws/src/index.ts
+++ b/extensions/ext-node-websocket-ws/src/index.ts
@@ -1,4 +1,4 @@
-/** Explicit `ws` implementation of Veryfront's Node WebSocket contract. */
+/** Default `ws` implementation of Veryfront's Node WebSocket contract. */
import type { ExtensionFactory } from "veryfront/extensions";
import {
@@ -13,7 +13,7 @@ export const WsNodeWebSocketServerProvider = createNodeWebSocketServerProvider(
(options) => captureNodeWebSocketServer(new WebSocketServer(options)),
);
-/** Create the explicitly selected Node.js `ws` transport extension. */
+/** Create the standard Node.js `ws` transport extension. */
export const extNodeWebSocketWs: ExtensionFactory = () => {
let active = false;
return {
diff --git a/extensions/ext-observability-sentry/README.md b/extensions/ext-observability-sentry/README.md
index 9260881c54..775087404d 100644
--- a/extensions/ext-observability-sentry/README.md
+++ b/extensions/ext-observability-sentry/README.md
@@ -2,6 +2,11 @@
First-party Sentry application error reporter for Veryfront runtimes.
+This package is service-conditional rather than an auto-activated application
+extension. The server or agent service imports the matching reporter only after
+its enablement policy passes; adding the package to `node_modules` alone does
+not initialize Sentry or enable network egress.
+
Enable the adapter explicitly and provide its credential:
```sh
diff --git a/extensions/ext-observability-sentry/deno.json b/extensions/ext-observability-sentry/deno.json
index 5b6ebb389b..fdf5860b13 100644
--- a/extensions/ext-observability-sentry/deno.json
+++ b/extensions/ext-observability-sentry/deno.json
@@ -8,6 +8,7 @@
},
"veryfront": {
"extension": true,
+ "activation": "explicit",
"npm": {
"stagedSources": [
{
diff --git a/scripts/build/build-npm-dnt.ts b/scripts/build/build-npm-dnt.ts
index 44bf48cff7..c3ed16550d 100644
--- a/scripts/build/build-npm-dnt.ts
+++ b/scripts/build/build-npm-dnt.ts
@@ -12,6 +12,7 @@
*/
import { build, emptyDir } from "#dnt";
+import { STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES } from "#veryfront/extensions/first-party-defaults.ts";
import {
BROWSER_SAFE_CLIENT_MODULES,
BROWSER_SAFE_DNT_TIMER_MODULES,
@@ -174,9 +175,6 @@ await build({
dependencies: {
"@types/react": npmDependencyRange(denoConfigSet, "@types/react"),
"@types/react-dom": npmDependencyRange(denoConfigSet, "@types/react-dom"),
- // Root deno.json intentionally rejects core npm imports; ws is a
- // Node-only dynamic import used by the npm server/HMR path.
- "ws": "8.21.0",
},
keywords: [
"react",
@@ -313,16 +311,9 @@ await build({
pkg.dependencies ??= {};
// Add after build-local npm install so releases do not require the
// just-built auto-loaded extension versions to already exist in the registry.
- pkg.dependencies["@veryfront/ext-bundler-esbuild"] = version;
- pkg.dependencies["@veryfront/ext-content-mdx"] = version;
- pkg.dependencies["@veryfront/ext-css-tailwind"] = version;
- // ext-parser-babel provides the CodeParser contract that `veryfront serve`
- // needs to vet client-page modules for /_veryfront/rsc/module hydration;
- // without it the endpoint 404s and client pages render without hydrating.
- pkg.dependencies["@veryfront/ext-parser-babel"] = version;
- // Skill discovery parses YAML through the extension contract; ship the
- // first-party implementation while keeping @std/yaml out of core.
- pkg.dependencies["@veryfront/ext-yaml"] = version;
+ for (const extensionDirectory of STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES) {
+ pkg.dependencies[`@veryfront/${extensionDirectory}`] = version;
+ }
pkg.files = ["esm", "script", "bin", "assets", "tsconfig.json", "LICENSE", "NOTICE", "README.md"];
pkg.exports["./tsconfig.json"] = "./tsconfig.json";
addTypesExportEntries(pkg.exports);
@@ -356,11 +347,9 @@ async function verifyNpmRootImportLifecycle(): Promise {
async function installBuiltNpmLifecycleConsumer(consumerDirectory: string): Promise {
const localPackageDirectories = await Promise.all([
Deno.realPath("./npm"),
- Deno.realPath("./npm/extensions/ext-bundler-esbuild"),
- Deno.realPath("./npm/extensions/ext-content-mdx"),
- Deno.realPath("./npm/extensions/ext-css-tailwind"),
- Deno.realPath("./npm/extensions/ext-parser-babel"),
- Deno.realPath("./npm/extensions/ext-yaml"),
+ ...STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES.map((extensionDirectory) =>
+ Deno.realPath(`./npm/extensions/${extensionDirectory}`)
+ ),
]);
await Deno.writeTextFile(
`${consumerDirectory}/package.json`,
@@ -406,7 +395,7 @@ if (metadata?.name !== "public-api") {
throw new Error("public runtime Skill parser default unavailable");
}
-const { createEvalCliBuiltinExtensions } = await import(
+const { createBuiltinExtensions, createEvalCliBuiltinExtensions } = await import(
"./node_modules/veryfront/esm/src/extensions/builtin-extensions.js"
);
const { getDeferredExtensionState } = await import(
@@ -429,6 +418,34 @@ const logger = {
warn() {},
error() {},
};
+
+for (const [extensionName, contractName] of [
+ ["ext-css-tailwind", "CSSProcessor"],
+ ["ext-dev-ui-react", "DevUiAssetProvider"],
+ ["ext-node-websocket-ws", "NodeWebSocketServerProvider"],
+]) {
+ const candidate = createBuiltinExtensions().find(
+ (entry) => entry.extension.name === extensionName,
+ );
+ if (!candidate) throw new Error("standard builtin missing: " + extensionName);
+ const standardDeferred = getDeferredExtensionState(candidate);
+ if (!standardDeferred) throw new Error("standard builtin was not deferred: " + extensionName);
+ const standardExtension = await standardDeferred.load(logger);
+ if (!standardExtension) throw new Error("standard builtin failed to load: " + extensionName);
+ let provided;
+ await standardExtension.setup?.({
+ get() {},
+ require(contract) { throw new Error("unexpected extension contract: " + contract); },
+ provide(contract, implementation) {
+ if (contract === contractName) provided = implementation;
+ },
+ config: {},
+ logger,
+ });
+ if (!provided) throw new Error("standard builtin did not provide " + contractName);
+ await standardExtension.teardown?.();
+}
+
const deferred = getDeferredExtensionState(resolved);
if (!deferred) throw new Error("bundled MLflow extension was not deferred");
const extension = await deferred.load(logger);
diff --git a/scripts/build/compile-binary.test.ts b/scripts/build/compile-binary.test.ts
index 1d9e0dcb9a..4802376632 100644
--- a/scripts/build/compile-binary.test.ts
+++ b/scripts/build/compile-binary.test.ts
@@ -1,8 +1,10 @@
-import { assertEquals } from "#std/assert";
import { walk } from "#std/fs/walk";
+import { assertEquals } from "#veryfront/testing/assert.ts";
+import { it } from "#veryfront/testing/bdd.ts";
import { createCompileArgs, DEFAULT_INCLUDES } from "./compile-binary.ts";
+import { FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES } from "#veryfront/extensions/first-party-defaults.ts";
-Deno.test("compiled CLI embeds the explicit Node WebSocket extension for opt-in activation", () => {
+it("compiled CLI embeds the default Node WebSocket extension for HMR", () => {
const args = createCompileArgs({
entrypoint: "cli/main.ts",
extraIncludes: [],
@@ -15,7 +17,7 @@ Deno.test("compiled CLI embeds the explicit Node WebSocket extension for opt-in
);
});
-Deno.test("compiled CLI embeds the explicit Redis extension for opt-in activation", () => {
+it("compiled CLI embeds the explicit Redis extension for opt-in activation", () => {
const args = createCompileArgs({
entrypoint: "cli/main.ts",
extraIncludes: [],
@@ -28,16 +30,10 @@ Deno.test("compiled CLI embeds the explicit Redis extension for opt-in activatio
);
});
-Deno.test("compiled CLI embeds optional builtin extension source files", async () => {
- const source = await Deno.readTextFile(
- "src/extensions/builtin-extensions.ts",
- );
- const sourceDirectories = Array.from(
- source.matchAll(/sourceDirectory:\s*"([^"]+)"/g),
- (match) => match[1]!,
- );
-
- for (const sourceDirectory of sourceDirectories) {
+it("compiled CLI embeds optional builtin extension source files", () => {
+ for (
+ const { sourceDirectory } of FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES
+ ) {
assertEquals(
DEFAULT_INCLUDES.includes(`extensions/${sourceDirectory}/src/index.ts`),
true,
@@ -46,7 +42,7 @@ Deno.test("compiled CLI embeds optional builtin extension source files", async (
}
});
-Deno.test("compiled CLI embeds every runtime-resolved sibling module", async () => {
+it("compiled CLI embeds every runtime-resolved sibling module", async () => {
// Modules picked through a `.ts`/`.js` distribution-format ternary are
// resolved from a computed URL, so `deno compile` never sees them in the
// static graph and only DEFAULT_INCLUDES can embed them.
@@ -85,7 +81,7 @@ Deno.test("compiled CLI embeds every runtime-resolved sibling module", async ()
);
});
-Deno.test("compiled CLI embeds the permissionless parser entry", () => {
+it("compiled CLI embeds the permissionless parser entry", () => {
assertEquals(
DEFAULT_INCLUDES.includes(
"extensions/ext-parser-babel/src/parser-only.ts",
@@ -94,7 +90,7 @@ Deno.test("compiled CLI embeds the permissionless parser entry", () => {
);
});
-Deno.test("compiled CLI embeds the auto-loaded Sentry reporter", () => {
+it("compiled CLI embeds the auto-loaded Sentry reporter", () => {
assertEquals(
DEFAULT_INCLUDES.includes(
"extensions/ext-observability-sentry/src/index.ts",
diff --git a/scripts/build/compile-binary.ts b/scripts/build/compile-binary.ts
index 2b7b036a6b..f37839c86a 100644
--- a/scripts/build/compile-binary.ts
+++ b/scripts/build/compile-binary.ts
@@ -12,8 +12,8 @@ export const DEFAULT_INCLUDES = [
"src/proxy/main.ts",
"src/security/sandbox/worker-script.ts",
"extensions/ext-auth-jwt/src/index.ts",
- // Embedding explicit extensions makes them available to compiled binaries;
- // discovery still requires the caller to select them before activation.
+ // Explicit extensions remain inert until selected. Default extensions are
+ // activated by the normal builtin composition when their source is embedded.
"extensions/ext-blob-gcs/src/index.ts",
"extensions/ext-blob-s3/src/index.ts",
"extensions/ext-node-websocket-ws/src/index.ts",
diff --git a/scripts/build/npm-package-metadata.test.ts b/scripts/build/npm-package-metadata.test.ts
index 02ec32f0c1..614bda6aa8 100644
--- a/scripts/build/npm-package-metadata.test.ts
+++ b/scripts/build/npm-package-metadata.test.ts
@@ -1,5 +1,9 @@
-import { assertEquals, assertStringIncludes } from "#std/assert";
-import { describe, it } from "#std/testing/bdd";
+import { STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES } from "#veryfront/extensions/first-party-defaults.ts";
+import {
+ assertEquals,
+ assertStringIncludes,
+} from "#veryfront/testing/assert.ts";
+import { describe, it } from "#veryfront/testing/bdd.ts";
import {
BROWSER_SAFE_CLIENT_MODULES,
BROWSER_SAFE_EXPORTS,
@@ -16,7 +20,7 @@ import {
type RootPackageConfig,
} from "./npm-extension-package-metadata.ts";
-Deno.test("exports agent skill helpers as a public package subpath", async () => {
+it("exports agent skill helpers as a public package subpath", async () => {
const denoConfig = JSON.parse(await Deno.readTextFile("deno.json"));
const exports = denoConfig.exports as Record;
const imports = denoConfig.imports as Record;
@@ -25,7 +29,7 @@ Deno.test("exports agent skill helpers as a public package subpath", async () =>
assertEquals(imports["veryfront/skill"], "./src/skill/index.ts");
});
-Deno.test("exports the UI adapter contract as a public package subpath", async () => {
+it("exports the UI adapter contract as a public package subpath", async () => {
const denoConfig = JSON.parse(await Deno.readTextFile("deno.json"));
const exports = denoConfig.exports as Record;
const imports = denoConfig.imports as Record;
@@ -35,7 +39,7 @@ Deno.test("exports the UI adapter contract as a public package subpath", async (
assertEquals(imports["veryfront/ui/adapter"], contract);
});
-Deno.test("exports CLI framework dependencies as public package subpaths", async () => {
+it("exports CLI framework dependencies as public package subpaths", async () => {
const denoConfig = JSON.parse(await Deno.readTextFile("deno.json"));
const exports = denoConfig.exports as Record;
const imports = denoConfig.imports as Record;
@@ -51,7 +55,7 @@ Deno.test("exports CLI framework dependencies as public package subpaths", async
}
});
-Deno.test("npm package provenance metadata points at veryfront-code", async () => {
+it("npm package provenance metadata points at veryfront-code", async () => {
const source = await Deno.readTextFile("scripts/build/build-npm-dnt.ts");
assertStringIncludes(
@@ -65,7 +69,7 @@ Deno.test("npm package provenance metadata points at veryfront-code", async () =
assertEquals(source.includes("github.com/veryfront/veryfront.git"), false);
});
-Deno.test("root npm build metadata does not inject extension implementation dependencies", async () => {
+it("root npm build metadata does not inject extension implementation dependencies", async () => {
const source = await Deno.readTextFile("scripts/build/build-npm-dnt.ts");
for (const packageName of ["@kreuzberg/node", "better-sqlite3"]) {
@@ -77,56 +81,56 @@ Deno.test("root npm build metadata does not inject extension implementation depe
}
});
-Deno.test("root npm CLI package declares auto-loaded first-party extensions after local install", async () => {
+it("standard npm extension policy covers baseline app and developer features", () => {
+ assertEquals([...STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES], [
+ "ext-bundler-esbuild",
+ "ext-content-mdx",
+ "ext-css-tailwind",
+ "ext-dev-ui-react",
+ "ext-node-websocket-ws",
+ "ext-parser-babel",
+ "ext-yaml",
+ ]);
+});
+
+it("root npm CLI package declares standard extensions after local install", async () => {
const source = await Deno.readTextFile("scripts/build/build-npm-dnt.ts");
const installIndex = source.indexOf(
"const { code } = await npmInstall.output();",
);
- for (
- const packageName of [
- "@veryfront/ext-bundler-esbuild",
- "@veryfront/ext-content-mdx",
- "@veryfront/ext-css-tailwind",
- "@veryfront/ext-parser-babel",
- "@veryfront/ext-yaml",
- ]
- ) {
- const dependencyAssignment =
- `pkg.dependencies["${packageName}"] = version;`;
- const dependencyIndex = source.indexOf(dependencyAssignment);
+ const dependencyLoop =
+ "for (const extensionDirectory of STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES)";
+ const dependencyIndex = source.indexOf(dependencyLoop);
- assertStringIncludes(source, dependencyAssignment);
- assertEquals(
- dependencyIndex > installIndex,
- true,
- `${packageName} dependency must be added after build-local npm install so prerelease builds do not require the extension to already be published`,
- );
- }
+ assertStringIncludes(source, dependencyLoop);
+ assertStringIncludes(
+ source,
+ "pkg.dependencies[`@veryfront/${extensionDirectory}`] = version;",
+ );
+ assertEquals(
+ dependencyIndex > installIndex,
+ true,
+ "standard extension dependencies must be added after the build-local npm install",
+ );
});
-Deno.test("npm lifecycle probe installs auto-loaded extensions in a real consumer layout", async () => {
+it("npm lifecycle probe installs auto-loaded extensions in a real consumer layout", async () => {
const source = await Deno.readTextFile("scripts/build/build-npm-dnt.ts");
assertStringIncludes(source, '"--install-links"');
assertStringIncludes(source, 'const agent = await import("veryfront/agent")');
assertStringIncludes(source, "agent.parseRuntimeSkillMetadata(");
- for (
- const extensionDirectory of [
- "ext-bundler-esbuild",
- "ext-content-mdx",
- "ext-css-tailwind",
- "ext-parser-babel",
- "ext-yaml",
- ]
- ) {
- assertStringIncludes(
- source,
- `Deno.realPath("./npm/extensions/${extensionDirectory}")`,
- );
- }
+ assertStringIncludes(
+ source,
+ "...STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES.map",
+ );
+ assertStringIncludes(
+ source,
+ "Deno.realPath(`./npm/extensions/${extensionDirectory}`)",
+ );
});
-Deno.test("npm publish version bump pins first-party extension dependencies to the publish version", async () => {
+it("npm publish version bump pins first-party extension dependencies to the publish version", async () => {
const packageDir = await Deno.makeTempDir();
const packagePath = `${packageDir}/package.json`;
const publishVersion = "0.1.1016-rc.123";
@@ -143,6 +147,7 @@ Deno.test("npm publish version bump pins first-party extension dependencies to t
"@veryfront/ext-bundler-esbuild": "0.1.1016",
"@veryfront/ext-content-mdx": "^0.1.1016",
"@veryfront/ext-css-tailwind": "^0.1.1016",
+ "@veryfront/ext-node-websocket-ws": "^0.1.1016",
"@veryfront/ext-parser-babel": "^0.1.1016",
"@veryfront/ext-yaml": "^0.1.1016",
"@veryfront/not-an-extension": "^0.1.1016",
@@ -184,6 +189,7 @@ Deno.test("npm publish version bump pins first-party extension dependencies to t
"@veryfront/ext-bundler-esbuild": publishVersion,
"@veryfront/ext-content-mdx": publishVersion,
"@veryfront/ext-css-tailwind": publishVersion,
+ "@veryfront/ext-node-websocket-ws": publishVersion,
"@veryfront/ext-parser-babel": publishVersion,
"@veryfront/ext-yaml": publishVersion,
"@veryfront/not-an-extension": "^0.1.1016",
@@ -197,7 +203,7 @@ Deno.test("npm publish version bump pins first-party extension dependencies to t
}
});
-Deno.test("npm publish orders extensions before the root package", async () => {
+it("npm publish orders extensions before the root package", async () => {
const packageRoot = await Deno.makeTempDir();
try {
@@ -232,7 +238,7 @@ Deno.test("npm publish orders extensions before the root package", async () => {
}
});
-Deno.test("npm publish skips extension packages marked publish false", async () => {
+it("npm publish skips extension packages marked publish false", async () => {
const packageRoot = await Deno.makeTempDir();
try {
@@ -331,7 +337,12 @@ const ROOT_BUNDLED_EXTENSIONS = new Set([
"ext-eval-report-mlflow",
]);
-Deno.test("EXTENSION_OWNED_DEPENDENCIES stays in sync with extension manifests", async () => {
+// The framework and ext-dev-ui-react both consume the application's React
+// generation. These remain root dependencies so npm resolves one React graph
+// instead of treating the extension's use as private implementation detail.
+const ROOT_SHARED_EXTENSION_DEPENDENCIES = new Set(["react", "react-dom"]);
+
+it("EXTENSION_OWNED_DEPENDENCIES stays in sync with extension manifests", async () => {
const denoConfig = JSON.parse(
await Deno.readTextFile("deno.json"),
) as RootPackageConfig;
@@ -365,7 +376,8 @@ Deno.test("EXTENSION_OWNED_DEPENDENCIES stays in sync with extension manifests",
for (const dependency of dependencies) {
assertEquals(
- owned.has(dependency) || optionalPeers.has(dependency),
+ owned.has(dependency) || optionalPeers.has(dependency) ||
+ ROOT_SHARED_EXTENSION_DEPENDENCIES.has(dependency),
true,
`${dependency} (declared by ${manifestPath}) must be added to EXTENSION_OWNED_DEPENDENCIES so it does not leak into root veryfront npm installs`,
);
@@ -598,6 +610,8 @@ describe("npm supply-chain policy", () => {
"ext-bundler-esbuild",
"ext-content-mdx",
"ext-css-tailwind",
+ "ext-dev-ui-react",
+ "ext-node-websocket-ws",
"ext-parser-babel",
"ext-yaml",
];
@@ -651,6 +665,7 @@ describe("npm supply-chain policy", () => {
"ext-bundler-esbuild",
"ext-content-mdx",
"ext-css-tailwind",
+ "ext-node-websocket-ws",
"ext-db-sqlite",
"ext-document-kreuzberg",
"ext-eval-report-mlflow",
diff --git a/scripts/test/npm-install-smoke.sh b/scripts/test/npm-install-smoke.sh
index c4b4fe1238..0ea126ae76 100755
--- a/scripts/test/npm-install-smoke.sh
+++ b/scripts/test/npm-install-smoke.sh
@@ -27,6 +27,8 @@ fail() {
[ -d "$ROOT_DIR/npm/extensions/ext-bundler-esbuild" ] || fail "ext-bundler-esbuild package output missing"
[ -d "$ROOT_DIR/npm/extensions/ext-content-mdx" ] || fail "ext-content-mdx package output missing"
[ -d "$ROOT_DIR/npm/extensions/ext-css-tailwind" ] || fail "ext-css-tailwind package output missing"
+[ -d "$ROOT_DIR/npm/extensions/ext-dev-ui-react" ] || fail "ext-dev-ui-react package output missing"
+[ -d "$ROOT_DIR/npm/extensions/ext-node-websocket-ws" ] || fail "ext-node-websocket-ws package output missing"
[ -d "$ROOT_DIR/npm/extensions/ext-parser-babel" ] || fail "ext-parser-babel package output missing"
[ -d "$ROOT_DIR/npm/extensions/ext-yaml" ] || fail "ext-yaml package output missing"
[ -d "$ROOT_DIR/npm/extensions/ext-auth-jwt" ] || fail "ext-auth-jwt package output missing"
@@ -35,13 +37,15 @@ fail() {
(cd "$ROOT_DIR/npm/extensions/ext-bundler-esbuild" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
(cd "$ROOT_DIR/npm/extensions/ext-content-mdx" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
(cd "$ROOT_DIR/npm/extensions/ext-css-tailwind" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
+(cd "$ROOT_DIR/npm/extensions/ext-dev-ui-react" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
+(cd "$ROOT_DIR/npm/extensions/ext-node-websocket-ws" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
(cd "$ROOT_DIR/npm/extensions/ext-parser-babel" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
(cd "$ROOT_DIR/npm/extensions/ext-yaml" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
(cd "$ROOT_DIR/npm/extensions/ext-auth-jwt" && npm pack --silent --pack-destination "$WORKDIR" >/dev/null)
cd "$WORKDIR"
npm init -y >/dev/null 2>&1
-npm install --no-fund --no-audit --silent --ignore-scripts ./veryfront-[0-9]*.tgz ./veryfront-ext-bundler-esbuild-*.tgz ./veryfront-ext-content-mdx-*.tgz ./veryfront-ext-css-tailwind-*.tgz ./veryfront-ext-parser-babel-*.tgz ./veryfront-ext-yaml-*.tgz
+npm install --no-fund --no-audit --silent --ignore-scripts ./veryfront-[0-9]*.tgz ./veryfront-ext-bundler-esbuild-*.tgz ./veryfront-ext-content-mdx-*.tgz ./veryfront-ext-css-tailwind-*.tgz ./veryfront-ext-dev-ui-react-*.tgz ./veryfront-ext-node-websocket-ws-*.tgz ./veryfront-ext-parser-babel-*.tgz ./veryfront-ext-yaml-*.tgz
echo "== 1. root install: CLI and parser extension run under Node"
node node_modules/veryfront/bin/veryfront.js --version | grep -q "Veryfront CLI" ||
@@ -52,7 +56,9 @@ node -e "
const p = require('./node_modules/veryfront/package.json');
if (p.dependencies?.['@veryfront/ext-parser-babel'] !== p.version) process.exit(1);
if (p.dependencies?.['@veryfront/ext-yaml'] !== p.version) process.exit(1);
-" || fail "root package does not pin required parser extensions to its version"
+if (p.dependencies?.['@veryfront/ext-dev-ui-react'] !== p.version) process.exit(1);
+if (p.dependencies?.['@veryfront/ext-node-websocket-ws'] !== p.version) process.exit(1);
+" || fail "root package does not pin standard extensions to its version"
node --input-type=module -e "
const m = await import('./node_modules/veryfront/esm/src/extensions/builtin-extensions.js');
const { getDeferredExtensionState } = await import(
diff --git a/src/extensions/builtin-extensions.test.ts b/src/extensions/builtin-extensions.test.ts
index e3b462883a..155ac654cc 100644
--- a/src/extensions/builtin-extensions.test.ts
+++ b/src/extensions/builtin-extensions.test.ts
@@ -15,6 +15,7 @@ import {
} from "./builtin-extensions.ts";
import { mergeExtensions } from "./discovery.ts";
import { getDeferredExtensionState } from "./deferred-extension.ts";
+import { FIRST_PARTY_EXTENSION_POLICIES } from "./first-party-defaults.ts";
import { createZodAdapter } from "@veryfront/ext-schema-zod";
import { ExtensionLoader } from "./loader.ts";
@@ -132,6 +133,15 @@ describe("createBuiltinExtensions", () => {
);
});
+ it("loads the generic HTTP eval exporter as a deferred builtin", async () => {
+ const httpExtension = await loadOptionalBuiltin("ext-eval-report-http");
+
+ assertEquals(
+ httpExtension.contracts?.requires?.includes(EvalReportExporterRegistryName),
+ true,
+ );
+ });
+
it("keeps optional candidates deferred until the loader selects them", () => {
const authCandidate = createBuiltinExtensions().find((entry) =>
entry.extension.name === "ext-auth-jwt"
@@ -141,16 +151,96 @@ describe("createBuiltinExtensions", () => {
assert(getDeferredExtensionState(authCandidate));
});
- it("never auto-loads the explicit Node WebSocket implementation", async () => {
- const source = await Deno.readTextFile(new URL("./builtin-extensions.ts", import.meta.url));
+ it("ships baseline CSS and Node WebSocket providers as deferred builtins", () => {
+ for (const name of ["ext-css-tailwind", "ext-node-websocket-ws"]) {
+ const definition = OPTIONAL_BUILTIN_EXTENSIONS.find((entry) => entry.name === name);
+ const candidate = createBuiltinExtensions().find((entry) => entry.extension.name === name);
+
+ assert(definition, `${name} must be part of the default runtime composition`);
+ assert(candidate, `${name} must have a builtin candidate`);
+ assert(getDeferredExtensionState(candidate), `${name} must remain lazy until activation`);
+ }
+ });
- assertEquals(source.includes("ext-node-websocket-ws"), false);
+ it("keeps builtin package discovery metadata auto-activated", async () => {
+ for (const definition of OPTIONAL_BUILTIN_EXTENSIONS) {
+ const manifest = JSON.parse(
+ await Deno.readTextFile(
+ new URL(
+ `../../extensions/${definition.sourceDirectory}/deno.json`,
+ import.meta.url,
+ ),
+ ),
+ ) as { veryfront?: { activation?: string } };
+
+ assertEquals(
+ manifest.veryfront?.activation ?? "auto",
+ "auto",
+ `${definition.name} cannot be both a builtin and explicit-only package`,
+ );
+ }
+ });
+
+ it("classifies every first-party extension exactly once", async () => {
+ const extensionDirectories: string[] = [];
+ for await (const entry of Deno.readDir(new URL("../../extensions", import.meta.url))) {
+ if (!entry.isDirectory || !entry.name.startsWith("ext-")) continue;
+ try {
+ await Deno.stat(
+ new URL(`../../extensions/${entry.name}/deno.json`, import.meta.url),
+ );
+ extensionDirectories.push(entry.name);
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) throw error;
+ }
+ }
+
+ const policyDirectories = FIRST_PARTY_EXTENSION_POLICIES.map((policy) =>
+ policy.sourceDirectory
+ );
assertEquals(
- OPTIONAL_BUILTIN_EXTENSIONS.some((definition) => definition.name === "ext-node-websocket-ws"),
- false,
+ [...new Set(policyDirectories)].sort(),
+ extensionDirectories.sort(),
+ "the activation policy must classify every first-party extension once",
+ );
+ assertEquals(
+ new Set(FIRST_PARTY_EXTENSION_POLICIES.map((policy) => policy.name)).size,
+ FIRST_PARTY_EXTENSION_POLICIES.length,
+ "first-party extension names must be unique",
);
});
+ it("keeps discovery activation aligned with first-party selection policy", async () => {
+ for (const policy of FIRST_PARTY_EXTENSION_POLICIES) {
+ const manifest = JSON.parse(
+ await Deno.readTextFile(
+ new URL(
+ `../../extensions/${policy.sourceDirectory}/deno.json`,
+ import.meta.url,
+ ),
+ ),
+ ) as { veryfront?: { activation?: string } };
+ const activation = manifest.veryfront?.activation ?? "auto";
+ const expected = policy.selection === "explicit" ||
+ policy.selection === "service-conditional"
+ ? "explicit"
+ : "auto";
+
+ assertEquals(
+ activation,
+ expected,
+ `${policy.name} manifest activation must match ${policy.selection}`,
+ );
+ if (policy.rootNpm) {
+ assertEquals(
+ policy.selection,
+ "builtin-deferred",
+ `${policy.name} cannot be a root npm dependency without builtin selection`,
+ );
+ }
+ }
+ });
+
it("does not statically import workspace implementation paths", async () => {
const source = await Deno.readTextFile(new URL("./builtin-extensions.ts", import.meta.url));
@@ -258,18 +348,25 @@ describe("createBuiltinExtensions", () => {
await loader.teardownAll();
});
- it("declares explicit eval exporter ids for optional exporter builtins", () => {
+ it("declares eval CLI selectors for optional exporter builtins", () => {
+ const http = OPTIONAL_BUILTIN_EXTENSIONS.find((definition) =>
+ definition.name === "ext-eval-report-http"
+ );
const mlflow = OPTIONAL_BUILTIN_EXTENSIONS.find((definition) =>
definition.name === "ext-eval-report-mlflow"
);
- assertEquals(mlflow?.evalExporterId, "mlflow");
+ assertEquals(http?.evalExporterSelection, { kind: "any-selected" });
+ assertEquals(mlflow?.evalExporterSelection, { kind: "id", id: "mlflow" });
});
it("builds a minimal eval CLI builtin set for selected eval exporters", () => {
- const names = createEvalCliBuiltinExtensions(["mlflow"]).map((entry) => entry.extension.name);
+ const names = createEvalCliBuiltinExtensions(["http", "mlflow"]).map((entry) =>
+ entry.extension.name
+ );
assertEquals(names.includes("ext-schema-zod"), true);
+ assertEquals(names.includes("ext-eval-report-http"), true);
assertEquals(names.includes("ext-eval-report-mlflow"), true);
assertEquals(names.includes("ext-auth-jwt"), false);
assertEquals(names.includes("ext-observability-opentelemetry"), false);
@@ -278,7 +375,17 @@ describe("createBuiltinExtensions", () => {
it("does not load optional eval exporter builtins when no exporters are selected", () => {
const names = createEvalCliBuiltinExtensions([]).map((entry) => entry.extension.name);
+ assertEquals(names.includes("ext-eval-report-http"), false);
assertEquals(names.includes("ext-eval-report-mlflow"), false);
assertEquals(names.includes("ext-auth-jwt"), false);
});
+
+ it("loads the configurable HTTP exporter for custom selected ids", () => {
+ const names = createEvalCliBuiltinExtensions(["internal-gateway"]).map((entry) =>
+ entry.extension.name
+ );
+
+ assertEquals(names.includes("ext-eval-report-http"), true);
+ assertEquals(names.includes("ext-eval-report-mlflow"), false);
+ });
});
diff --git a/src/extensions/builtin-extensions.ts b/src/extensions/builtin-extensions.ts
index 5f515c1243..4e591082bb 100644
--- a/src/extensions/builtin-extensions.ts
+++ b/src/extensions/builtin-extensions.ts
@@ -12,6 +12,10 @@ import { createDeferredResolvedExtension } from "./deferred-extension.ts";
import { captureRegistrationId } from "./runtime-validation.ts";
import type { LLMProvider, LLMProviderRegistry } from "./llm/index.ts";
import { createLLMProviderRegistry, LLMProviderRegistryName } from "./llm/index.ts";
+import {
+ FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES,
+ type FirstPartyEvalExporterSelection,
+} from "./first-party-defaults.ts";
import { OpenAIProvider } from "@veryfront/ext-llm-openai";
import { AnthropicProvider } from "@veryfront/ext-llm-anthropic";
import { GoogleProvider } from "@veryfront/ext-llm-google";
@@ -29,7 +33,7 @@ export type OptionalBuiltinExtensionDefinition = {
readonly name: string;
readonly origin: string;
readonly sourceDirectory: string;
- readonly evalExporterId?: string;
+ readonly evalExporterSelection?: FirstPartyEvalExporterSelection;
readonly factory?: ExtensionFactory;
};
@@ -51,67 +55,25 @@ const BUILTIN_LLM_PROVIDERS: BuiltinLLMProviderDefinition[] = [
},
];
-export const OPTIONAL_BUILTIN_EXTENSIONS = Object.freeze(([
- {
- name: "ext-auth-jwt",
- origin: "veryfront/ext-auth-jwt",
- sourceDirectory: "ext-auth-jwt",
- },
- {
- name: "ext-observability-opentelemetry",
- origin: "veryfront/ext-observability-opentelemetry",
- sourceDirectory: "ext-observability-opentelemetry",
- },
- {
- name: "ext-bundler-esbuild",
- origin: "veryfront/ext-bundler-esbuild",
- sourceDirectory: "ext-bundler-esbuild",
- },
- {
- name: "ext-dev-ui-react",
- origin: "veryfront/ext-dev-ui-react",
- sourceDirectory: "ext-dev-ui-react",
- },
- {
- name: "ext-parser-babel",
- origin: "veryfront/ext-parser-babel",
- sourceDirectory: "ext-parser-babel",
- },
- {
- name: "ext-yaml",
- origin: "veryfront/ext-yaml",
- sourceDirectory: "ext-yaml",
- },
- {
- name: "ext-content-mdx",
- origin: "veryfront/ext-content-mdx",
- sourceDirectory: "ext-content-mdx",
- },
- {
- name: "ext-document-kreuzberg",
- origin: "veryfront/ext-document-kreuzberg",
- sourceDirectory: "ext-document-kreuzberg",
- },
- {
- name: "ext-db-sqlite",
- origin: "veryfront/ext-db-sqlite",
- sourceDirectory: "ext-db-sqlite",
- },
- {
- name: "ext-sandbox-shell-tools",
- origin: "veryfront/ext-sandbox-shell-tools",
- sourceDirectory: "ext-sandbox-shell-tools",
- },
- {
- name: "ext-eval-report-mlflow",
- origin: "veryfront/ext-eval-report-mlflow",
- sourceDirectory: "ext-eval-report-mlflow",
- evalExporterId: "mlflow",
- // MLflow is deliberately shipped inside the root npm package rather than
- // published as a standalone extension package.
- factory: extEvalReportMlflow,
- },
-] satisfies OptionalBuiltinExtensionDefinition[]).map((definition) => Object.freeze(definition)));
+export const OPTIONAL_BUILTIN_EXTENSIONS = Object.freeze(
+ FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES.map((policy) =>
+ Object.freeze({
+ name: policy.name,
+ origin: `veryfront/${policy.sourceDirectory}`,
+ sourceDirectory: policy.sourceDirectory,
+ ...(policy.evalExporterSelection
+ ? { evalExporterSelection: policy.evalExporterSelection }
+ : {}),
+ ...(policy.name === "ext-eval-report-mlflow"
+ ? {
+ // MLflow is deliberately shipped inside the root npm package rather
+ // than published as a standalone extension package.
+ factory: extEvalReportMlflow,
+ }
+ : {}),
+ }) satisfies OptionalBuiltinExtensionDefinition
+ ),
+);
function getOrCreateLLMProviderRegistry(): LLMProviderRegistry {
const existing = tryResolve(LLMProviderRegistryName);
@@ -299,10 +261,11 @@ export function createEvalCliBuiltinExtensions(
selectedExporterIds: string[] = [],
): ResolvedExtension[] {
const selected = new Set(selectedExporterIds);
- const exporterExtensions = OPTIONAL_BUILTIN_EXTENSIONS.filter((definition) =>
- definition.evalExporterId !== undefined &&
- selected.has(definition.evalExporterId)
- );
+ const exporterExtensions = OPTIONAL_BUILTIN_EXTENSIONS.filter((definition) => {
+ const selection = definition.evalExporterSelection;
+ if (!selection) return false;
+ return selection.kind === "any-selected" ? selected.size > 0 : selected.has(selection.id);
+ });
return [
{
diff --git a/src/extensions/first-party-defaults.ts b/src/extensions/first-party-defaults.ts
new file mode 100644
index 0000000000..9f8009f861
--- /dev/null
+++ b/src/extensions/first-party-defaults.ts
@@ -0,0 +1,206 @@
+/** Selection policy for every first-party extension package. */
+export type FirstPartyExtensionSelection =
+ | "builtin-direct"
+ | "builtin-deferred"
+ | "service-conditional"
+ | "explicit";
+
+export type FirstPartyEvalExporterSelection = Readonly<
+ | { readonly kind: "any-selected" }
+ | { readonly kind: "id"; readonly id: string }
+>;
+
+export type FirstPartyExtensionPolicy = Readonly<{
+ readonly name: string;
+ readonly sourceDirectory: string;
+ readonly selection: FirstPartyExtensionSelection;
+ readonly rootNpm: boolean;
+ readonly evalExporterSelection?: FirstPartyEvalExporterSelection;
+}>;
+
+/**
+ * Authoritative inventory for first-party extension activation.
+ *
+ * `rootNpm` is intentionally narrower than builtin selection. Source builds
+ * and dedicated services can provide every builtin from their own package
+ * set, while the standard npm/CLI distribution installs only baseline app and
+ * developer-runtime capabilities. Credentialed, mutually exclusive, native,
+ * output-changing, and service-lifecycle integrations stay out of that root
+ * dependency set.
+ */
+export const FIRST_PARTY_EXTENSION_POLICIES = Object.freeze(([
+ {
+ name: "ext-auth-jwt",
+ sourceDirectory: "ext-auth-jwt",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ },
+ {
+ name: "ext-blob-gcs",
+ sourceDirectory: "ext-blob-gcs",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-blob-s3",
+ sourceDirectory: "ext-blob-s3",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-bundler-esbuild",
+ sourceDirectory: "ext-bundler-esbuild",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-cache-redis",
+ sourceDirectory: "ext-cache-redis",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-content-mdx",
+ sourceDirectory: "ext-content-mdx",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-css-lightning",
+ sourceDirectory: "ext-css-lightning",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-css-purgecss",
+ sourceDirectory: "ext-css-purgecss",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-css-tailwind",
+ sourceDirectory: "ext-css-tailwind",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-db-sqlite",
+ sourceDirectory: "ext-db-sqlite",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ },
+ {
+ name: "ext-dev-ui-react",
+ sourceDirectory: "ext-dev-ui-react",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-document-kreuzberg",
+ sourceDirectory: "ext-document-kreuzberg",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ },
+ {
+ name: "ext-eval-report-http",
+ sourceDirectory: "ext-eval-report-http",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ evalExporterSelection: Object.freeze({ kind: "any-selected" }),
+ },
+ {
+ name: "ext-eval-report-mlflow",
+ sourceDirectory: "ext-eval-report-mlflow",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ evalExporterSelection: Object.freeze({ kind: "id", id: "mlflow" }),
+ },
+ {
+ name: "ext-image-sharp",
+ sourceDirectory: "ext-image-sharp",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-llm-anthropic",
+ sourceDirectory: "ext-llm-anthropic",
+ selection: "builtin-direct",
+ rootNpm: false,
+ },
+ {
+ name: "ext-llm-google",
+ sourceDirectory: "ext-llm-google",
+ selection: "builtin-direct",
+ rootNpm: false,
+ },
+ {
+ name: "ext-llm-openai",
+ sourceDirectory: "ext-llm-openai",
+ selection: "builtin-direct",
+ rootNpm: false,
+ },
+ {
+ name: "ext-node-websocket-ws",
+ sourceDirectory: "ext-node-websocket-ws",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-observability-opentelemetry",
+ sourceDirectory: "ext-observability-opentelemetry",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ },
+ {
+ name: "ext-observability-sentry",
+ sourceDirectory: "ext-observability-sentry",
+ selection: "service-conditional",
+ rootNpm: false,
+ },
+ {
+ name: "ext-parser-babel",
+ sourceDirectory: "ext-parser-babel",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+ {
+ name: "ext-react-ssr",
+ sourceDirectory: "ext-react-ssr",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-redis",
+ sourceDirectory: "ext-redis",
+ selection: "explicit",
+ rootNpm: false,
+ },
+ {
+ name: "ext-sandbox-shell-tools",
+ sourceDirectory: "ext-sandbox-shell-tools",
+ selection: "builtin-deferred",
+ rootNpm: false,
+ },
+ {
+ name: "ext-schema-zod",
+ sourceDirectory: "ext-schema-zod",
+ selection: "builtin-direct",
+ rootNpm: false,
+ },
+ {
+ name: "ext-yaml",
+ sourceDirectory: "ext-yaml",
+ selection: "builtin-deferred",
+ rootNpm: true,
+ },
+] satisfies FirstPartyExtensionPolicy[]).map((policy) => Object.freeze(policy)));
+
+export const FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES = Object.freeze(
+ FIRST_PARTY_EXTENSION_POLICIES.filter((policy) => policy.selection === "builtin-deferred"),
+);
+
+export const STANDARD_ROOT_NPM_EXTENSION_DIRECTORIES = Object.freeze(
+ FIRST_PARTY_EXTENSION_POLICIES.filter((policy) => policy.rootNpm).map(
+ (policy) => policy.sourceDirectory,
+ ),
+);
diff --git a/src/extensions/orchestrate.test.ts b/src/extensions/orchestrate.test.ts
index 02fb329c8f..e4cae07cad 100644
--- a/src/extensions/orchestrate.test.ts
+++ b/src/extensions/orchestrate.test.ts
@@ -13,7 +13,11 @@ import { reset, resolve as resolveContract, tryResolve } from "./contracts.ts";
import type { Extension, ExtensionSource, ResolvedExtension } from "./types.ts";
import type { LLMProvider, LLMProviderRegistry } from "./llm/index.ts";
import { createLLMProviderRegistry, LLMProviderRegistryName } from "./llm/index.ts";
-import { createBuiltinExtensions } from "./builtin-extensions.ts";
+import {
+ createBuiltinExtensions,
+ createEvalCliBuiltinExtensions,
+ createOptionalBuiltinExtension,
+} from "./builtin-extensions.ts";
import { join } from "@std/path";
const noopLogger = {
@@ -609,6 +613,164 @@ describe("orchestrateExtensions()", () => {
await loader.teardownAll();
});
+ it("keeps installed first-party builtin packages deferred and prefilters their disable aliases", async () => {
+ const packageHits = [
+ {
+ packageName: "@veryfront/ext-css-tailwind",
+ importTarget: "/canonical/ext-css-tailwind.js",
+ metadata: {
+ isExtension: true as const,
+ activation: "auto" as const,
+ capabilities: [],
+ },
+ },
+ {
+ packageName: "@veryfront/ext-node-websocket-ws",
+ importTarget: "/canonical/ext-node-websocket-ws.js",
+ metadata: {
+ isExtension: true as const,
+ activation: "auto" as const,
+ capabilities: [],
+ },
+ },
+ ];
+
+ for (
+ const [disabledName, expectedDeferredFactoryCalls] of [
+ ["ext-css-tailwind", ["ext-node-websocket-ws"]],
+ ["@veryfront/ext-node-websocket-ws", ["ext-css-tailwind"]],
+ ] as const
+ ) {
+ const loadCalls: string[] = [];
+ const deferredFactoryCalls: string[] = [];
+ const loader = await orchestrateExtensions({
+ projectDir: "/fake",
+ config: {
+ extensions: [{ name: disabledName, enabled: false }],
+ },
+ logger: noopLogger,
+ discovery: {
+ ...emptyDiscovery(),
+ discoverPackageExtensions: () => Promise.resolve(packageHits),
+ },
+ builtinExtensions: [
+ createOptionalBuiltinExtension({
+ name: "ext-css-tailwind",
+ origin: "veryfront/ext-css-tailwind",
+ sourceDirectory: "ext-css-tailwind",
+ factory: () => {
+ deferredFactoryCalls.push("ext-css-tailwind");
+ return stubExt("ext-css-tailwind", {
+ contracts: { provides: ["CSSProcessor"] },
+ setup: (ctx) => ctx.provide("CSSProcessor", { id: "tailwind" }),
+ });
+ },
+ }),
+ createOptionalBuiltinExtension({
+ name: "ext-node-websocket-ws",
+ origin: "veryfront/ext-node-websocket-ws",
+ sourceDirectory: "ext-node-websocket-ws",
+ factory: () => {
+ deferredFactoryCalls.push("ext-node-websocket-ws");
+ return stubExt("ext-node-websocket-ws", {
+ contracts: { provides: ["NodeWebSocketServerProvider"] },
+ setup: (ctx) => ctx.provide("NodeWebSocketServerProvider", { id: "ws" }),
+ });
+ },
+ }),
+ ],
+ loadFactory: (path: string, source: ExtensionSource) => {
+ loadCalls.push(path);
+ return Promise.resolve({
+ extension: stubExt(path),
+ source,
+ origin: path,
+ });
+ },
+ });
+
+ assertEquals(loadCalls, []);
+ assertEquals(deferredFactoryCalls, [...expectedDeferredFactoryCalls]);
+ await loader.teardownAll();
+ }
+ });
+
+ it("keeps package discovery above ordinary builtins with the same first-party name", async () => {
+ const loader = await orchestrateExtensions({
+ projectDir: "/fake",
+ config: {},
+ logger: noopLogger,
+ discovery: {
+ ...emptyDiscovery(),
+ discoverPackageExtensions: () =>
+ Promise.resolve([{
+ packageName: "@veryfront/ext-css-tailwind",
+ importTarget: "/canonical/ext-css-tailwind.js",
+ metadata: {
+ isExtension: true as const,
+ activation: "auto" as const,
+ capabilities: [],
+ },
+ }]),
+ },
+ builtinExtensions: [{
+ extension: stubExt("ext-css-tailwind", {
+ provides: { SelectedExtensionSource: { from: "builtin" } },
+ }),
+ source: "builtin",
+ origin: "custom-direct-builtin",
+ }],
+ loadFactory: (_path: string, source: ExtensionSource) =>
+ Promise.resolve({
+ extension: stubExt("ext-css-tailwind", {
+ provides: { SelectedExtensionSource: { from: "package" } },
+ }),
+ source,
+ origin: "canonical-package",
+ }),
+ });
+
+ assertEquals(tryResolve("SelectedExtensionSource"), { from: "package" });
+ await loader.teardownAll();
+ });
+
+ it("keeps deferred packages lazy for the reduced eval CLI builtin set", async () => {
+ const loadCalls: string[] = [];
+ const loader = await orchestrateExtensions({
+ projectDir: "/fake",
+ config: {},
+ logger: noopLogger,
+ primeContracts: {
+ [LLMProviderRegistryName]: createLLMProviderRegistry(),
+ },
+ discovery: {
+ ...emptyDiscovery(),
+ discoverPackageExtensions: () =>
+ Promise.resolve([{
+ packageName: "@veryfront/ext-css-tailwind",
+ importTarget: "/canonical/ext-css-tailwind.js",
+ metadata: {
+ isExtension: true as const,
+ activation: "auto" as const,
+ capabilities: [],
+ },
+ }]),
+ },
+ builtinExtensions: createEvalCliBuiltinExtensions([]),
+ loadFactory: (path: string, source: ExtensionSource) => {
+ loadCalls.push(path);
+ return Promise.resolve({
+ extension: stubExt("ext-css-tailwind"),
+ source,
+ origin: path,
+ });
+ },
+ });
+
+ assertEquals(loadCalls, []);
+ await loader.teardownAll();
+ });
+
it("skips loadFactory for disabled project extensions (src/index.ts variant)", async () => {
const loadCalls: string[] = [];
diff --git a/src/extensions/orchestrate.ts b/src/extensions/orchestrate.ts
index 3ee1ef98b2..0e17156655 100644
--- a/src/extensions/orchestrate.ts
+++ b/src/extensions/orchestrate.ts
@@ -9,9 +9,11 @@
*/
import { basename, dirname } from "#veryfront/compat/path";
+import { getDeferredExtensionState } from "./deferred-extension.ts";
import * as defaultDiscovery from "./discovery.ts";
import type { BoundExtensionEntrypoint } from "./entrypoint-identity.ts";
import { loadExtensionFactory as defaultLoadFactory } from "./factory-loader.ts";
+import { FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES } from "./first-party-defaults.ts";
import { ExtensionLoader } from "./loader.ts";
import type {
Extension,
@@ -69,6 +71,18 @@ export interface OrchestrateOptions {
let orchestrationTail: Promise = Promise.resolve();
let activeLoader: ExtensionLoader | undefined;
let failedCandidate: ExtensionLoader | undefined;
+const FIRST_PARTY_BUILTIN_PACKAGE_TO_EXTENSION = new Map(
+ FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES.map((policy) => [
+ `@veryfront/${policy.sourceDirectory}`,
+ policy.name,
+ ]),
+);
+const FIRST_PARTY_BUILTIN_EXTENSION_TO_PACKAGE = new Map(
+ FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES.map((policy) => [
+ policy.name,
+ `@veryfront/${policy.sourceDirectory}`,
+ ]),
+);
function isDisableDirective(
entry: ExtensionConfigEntry,
@@ -117,6 +131,30 @@ interface ProjectLoadCandidate {
target: FactoryLoadTarget;
}
+function buildDisableFilters(
+ disables: Array<{ name: string; enabled: false }>,
+): { extensionNames: Set; packageNames: Set } {
+ const extensionNames = new Set();
+ const packageNames = new Set();
+ for (const { name } of disables) {
+ extensionNames.add(name);
+ packageNames.add(name);
+ const extensionName = FIRST_PARTY_BUILTIN_PACKAGE_TO_EXTENSION.get(name);
+ if (extensionName) extensionNames.add(extensionName);
+ const packageName = FIRST_PARTY_BUILTIN_EXTENSION_TO_PACKAGE.get(name);
+ if (packageName) packageNames.add(packageName);
+ }
+ return { extensionNames, packageNames };
+}
+
+function isDeferredBuiltinPackageHit(
+ hit: PackageLoadCandidate,
+ ordinaryBuiltinExtensionNames: ReadonlySet,
+): boolean {
+ const extensionName = FIRST_PARTY_BUILTIN_PACKAGE_TO_EXTENSION.get(hit.packageName);
+ return extensionName !== undefined && !ordinaryBuiltinExtensionNames.has(extensionName);
+}
+
/**
* Run the full extension pipeline against a resolved project config.
*
@@ -173,7 +211,15 @@ async function orchestrateExtensionGeneration(
// extensions the user has explicitly turned off. A factory whose module
// fails to import or invoke would otherwise take down bootstrap even
// though the user asked for it to be disabled.
- const disabledNames = new Set(disables.map((d) => d.name));
+ const disabled = buildDisableFilters(disables);
+ // First-party deferred packages stay lazy even when a reduced caller omits
+ // their candidate. Ordinary builtins are exempt so package discovery keeps
+ // its documented priority over direct builtin entries with the same name.
+ const ordinaryBuiltinExtensionNames = new Set(
+ (options.builtinExtensions ?? [])
+ .filter((entry) => getDeferredExtensionState(entry) === undefined)
+ .map((entry) => entry.extension.name),
+ );
let packageHits: PackageLoadCandidate[];
let projectHits: ProjectLoadCandidate[];
@@ -217,14 +263,20 @@ async function orchestrateExtensionGeneration(
// This prevents an import map from redirecting the authorized package name.
const enabledPackageTargets = packageHits
.filter((hit) => defaultDiscovery.resolvePackageActivation(hit.metadata) === "auto")
- .filter((hit) => !disabledNames.has(hit.packageName))
+ .filter((hit) =>
+ !disabled.packageNames.has(hit.packageName) &&
+ !disabled.extensionNames.has(hit.packageName) &&
+ !isDeferredBuiltinPackageHit(hit, ordinaryBuiltinExtensionNames)
+ )
.map((hit) => hit.target);
// Project paths have the shape `/extensions//src/index.ts`
// (or `/extensions//index.ts`). `mergeExtensions` is the
// safety net for any path whose name cannot be derived.
const enabledProjectTargets = projectHits
- .filter((hit) => hit.extensionName === undefined || !disabledNames.has(hit.extensionName))
+ .filter((hit) =>
+ hit.extensionName === undefined || !disabled.extensionNames.has(hit.extensionName)
+ )
.map((hit) => hit.target);
// Local-file paths cannot be reliably filtered pre-load: the filename
@@ -251,7 +303,7 @@ async function orchestrateExtensionGeneration(
packageResolved,
projectResolved,
localResolved,
- disables,
+ [...disabled.extensionNames].map((name) => ({ name, enabled: false as const })),
options.builtinExtensions,
);
diff --git a/src/extensions/websocket/node-websocket-server-provider.test.ts b/src/extensions/websocket/node-websocket-server-provider.test.ts
index df320226b2..f2c086cfe7 100644
--- a/src/extensions/websocket/node-websocket-server-provider.test.ts
+++ b/src/extensions/websocket/node-websocket-server-provider.test.ts
@@ -125,6 +125,6 @@ Deno.test("Node WebSocket provider helper and missing-contract diagnostic are ac
);
assertStringIncludes(
NODE_WEBSOCKET_SERVER_PROVIDER_MISSING_MESSAGE,
- "explicitly enabled",
+ "install it or remove the extension disable directive",
);
});
diff --git a/src/extensions/websocket/node-websocket-server-provider.ts b/src/extensions/websocket/node-websocket-server-provider.ts
index 4b3b8a8f12..48a63277ba 100644
--- a/src/extensions/websocket/node-websocket-server-provider.ts
+++ b/src/extensions/websocket/node-websocket-server-provider.ts
@@ -1,9 +1,9 @@
/**
* Dependency-free contract for Node.js WebSocket server implementations.
*
- * Core owns HTTP upgrade authorization and lifecycle. An explicitly composed
- * extension owns the protocol implementation used to complete an authorized
- * upgrade on an existing Node socket.
+ * Core owns HTTP upgrade authorization and lifecycle. An extension owns the
+ * protocol implementation used to complete an authorized upgrade on an
+ * existing Node socket.
*/
import { types as nodeUtilTypes } from "node:util";
@@ -11,7 +11,7 @@ import { types as nodeUtilTypes } from "node:util";
export const NodeWebSocketServerProviderName = "NodeWebSocketServerProvider";
export const NODE_WEBSOCKET_SERVER_PROVIDER_PACKAGE = "@veryfront/ext-node-websocket-ws";
export const NODE_WEBSOCKET_SERVER_PROVIDER_MISSING_MESSAGE =
- `Node.js WebSocket upgrades require an explicitly enabled ${NODE_WEBSOCKET_SERVER_PROVIDER_PACKAGE} extension.`;
+ `Node.js WebSocket upgrades require ${NODE_WEBSOCKET_SERVER_PROVIDER_PACKAGE}; install it or remove the extension disable directive.`;
const isProxy = nodeUtilTypes.isProxy;
diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts
index 535fe3b384..0c753fb85f 100644
--- a/src/server/bootstrap.ts
+++ b/src/server/bootstrap.ts
@@ -166,7 +166,7 @@ function createBootstrapPrimeContracts(): Record {
};
}
-/** @internal Snapshot the explicit Node WebSocket implementation for this generation. */
+/** @internal Snapshot the extension-provided Node WebSocket implementation for this generation. */
export function resolveNodeWebSocketServerProviderForBootstrap():
| Readonly
| undefined {
diff --git a/src/server/dev-server/server.ts b/src/server/dev-server/server.ts
index 552a0c4a71..72bb0a853b 100644
--- a/src/server/dev-server/server.ts
+++ b/src/server/dev-server/server.ts
@@ -286,7 +286,7 @@ export class DevServer {
return this._handler;
}
- /** Explicit Node WebSocket implementation captured with this bootstrap generation. */
+ /** Extension-provided Node WebSocket implementation captured with this bootstrap generation. */
get nodeWebSocketServerProvider(): Readonly | undefined {
return this._nodeWebSocketServerProvider;
}
diff --git a/src/server/service-server.ts b/src/server/service-server.ts
index e1d0649fa6..49102326c8 100644
--- a/src/server/service-server.ts
+++ b/src/server/service-server.ts
@@ -51,7 +51,7 @@ export type StartNodeVeryfrontServerOptions = {
logger?: VeryfrontServiceServerLogger;
signals?: readonly NodeJS.Signals[];
hardShutdownTimeoutMs?: number;
- /** Explicit Node WebSocket implementation; upgrades fail closed when omitted. */
+ /** Extension-provided Node WebSocket implementation; upgrades fail closed when omitted. */
nodeWebSocketServerProvider?: Readonly;
};