Add JavaScript publish methods, AddNextJsApp, and switch to YARP#15736
Add JavaScript publish methods, AddNextJsApp, and switch to YARP#15736
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15736Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15736" |
| /// </example> | ||
| /// </remarks> | ||
| [AspireExport("addNextJsApp", Description = "Adds a Next.js application resource")] | ||
| public static IResourceBuilder<NextJsAppResource> AddNextJsApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string appDirectory, string runScriptName = "dev") |
There was a problem hiding this comment.
This is a slippery slope, are we going to ever see APIs like AddAngularApp, AddVueApp, or AddSvelte... you know? What makes NextJs special?
There was a problem hiding this comment.
If the publish pattern can't be expressed with the generic methods, the framework needs its own Add*. That's the litmus test.
Next.js passes that test:
- Publish is unique — 3-COPY pattern (public/, .next/standalone/, .next/static/) can't be expressed with publishAsNodeServer or publishAsNpmScript
- Run mode is wrong on addViteApp — Next.js uses next dev -p , not Vite's --port
- Needs HOSTNAME=0.0.0.0 — container binding quirk
2936940 to
ada8362
Compare
There was a problem hiding this comment.
Pull request overview
Adds deployable JavaScript publish patterns to Aspire.Hosting.JavaScript (addressing #12697) and introduces a Next.js-specific AddNextJsApp API to support both run-mode (next dev) and publish-mode (standalone output) workflows.
Changes:
- Add publish APIs for JS apps:
PublishAsStaticWebsite,PublishAsNodeServer, andPublishAsNpmScript. - Add
AddNextJsApp+ supporting resource/annotations and Dockerfile generation logic. - Expand unit/snapshot coverage and add a CLI E2E test with checked-in JS fixtures.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs | Implements new publish methods, Next.js support, and Dockerfile generation updates. |
| src/Aspire.Hosting.JavaScript/NextJsAppResource.cs | Adds a dedicated public resource type for Next.js apps. |
| src/Aspire.Hosting.JavaScript/JavaScriptStaticWebsiteAnnotation.cs | Adds internal annotation to drive static website publish behavior. |
| src/Aspire.Hosting.JavaScript/JavaScriptNodeServerAnnotation.cs | Adds internal annotation to drive node-server publish behavior. |
| src/Aspire.Hosting.JavaScript/JavaScriptNpmScriptAnnotation.cs | Adds internal annotation to drive npm-script runtime publish behavior. |
| src/Aspire.Hosting.JavaScript/JavaScriptNextStandaloneAnnotation.cs | Adds internal marker annotation to drive Next.js standalone publish behavior. |
| tests/Aspire.Hosting.JavaScript.Tests/NodeJsPublicApiTests.cs | Adds argument validation tests for new public APIs. |
| tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs | Adds publish-mode Dockerfile verification tests (static, node server, Next standalone). |
| tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs | Adds publish-mode Dockerfile verification tests for all new publish patterns. |
| tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddViteAppTests.VerifyDockerfileWhenPublishedAsStaticWebsite.verified.txt | Snapshot for static-website Dockerfile output. |
| tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddViteAppTests.VerifyDockerfileWhenPublishedAsStaticWebsiteWithCustomOutputPath.verified.txt | Snapshot for static-website Dockerfile with custom output path. |
| tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddViteAppTests.VerifyDockerfileWhenPublishedAsNodeServer.verified.txt | Snapshot for node-server Dockerfile output. |
| tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddViteAppTests.VerifyDockerfileWhenPublishedAsNextStandalone.verified.txt | Snapshot for Next.js standalone Dockerfile output. |
| tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs | New E2E test that initializes a TS AppHost, adds packages, deploys, and builds images. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/package.json | Fixture backend API package config. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/server.js | Fixture backend API implementation. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/package.json | Fixture static-site package config. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/index.html | Fixture static-site page calling /api/*. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/package.json | Fixture node-server package config. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/server.js | Fixture node-server implementation. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package.json | Fixture npm-script package config. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/server.js | Fixture npm-script server implementation. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/tsconfig.json | Next.js fixture TypeScript config. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/public/.gitkeep | Ensures public/ exists for Next.js standalone output. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package.json | Next.js fixture dependencies/scripts. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json | Next.js fixture lockfile for deterministic installs. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/next.config.ts | Enables output: 'standalone' for the Next.js fixture. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/app/page.tsx | Next.js fixture page. |
| tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/app/layout.tsx | Next.js fixture layout. |
| playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs | Demonstrates usage of the new publish methods in the playground AppHost. |
Files not reviewed (1)
- tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json: Language not supported
| // Example: SvelteKit with adapter-node builds to build/index.js, Nuxt/TanStack build to .output/server/index.mjs | ||
| // Uncomment the following if you add a SvelteKit or Nuxt app to this playground: | ||
| // builder.AddViteApp("sveltekit", "../SvelteKitApp") | ||
| // .PublishAsNodeServer(entryPoint: "build/index.js", outputPath: "build"); |
There was a problem hiding this comment.
I'm not sure I understand this scenario. Why doesn't this use AddNodeApp?
There was a problem hiding this comment.
Dev time uses the vite dev server, publish time uses a node based server.
| }) | ||
| .WithHttpEndpoint(env: "PORT") | ||
| .WithAnnotation(new JavaScriptPublishModeAnnotation(JavaScriptPublishMode.NextStandalone)) | ||
| .ClearContainerFilesSources() |
There was a problem hiding this comment.
Does this mean I can't use AddNextJsApp and pass it into server.PublishWithContainerFiles?
There was a problem hiding this comment.
That's right. Those do not compose, just like it doesnt work with addNodeApp. NextJS does support emitting a static site, but this doesn't cover that scenario.
There was a problem hiding this comment.
How are users supposed to know what they can use PublishWithContainerFiles and what they can't?
There was a problem hiding this comment.
My folllow ups are to:
- My first plan is to get rid of these pits of failure https://aspire.dev/deployment/javascript-apps/#pits-of-failure. That means throwing errors when you don't use PublishWithContainerFiles, or one of the above methods.
- Next I'll look into invalid combinations and see if we shoud constrain these methods to specific types (just vite and JavaScript?)
e3111a6 to
3e78970
Compare
JamesNK
left a comment
There was a problem hiding this comment.
Reviewed the JavaScript publish methods and AddNextJsApp additions. Found 5 issues:
- 1 definite bug in the E2E test (swapped nodeserver/npmscript assertions)
- 1 definite bug in
PublishAsStaticWebsiteCore(apiPath"/"produces a catch-all that shadows static files) - 1 likely issue with signal propagation in
NpmScriptcontainers (sh -cwithoutexec) - 2 design concerns in
ValidateNextJsStandaloneOutput(silent pass on missing config file; overly loose keyword match)
| /// </para> | ||
| /// </remarks> | ||
| [AspireExportIgnore(Reason = "Use the polyglot-compatible overload instead.")] | ||
| public static IResourceBuilder<TResource> PublishAsStaticWebsite<TResource>( |
There was a problem hiding this comment.
Did you want to make these [Experimental] for a release?
| [AspireExportIgnore(Reason = "Use the polyglot-compatible overload instead.")] | ||
| public static IResourceBuilder<TResource> PublishAsStaticWebsite<TResource>( | ||
| this IResourceBuilder<TResource> builder, | ||
| Action<PublishAsStaticWebsiteOptions>? configure = null) |
There was a problem hiding this comment.
Why a callback and not just take PublishAsStaticWebsiteOptions??
There was a problem hiding this comment.
Have you seen our APIs eric, we love callbacks! Seriously though its usually when the control could be deferred. This one is pretty simple though.
There was a problem hiding this comment.
Code Review Findings (High Severity)
Reviewed across the Dockerfile generation logic in JavaScriptHostingExtensions.cs. Five issues that could cause runtime failures for users:
1️⃣ glibc → musl ABI mismatch (build=slim, runtime=alpine)
Lines 678, 743, 757, 811
The build stage defaults to node:22-slim (Debian/glibc, line 678) but the runtime stages for NodeServer, NpmScript, and NextStandalone all default to node:22-alpine (musl). Apps with native Node addons (sharp, sqlite3, Prisma engines, etc.) will compile for glibc during the build and then crash on the musl-based runtime.
Consider aligning the runtime distro with the build distro (e.g. both slim or both alpine), or documenting this limitation.
2️⃣ PublishAsNpmScript with pnpm/bun: package manager missing in runtime image
Lines 806, 1390–1398
The NpmScript entrypoint can emit pnpm run start or bun run start, but:
- pnpm is only enabled via
corepack enable pnpmin the build/prod-deps stages (InitializeDockerBuildStageat line 1397) — it's never run in the runtime stage. - bun only sets
BuildImage = "oven/bun:1"but the runtime still defaults tonode:*-alpine.
The container will fail immediately on start because the package manager binary doesn't exist in the runtime image.
3️⃣ Yarn Berry (v2+) ProductionInstallArgs is broken
Line 1337
ProductionInstallArgs is hardcoded to "install --production" for all Yarn versions, but Yarn Berry (v2+) doesn't recognize the --production flag. The code already detects Berry vs v1 for regular install args in GetDefaultYarnInstallArgs() (lines 1345–1365) using --immutable for Berry and --frozen-lockfile for v1. Should this use the same detection to conditionally set the production args? For Berry, the typical approach is NODE_ENV=production yarn install (without --production).
4️⃣ sh -c entrypoint with unescaped user strings
Lines 796–806
startScriptName and runScriptArguments are string-concatenated into a raw sh -c "exec {runCommand}" entrypoint without any shell escaping. While the risk is low in practice (developers control these values), any shell metacharacters (;, &&, $(), etc.) in runScriptArguments would be interpreted by the shell. Consider using exec-form ENTRYPOINT without sh -c, or sanitizing/quoting the values.
5️⃣ devDependencies leak into NpmScript runtime image
Lines 803–804
Line 803 copies the entire /app from the build stage (including node_modules with dev deps), then line 804 overlays prod-only node_modules on top. However, Docker COPY merges directories — it doesn't delete files that only exist in the destination. Dev-only packages from the build stage survive into the final image.
Consider either excluding node_modules from the first copy (e.g., via .dockerignore in the build context or a selective copy), or adding RUN rm -rf node_modules before the prod-deps overlay.
Add PublishAsStaticWebsite, PublishAsNodeServer, PublishAsNpmScript, and AddNextJsApp to Aspire.Hosting.JavaScript. PublishAsStaticWebsite: - Uses YARP reverse proxy image for static file serving - Optional API reverse-proxy with service discovery (accepts resource, YARP resolves HTTPS/HTTP endpoints automatically) - Options pattern: OutputPath, StripPrefix, TargetEndpointName - Two C# overloads (no proxy / with proxy) + polyglot overload - Bridges PORT to ASPNETCORE_HTTP_PORTS for Kestrel PublishAsNodeServer: - Node.js runtime container running a self-contained build artifact - exec in entrypoint for proper SIGTERM forwarding PublishAsNpmScript: - Multi-stage Dockerfile with production node_modules - Package-manager aware (npm/yarn/pnpm/bun) - ProductionInstallArgs on JavaScriptInstallCommandAnnotation AddNextJsApp: - Standalone 3-COPY Dockerfile pattern - Deploy-time validation: checks next.config for 'standalone' - DisableBuildValidation() opt-out via pipeline prereq step All publish methods marked [Experimental]. E2E test verifies pipeline deploys and API responds correctly. Fixes #12697 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e66f347 to
b661852
Compare
- verify.sh is a checked-in fixture (no generation, no line ending issues) - Phase 1: captures docker state, all curl responses, ports, and container logs unconditionally to diagnostics/ - Phase 2: asserts expected content in captured responses - On failure, CaptureWorkspaceOnFailure uploads diagnostics/ with all raw responses for debugging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopyFixtures only copied subdirectories, missing verify.sh at the fixtures root. Now copies root files first, then subdirectories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…RP tags, publish-mode-only HOST - Use lockfile-aware install for prod-deps stage: ProductionInstallArgs is now just the flag (--omit=dev, --production, --prod), appended to the base install args. npm gets 'ci --omit=dev', pnpm gets 'install --frozen-lockfile --prod', yarn gets 'install --immutable --production'. (eerhardt feedback) - Source-share YarpContainerImageTags.cs via Compile Include instead of duplicating in JavaScriptContainerImageTags.cs. (eerhardt feedback) - Move HOST/HOSTNAME to publish-mode-only WithEnvironment — not needed in dev mode where apps run on the host, not in containers. Remove from Dockerfile ENV to avoid duplication. - Add IsPublishMode early-return guards to PublishAsNodeServer and PublishAsNpmScript, matching PublishAsStaticWebsite pattern. - AddNextJsApp: publish annotation + HOSTNAME conditional on publish mode, WithOtlpExporter stays in both modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🎬 CLI E2E Test Recordings — 55 recordings uploaded (commit View recordings
📹 Recordings uploaded automatically from CI run #23949654140 |
Description
Add JavaScript publish methods and
AddNextJsApptoAspire.Hosting.JavaScript, using YARP for static site serving.Publish methods (generic patterns)
PublishAsStaticWebsite— YARP reverse proxy serving built static files with optional API reverse-proxy via service discovery. AcceptsPublishAsStaticWebsiteOptionsforOutputPath,StripPrefix, andTargetEndpointName.PublishAsNodeServer— Node.js runtime container running a self-contained build artifact directly (SvelteKit, TanStack Start). SetsHOST=0.0.0.0andHOSTNAME=0.0.0.0for container networking.PublishAsNpmScript— Multi-stage Dockerfile with productionnode_modules, using package manager script as entrypoint withexecfor proper SIGTERM handling. SetsHOST=0.0.0.0andHOSTNAME=0.0.0.0.Framework-specific method
AddNextJsApp— Dedicated method for Next.js applications:next devwith correct port binding (-p, not Vite's--port)next.config.ts/js/mjsforoutput: "standalone"via pipeline prereq step. Opt out with.DisableBuildValidation().API design
PublishAsStaticWebsite: no proxy + with proxy (apiPath,apiTarget)Action<PublishAsStaticWebsiteOptions>?forOutputPath,StripPrefix(default:false),TargetEndpointNameapiTargetacceptsIResourceBuilder<IResourceWithServiceDiscovery>— YARP resolves endpoints via service discovery, preferring HTTPS[Experimental]ProductionInstallArgsmoved toJavaScriptInstallCommandAnnotationper review feedbackJavaScriptContainerImageTags.csfor YARP image tags (tooling-compatible)Key technical decisions
StripPrefixdefaults tofalse— forwards the full path to the backend. Opt in withstripPrefix: true.PublishAsStaticWebsiteto match YARP image default.HOST=0.0.0.0andHOSTNAME=0.0.0.0set viaWithEnvironment(not Dockerfile) onPublishAsNodeServerandPublishAsNpmScriptso containers bind to all interfaces.E2E Validation
All 10+ framework configurations deployed and verified locally with
aspire deployto Docker Compose:PublishAsStaticWebsite+ API proxyPublishAsStaticWebsite+ API proxyPublishAsStaticWebsite+ API proxyPublishAsStaticWebsitePublishAsNodeServerPublishAsNodeServerAddNextJsApp(standalone)PublishAsNpmScriptPublishAsNpmScriptPublishAsNpmScriptE2E test (
AllPublishMethodsBuildDockerImages) verifies pipeline deploys and services respond, with checked-inverify.shfor diagnostics.Fixes #12697
Checklist
<see>and<code/>elements on your triple slash comments?aspire.devissue: