├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── Build.ps1
├── LICENSE
├── NuGet.config
├── Push.ps1
├── README.md
├── Tailwind.Extensions.AspNetCore.sln
├── Tailwind.Extensions.AspNetCore.sln.DotSettings.user
└── src
├── SampleApps
└── BlazorServer
│ ├── .dockerignore
│ ├── App.razor
│ ├── BlazorServer.csproj
│ ├── Dockerfile
│ ├── Pages
│ ├── Error.cshtml
│ ├── Error.cshtml.cs
│ ├── Index.razor
│ ├── _Host.cshtml
│ └── _Layout.cshtml
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Shared
│ ├── MainLayout.razor
│ ├── MainLayout.razor.css
│ ├── NavMenu.razor
│ ├── NavMenu.razor.css
│ └── SurveyPrompt.razor
│ ├── Styles
│ └── input.css
│ ├── _Imports.razor
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── tailwind.config.js
│ └── wwwroot
│ ├── css
│ └── open-iconic
│ │ ├── FONT-LICENSE
│ │ ├── ICON-LICENSE
│ │ ├── README.md
│ │ └── font
│ │ ├── css
│ │ └── open-iconic-bootstrap.min.css
│ │ └── fonts
│ │ ├── open-iconic.eot
│ │ ├── open-iconic.otf
│ │ ├── open-iconic.svg
│ │ ├── open-iconic.ttf
│ │ └── open-iconic.woff
│ └── favicon.ico
├── Tailwind.Extensions.AspNetCore.Tests
├── GlobalUsings.cs
├── Tailwind.Extensions.AspNetCore.Tests.csproj
└── TailwindCliHostedServiceTests.cs
└── Tailwind.Extensions.AspNetCore
├── Guard.cs
├── Npm
└── NodeScriptRunner.cs
├── Tailwind.Extensions.AspNetCore.csproj
├── TailwindHostedService.cs
├── TailwindMiddleware.cs
├── TailwindMiddlewareExtensions.cs
├── TailwindProcessInterop.cs
└── Util
├── EventedStreamReader.cs
├── EventedStreamStringReader.cs
├── LoggerFinder.cs
├── TaskTimeoutExtensions.cs
└── TcpPortFinder.cs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | - dev
9 | pull_request:
10 | branches:
11 | - master
12 | - main
13 | - dev
14 |
15 | jobs:
16 | build:
17 | runs-on: windows-latest
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v2
21 | with:
22 | fetch-depth: 0
23 | - name: Setup .NET 6
24 | uses: actions/setup-dotnet@v1
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Build
28 | run: ./Build.ps1
29 | shell: pwsh
30 | - name: Artifacts
31 | uses: actions/upload-artifact@v2
32 | with:
33 | name: artifacts
34 | path: artifacts/**/*
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | tags:
4 | - '*.*.*'
5 | jobs:
6 | build:
7 | runs-on: windows-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | with:
12 | fetch-depth: 0
13 | - name: Setup .NET 6
14 | uses: actions/setup-dotnet@v1
15 | with:
16 | dotnet-version: '6.0.x'
17 | - name: Build
18 | run: ./Build.ps1
19 | shell: pwsh
20 | - name: Push to NuGet
21 | env:
22 | NUGET_URL: https://api.nuget.org/v3/index.json
23 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
24 | run: ./Push.ps1
25 | shell: pwsh
26 | - name: Artifacts
27 | uses: actions/upload-artifact@v2
28 | with:
29 | name: artifacts
30 | path: artifacts/**/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | /packages/
4 | riderModule.iml
5 | /_ReSharper.Caches/
6 | /.idea/
7 | /artifacts
8 | **/node_modules/
9 | /src/SampleApps/BlazorServer/wwwroot/css/output.css
10 |
--------------------------------------------------------------------------------
/Build.ps1:
--------------------------------------------------------------------------------
1 | # Taken from MediatR.Extensions.DependencyInjection https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection/
2 |
3 | <#
4 | .SYNOPSIS
5 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode
6 | to see if an error occcured. If an error is detected then an exception is thrown.
7 | This function allows you to run command-line programs without having to
8 | explicitly check the $lastexitcode variable.
9 | .EXAMPLE
10 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed"
11 | #>
12 | function Exec
13 | {
14 | [CmdletBinding()]
15 | param(
16 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd,
17 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd)
18 | )
19 | & $cmd
20 | if ($lastexitcode -ne 0) {
21 | throw ("Exec: " + $errorMessage)
22 | }
23 | }
24 |
25 | $artifacts = ".\artifacts"
26 |
27 | if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse }
28 |
29 | exec { & dotnet clean -c Release }
30 |
31 | exec { & dotnet build -c Release }
32 |
33 | exec { & dotnet test -c Release -r $artifacts --no-build -l trx --verbosity=normal }
34 |
35 | exec { dotnet pack .\src\Tailwind.Extensions.AspNetCore -c Release -o $artifacts --no-build }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Jon Hilton
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Push.ps1:
--------------------------------------------------------------------------------
1 | # Taken from MediatR.Extensions.DependencyInjection https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection/
2 |
3 | $scriptName = $MyInvocation.MyCommand.Name
4 | $artifacts = "./artifacts"
5 |
6 | if ([string]::IsNullOrEmpty($Env:NUGET_API_KEY)) {
7 | Write-Host "${scriptName}: NUGET_API_KEY is empty or not set. Skipped pushing package(s)."
8 | } else {
9 | Get-ChildItem $artifacts -Filter "*.nupkg" | ForEach-Object {
10 | Write-Host "$($scriptName): Pushing $($_.Name)"
11 | dotnet nuget push $_ --source $Env:NUGET_URL --api-key $Env:NUGET_API_KEY
12 | if ($lastexitcode -ne 0) {
13 | throw ("Exec: " + $errorMessage)
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | _Disclaimer: This project is not affiliated with or supported by Tailwind Labs._
2 |
3 | # Use Tailwind's JIT mode with `dotnet run` and `dotnet watch run`
4 |
5 | Makes it possible to use the new "Just In Time" builds for Tailwind (Tailwind 3+) with ASP.NET Core.
6 |
7 | Works for Blazor WASM applications that are hosted via ASP.NET Core, and Blazor Server applications.
8 |
9 | Note it doesn't work with Blazor WASM apps that aren't hosted via ASP.NET Core.
10 |
11 | ## Usage
12 |
13 | Create a new Hosted Blazor WASM, or Blazor Server project.
14 |
15 | CD to the Client App's folder (if Blazor WASM) or Blazor Server App's folder.
16 |
17 | Run these commands:
18 | ``` powershell
19 | npm install -D tailwindcss cross-env
20 | npx tailwindcss init
21 | ```
22 |
23 | This will install Tailwind and the handy cross-env utility via NPM, then create a `tailwind.config.js` file.
24 |
25 | Now update the `tailwind.config.js` file to include all your .razor and .cshtml files.
26 |
27 | ``` javascript
28 | module.exports = {
29 | content: ["**/*.razor", "**/*.cshtml", "**/*.html"],
30 | theme: {
31 | extend: {},
32 | },
33 | plugins: [],
34 | }
35 | ```
36 |
37 | Now you'll want to create the Tailwind input stylesheet. This is the stylesheet that Tailwind will then pick up and build.
38 |
39 | Here's the minimum you'll need...
40 |
41 | **Styles\input.css**
42 |
43 | ``` css
44 | @tailwind base;
45 | @tailwind components;
46 | @tailwind utilities;
47 | ```
48 |
49 | Finally, update your `package.json` file to add this script.
50 |
51 | ``` json
52 | "scripts": {
53 | "tailwind": "cross-env NODE_ENV=development ./node_modules/tailwindcss/lib/cli.js -i ./Styles/input.css -o ./wwwroot/css/output.css --watch"
54 | },
55 | ```
56 |
57 | Make sure **.Styles/input.css** is pointing to the css file you created in the last step. You can control where the resulting css file is created by specifying your own value for the `-o` parameter.
58 |
59 | That takes care of the Tailwind setup, now we just need to make this run when you run your ASP.NET Core project during Development.
60 |
61 | Make sure you switch to the Server App's folder if you're using Blazor WASM (hosted).
62 |
63 | Run this command to install the Tailwind AspNetCore NuGet package.
64 |
65 | ``` powershell
66 | dotnet add package Tailwind.Extensions.AspNetCore --version 1.0.0-beta2
67 | ```
68 |
69 | Now head over to `Program.cs` and add this code before `app.Run()`;
70 |
71 | ``` csharp
72 | if (app.Environment.IsDevelopment())
73 | {
74 | _ = app.RunTailwind("tailwind", "./");
75 | }
76 | ```
77 |
78 | The second argument is the path to the folder containing your package.json file. If you're using Blazor WASM you'll probably need something like this...
79 |
80 | ``` csharp
81 | if (app.Environment.IsDevelopment())
82 | {
83 | _ = app.RunTailwind("tailwind", "../Client/");
84 | }
85 | ```
86 |
87 | You'll also need to add this `using` statement:
88 |
89 | ``` csharp
90 | using Tailwind;
91 | ```
92 |
93 | Note we're using the discard parameter `_` when we call `app.RunTailwind`. This is because the method is async, but we don't want to wait for it to complete (as this would cause your app to wait for it to finish, and we want it continue running in the background alongside your app). Using the `_` parameter here stops your IDE nagging at you to await the call 😀
94 |
95 | Now, run `dotnet watch run` and try modifying your Razor components (using Tailwind's utility classes).
96 |
97 | You should see logs indicating that tailwind has rebuilt the CSS stylesheet successfully.
98 |
--------------------------------------------------------------------------------
/Tailwind.Extensions.AspNetCore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tailwind.Extensions.AspNetCore", "src\Tailwind.Extensions.AspNetCore\Tailwind.Extensions.AspNetCore.csproj", "{3A29D45C-D8D5-4D52-8E4E-F295F3EF14C3}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorServer", "src\SampleApps\BlazorServer\BlazorServer.csproj", "{AA1AF6B6-6B5B-45B4-867C-9B04CAFDFB84}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tailwind.Extensions.AspNetCore.Tests", "src\Tailwind.Extensions.AspNetCore.Tests\Tailwind.Extensions.AspNetCore.Tests.csproj", "{FF0BEEBB-F708-4F4B-9839-A82AE71835C4}"
8 | EndProject
9 | Global
10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
11 | Debug|Any CPU = Debug|Any CPU
12 | Release|Any CPU = Release|Any CPU
13 | EndGlobalSection
14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
15 | {3A29D45C-D8D5-4D52-8E4E-F295F3EF14C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
16 | {3A29D45C-D8D5-4D52-8E4E-F295F3EF14C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
17 | {3A29D45C-D8D5-4D52-8E4E-F295F3EF14C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
18 | {3A29D45C-D8D5-4D52-8E4E-F295F3EF14C3}.Release|Any CPU.Build.0 = Release|Any CPU
19 | {AA1AF6B6-6B5B-45B4-867C-9B04CAFDFB84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {AA1AF6B6-6B5B-45B4-867C-9B04CAFDFB84}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {AA1AF6B6-6B5B-45B4-867C-9B04CAFDFB84}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {AA1AF6B6-6B5B-45B4-867C-9B04CAFDFB84}.Release|Any CPU.Build.0 = Release|Any CPU
23 | {FF0BEEBB-F708-4F4B-9839-A82AE71835C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {FF0BEEBB-F708-4F4B-9839-A82AE71835C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {FF0BEEBB-F708-4F4B-9839-A82AE71835C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {FF0BEEBB-F708-4F4B-9839-A82AE71835C4}.Release|Any CPU.Build.0 = Release|Any CPU
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/Tailwind.Extensions.AspNetCore.sln.DotSettings.user:
--------------------------------------------------------------------------------
1 |
2 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="MissingOptionsThrowsError" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
3 | <Project Location="C:\Users\hilto\RiderProjects\Tailwind.Extensions.AspNetCore\Tailwind.Extensions.AspNetCore.Tests" Presentation="<Tailwind.Extensions.AspNetCore.Tests>" />
4 | </SessionState>
5 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.dockerignore
2 | **/.env
3 | **/.git
4 | **/.gitignore
5 | **/.project
6 | **/.settings
7 | **/.toolstarget
8 | **/.vs
9 | **/.vscode
10 | **/.idea
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Not found
8 |
9 | Sorry, there's nothing at this address.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/BlazorServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | Linux
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
16 | <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
2 | WORKDIR /app
3 | EXPOSE 80
4 | EXPOSE 443
5 |
6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
7 | WORKDIR /src
8 | COPY ["src/SampleApps/BlazorServer/BlazorServer.csproj", "BlazorServer/"]
9 | RUN dotnet restore "src/SampleApps/BlazorServer/BlazorServer.csproj"
10 | COPY . .
11 | WORKDIR "/src/BlazorServer"
12 | RUN dotnet build "BlazorServer.csproj" -c Release -o /app/build
13 |
14 | FROM build AS publish
15 | RUN dotnet publish "BlazorServer.csproj" -c Release -o /app/publish
16 |
17 | FROM base AS final
18 | WORKDIR /app
19 | COPY --from=publish /app/publish .
20 | ENTRYPOINT ["dotnet", "BlazorServer.dll"]
21 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Pages/Error.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model BlazorServer.Pages.ErrorModel
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Error
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Error.
19 |
An error occurred while processing your request.
20 |
21 | @if (Model.ShowRequestId)
22 | {
23 |
24 | Request ID: @Model.RequestId
25 |
26 | }
27 |
28 |
Development Mode
29 |
30 | Swapping to the Development environment displays detailed information about the error that occurred.
31 |
32 |
33 | The Development environment shouldn't be enabled for deployed applications.
34 | It can result in displaying sensitive information from exceptions to end users.
35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
36 | and restarting the app.
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Pages/Error.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace BlazorServer.Pages;
6 |
7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
8 | [IgnoreAntiforgeryToken]
9 | public class ErrorModel : PageModel
10 | {
11 | public string? RequestId { get; set; }
12 |
13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
14 |
15 | private readonly ILogger _logger;
16 |
17 | public ErrorModel(ILogger logger)
18 | {
19 | _logger = logger;
20 | }
21 |
22 | public void OnGet()
23 | {
24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Pages/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 |
4 |
Hello World
5 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Pages/_Host.cshtml:
--------------------------------------------------------------------------------
1 | @page "/"
2 | @namespace BlazorServer.Pages
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 | @{
5 | Layout = "_Layout";
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Pages/_Layout.cshtml:
--------------------------------------------------------------------------------
1 | @using Microsoft.AspNetCore.Components.Web
2 | @namespace BlazorServer.Pages
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | @RenderBody()
17 |
18 |
19 |
20 | An error has occurred. This application may no longer respond until reloaded.
21 |
22 |
23 | An unhandled exception has occurred. See browser dev tools for details.
24 |
25 |
Reload
26 |
🗙
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Program.cs:
--------------------------------------------------------------------------------
1 | using Tailwind;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 |
5 | // Add services to the container.
6 | builder.Services.AddRazorPages();
7 | builder.Services.AddServerSideBlazor();
8 |
9 | // use this to automatically watch for css changes using Tailwind CLI
10 | // configure via appsettings "Tailwind"
11 | builder.UseTailwindCli();
12 |
13 | var app = builder.Build();
14 |
15 | // Configure the HTTP request pipeline.
16 | if (!app.Environment.IsDevelopment())
17 | {
18 | app.UseExceptionHandler("/Error");
19 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
20 | app.UseHsts();
21 | }
22 |
23 | app.UseHttpsRedirection();
24 |
25 | app.UseStaticFiles();
26 |
27 | app.UseRouting();
28 |
29 | app.MapBlazorHub();
30 | app.MapFallbackToPage("/_Host");
31 |
32 | if (app.Environment.IsDevelopment())
33 | {
34 | // app.RunTailwind("tailwind", "./");
35 | }
36 |
37 | app.Run();
38 |
39 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:17460",
7 | "sslPort": 44325
8 | }
9 | },
10 | "profiles": {
11 | "BlazorServer": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:7076;http://localhost:5076",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "launchBrowser": true,
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Shared/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
3 | Blazor Server Tailwind Demo
4 |
5 |
6 |
7 | @Body
8 |
9 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Shared/MainLayout.razor.css:
--------------------------------------------------------------------------------
1 | .page {
2 | position: relative;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | main {
8 | flex: 1;
9 | }
10 |
11 | .sidebar {
12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
13 | }
14 |
15 | .top-row {
16 | background-color: #f7f7f7;
17 | border-bottom: 1px solid #d6d5d5;
18 | justify-content: flex-end;
19 | height: 3.5rem;
20 | display: flex;
21 | align-items: center;
22 | }
23 |
24 | .top-row ::deep a, .top-row .btn-link {
25 | white-space: nowrap;
26 | margin-left: 1.5rem;
27 | }
28 |
29 | .top-row a:first-child {
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | }
33 |
34 | @media (max-width: 640.98px) {
35 | .top-row:not(.auth) {
36 | display: none;
37 | }
38 |
39 | .top-row.auth {
40 | justify-content: space-between;
41 | }
42 |
43 | .top-row a, .top-row .btn-link {
44 | margin-left: 0;
45 | }
46 | }
47 |
48 | @media (min-width: 641px) {
49 | .page {
50 | flex-direction: row;
51 | }
52 |
53 | .sidebar {
54 | width: 250px;
55 | height: 100vh;
56 | position: sticky;
57 | top: 0;
58 | }
59 |
60 | .top-row {
61 | position: sticky;
62 | top: 0;
63 | z-index: 1;
64 | }
65 |
66 | .top-row, article {
67 | padding-left: 2rem !important;
68 | padding-right: 1.5rem !important;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Shared/NavMenu.razor:
--------------------------------------------------------------------------------
1 |
9 |
10 |
29 |
30 | @code {
31 | private bool collapseNavMenu = true;
32 |
33 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
34 |
35 | private void ToggleNavMenu()
36 | {
37 | collapseNavMenu = !collapseNavMenu;
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Shared/NavMenu.razor.css:
--------------------------------------------------------------------------------
1 | .navbar-toggler {
2 | background-color: rgba(255, 255, 255, 0.1);
3 | }
4 |
5 | .top-row {
6 | height: 3.5rem;
7 | background-color: rgba(0,0,0,0.4);
8 | }
9 |
10 | .navbar-brand {
11 | font-size: 1.1rem;
12 | }
13 |
14 | .oi {
15 | width: 2rem;
16 | font-size: 1.1rem;
17 | vertical-align: text-top;
18 | top: -2px;
19 | }
20 |
21 | .nav-item {
22 | font-size: 0.9rem;
23 | padding-bottom: 0.5rem;
24 | }
25 |
26 | .nav-item:first-of-type {
27 | padding-top: 1rem;
28 | }
29 |
30 | .nav-item:last-of-type {
31 | padding-bottom: 1rem;
32 | }
33 |
34 | .nav-item ::deep a {
35 | color: #d7d7d7;
36 | border-radius: 4px;
37 | height: 3rem;
38 | display: flex;
39 | align-items: center;
40 | line-height: 3rem;
41 | }
42 |
43 | .nav-item ::deep a.active {
44 | background-color: rgba(255,255,255,0.25);
45 | color: white;
46 | }
47 |
48 | .nav-item ::deep a:hover {
49 | background-color: rgba(255,255,255,0.1);
50 | color: white;
51 | }
52 |
53 | @media (min-width: 641px) {
54 | .navbar-toggler {
55 | display: none;
56 | }
57 |
58 | .collapse {
59 | /* Never collapse the sidebar for wide screens */
60 | display: block;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Shared/SurveyPrompt.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
@Title
4 |
5 |
6 | Please take our
7 | brief survey
8 |
9 | and tell us what you think.
10 |
11 |
12 | @code {
13 | // Demonstrates how a parent component can supply parameters
14 | [Parameter]
15 | public string? Title { get; set; }
16 |
17 | }
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/Styles/input.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .valid.modified:not([type=checkbox]) {
6 | outline: 1px solid #26b050;
7 | }
8 |
9 | .invalid {
10 | outline: 1px solid red;
11 | }
12 |
13 | .validation-message {
14 | color: red;
15 | }
16 |
17 | #blazor-error-ui {
18 | background: lightyellow;
19 | bottom: 0;
20 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
21 | display: none;
22 | left: 0;
23 | padding: 0.6rem 1.25rem 0.7rem 1.25rem;
24 | position: fixed;
25 | width: 100%;
26 | z-index: 1000;
27 | }
28 |
29 | #blazor-error-ui .dismiss {
30 | cursor: pointer;
31 | position: absolute;
32 | right: 0.75rem;
33 | top: 0.5rem;
34 | }
35 |
36 | .blazor-error-boundary {
37 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
38 | padding: 1rem 1rem 1rem 3.7rem;
39 | color: white;
40 | }
41 |
42 | .blazor-error-boundary::after {
43 | content: "An error has occurred."
44 | }
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using Microsoft.AspNetCore.Authorization
3 | @using Microsoft.AspNetCore.Components.Authorization
4 | @using Microsoft.AspNetCore.Components.Forms
5 | @using Microsoft.AspNetCore.Components.Routing
6 | @using Microsoft.AspNetCore.Components.Web
7 | @using Microsoft.AspNetCore.Components.Web.Virtualization
8 | @using Microsoft.JSInterop
9 | @using BlazorServer
10 | @using BlazorServer.Shared
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "DetailedErrors": true,
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Tailwind": {
10 | "InputFile": "./Styles/input.css",
11 | "OutputFile": "./wwwroot/css/output.css",
12 | "TailwindCliPath": ""
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["**/*.razor", "**/*.cshtml", "**/*.html"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | }
8 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/FONT-LICENSE:
--------------------------------------------------------------------------------
1 | SIL OPEN FONT LICENSE Version 1.1
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | PREAMBLE
6 | The goals of the Open Font License (OFL) are to stimulate worldwide
7 | development of collaborative font projects, to support the font creation
8 | efforts of academic and linguistic communities, and to provide a free and
9 | open framework in which fonts may be shared and improved in partnership
10 | with others.
11 |
12 | The OFL allows the licensed fonts to be used, studied, modified and
13 | redistributed freely as long as they are not sold by themselves. The
14 | fonts, including any derivative works, can be bundled, embedded,
15 | redistributed and/or sold with any software provided that any reserved
16 | names are not used by derivative works. The fonts and derivatives,
17 | however, cannot be released under any other type of license. The
18 | requirement for fonts to remain under this license does not apply
19 | to any document created using the fonts or their derivatives.
20 |
21 | DEFINITIONS
22 | "Font Software" refers to the set of files released by the Copyright
23 | Holder(s) under this license and clearly marked as such. This may
24 | include source files, build scripts and documentation.
25 |
26 | "Reserved Font Name" refers to any names specified as such after the
27 | copyright statement(s).
28 |
29 | "Original Version" refers to the collection of Font Software components as
30 | distributed by the Copyright Holder(s).
31 |
32 | "Modified Version" refers to any derivative made by adding to, deleting,
33 | or substituting -- in part or in whole -- any of the components of the
34 | Original Version, by changing formats or by porting the Font Software to a
35 | new environment.
36 |
37 | "Author" refers to any designer, engineer, programmer, technical
38 | writer or other person who contributed to the Font Software.
39 |
40 | PERMISSION & CONDITIONS
41 | Permission is hereby granted, free of charge, to any person obtaining
42 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
43 | redistribute, and sell modified and unmodified copies of the Font
44 | Software, subject to the following conditions:
45 |
46 | 1) Neither the Font Software nor any of its individual components,
47 | in Original or Modified Versions, may be sold by itself.
48 |
49 | 2) Original or Modified Versions of the Font Software may be bundled,
50 | redistributed and/or sold with any software, provided that each copy
51 | contains the above copyright notice and this license. These can be
52 | included either as stand-alone text files, human-readable headers or
53 | in the appropriate machine-readable metadata fields within text or
54 | binary files as long as those fields can be easily viewed by the user.
55 |
56 | 3) No Modified Version of the Font Software may use the Reserved Font
57 | Name(s) unless explicit written permission is granted by the corresponding
58 | Copyright Holder. This restriction only applies to the primary font name as
59 | presented to the users.
60 |
61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
62 | Software shall not be used to promote, endorse or advertise any
63 | Modified Version, except to acknowledge the contribution(s) of the
64 | Copyright Holder(s) and the Author(s) or with their explicit written
65 | permission.
66 |
67 | 5) The Font Software, modified or unmodified, in part or in whole,
68 | must be distributed entirely under this license, and must not be
69 | distributed under any other license. The requirement for fonts to
70 | remain under this license does not apply to any document created
71 | using the Font Software.
72 |
73 | TERMINATION
74 | This license becomes null and void if any of the above conditions are
75 | not met.
76 |
77 | DISCLAIMER
78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
86 | OTHER DEALINGS IN THE FONT SOFTWARE.
87 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/ICON-LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/README.md:
--------------------------------------------------------------------------------
1 | [Open Iconic v1.1.1](http://useiconic.com/open)
2 | ===========
3 |
4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons)
5 |
6 |
7 |
8 | ## What's in Open Iconic?
9 |
10 | * 223 icons designed to be legible down to 8 pixels
11 | * Super-light SVG files - 61.8 for the entire set
12 | * SVG sprite—the modern replacement for icon fonts
13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats
14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats
15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px.
16 |
17 |
18 | ## Getting Started
19 |
20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections.
21 |
22 | ### General Usage
23 |
24 | #### Using Open Iconic's SVGs
25 |
26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute).
27 |
28 | ```
29 |
30 | ```
31 |
32 | #### Using Open Iconic's SVG Sprite
33 |
34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack.
35 |
36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.*
37 |
38 | ```
39 |
40 |
41 |
42 | ```
43 |
44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions.
45 |
46 | ```
47 | .icon {
48 | width: 16px;
49 | height: 16px;
50 | }
51 | ```
52 |
53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag.
54 |
55 | ```
56 | .icon-account-login {
57 | fill: #f00;
58 | }
59 | ```
60 |
61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/).
62 |
63 | #### Using Open Iconic's Icon Font...
64 |
65 |
66 | ##### …with Bootstrap
67 |
68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}`
69 |
70 |
71 | ```
72 |
73 | ```
74 |
75 |
76 | ```
77 |
78 | ```
79 |
80 | ##### …with Foundation
81 |
82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}`
83 |
84 | ```
85 |
86 | ```
87 |
88 |
89 | ```
90 |
91 | ```
92 |
93 | ##### …on its own
94 |
95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}`
96 |
97 | ```
98 |
99 | ```
100 |
101 | ```
102 |
103 | ```
104 |
105 |
106 | ## License
107 |
108 | ### Icons
109 |
110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT).
111 |
112 | ### Fonts
113 |
114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web).
115 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'}
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/0f01d2bfe18ebe6fa32bc16e0b525e7755bc00d6/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/0f01d2bfe18ebe6fa32bc16e0b525e7755bc00d6/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014
9 | By P.J. Onori
10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net)
11 |
12 |
13 |
14 |
27 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
45 |
47 |
49 |
51 |
53 |
55 |
57 |
59 |
61 |
63 |
65 |
67 |
69 |
71 |
74 |
76 |
79 |
81 |
84 |
86 |
88 |
91 |
93 |
95 |
98 |
100 |
102 |
104 |
106 |
109 |
112 |
115 |
117 |
121 |
123 |
125 |
127 |
130 |
132 |
134 |
136 |
138 |
141 |
143 |
145 |
147 |
149 |
151 |
153 |
155 |
157 |
159 |
162 |
165 |
167 |
169 |
172 |
174 |
177 |
179 |
181 |
183 |
185 |
189 |
191 |
194 |
196 |
198 |
200 |
202 |
205 |
207 |
209 |
211 |
213 |
215 |
218 |
220 |
222 |
224 |
226 |
228 |
230 |
232 |
234 |
236 |
238 |
241 |
243 |
245 |
247 |
249 |
251 |
253 |
256 |
259 |
261 |
263 |
265 |
267 |
269 |
272 |
274 |
276 |
280 |
282 |
285 |
287 |
289 |
292 |
295 |
298 |
300 |
302 |
304 |
306 |
309 |
312 |
314 |
316 |
318 |
320 |
322 |
324 |
326 |
330 |
334 |
338 |
340 |
343 |
345 |
347 |
349 |
351 |
353 |
355 |
358 |
360 |
363 |
365 |
367 |
369 |
371 |
373 |
375 |
377 |
379 |
381 |
383 |
386 |
388 |
390 |
392 |
394 |
396 |
399 |
401 |
404 |
406 |
408 |
410 |
412 |
414 |
416 |
419 |
421 |
423 |
425 |
428 |
431 |
435 |
438 |
440 |
442 |
444 |
446 |
448 |
451 |
453 |
455 |
457 |
460 |
462 |
464 |
466 |
468 |
471 |
473 |
477 |
479 |
481 |
483 |
486 |
488 |
490 |
492 |
494 |
496 |
499 |
501 |
504 |
506 |
509 |
512 |
515 |
517 |
520 |
522 |
524 |
526 |
529 |
532 |
534 |
536 |
539 |
542 |
543 |
544 |
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/0f01d2bfe18ebe6fa32bc16e0b525e7755bc00d6/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/0f01d2bfe18ebe6fa32bc16e0b525e7755bc00d6/src/SampleApps/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
--------------------------------------------------------------------------------
/src/SampleApps/BlazorServer/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/0f01d2bfe18ebe6fa32bc16e0b525e7755bc00d6/src/SampleApps/BlazorServer/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore.Tests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore.Tests/Tailwind.Extensions.AspNetCore.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 | all
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore.Tests/TailwindCliHostedServiceTests.cs:
--------------------------------------------------------------------------------
1 | using FakeItEasy;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Options;
5 |
6 | namespace Tailwind.Extensions.AspNetCore.Tests;
7 |
8 | public class TailwindCliHostedServiceTests
9 | {
10 | [Fact]
11 | public async Task MissingOptionsShouldThrowException()
12 | {
13 | var tailwindOptions = new TailwindOptions
14 | {
15 | InputFile = "", // empty InputFile
16 | OutputFile = "", // empty OutputFile
17 | TailwindCliPath = null // null TailwindCliPath
18 | };
19 | var options = Options.Create(tailwindOptions);
20 | var tailwindProcess = A.Fake();
21 | var hostEnvironment = A.Fake();
22 | var logger = A.Fake>();
23 |
24 | A.CallTo(() => hostEnvironment.EnvironmentName).Returns(Environments.Development);
25 | var tailwindHostedService = new TailwindHostedService(options, hostEnvironment, tailwindProcess, logger);
26 |
27 | await Assert.ThrowsAsync(async () =>
28 | await tailwindHostedService.StartAsync(CancellationToken.None));
29 | }
30 |
31 | [Theory]
32 | [InlineData(null, "tailwind")]
33 | [InlineData("", "tailwind")]
34 | [InlineData("tailwindoverride.exe", "tailwindoverride.exe")]
35 | [InlineData("tailwindoverride", "tailwindoverride")]
36 | public async Task SpecifiedTailwindCliPathOverridesDefault(string tailwindCliPath, string expectedCliPath)
37 | {
38 | var tailwindOptions = new TailwindOptions
39 | {
40 | InputFile = "input.css", // empty InputFile
41 | OutputFile = "output.css", // empty OutputFile
42 | TailwindCliPath = tailwindCliPath // null TailwindCliPath
43 | };
44 | var options = Options.Create(tailwindOptions);
45 | var tailwindProcess = A.Fake();
46 | var hostEnvironment = A.Fake();
47 | var logger = A.Fake>();
48 |
49 | A.CallTo(() => hostEnvironment.EnvironmentName).Returns(Environments.Development);
50 |
51 | var tailwindHostedService = new TailwindHostedService(options, hostEnvironment, tailwindProcess, logger);
52 | await tailwindHostedService.StartAsync(CancellationToken.None);
53 |
54 | A.CallTo(() => tailwindProcess.StartProcess(expectedCliPath,A.Ignored)).MustHaveHappened();
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Guard.cs:
--------------------------------------------------------------------------------
1 | namespace Tailwind;
2 |
3 | public static class Guard
4 | {
5 | public static void AgainstNull(string? parameterName, string? errorMessage = "")
6 | {
7 | if (string.IsNullOrEmpty(parameterName))
8 | {
9 | throw new ArgumentNullException($"{parameterName} must not be null or white space. ${errorMessage}");
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Npm/NodeScriptRunner.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Diagnostics;
5 | using System.Text.RegularExpressions;
6 | using Microsoft.AspNetCore.NodeServices.Util;
7 | using Microsoft.Extensions.Logging;
8 |
9 | // This is under the NodeServices namespace because post 2.1 it will be moved to that package
10 | namespace Microsoft.AspNetCore.NodeServices.Npm;
11 |
12 | ///
13 | /// Executes the script entries defined in a package.json file,
14 | /// capturing any output written to stdio.
15 | ///
16 | internal class NodeScriptRunner : IDisposable
17 | {
18 | private Process? _npmProcess;
19 | public EventedStreamReader StdOut { get; }
20 | public EventedStreamReader StdErr { get; }
21 |
22 | private static readonly Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
23 |
24 | public NodeScriptRunner(string workingDirectory, string scriptName, string? arguments, IDictionary? envVars, string pkgManagerCommand, DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken)
25 | {
26 | if (string.IsNullOrEmpty(workingDirectory))
27 | {
28 | throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
29 | }
30 |
31 | if (string.IsNullOrEmpty(scriptName))
32 | {
33 | throw new ArgumentException("Cannot be null or empty.", nameof(scriptName));
34 | }
35 |
36 | if (string.IsNullOrEmpty(pkgManagerCommand))
37 | {
38 | throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerCommand));
39 | }
40 |
41 | var exeToRun = pkgManagerCommand;
42 | var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}";
43 | if (OperatingSystem.IsWindows())
44 | {
45 | // On Windows, the node executable is a .cmd file, so it can't be executed
46 | // directly (except with UseShellExecute=true, but that's no good, because
47 | // it prevents capturing stdio). So we need to invoke it via "cmd /c".
48 | exeToRun = "cmd";
49 | completeArguments = $"/c {pkgManagerCommand} {completeArguments}";
50 | }
51 |
52 | var processStartInfo = new ProcessStartInfo(exeToRun)
53 | {
54 | Arguments = completeArguments,
55 | UseShellExecute = false,
56 | RedirectStandardInput = true,
57 | RedirectStandardOutput = true,
58 | RedirectStandardError = true,
59 | WorkingDirectory = workingDirectory
60 | };
61 |
62 | if (envVars != null)
63 | {
64 | foreach (var keyValuePair in envVars)
65 | {
66 | processStartInfo.Environment[keyValuePair.Key] = keyValuePair.Value;
67 | }
68 | }
69 |
70 | _npmProcess = LaunchNodeProcess(processStartInfo, pkgManagerCommand);
71 | StdOut = new EventedStreamReader(_npmProcess.StandardOutput);
72 | StdErr = new EventedStreamReader(_npmProcess.StandardError);
73 |
74 | applicationStoppingToken.Register(((IDisposable)this).Dispose);
75 |
76 | if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.NodeServices.Npm.NpmStarted"))
77 | {
78 | diagnosticSource.Write(
79 | "Microsoft.AspNetCore.NodeServices.Npm.NpmStarted",
80 | new
81 | {
82 | processStartInfo = processStartInfo,
83 | process = _npmProcess
84 | });
85 | }
86 | }
87 |
88 | public void AttachToLogger(ILogger logger, bool treatErrorsAsInfo = false)
89 | {
90 | // When the node task emits complete lines, pass them through to the real logger
91 | StdOut.OnReceivedLine += line =>
92 | {
93 | if (!string.IsNullOrWhiteSpace(line))
94 | {
95 | // Node tasks commonly emit ANSI colors, but it wouldn't make sense to forward
96 | // those to loggers (because a logger isn't necessarily any kind of terminal)
97 | logger.LogInformation(StripAnsiColors(line));
98 | }
99 | };
100 |
101 | StdErr.OnReceivedLine += line =>
102 | {
103 | if (!string.IsNullOrWhiteSpace(line))
104 | {
105 | if(treatErrorsAsInfo)
106 | logger.LogInformation(StripAnsiColors(line));
107 | else
108 | logger.LogError(StripAnsiColors(line));
109 | }
110 | };
111 |
112 | // But when it emits incomplete lines, assume this is progress information and
113 | // hence just pass it through to StdOut regardless of logger config.
114 | StdErr.OnReceivedChunk += chunk =>
115 | {
116 | Debug.Assert(chunk.Array != null);
117 |
118 | var containsNewline = Array.IndexOf(
119 | chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
120 | if (!containsNewline)
121 | {
122 | Console.Write(chunk.Array, chunk.Offset, chunk.Count);
123 | }
124 | };
125 | }
126 |
127 | private static string StripAnsiColors(string line)
128 | => AnsiColorRegex.Replace(line, string.Empty);
129 |
130 | private static Process LaunchNodeProcess(ProcessStartInfo startInfo, string commandName)
131 | {
132 | try
133 | {
134 | var process = Process.Start(startInfo)!;
135 |
136 | // See equivalent comment in OutOfProcessNodeInstance.cs for why
137 | process.EnableRaisingEvents = true;
138 |
139 | return process;
140 | }
141 | catch (Exception ex)
142 | {
143 | var message = $"Failed to start '{commandName}'. To resolve this:.\n\n"
144 | + $"[1] Ensure that '{commandName}' is installed and can be found in one of the PATH directories.\n"
145 | + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
146 | + " Make sure the executable is in one of those directories, or update your PATH.\n\n"
147 | + "[2] See the InnerException for further details of the cause.";
148 | throw new InvalidOperationException(message, ex);
149 | }
150 | }
151 |
152 | void IDisposable.Dispose()
153 | {
154 | if (_npmProcess != null && !_npmProcess.HasExited)
155 | {
156 | _npmProcess.Kill(entireProcessTree: true);
157 | _npmProcess = null;
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Tailwind.Extensions.AspNetCore.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | Tailwind extensions for ASP.NET Core
8 | Tailwind
9 | Tailwind.Extensions.AspNetCore
10 | Tailwind.Extensions.AspNetCore
11 | tailwind;devtools;
12 | latest
13 | v
14 | https://github.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore
15 | https://github.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore/blob/main/LICENSE
16 | https://github.com/Practical-ASP-NET/Tailwind.Extensions.AspNetCore
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/TailwindHostedService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 |
7 | namespace Tailwind;
8 |
9 | public class TailwindOptions
10 | {
11 | public string? InputFile { get; set; }
12 | public string? OutputFile { get; set; }
13 | public string? TailwindCliPath { get; set; }
14 | }
15 |
16 | ///
17 | /// Launches the Tailwind CLI in watch mode (if the CLI is installed and exists in the system Path)
18 | /// Runs as a background service (will be shut down when the app is stopped)
19 | /// Only works in development environments
20 | ///
21 | public class TailwindHostedService : IHostedService, IDisposable
22 | {
23 | private Process? _process;
24 | private readonly IHostEnvironment _hostEnvironment;
25 | private readonly TailwindOptions _options;
26 | private readonly ITailwindProcessInterop _tailwindProcess;
27 | private readonly ILogger _logger;
28 |
29 | public TailwindHostedService(IOptions options, IHostEnvironment hostEnvironment,
30 | ITailwindProcessInterop tailwindProcess, ILogger logger)
31 | {
32 | _logger = logger;
33 | _tailwindProcess = tailwindProcess;
34 | _options = options.Value;
35 | _hostEnvironment = hostEnvironment;
36 | }
37 |
38 | public Task StartAsync(CancellationToken cancellationToken)
39 | {
40 | // don't want this running in production, only useful for development time generation of the Tailwind file
41 | // use another mechanism such as CI/CD to generate the production-ready CSS style sheet
42 | if (!_hostEnvironment.IsDevelopment())
43 | return Task.CompletedTask;
44 |
45 |
46 | var input = _options.InputFile;
47 | var output = _options.OutputFile;
48 |
49 | Guard.AgainstNull(input, "check Tailwind configuration");
50 | Guard.AgainstNull(output, "check Tailwind configuration");
51 |
52 | _logger.LogInformation($"tailwind -i {input} -o {output} --watch");
53 |
54 | var processName = string.IsNullOrEmpty(_options.TailwindCliPath) ? "tailwind" : _options.TailwindCliPath;
55 | _process = _tailwindProcess.StartProcess(processName, $"-i {input} -o {output} --watch");
56 |
57 | return Task.CompletedTask;
58 | }
59 |
60 | public Task StopAsync(CancellationToken cancellationToken)
61 | {
62 | _process?.Kill();
63 | return Task.CompletedTask;
64 | }
65 |
66 | public void Dispose()
67 | {
68 | _process?.Dispose();
69 | }
70 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/TailwindMiddleware.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Diagnostics;
5 | using System.Text.RegularExpressions;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.NodeServices.Npm;
8 | using Microsoft.AspNetCore.NodeServices.Util;
9 | using Microsoft.AspNetCore.SpaServices.Util;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 |
15 | namespace Tailwind;
16 |
17 | internal static class TailwindMiddleware
18 | {
19 | private const string LogCategoryName = "NodeServices";
20 |
21 | private static readonly TimeSpan
22 | RegexMatchTimeout =
23 | TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
24 |
25 | public static async Task Attach(IApplicationBuilder appBuilder, string scriptName, string workingDir)
26 | {
27 | var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService()
28 | .ApplicationStopping;
29 | var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
30 | var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService();
31 | await ExecuteScript(workingDir, scriptName, logger, diagnosticSource, applicationStoppingToken);
32 | }
33 |
34 | private static async Task ExecuteScript(
35 | string sourcePath, string scriptName, ILogger logger,
36 | DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken)
37 | {
38 | var envVars = new Dictionary() { };
39 |
40 | var scriptRunner = new NodeScriptRunner(
41 | sourcePath, scriptName, null, envVars, "npm", diagnosticSource, applicationStoppingToken);
42 | scriptRunner.AttachToLogger(logger, true);
43 |
44 | using var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr);
45 | try
46 | {
47 | await scriptRunner.StdOut.WaitForMatch(
48 | new Regex("Done in", RegexOptions.None, RegexMatchTimeout));
49 | }
50 | catch (EndOfStreamException ex)
51 | {
52 | throw new InvalidOperationException(
53 | $"The npm script '{scriptName}' exited without indicating that the " +
54 | "Tailwind CLI had finished. The error output was: " +
55 | $"{stdErrReader.ReadAsString()}", ex);
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/TailwindMiddlewareExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace Tailwind;
5 |
6 | ///
7 | /// Extension methods for enabling tailwind middleware support.
8 | ///
9 | public static class TailwindMiddlewareExtensions
10 | {
11 | ///
12 | /// Automatically runs an npm script
13 | /// This feature should probably only be used in development.
14 | ///
15 | /// The .
16 | /// The name of the script in your package.json file that launches Tailwind.
17 | /// The directory where your package.json file resides
18 | public static Task RunTailwind(
19 | this IApplicationBuilder applicationBuilder,
20 | string npmScript, string workingDir = "./")
21 | {
22 | if (applicationBuilder == null)
23 | {
24 | throw new ArgumentNullException(nameof(applicationBuilder));
25 | }
26 |
27 | return TailwindMiddleware.Attach(applicationBuilder, npmScript, workingDir);
28 | }
29 |
30 | ///
31 | /// Run the Tailwind Cli in watch mode in the background (Development environments only
32 | ///
33 | ///
34 | public static void UseTailwindCli(this WebApplicationBuilder webApplicationBuilder)
35 | {
36 | webApplicationBuilder.Services.Configure(webApplicationBuilder.Configuration.GetSection("Tailwind"));
37 | webApplicationBuilder.Services.AddSingleton();
38 | webApplicationBuilder.Services.AddHostedService();
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/TailwindProcessInterop.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace Tailwind;
5 |
6 | public interface ITailwindProcessInterop
7 | {
8 | Process? StartProcess(string command, string arguments);
9 | }
10 |
11 | public class TailwindProcessInterop : ITailwindProcessInterop
12 | {
13 | public Process? StartProcess(string command, string arguments)
14 | {
15 | string cmdName;
16 | if (!command.EndsWith(".exe") && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
17 | {
18 | cmdName = $"{command}.exe";
19 | }
20 | else
21 | cmdName = command;
22 |
23 | try
24 | {
25 | var startInfo = new ProcessStartInfo
26 | {
27 | FileName = cmdName,
28 | Arguments = arguments,
29 | RedirectStandardOutput = true,
30 | RedirectStandardError = true,
31 | UseShellExecute = false,
32 | CreateNoWindow = true
33 | };
34 |
35 | var process = Process.Start(startInfo);
36 |
37 | if (process == null)
38 | {
39 | Console.WriteLine($"Could not start process: {cmdName}");
40 | return null;
41 | }
42 |
43 | process.OutputDataReceived += (sender, data) =>
44 | {
45 | if (!string.IsNullOrEmpty(data.Data))
46 | {
47 | Console.WriteLine("tailwind: " + data.Data);
48 | }
49 | };
50 |
51 | process.ErrorDataReceived += (sender, data) =>
52 | {
53 | if (!string.IsNullOrEmpty(data.Data))
54 | {
55 | Console.WriteLine("tailwind: " +data.Data);
56 | }
57 | };
58 |
59 | process.BeginOutputReadLine();
60 | process.BeginErrorReadLine();
61 |
62 | return process;
63 | }
64 | catch (Exception ex)
65 | {
66 | Console.WriteLine($"Error starting process: {ex.Message}");
67 | throw;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Util/EventedStreamReader.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace Microsoft.AspNetCore.NodeServices.Util;
8 |
9 | ///
10 | /// Wraps a to expose an evented API, issuing notifications
11 | /// when the stream emits partial lines, completed lines, or finally closes.
12 | ///
13 | internal class EventedStreamReader
14 | {
15 | public delegate void OnReceivedChunkHandler(ArraySegment chunk);
16 | public delegate void OnReceivedLineHandler(string line);
17 | public delegate void OnStreamClosedHandler();
18 |
19 | public event OnReceivedChunkHandler? OnReceivedChunk;
20 | public event OnReceivedLineHandler? OnReceivedLine;
21 | public event OnStreamClosedHandler? OnStreamClosed;
22 |
23 | private readonly StreamReader _streamReader;
24 | private readonly StringBuilder _linesBuffer;
25 |
26 | public EventedStreamReader(StreamReader streamReader)
27 | {
28 | _streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader));
29 | _linesBuffer = new StringBuilder();
30 | Task.Factory.StartNew(Run, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
31 | }
32 |
33 | public Task WaitForMatch(Regex regex)
34 | {
35 | var tcs = new TaskCompletionSource();
36 | var completionLock = new object();
37 |
38 | OnReceivedLineHandler? onReceivedLineHandler = null;
39 | OnStreamClosedHandler? onStreamClosedHandler = null;
40 |
41 | void ResolveIfStillPending(Action applyResolution)
42 | {
43 | lock (completionLock)
44 | {
45 | if (!tcs.Task.IsCompleted)
46 | {
47 | OnReceivedLine -= onReceivedLineHandler;
48 | OnStreamClosed -= onStreamClosedHandler;
49 | applyResolution();
50 | }
51 | }
52 | }
53 |
54 | onReceivedLineHandler = line =>
55 | {
56 | var match = regex.Match(line);
57 | if (match.Success)
58 | {
59 | ResolveIfStillPending(() => tcs.SetResult(match));
60 | }
61 | };
62 |
63 | onStreamClosedHandler = () =>
64 | {
65 | ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));
66 | };
67 |
68 | OnReceivedLine += onReceivedLineHandler;
69 | OnStreamClosed += onStreamClosedHandler;
70 |
71 | return tcs.Task;
72 | }
73 |
74 | private async Task Run()
75 | {
76 | var buf = new char[8 * 1024];
77 | while (true)
78 | {
79 | var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
80 | if (chunkLength == 0)
81 | {
82 | if (_linesBuffer.Length > 0)
83 | {
84 | OnCompleteLine(_linesBuffer.ToString());
85 | _linesBuffer.Clear();
86 | }
87 |
88 | OnClosed();
89 | break;
90 | }
91 |
92 | OnChunk(new ArraySegment(buf, 0, chunkLength));
93 |
94 | int lineBreakPos;
95 | var startPos = 0;
96 |
97 | // get all the newlines
98 | while ((lineBreakPos = Array.IndexOf(buf, '\n', startPos, chunkLength - startPos)) >= 0 && startPos < chunkLength)
99 | {
100 | var length = (lineBreakPos + 1) - startPos;
101 | _linesBuffer.Append(buf, startPos, length);
102 | OnCompleteLine(_linesBuffer.ToString());
103 | _linesBuffer.Clear();
104 | startPos = lineBreakPos + 1;
105 | }
106 |
107 | // get the rest
108 | if (lineBreakPos < 0 && startPos < chunkLength)
109 | {
110 | _linesBuffer.Append(buf, startPos, chunkLength - startPos);
111 | }
112 | }
113 | }
114 |
115 | private void OnChunk(ArraySegment chunk)
116 | {
117 | var dlg = OnReceivedChunk;
118 | dlg?.Invoke(chunk);
119 | }
120 |
121 | private void OnCompleteLine(string line)
122 | {
123 | var dlg = OnReceivedLine;
124 | dlg?.Invoke(line);
125 | }
126 |
127 | private void OnClosed()
128 | {
129 | var dlg = OnStreamClosed;
130 | dlg?.Invoke();
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Util/EventedStreamStringReader.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Text;
5 |
6 | namespace Microsoft.AspNetCore.NodeServices.Util;
7 |
8 | ///
9 | /// Captures the completed-line notifications from a ,
10 | /// combining the data into a single .
11 | ///
12 | internal class EventedStreamStringReader : IDisposable
13 | {
14 | private readonly EventedStreamReader _eventedStreamReader;
15 | private bool _isDisposed;
16 | private readonly StringBuilder _stringBuilder = new StringBuilder();
17 |
18 | public EventedStreamStringReader(EventedStreamReader eventedStreamReader)
19 | {
20 | _eventedStreamReader = eventedStreamReader
21 | ?? throw new ArgumentNullException(nameof(eventedStreamReader));
22 | _eventedStreamReader.OnReceivedLine += OnReceivedLine;
23 | }
24 |
25 | public string ReadAsString() => _stringBuilder.ToString();
26 |
27 | private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line);
28 |
29 | public void Dispose()
30 | {
31 | if (!_isDisposed)
32 | {
33 | _eventedStreamReader.OnReceivedLine -= OnReceivedLine;
34 | _isDisposed = true;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Util/LoggerFinder.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Logging;
7 | using Microsoft.Extensions.Logging.Abstractions;
8 |
9 | namespace Microsoft.AspNetCore.SpaServices.Util;
10 |
11 | internal static class LoggerFinder
12 | {
13 | public static ILogger GetOrCreateLogger(
14 | IApplicationBuilder appBuilder,
15 | string logCategoryName)
16 | {
17 | // If the DI system gives us a logger, use it. Otherwise, set up a default one
18 | var loggerFactory = appBuilder.ApplicationServices.GetService();
19 | var logger = loggerFactory != null
20 | ? loggerFactory.CreateLogger(logCategoryName)
21 | : NullLogger.Instance;
22 | return logger;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Util/TaskTimeoutExtensions.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | namespace Microsoft.AspNetCore.SpaServices.Extensions.Util;
5 |
6 | internal static class TaskTimeoutExtensions
7 | {
8 | public static async Task WithTimeout(this Task task, TimeSpan timeoutDelay, string message)
9 | {
10 | if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
11 | {
12 | task.Wait(); // Allow any errors to propagate
13 | }
14 | else
15 | {
16 | throw new TimeoutException(message);
17 | }
18 | }
19 |
20 | public static async Task WithTimeout(this Task task, TimeSpan timeoutDelay, string message)
21 | {
22 | if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
23 | {
24 | return task.Result;
25 | }
26 | else
27 | {
28 | throw new TimeoutException(message);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Tailwind.Extensions.AspNetCore/Util/TcpPortFinder.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Net;
5 | using System.Net.Sockets;
6 |
7 | namespace Microsoft.AspNetCore.SpaServices.Util;
8 |
9 | internal static class TcpPortFinder
10 | {
11 | public static int FindAvailablePort()
12 | {
13 | var listener = new TcpListener(IPAddress.Loopback, 0);
14 | listener.Start();
15 | try
16 | {
17 | return ((IPEndPoint)listener.LocalEndpoint).Port;
18 | }
19 | finally
20 | {
21 | listener.Stop();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------