Repository navigation
fix(cli): compose the project's extensions before a production build #3413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import "#veryfront/schemas/_test-setup.ts"; | ||
| import { assertEquals, assertExists } from "#veryfront/testing/assert.ts"; | ||
| import { describe, it } from "#veryfront/testing/bdd.ts"; | ||
| import { setupBuildCliExtensions } from "./build-extensions.ts"; | ||
|
|
||
| /** Minimal stand-in for the loader; the build path only needs it to resolve. */ | ||
| const loaderStub = {} as Awaited<ReturnType<typeof setupBuildCliExtensions>>; | ||
|
|
||
| /** | ||
| * Deferred builtins do not declare their contracts until they load, so identity | ||
| * is what a caller can assert on before orchestration runs. | ||
| */ | ||
| function extensionNames( | ||
| builtins: readonly { extension: { name: string } }[], | ||
| ): Set<string> { | ||
| return new Set(builtins.map((builtin) => builtin.extension.name)); | ||
| } | ||
|
|
||
| describe("cli/shared/build-extensions", () => { | ||
| it("composes the project's configured extensions", async () => { | ||
| let seen: { projectDir?: string; config?: unknown } = {}; | ||
| // A full Extension entry: ExtensionConfigEntry only admits an Extension or | ||
| // an explicit { name, enabled: false } disable. | ||
| const config = { | ||
| extensions: [{ name: "ext-css-lightning", version: "1.0.0", capabilities: [] }], | ||
| }; | ||
|
|
||
| await setupBuildCliExtensions("/projects/app", config, (options) => { | ||
| seen = { projectDir: options.projectDir, config: options.config }; | ||
| return Promise.resolve(loaderStub); | ||
| }); | ||
|
|
||
| assertEquals(seen.projectDir, "/projects/app"); | ||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Remove absolute fixture paths from the tests. These stubs only forward Proposed fix- await setupBuildCliExtensions("/projects/app", config, (options) => {
+ await setupBuildCliExtensions("test-project", config, (options) => {
seen = { projectDir: options.projectDir, config: options.config };
return Promise.resolve(loaderStub);
});
- assertEquals(seen.projectDir, "/projects/app");
+ assertEquals(seen.projectDir, "test-project");
- await setupBuildCliExtensions("/projects/app", {}, (options) => {
+ await setupBuildCliExtensions("test-project", {}, (options) => {
- await setupBuildCliExtensions("/projects/app", {}, (options) => {
+ await setupBuildCliExtensions("test-project", {}, (options) => {
- await setupBuildCliExtensions("/projects/app", {}, (options) => {
+ await setupBuildCliExtensions("test-project", {}, (options) => {
- "/projects/app",
+ "test-project",As per coding guidelines, never place local absolute paths in tests. Also applies to: 41-41, 54-54, 71-71, 83-85 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| // The build must honor what the project declares, not a hardcoded default. | ||
| assertEquals(seen.config, config); | ||
| }); | ||
|
|
||
| it("offers the CSSProcessor provider among the builtins", async () => { | ||
| let builtins: readonly { extension: { name: string } }[] = []; | ||
|
|
||
| await setupBuildCliExtensions("/projects/app", {}, (options) => { | ||
| builtins = options.builtinExtensions ?? []; | ||
| return Promise.resolve(loaderStub); | ||
| }); | ||
|
|
||
| // Without this, `veryfront build` reaches the release-asset CSS compile with | ||
| // no CSSProcessor registered and fails with "Missing extension for contract". | ||
| assertEquals(extensionNames(builtins).has("ext-css-tailwind"), true); | ||
| }); | ||
|
|
||
| it("offers the bundler and content providers the build also needs", async () => { | ||
| let builtins: readonly { extension: { name: string } }[] = []; | ||
|
|
||
| await setupBuildCliExtensions("/projects/app", {}, (options) => { | ||
| builtins = options.builtinExtensions ?? []; | ||
| return Promise.resolve(loaderStub); | ||
| }); | ||
|
|
||
| const names = extensionNames(builtins); | ||
| assertEquals(names.has("ext-bundler-esbuild"), true); | ||
| assertEquals(names.has("ext-content-mdx"), true); | ||
| }); | ||
|
|
||
| it("hands orchestration a logger it can actually log through", async () => { | ||
| // Not "names the build": cliLogger.component() deliberately returns the | ||
| // same logger, because CLI output carries no structured component tag. So | ||
| // there is no attribution to assert on, only that orchestration receives | ||
| // something usable. `typeof x === "object"` alone would also accept null. | ||
| let logger: Record<string, unknown> | undefined; | ||
|
|
||
| await setupBuildCliExtensions("/projects/app", {}, (options) => { | ||
| logger = options.logger as unknown as Record<string, unknown>; | ||
| return Promise.resolve(loaderStub); | ||
| }); | ||
|
|
||
| assertExists(logger); | ||
| for (const method of ["debug", "info", "warn", "error"] as const) { | ||
| assertEquals(typeof logger[method], "function", `logger.${method} must be callable`); | ||
| } | ||
| }); | ||
|
|
||
| it("returns the composed loader to the caller", async () => { | ||
| const result = await setupBuildCliExtensions( | ||
| "/projects/app", | ||
| {}, | ||
| () => Promise.resolve(loaderStub), | ||
| ); | ||
|
|
||
| assertEquals(result, loaderStub); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /** | ||
| * Extension composition for `veryfront build`. | ||
| * | ||
| * Extension orchestration used to happen only in server bootstrap, so commands | ||
| * that never start a server ran with an almost empty contract registry. The | ||
| * build path compensated with one-off shims (`ensureCliBundlerContracts`, | ||
| * `ensureBuiltinContentProcessor`) that covered the contracts someone had | ||
| * already been bitten by, and nothing else. | ||
| * | ||
| * CSS was the contract nobody had added a shim for. A scaffolded project whose | ||
| * stylesheet is `@import "tailwindcss";` reached the release-asset CSS compile | ||
| * with no CSSProcessor registered and failed with: | ||
| * | ||
| * Missing extension for contract "CSSProcessor". | ||
| * Install it with: deno add @veryfront/ext-css-tailwind | ||
| * | ||
| * The extension was installed the whole time. `veryfront dev` compiled the same | ||
| * stylesheet correctly because starting a server orchestrated it. | ||
| * | ||
| * Composing extensions here fixes that class of failure rather than one | ||
| * instance of it, and honors what the project configures: a project that | ||
| * declares `ext-css-lightning` gets its own processor instead of whichever one | ||
| * a shim happened to hardcode. `veryfront eval` already does this. | ||
| * | ||
| * @module cli/shared/build-extensions | ||
| */ | ||
|
|
||
| import { orchestrateExtensions } from "veryfront/extensions"; | ||
| import { cliLogger } from "#cli/utils"; | ||
| import { createBuiltinExtensions } from "../../src/extensions/builtin-extensions.ts"; | ||
|
|
||
| type OrchestrateExtensions = typeof orchestrateExtensions; | ||
| type OrchestrateOptions = Parameters<OrchestrateExtensions>[0]; | ||
|
|
||
| /** | ||
| * Compose the extensions a production build needs. | ||
| * | ||
| * `orchestrate` is a test seam and defaults to the real implementation. | ||
| */ | ||
| export async function setupBuildCliExtensions( | ||
| projectDir: string, | ||
| config: OrchestrateOptions["config"], | ||
| orchestrate: OrchestrateExtensions = orchestrateExtensions, | ||
| ): Promise<Awaited<ReturnType<OrchestrateExtensions>>> { | ||
| return await orchestrate({ | ||
| projectDir, | ||
| config, | ||
| logger: cliLogger.component("build-extensions"), | ||
| builtinExtensions: createBuiltinExtensions(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the required public warning style.
When JSON mode is disabled, Line 63 emits user-facing copy that does not address the reader. Use direct copy such as
Veryfront cannot release your build extensions after this build.As per coding guidelines, use direct, concise, present-tense, active-voice public copy and address the reader as
you.🤖 Prompt for AI Agents
Source: Coding guidelines