Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only

var builder = DistributedApplication.CreateBuilder(args);

var weatherApi = builder.AddProject<Projects.AspireJavaScript_MinimalApi>("weatherapi")
Expand Down Expand Up @@ -37,6 +39,28 @@
.WithExternalHttpEndpoints();
#pragma warning restore ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

// Demonstrate the new publish methods:

// PublishAsStaticWebsite: deploys the Vite app as a static site served by YARP.
// With apiPath/apiTarget, YARP reverse-proxies /api/* to the weather API via service discovery — no CORS needed.
#pragma warning disable ASPIREEXTENSION001
builder.AddViteApp("vite-static", "../AspireJavaScript.Vite")
.WithExternalHttpEndpoints()
.PublishAsStaticWebsite("/api", weatherApi);
#pragma warning restore ASPIREEXTENSION001

// PublishAsNodeServer: for frameworks that produce a self-contained Node.js server artifact.
// 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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this scenario. Why doesn't this use AddNodeApp?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev time uses the vite dev server, publish time uses a node based server.


// PublishAsNpmScript: for frameworks that need node_modules at runtime.
// Example: Remix needs react-router-serve (an npm dependency), Nuxt needs the full Nitro environment.
// Uncomment the following if you add a Remix or Nuxt app to this playground:
// builder.AddViteApp("remix", "../RemixApp")
// .PublishAsNpmScript(startScriptName: "start", runScriptArguments: "-- --port $PORT");

builder.AddNodeApp("node", "../AspireJavaScript.NodeApp", "app.js")
.WithRunScript("dev") // Use 'npm run dev' for development
.WithHttpEndpoint(env: "PORT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<InternalsVisibleTo Include="Aspire.Hosting.JavaScript.Tests" />
</ItemGroup>

<ItemGroup>
<!-- Source-share YARP container image tags with Aspire.Hosting.Yarp -->
<Compile Include="..\Aspire.Hosting.Yarp\YarpContainerImageTags.cs" Link="YarpContainerImageTags.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>
Expand Down
669 changes: 657 additions & 12 deletions src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ public sealed class JavaScriptInstallCommandAnnotation(string[] args) : IResourc
/// Gets the command-line arguments supplied to the JavaScript package manager.
/// </summary>
public string[] Args { get; } = args;

/// <summary>
/// Gets or sets the additional arguments for installing production-only dependencies (excluding devDependencies).
/// This flag is appended to the base install command (from <see cref="Args"/>) when generating the
/// production dependencies stage in the Dockerfile. Each package manager sets its own flag
/// (e.g. npm uses <c>--omit=dev</c>, yarn uses <c>--production</c>, pnpm uses <c>--prod</c>).
/// </summary>
public string? ProductionInstallArgs { get; init; }
}
28 changes: 28 additions & 0 deletions src/Aspire.Hosting.JavaScript/JavaScriptPublishModeAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.JavaScript;

internal enum JavaScriptPublishMode
{
StaticWebsite,
NodeServer,
NpmScript,
NextStandalone
}

internal sealed class JavaScriptPublishModeAnnotation(JavaScriptPublishMode mode) : IResourceAnnotation
{
public JavaScriptPublishMode Mode { get; } = mode;

public string OutputPath { get; init; } = "dist";

// NodeServer properties
public string? EntryPoint { get; init; }

// NpmScript properties
public string? StartScriptName { get; init; }
public string? RunScriptArguments { get; init; }
}
14 changes: 14 additions & 0 deletions src/Aspire.Hosting.JavaScript/NextJsAppResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.JavaScript;

/// <summary>
/// Represents a Next.js application resource.
/// </summary>
/// <param name="name">The unique name used to identify the Next.js application resource.</param>
/// <param name="command">The command to execute the application.</param>
/// <param name="workingDirectory">The working directory from which the application command is executed.</param>
[AspireExport(ExposeProperties = true)]
public class NextJsAppResource(string name, string command, string workingDirectory)
: JavaScriptAppResource(name, command, workingDirectory);
32 changes: 32 additions & 0 deletions src/Aspire.Hosting.JavaScript/PublishAsStaticWebsiteOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.JavaScript;

/// <summary>
/// Options for configuring the static website publish mode.
/// </summary>
public class PublishAsStaticWebsiteOptions
{
/// <summary>
/// Gets or sets the relative path to the directory containing the built static files.
/// Defaults to <c>dist</c>. Some frameworks use a different output directory,
/// for example Angular uses <c>dist/browser</c>.
/// </summary>
public string OutputPath { get; set; } = "dist";

/// <summary>
/// Gets or sets whether to remove the API path prefix before forwarding to the backend.
/// For example, with <c>apiPath="/api"</c> and <c>StripPrefix=true</c>, a request to
/// <c>/api/weatherforecast</c> is forwarded as <c>/weatherforecast</c>.
/// Defaults to <see langword="true"/>.
/// </summary>
public bool StripPrefix { get; set; }

/// <summary>
/// Gets or sets the name of a specific endpoint on the API target resource to proxy to.
/// When <see langword="null"/>, YARP uses service discovery to resolve the appropriate endpoint,
/// preferring HTTPS when available.
/// </summary>
public string? TargetEndpointName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.JavaScript;

internal sealed class SuppressPublishValidationAnnotation : IResourceAnnotation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "api",
"private": true,
"scripts": {
"dev": "node server.js",
"build": "echo 'no build needed'",
"start": "node server.js"
}
}
10 changes: 10 additions & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const http = require('http');
const port = process.env.PORT || 3001;
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([
{ date: '2026-04-01', temperatureC: 22, summary: 'Warm' },
{ date: '2026-04-02', temperatureC: 18, summary: 'Cool' },
{ date: '2026-04-03', temperatureC: 30, summary: 'Hot' }
]));
}).listen(port, '0.0.0.0', () => console.log(`API on port ${port}`));
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <h1>Hello from Next.js</h1>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;
Loading
Loading