From 53841c6b62fdcff65439e6e99dafca54240199b3 Mon Sep 17 00:00:00 2001 From: koeeenig Date: Wed, 7 Feb 2024 10:34:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Added=20basic=20frontmatter=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .frontmatter/database/mediaDb.json | 1 + .frontmatter/database/pinnedItemsDb.json | 1 + .frontmatter/database/taxonomyDb.json | 1 + BlazeKit.sln | 28 +- Directory.Build.props | 2 +- frontmatter.json | 66 + src/BlazeKit.Abstraction/Config/BkConfig.cs | 1 + src/BlazeKit.Abstraction/IServerLoad.cs | 10 + src/BlazeKit.CLI/AnsiConsoleExtensions.cs | 12 + src/BlazeKit.CLI/BlazeKit.CLI.csproj | 10 + .../Commands/Build/BuildCommand.cs | 176 +- .../Commands/Build/BuildSettings.cs | 23 + .../Commands/Run/{ => Dev}/DevCommand.cs | 30 +- .../Commands/Run/Dev/DevSettings.cs | 24 + src/BlazeKit.CLI/Commands/Run/RunSettings.cs | 25 +- .../Commands/Run/Tailwind/TailwindCommand.cs | 90 + .../Commands/Run/Tailwind/TailwindSettings.cs | 11 + src/BlazeKit.CLI/IProcess.cs | 1 + src/BlazeKit.CLI/Program.cs | 7 +- .../Tasks/Tools/TskDotNetWatch.cs | 23 +- .../Tasks/Tools/TskTailwindCss.cs | 4 +- .../Tasks/Utils/ExecCliCommand.cs | 15 +- src/BlazeKit.Core/Routing/Navigating.cs | 61 - src/BlazeKit.Hydration/BKitData.razor | 18 + .../BlazeKit.Hydration.csproj | 6 +- .../DataHydrationContext.cs | 70 +- .../BlazeKit.Reactivity.csproj | 10 +- src/BlazeKit.Static/BlazeKit.Static.csproj | 5 +- src/BlazeKit.Static/BlazorRenderer.cs | 79 +- .../ContentCollectionEnvelope.cs | 64 + .../ContentCollections/IContentCollection.cs | 9 + .../ContentCollections/ISchema.cs | 5 + .../IStaticServiceCollection.cs | 14 + src/BlazeKit.Static/StaticSiteGenerator.cs | 194 +- .../Utils/MarkdownExtensions.cs | 55 + src/BlazeKit.Web/BlazeKit.Web.csproj | 24 + src/BlazeKit.Web/Component1.razor | 3 + src/BlazeKit.Web/Component1.razor.css | 6 + .../Components/ClientHydrateMode.cs | 32 + .../Components/IslandComponent.cs | 205 ++ .../Components/IslandComponentBase.cs | 423 +++ .../Components/IslandComponentBase2.cs | 452 +++ .../Components/PageComponentBase.cs | 102 + .../Components/WebAssemblyLoadProgress.razor | 0 src/BlazeKit.Web/ExampleJsInterop.cs | 36 + src/BlazeKit.Web/PageDataBase.cs | 19 + .../Utils/CssClass.cs} | 10 +- .../Utils/OperatingSystemExtensions.cs | 10 + src/BlazeKit.Web/_Imports.razor | 1 + src/BlazeKit.Web/wwwroot/background.png | Bin 0 -> 378 bytes src/BlazeKit.Web/wwwroot/exampleJsInterop.js | 6 + .../BKitHostingEnvironment.cs | 16 + .../BlazeKit.Website.Islands.csproj | 12 +- .../ClientLoadMode.cs | 12 - .../Components/Counter.razor | 9 +- .../Components/DevTools.razor | 82 +- .../Components/Link.cs | 87 + .../Components/LoadTest.razor | 31 + .../Components/MVVM/Counter.razor | 2 +- .../Components/MVVM/VMCounter.cs | 0 .../Components/MyCounter.razor | 3 +- .../Components/NewCounter.razor | 136 + .../Components/StateTest.razor | 8 + .../DevTools/IDevTools.cs | 78 +- .../MyCustomPageData.cs | 27 + src/BlazeKit.Website.Islands/Program.cs | 12 + src/BlazeKit.Website.Islands/_Imports.razor | 1 - src/BlazeKit.Website/AppRouter.razor | 8 +- src/BlazeKit.Website/BlazeKit.Website.csproj | 63 +- .../Components/BlogPost.razor | 19 + src/BlazeKit.Website/Components/Footer.razor | 14 + src/BlazeKit.Website/Components/Header.razor | 1 + src/BlazeKit.Website/Components/Hero.razor | 6 +- src/BlazeKit.Website/Components/Sidebar.razor | 8 + src/BlazeKit.Website/Components/Sitemap.razor | 35 + ...4-01-24-auto-generated-route-parameters.md | 12 + .../Content/Blog/2024-01-31-second-post.md | 12 + .../Content/Blog/2024-02-02-another-post.md | 32 + .../Content/Blog/BlogCollection.cs | 39 + .../Content/Blog/first-post.md | 20 + .../Content/News/NewsCollection.cs | 35 + .../Content/News/release-v1.md | 11 + src/BlazeKit.Website/Index.razor | 10 +- .../Islands/Components/BlzIsland.razor | 39 - src/BlazeKit.Website/PageLoad.razor | 34 + src/BlazeKit.Website/Program.cs | 70 +- .../Properties/launchSettings.json | 2 +- src/BlazeKit.Website/Routes/Blog/Layout.razor | 2 +- src/BlazeKit.Website/Routes/Blog/Page.md | 1 - src/BlazeKit.Website/Routes/Blog/Page.razor | 22 + .../Routes/Blog/[Slug]/Layout.razor | 4 + .../Routes/Blog/[Slug]/Page.razor | 16 + .../Routes/Docs/Blazekit-CLI/Layout.razor | 7 + .../Routes/Docs/Blazekit-CLI/Page.md | 14 +- .../Routes/Docs/Installation/Layout.razor | 16 + .../Routes/Docs/Interactivity/Page.md | 4 + .../Basic/{Page.md => Page@Docs.md} | 4 +- .../Routes/Docs/Reactivity/Layout.razor | 9 + .../MVVM/{Page.razor => Page@Docs.razor} | 5 +- .../Routes/Docs/Reactivity/Page.md | 10 +- .../Routes/Docs/Roadmap/Page.md | 17 +- .../Docs/Routing/Endpoints/Layout.razor | 7 + .../Routes/Docs/Routing/Layouts/Layout.razor | 14 +- .../Routes/Docs/Routing/Pages/Layout.razor | 8 + src/BlazeKit.Website/Routes/Layout.razor | 1 + src/BlazeKit.Website/Routes/News/Layout.razor | 4 + src/BlazeKit.Website/Routes/News/Page.razor | 16 + src/BlazeKit.Website/Routes/Page.razor | 3 +- .../StaticServiceCollection.cs | 34 + src/BlazeKit.Website/_Imports.razor | 7 +- src/BlazeKit.Website/app.css | 6 +- src/BlazeKit.Website/blazekit.config.json | 1 + src/BlazeKit.Website/tailwind.config.js | 2 +- .../wwwroot/_framework/blazekit.web.js | 2 +- .../_framework/blazekit.web.js.LICENSE.txt | 35 + .../wwwroot/_framework/blazekit.web.js.map | 2 +- src/BlazeKit.Website/wwwroot/css/app.css | 2450 +---------------- src/BlazeKit/BlazeKit.csproj | 1 - src/BlazeKit/Layout/ClosestLayout.cs | 1 + .../Routes/Page/MarkdownRoutesGenerator.cs | 39 +- .../Routes/Page/PageRoutesGenerator.cs | 26 +- .../Page/Templates/SitemapClassSource.cs | 43 + tests/BlazeKit.Tests/Markdown/FrontMatter.cs | 26 + 123 files changed, 3289 insertions(+), 3036 deletions(-) create mode 100644 .frontmatter/database/mediaDb.json create mode 100644 .frontmatter/database/pinnedItemsDb.json create mode 100644 .frontmatter/database/taxonomyDb.json create mode 100644 frontmatter.json create mode 100644 src/BlazeKit.Abstraction/IServerLoad.cs create mode 100644 src/BlazeKit.CLI/AnsiConsoleExtensions.cs create mode 100644 src/BlazeKit.CLI/Commands/Build/BuildSettings.cs rename src/BlazeKit.CLI/Commands/Run/{ => Dev}/DevCommand.cs (87%) create mode 100644 src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs create mode 100644 src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs create mode 100644 src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs delete mode 100644 src/BlazeKit.Core/Routing/Navigating.cs create mode 100644 src/BlazeKit.Hydration/BKitData.razor create mode 100644 src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs create mode 100644 src/BlazeKit.Static/ContentCollections/IContentCollection.cs create mode 100644 src/BlazeKit.Static/ContentCollections/ISchema.cs create mode 100644 src/BlazeKit.Static/IStaticServiceCollection.cs create mode 100644 src/BlazeKit.Static/Utils/MarkdownExtensions.cs create mode 100644 src/BlazeKit.Web/BlazeKit.Web.csproj create mode 100644 src/BlazeKit.Web/Component1.razor create mode 100644 src/BlazeKit.Web/Component1.razor.css create mode 100644 src/BlazeKit.Web/Components/ClientHydrateMode.cs create mode 100644 src/BlazeKit.Web/Components/IslandComponent.cs create mode 100644 src/BlazeKit.Web/Components/IslandComponentBase.cs create mode 100644 src/BlazeKit.Web/Components/IslandComponentBase2.cs create mode 100644 src/BlazeKit.Web/Components/PageComponentBase.cs rename src/{BlazeKit.Website => BlazeKit.Web}/Components/WebAssemblyLoadProgress.razor (100%) create mode 100644 src/BlazeKit.Web/ExampleJsInterop.cs create mode 100644 src/BlazeKit.Web/PageDataBase.cs rename src/{BlazeKit.Website/Islands/Components/Web/CssClasses.cs => BlazeKit.Web/Utils/CssClass.cs} (72%) create mode 100644 src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs create mode 100644 src/BlazeKit.Web/_Imports.razor create mode 100644 src/BlazeKit.Web/wwwroot/background.png create mode 100644 src/BlazeKit.Web/wwwroot/exampleJsInterop.js create mode 100644 src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs delete mode 100644 src/BlazeKit.Website.Islands/ClientLoadMode.cs rename src/{BlazeKit.Website/Islands => BlazeKit.Website.Islands}/Components/Counter.razor (96%) create mode 100644 src/BlazeKit.Website.Islands/Components/Link.cs create mode 100644 src/BlazeKit.Website.Islands/Components/LoadTest.razor rename src/{BlazeKit.Website/Islands => BlazeKit.Website.Islands}/Components/MVVM/Counter.razor (93%) rename src/{BlazeKit.Website/Islands => BlazeKit.Website.Islands}/Components/MVVM/VMCounter.cs (100%) create mode 100644 src/BlazeKit.Website.Islands/Components/NewCounter.razor create mode 100644 src/BlazeKit.Website.Islands/Components/StateTest.razor create mode 100644 src/BlazeKit.Website.Islands/MyCustomPageData.cs create mode 100644 src/BlazeKit.Website/Components/BlogPost.razor create mode 100644 src/BlazeKit.Website/Components/Footer.razor create mode 100644 src/BlazeKit.Website/Components/Sitemap.razor create mode 100644 src/BlazeKit.Website/Content/Blog/2024-01-24-auto-generated-route-parameters.md create mode 100644 src/BlazeKit.Website/Content/Blog/2024-01-31-second-post.md create mode 100644 src/BlazeKit.Website/Content/Blog/2024-02-02-another-post.md create mode 100644 src/BlazeKit.Website/Content/Blog/BlogCollection.cs create mode 100644 src/BlazeKit.Website/Content/Blog/first-post.md create mode 100644 src/BlazeKit.Website/Content/News/NewsCollection.cs create mode 100644 src/BlazeKit.Website/Content/News/release-v1.md delete mode 100644 src/BlazeKit.Website/Islands/Components/BlzIsland.razor create mode 100644 src/BlazeKit.Website/PageLoad.razor delete mode 100644 src/BlazeKit.Website/Routes/Blog/Page.md create mode 100644 src/BlazeKit.Website/Routes/Blog/Page.razor create mode 100644 src/BlazeKit.Website/Routes/Blog/[Slug]/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/Blog/[Slug]/Page.razor create mode 100644 src/BlazeKit.Website/Routes/Docs/Blazekit-CLI/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/Docs/Installation/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/Docs/Interactivity/Page.md rename src/BlazeKit.Website/Routes/Docs/Reactivity/Basic/{Page.md => Page@Docs.md} (87%) create mode 100644 src/BlazeKit.Website/Routes/Docs/Reactivity/Layout.razor rename src/BlazeKit.Website/Routes/Docs/Reactivity/MVVM/{Page.razor => Page@Docs.razor} (76%) create mode 100644 src/BlazeKit.Website/Routes/Docs/Routing/Endpoints/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/Docs/Routing/Pages/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/News/Layout.razor create mode 100644 src/BlazeKit.Website/Routes/News/Page.razor create mode 100644 src/BlazeKit.Website/StaticServiceCollection.cs create mode 100644 src/BlazeKit.Website/wwwroot/_framework/blazekit.web.js.LICENSE.txt create mode 100644 src/BlazeKit/Routes/Page/Templates/SitemapClassSource.cs create mode 100644 tests/BlazeKit.Tests/Markdown/FrontMatter.cs diff --git a/.frontmatter/database/mediaDb.json b/.frontmatter/database/mediaDb.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.frontmatter/database/mediaDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.frontmatter/database/pinnedItemsDb.json b/.frontmatter/database/pinnedItemsDb.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.frontmatter/database/pinnedItemsDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.frontmatter/database/taxonomyDb.json b/.frontmatter/database/taxonomyDb.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.frontmatter/database/taxonomyDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/BlazeKit.sln b/BlazeKit.sln index 761cb3f..cb832c8 100644 --- a/BlazeKit.sln +++ b/BlazeKit.sln @@ -25,19 +25,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Reactivity.Tests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{6B6EFC67-31B4-4D2E-98D5-1A731AF98B0B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Reactivity", "src\BlazeKit.Reactivity\BlazeKit.Reactivity.csproj", "{3CD31A61-9A86-4D79-A7A6-B6A4AD0BF7D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Reactivity", "src\BlazeKit.Reactivity\BlazeKit.Reactivity.csproj", "{3CD31A61-9A86-4D79-A7A6-B6A4AD0BF7D8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Routing.TestApp", "tests\BlazeKit.Routing.TestApp\BlazeKit.Routing.TestApp.csproj", "{196C36B5-D580-4C2D-BDFF-AFD530255F2E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Routing.TestApp", "tests\BlazeKit.Routing.TestApp\BlazeKit.Routing.TestApp.csproj", "{196C36B5-D580-4C2D-BDFF-AFD530255F2E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{98E8084D-CE3D-429D-97B4-A7E41EDF1431}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Static", "src\BlazeKit.Static\BlazeKit.Static.csproj", "{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Static", "src\BlazeKit.Static\BlazeKit.Static.csproj", "{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Site.Static", "src\BlazeKit.Site.Static\BlazeKit.Site.Static.csproj", "{FB0F7358-8500-46DF-B213-3E8980F70856}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Website", "src\BlazeKit.Website\BlazeKit.Website.csproj", "{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Website", "src\BlazeKit.Website\BlazeKit.Website.csproj", "{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Website.Islands", "src\BlazeKit.Website.Islands\BlazeKit.Website.Islands.csproj", "{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Website.Islands", "src\BlazeKit.Website.Islands\BlazeKit.Website.Islands.csproj", "{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Web", "src\BlazeKit.Web\BlazeKit.Web.csproj", "{788D739C-CA07-4E74-9F13-F3DA1A49C3A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Hydration", "src\BlazeKit.Hydration\BlazeKit.Hydration.csproj", "{90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -91,10 +93,6 @@ Global {B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Debug|Any CPU.Build.0 = Debug|Any CPU {B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Release|Any CPU.ActiveCfg = Release|Any CPU {B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Release|Any CPU.Build.0 = Release|Any CPU - {FB0F7358-8500-46DF-B213-3E8980F70856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB0F7358-8500-46DF-B213-3E8980F70856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB0F7358-8500-46DF-B213-3E8980F70856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB0F7358-8500-46DF-B213-3E8980F70856}.Release|Any CPU.Build.0 = Release|Any CPU {AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -103,6 +101,14 @@ Global {1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Release|Any CPU.Build.0 = Release|Any CPU + {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Release|Any CPU.Build.0 = Release|Any CPU + {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -113,9 +119,9 @@ Global {4D98C97D-CFCC-4473-BFD8-B28DA3199EF6} = {952B525D-297C-4F66-85E2-967C9982771F} {196C36B5-D580-4C2D-BDFF-AFD530255F2E} = {952B525D-297C-4F66-85E2-967C9982771F} {B28BBCD3-5A90-447E-8BAD-D78D7517AAD7} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431} - {FB0F7358-8500-46DF-B213-3E8980F70856} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431} {AB38EF74-D766-4A77-A7F2-EA8B74AEA495} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431} {1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431} + {788D739C-CA07-4E74-9F13-F3DA1A49C3A5} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8F0560BE-EE70-4DE3-8ACA-590A2B56C96A} diff --git a/Directory.Build.props b/Directory.Build.props index 3a4619f..b5d2c72 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.2.0 + 0.2.1 Andreas König BlazeKit diff --git a/frontmatter.json b/frontmatter.json new file mode 100644 index 0000000..b4d0883 --- /dev/null +++ b/frontmatter.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://frontmatter.codes/frontmatter.schema.json", + "frontMatter.taxonomy.contentTypes": [ + { + "name": "default", + "pageBundle": false, + "previewPath": null, + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Description", + "name": "description", + "type": "string" + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Content preview", + "name": "preview", + "type": "image" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "draft" + }, + { + "title": "Tags", + "name": "tags", + "type": "tags" + }, + { + "title": "Categories", + "name": "categories", + "type": "categories" + }, + { + "title": "Author", + "name": "author", + "type": "string" + } + ] + } + ], + "frontMatter.framework.id": "other", + "frontMatter.content.publicFolder": "", + "frontMatter.content.pageFolders": [ + { + "title": "Blog", + "path": "[[workspace]]/src/BlazeKit.Website/Content/Blog" + }, + { + "title": "News", + "path": "[[workspace]]/src/BlazeKit.Website/Content/News" + } + ] +} diff --git a/src/BlazeKit.Abstraction/Config/BkConfig.cs b/src/BlazeKit.Abstraction/Config/BkConfig.cs index a28f306..b69eee8 100644 --- a/src/BlazeKit.Abstraction/Config/BkConfig.cs +++ b/src/BlazeKit.Abstraction/Config/BkConfig.cs @@ -13,6 +13,7 @@ public BkConfig() public string Routes { get; set; } public TailwindcssConfig Tailwindcss { get; set; } + public bool PreRender { get; set; } = false; public static bool TryLoad(out BkConfig config) { diff --git a/src/BlazeKit.Abstraction/IServerLoad.cs b/src/BlazeKit.Abstraction/IServerLoad.cs new file mode 100644 index 0000000..d000118 --- /dev/null +++ b/src/BlazeKit.Abstraction/IServerLoad.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace BlazeKit.Abstraction; + +public interface IServerLoad +{ + Task LoadAsync(); + + TResult Load(); +} diff --git a/src/BlazeKit.CLI/AnsiConsoleExtensions.cs b/src/BlazeKit.CLI/AnsiConsoleExtensions.cs new file mode 100644 index 0000000..7b68d7a --- /dev/null +++ b/src/BlazeKit.CLI/AnsiConsoleExtensions.cs @@ -0,0 +1,12 @@ +using Spectre.Console; + +namespace BlazeKit.CLI; + +public static class AnsiConsoleExtensions +{ + [System.Diagnostics.Conditional("DEBUG")] + public static void Debug(this IAnsiConsole console, string message) + { + console.MarkupLine($"[bold yellow on blue] DEBUG: {message.EscapeMarkup()}[/]"); + } +} diff --git a/src/BlazeKit.CLI/BlazeKit.CLI.csproj b/src/BlazeKit.CLI/BlazeKit.CLI.csproj index ae87b14..f4da95b 100644 --- a/src/BlazeKit.CLI/BlazeKit.CLI.csproj +++ b/src/BlazeKit.CLI/BlazeKit.CLI.csproj @@ -6,6 +6,7 @@ enable enable true + 0.2.1 bkit @@ -39,4 +40,13 @@ + + + + + + + + + diff --git a/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs b/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs index c116db4..f97f047 100644 --- a/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs +++ b/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs @@ -1,29 +1,32 @@ -using BlazeKit.CLI.Commands.New; +using BlazeKit.Abstraction.Config; using BlazeKit.CLI.Tasks.Utils; +using Microsoft.AspNetCore.Components; using Spectre.Console; using Spectre.Console.Cli; using System.Diagnostics; -using System.Text.Json; +using System.Runtime.Loader; namespace BlazeKit.CLI.Commands.Build; -public class BuildCommand : AsyncCommand +public class BuildCommand : Command { - public override async Task ExecuteAsync(CommandContext context, RunSettings settings) + public override int Execute(CommandContext context, BuildSettings settings) { - // load blazekit.config.json file if it exists - if (File.Exists("blazekit.config.json")) - { - AnsiConsole.MarkupLine("[yellow]Loading blazekit.config.json...[/]"); - var config = JsonSerializer.Deserialize(File.ReadAllText("blazekit.config.json")); + if (!BkConfig.TryLoad(out var bkConfig)) + { + AnsiConsole.MarkupLine("[red]blazekit.config.json not found...Using default values[/]"); + } - config!.RootElement.TryGetProperty("tailwindcss", out var tailwindcss); + var hasTailwindcss = bkConfig.HasTailwindcss(); - var input = tailwindcss.GetProperty("input").GetString(); - var output = tailwindcss.GetProperty("output").GetString(); + // load blazekit.config.json file if it exists + if (hasTailwindcss && string.IsNullOrEmpty(settings.Tailwindcss)) + { + var input = bkConfig.Tailwindcss.Input; + var output = bkConfig.Tailwindcss.Output; - settings.Tailwindcss = $"-i {input} -o {output}"; - } + settings.Tailwindcss = $"-i {input} -o {output} --minify"; + } // run dotnet watch command AnsiConsole.MarkupLine("[yellow] Running Tailwindcss build process...[/]"); @@ -55,9 +58,16 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings } }); - await Task.WhenAll(publishTask); - + Task.WhenAll(publishTask).Wait(); + // var tempOutput = bkConfig.PreRender ? ".blazekit/tmp/" : settings.Output; + var tempOutput = bkConfig.PreRender ? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()) : settings.Output; + // clean the temp output directory + if (Directory.Exists(tempOutput)) + { + AnsiConsole.MarkupLine($"[yellow]Cleaning {tempOutput}...[/]"); + Directory.Delete(tempOutput, true); + } publishTask = Task.Run(async () => { @@ -66,9 +76,11 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings "dotnet", output => { - AnsiConsole.MarkupLine($"[purple]{$"[DOTNET PUBLISH]".EscapeMarkup()} {output.EscapeMarkup()}[/]"); + //AnsiConsole.MarkupLine($"[purple]{$"[DOTNET PUBLISH]".EscapeMarkup()}[/] {output.EscapeMarkup()}"); + AnsiConsole.MarkupLine($"{output.EscapeMarkup()}"); }, - (string.IsNullOrEmpty(settings.Dotnet) ? "publish -c Release" : settings.Dotnet) + " -o ./blazekit/build" + info => info.EnvironmentVariables.Add("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "True"), + (string.IsNullOrEmpty(settings.Dotnet) ? "publish -c Debug" : settings.Dotnet) + $" -o {tempOutput}" ).Run(); while (!process.HasExited) @@ -82,92 +94,72 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings await Task.Delay(500); } - }); - // Wait for CTRL+C input to cancel running dotnet watch - AnsiConsole.MarkupLine("[yellow]Press CTRL+C to stop dotnet watch...[/]"); - // Start a console read operation. Do not display the input. - Console.TreatControlCAsInput = true; - var cancelTask = Task.Run(async () => + Task.WhenAll(publishTask).Wait(); + // check if preprender is enabled + if(bkConfig.PreRender) { - while (true) + // pre-render the app (SSG output) + AnsiConsole.MarkupLine("[yellow] Pre-rendering app...[/]"); + // get the name of the csproj file in the current directory + var csproj = new FileInfo(Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj").FirstOrDefault()); + // find the assembly name from the csproj file + var assemblyName = csproj.Name.Replace(csproj.Extension, ""); + // get the path to the assembly + var assemblyPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tempOutput, assemblyName + ".dll")); + // use BlazeKit.Static to pre-render the app + // var loadCtx = AssemblyLoadContext.Default; + + System.Runtime.Loader.AssemblyLoadContext loadCtx = new AssemblyLoadContext("ssg",isCollectible:true); + loadCtx.Resolving += (ctx, name) => { - var key = Console.ReadKey(true); - if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) + AnsiConsole.MarkupLine($"Resolving assembly '{name.Name}'"); + + var resolved = ctx.LoadFromAssemblyPath(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tempOutput, name.Name + ".dll"))); + if(resolved != null) { - Console.WriteLine("CTRL+C pressed"); - cancel.Cancel(); - break; + AnsiConsole.MarkupLine($"Resolved assembly '{name.Name}'"); + } else { + AnsiConsole.MarkupLine($"Failed to resolve assembly '{name.Name}'"); } - await Task.Delay(100); - } - }); - await Task.WhenAll(publishTask); - return 0; - } - - private Process RunDotNetCommand(string command, IList output, IList error, Action refresh, params string[] arguments) - { - AnsiConsole.MarkupLine($"[yellow]{"[BlazeKit]".EscapeMarkup()} running 'dotnet {command} {string.Join(" ", arguments)}'[/]"); - // print working directory - // AnsiConsole.MarkupLine($"[yellow]Working directory: {Directory.GetCurrentDirectory()}[/]"); - var startInfo = - new ProcessStartInfo("dotnet", $"{command} {string.Join(" ", arguments)}") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true, - // DOTNET_WATCH_SUPPRESS_EMOJIS=1 - WorkingDirectory = Directory.GetCurrentDirectory() + return resolved; }; - startInfo.Environment.Add("DOTNET_WATCH_SUPPRESS_EMOJIS", "1"); - var process = - Process.Start( - startInfo - ); - process.EnableRaisingEvents = true; - process.OutputDataReceived += (sender, args) => { output.Add(args.Data); refresh(args.Data); }; - process.ErrorDataReceived += (sender, args) => { error.Add(args.Data); refresh(args.Data); }; - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - return process; - } + AnsiConsole.MarkupLine($"Try load assembly '{assemblyPath}'"); + var asm = loadCtx.LoadFromAssemblyPath(assemblyPath); + var ssg = new BlazeKit.Static.StaticSiteGenerator(settings.Output,Path.Combine(tempOutput,"wwwroot"),asm); + ssg.Build().Wait(); + AnsiConsole.MarkupLine($"[green]Succesfully created static site at '{settings.Output}'[/]"); + + // loadCtx.Unload(); + // delete the temp output directory + // TODO: throws Error: Access to the path 'BlazeKit.Abstraction.dll' is denied. + // Directory.Delete(tempOutput, true); + } - private Process RunTailwindcss(string command, IList output, IList error, Action refresh, params string[] arguments) - { - AnsiConsole.MarkupLine($"[yellow]{"[BlazeKit]".EscapeMarkup()} running 'tailwindcss {command} {string.Join(" ", arguments)}'[/]"); - // print working directory - // AnsiConsole.MarkupLine($"[yellow]Working directory: {Directory.GetCurrentDirectory()}[/]"); - var startInfo = - new ProcessStartInfo("tailwindcss", $"{command} {string.Join(" ", arguments)}") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true, - // DOTNET_WATCH_SUPPRESS_EMOJIS=1 - WorkingDirectory = Directory.GetCurrentDirectory() - }; - var process = - Process.Start( - startInfo - ); - process.EnableRaisingEvents = true; - process.OutputDataReceived += (sender, args) => { output.Add(args.Data); refresh(args.Data); }; - process.ErrorDataReceived += (sender, args) => { error.Add(args.Data); refresh(args.Data); }; - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); + // // Wait for CTRL+C input to cancel running dotnet watch + // AnsiConsole.MarkupLine("[yellow]Press CTRL+C to stop dotnet watch...[/]"); + // // Start a console read operation. Do not display the input. + // Console.TreatControlCAsInput = true; + // var cancelTask = Task.Run(async () => + // { + // while (true) + // { + // var key = Console.ReadKey(true); + // if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) + // { + // Console.WriteLine("CTRL+C pressed"); + // cancel.Cancel(); + // break; + // } + // await Task.Delay(100); + // } + // }); - return process; + return 0; } } diff --git a/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs b/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs new file mode 100644 index 0000000..6dbaf85 --- /dev/null +++ b/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs @@ -0,0 +1,23 @@ +using Spectre.Console.Cli; +using System.ComponentModel; + +namespace BlazeKit.CLI.Commands.Build +{ + public sealed class BuildSettings : Run.RunSettings + { + [CommandOption("-p|--project ")] + public string Project { get; set; } + + [CommandOption("-t|--tailwindcss")] + [DefaultValue("")] + public string Tailwindcss { get; set; } + + [CommandOption("-d|--dotnet")] + [DefaultValue("")] + public string Dotnet { get; set; } + + [CommandOption("-o|--output")] + [DefaultValue(".blazekit/build")] + public string Output { get; set; } + } +} diff --git a/src/BlazeKit.CLI/Commands/Run/DevCommand.cs b/src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs similarity index 87% rename from src/BlazeKit.CLI/Commands/Run/DevCommand.cs rename to src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs index 20e41d9..fa0c20d 100644 --- a/src/BlazeKit.CLI/Commands/Run/DevCommand.cs +++ b/src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs @@ -1,5 +1,5 @@ using BlazeKit.Abstraction.Config; -using BlazeKit.CLI.Commands.New; +using BlazeKit.CLI.Commands.Run; using BlazeKit.CLI.Tasks; using BlazeKit.CLI.Tasks.Tasks; using BlazeKit.CLI.Tasks.Tools; @@ -9,11 +9,11 @@ using System.Text; using Yaapii.Atoms.List; -namespace BlazeKit.CLI; +namespace BlazeKit.CLI.Commands.Run; -public class DevCommand : AsyncCommand +public class DevCommand : AsyncCommand { - public override async Task ExecuteAsync(CommandContext context, RunSettings settings) + public override async Task ExecuteAsync(CommandContext context, DevSettings settings) { try { @@ -28,7 +28,7 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings } var hasTailwindcss = bkConfig.HasTailwindcss(); - + // load blazekit.config.json file if it exists if (hasTailwindcss && string.IsNullOrEmpty(settings.Tailwindcss)) { @@ -54,18 +54,20 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings new TskDotNetWatch( settings, output => { - AnsiConsole.MarkupLine($"[purple]{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}[/]"); + //AnsiConsole.MarkupLine($"[purple]{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}[/]"); + //AnsiConsole.MarkupLine($"{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}"); + AnsiConsole.MarkupLine($"{output.EscapeMarkup()}"); }, cancel ), new TskWhen( () => hasTailwindcss, new TskTailwindCss( - settings, output => { AnsiConsole.MarkupLine($"[blue]{$"[TAILWINDCSS {Emoji.Known.ArtistPalette}]".EscapeMarkup()} {output.EscapeMarkup()}[/]"); }, - cancel + cancel, + string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss ) ) ).Run(); @@ -80,7 +82,7 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings var key = Console.ReadKey(true); if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) { - Console.WriteLine("CTRL+C pressed"); + AnsiConsole.WriteLine("CTRL+C pressed"); AnsiConsole.MarkupLine("[yellow]Stopping dev mode...[/]"); cancel.Cancel(); break; @@ -117,7 +119,7 @@ private void AlternateOrClearScreen(Action action) } } - private async void UseLayout(RunSettings settings, bool hasTailwindcss) + private async void UseLayout(DevSettings settings, bool hasTailwindcss) { var outputLayouts = hasTailwindcss ? ListOf.New(new Layout("Left"), new Layout("Right")) : ListOf.New(new Layout("Left")); // Create the layout @@ -162,7 +164,7 @@ await AnsiConsole.Live(layout).StartAsync(async ctx => .Update( panel ); - + ctx.Refresh(); Debug.WriteLine($"Refresh took {sw.ElapsedMilliseconds}ms"); }, @@ -171,7 +173,6 @@ await AnsiConsole.Live(layout).StartAsync(async ctx => new TskWhen( () => hasTailwindcss, new TskTailwindCss( - settings, output => { tailwind.Add(output.EscapeMarkup()); @@ -192,7 +193,8 @@ await AnsiConsole.Live(layout).StartAsync(async ctx => ctx.Refresh(); //AnsiConsole.MarkupLine($"[blue]{$"[TAILWINDCSS {Emoji.Known.ArtistPalette}]".EscapeMarkup()} {output.EscapeMarkup()}[/]"); }, - cancel + cancel, + string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss ) ) ).Run(); @@ -208,7 +210,7 @@ await AnsiConsole.Live(layout).StartAsync(async ctx => var key = Console.ReadKey(true); if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) { - Console.WriteLine("CTRL+C pressed"); + AnsiConsole.WriteLine("CTRL+C pressed"); AnsiConsole.MarkupLine("[yellow]Stopping dev mode...[/]"); cancel.Cancel(); break; diff --git a/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs b/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs new file mode 100644 index 0000000..3ddf809 --- /dev/null +++ b/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs @@ -0,0 +1,24 @@ +using Spectre.Console.Cli; +using System.ComponentModel; + +namespace BlazeKit.CLI.Commands.Run +{ + public sealed class DevSettings : RunSettings + { + [CommandOption("-p|--project ")] + public string Project { get; set; } + + [CommandOption("-t|--tailwindcss")] + [DefaultValue("")] + public string Tailwindcss { get; set; } + + [CommandOption("-d|--dotnet")] + [DefaultValue("")] + public string Dotnet { get; set; } + + + [CommandOption("-l|--layout-view")] + [DefaultValue(false)] + public bool UseLayoutView { get; set; } + } +} diff --git a/src/BlazeKit.CLI/Commands/Run/RunSettings.cs b/src/BlazeKit.CLI/Commands/Run/RunSettings.cs index 1ade48f..b0975f0 100644 --- a/src/BlazeKit.CLI/Commands/Run/RunSettings.cs +++ b/src/BlazeKit.CLI/Commands/Run/RunSettings.cs @@ -1,24 +1,5 @@ using Spectre.Console.Cli; -using System.ComponentModel; +namespace BlazeKit.CLI.Commands.Run; -namespace BlazeKit.CLI.Commands.New -{ - public sealed class RunSettings : CommandSettings - { - [CommandOption("-p|--project ")] - public string Project { get; set; } - - [CommandOption("-t|--tailwindcss")] - [DefaultValue("")] - public string Tailwindcss { get; set; } - - [CommandOption("-d|--dotnet")] - [DefaultValue("")] - public string Dotnet { get; set; } - - - [CommandOption("-l|--layout-view")] - [DefaultValue(false)] - public bool UseLayoutView { get; set; } - } -} +public abstract class RunSettings : CommandSettings +{ } diff --git a/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs new file mode 100644 index 0000000..d7e7eec --- /dev/null +++ b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs @@ -0,0 +1,90 @@ +using BlazeKit.Abstraction.Config; +using BlazeKit.CLI.Commands.Run; +using BlazeKit.CLI.Tasks; +using BlazeKit.CLI.Tasks.Tasks; +using BlazeKit.CLI.Tasks.Tools; +using Spectre.Console; +using Spectre.Console.Cli; +using System.Diagnostics; +using System.Text; +using Yaapii.Atoms.List; + +namespace BlazeKit.CLI; + +public class TailwindCommand : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context, TailwindSettings settings) + { + try + { + + var content = new StringBuilder(); + var tailwindContent = new StringBuilder(); + + + if (!BkConfig.TryLoad(out var bkConfig)) + { + AnsiConsole.MarkupLine("[red]blazekit.config.json not found...Using default values[/]"); + } + + var hasTailwindcss = bkConfig.HasTailwindcss(); + var twSettings = "-i app.css -o wwwroot/css/app.css --watch"; + // load blazekit.config.json file if it exists + if (hasTailwindcss) + { + var input = bkConfig.Tailwindcss.Input; + var output = bkConfig.Tailwindcss.Output; + + twSettings = $"-i {input} -o {output} --watch"; + } + + // run tailwind cli command + AnsiConsole.MarkupLine("[yellow]Starting Tailwind ...[/]"); + var prefix = $"[Tailwind {Emoji.Known.ArtistPalette}]"; + var cancel = new CancellationTokenSource(); + var tailwind = new List(); + var devTask = + new TskChain( + new TskWhen( + () => hasTailwindcss, + new TskTailwindCss( + output => { + AnsiConsole.MarkupLine($"{prefix} {output}".EscapeMarkup()); + }, + cancel, + twSettings + ) + ) + ).Run(); + + // Wait for CTRL+C input to cancel running dotnet watch + // Start a console read operation. Do not display the input. + Console.TreatControlCAsInput = true; + var cancelTask = Task.Run(async () => + { + while (true) + { + var key = Console.ReadKey(true); + if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) + { + AnsiConsole.WriteLine("CTRL+C pressed"); + AnsiConsole.MarkupLine("[yellow]Stopping Tailwind ...[/]"); + cancel.Cancel(); + break; + } + await Task.Delay(100); + } + }); + Task.WaitAll(devTask); + + AnsiConsole.MarkupLine("[yellow]Stopped Tailwind...[/]"); + + + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex); + } + return 0; + } +} diff --git a/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs new file mode 100644 index 0000000..061a3b4 --- /dev/null +++ b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs @@ -0,0 +1,11 @@ +using Spectre.Console.Cli; +using System.ComponentModel; + +namespace BlazeKit.CLI.Commands.Run +{ + public sealed class TailwindSettings : RunSettings + { + // [CommandOption("-p|--project ")] + // public string Project { get; set; } + } +} diff --git a/src/BlazeKit.CLI/IProcess.cs b/src/BlazeKit.CLI/IProcess.cs index ccb2940..703943e 100644 --- a/src/BlazeKit.CLI/IProcess.cs +++ b/src/BlazeKit.CLI/IProcess.cs @@ -4,5 +4,6 @@ namespace BlazeKit; public interface IProcess { + void Input(T cmd); Process Run(); } diff --git a/src/BlazeKit.CLI/Program.cs b/src/BlazeKit.CLI/Program.cs index c41b675..41ee58f 100644 --- a/src/BlazeKit.CLI/Program.cs +++ b/src/BlazeKit.CLI/Program.cs @@ -3,11 +3,13 @@ using BlazeKit.CLI.Commands.Add; using BlazeKit.CLI.Commands.Build; using BlazeKit.CLI.Commands.New; +using BlazeKit.CLI.Commands.Run; using Spectre.Console; using Spectre.Console.Cli; +using System.Diagnostics; Console.OutputEncoding = System.Text.Encoding.UTF8; - +Debugger.Launch(); var app = new Spectre.Console.Cli.CommandApp(); app.Configure(config => @@ -16,8 +18,9 @@ config.AddCommand("new"); config.AddBranch("run", c => { c.AddCommand("dev"); - c.AddCommand("build"); + c.AddCommand("tailwind"); }); + config.AddCommand("build"); config.AddBranch("add", c => { c.AddCommand("tailwindcss"); }); diff --git a/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs b/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs index cb67a7f..4c9b760 100644 --- a/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs +++ b/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs @@ -1,19 +1,28 @@ using BlazeKit.CLI.Commands.New; using BlazeKit.CLI.Tasks.Utils; - +using Spectre.Console; +using BlazeKit.CLI; +using BlazeKit.CLI.Commands.Run; +using System.Diagnostics; namespace BlazeKit.CLI.Tasks.Tools { internal class TskDotNetWatch : TskEnveleope { - public TskDotNetWatch(RunSettings settings,Action output, CancellationTokenSource cancel) : base(() => + public TskDotNetWatch(DevSettings settings,Action output, CancellationTokenSource cancel) : base(() => { + var watchProcess = + new ExecCliCommand( + "dotnet", + (msg) => output(msg), + info => info.EnvironmentVariables.Add("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "True"), + "watch --non-interactive", settings.Dotnet ?? "" + ); + + + return new CancableProcess( - new ExecCliCommand( - "dotnet", (msg) => output(msg), - // "watch", settings.Dotnet ?? "--non-interactive" - "watch", settings.Dotnet ?? "" - ), + watchProcess, cancel.Token ).Invoke(); }) diff --git a/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs b/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs index e12092d..1dd9a70 100644 --- a/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs +++ b/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs @@ -5,13 +5,13 @@ namespace BlazeKit.CLI.Tasks.Tasks { internal class TskTailwindCss : TskEnveleope { - public TskTailwindCss(RunSettings settings, Action output, CancellationTokenSource cancel) : base(() => + public TskTailwindCss(Action output, CancellationTokenSource cancel, params string[] arguments) : base(() => { return new CancableProcess( new ExecCliCommand( "tailwindcss", (msg) => output(msg), - string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss + arguments ), cancel.Token ).Invoke(); diff --git a/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs b/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs index 118f6c1..12cc8dd 100644 --- a/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs +++ b/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Spectre.Console; using Yaapii.Atoms; using Yaapii.Atoms.Scalar; @@ -20,19 +21,21 @@ public ExecCliCommand(string command, Action output, Action { + var args = string.Join(" ", arguments); + AnsiConsole.Console.Debug($"Execute {command} {args}"); var process = new Process { StartInfo = { FileName = command, - Arguments = string.Join(" ", arguments), + Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, - WorkingDirectory = Directory.GetCurrentDirectory() + WorkingDirectory = Directory.GetCurrentDirectory(), }, EnableRaisingEvents = true }; @@ -55,11 +58,19 @@ public ExecCliCommand(string command, Action output, Action(T cmd) + { + AnsiConsole.Console.Debug($"Call Input with Command '{cmd}'"); + process.Value(). StandardInput.WriteLine(cmd); + } + public Process Run() { return process.Value(); diff --git a/src/BlazeKit.Core/Routing/Navigating.cs b/src/BlazeKit.Core/Routing/Navigating.cs deleted file mode 100644 index 4e3c440..0000000 --- a/src/BlazeKit.Core/Routing/Navigating.cs +++ /dev/null @@ -1,61 +0,0 @@ -// using BlazeKit.Reactivity.Signals; -// using Microsoft.AspNetCore.Components; -// using Microsoft.JSInterop; - -// namespace BlazeKit.Core.Routing -// { -// public class Navigating : SignalEnvelope -// { -// private readonly IDictionary scrollPositions; -// private readonly NavigationManager router; -// private readonly IJSRuntime jsRuntime; - -// public Navigating() : base(true) -// { } -// public Navigating(NavigationManager router, IJSRuntime jsRuntime) : base(false) -// { -// scrollPositions = new Dictionary(); -// if(router != null) -// { -// router.RegisterLocationChangingHandler(async ctx => -// { -// // strore scroll position -// var scrollPosition = await jsRuntime.InvokeAsync("getScrollPosition"); -// //Console.WriteLine($"Storing scroll position for {router.Uri}"); -// scrollPositions[router.Uri] = scrollPosition; -// this.Value = true; -// }); - -// router.LocationChanged += (sender, args) => -// { -// //// check if we have a scroll position for this route -// //if (scrollPositions.ContainsKey(args.Location)) -// //{ -// // // restore scroll position -// // var scrollPosition = scrollPositions[args.Location]; -// // jsRuntime.InvokeVoidAsync("setScrollPosition", scrollPosition.X, scrollPosition.Y); -// //} -// //Console.WriteLine(args.Location); -// this.Value = false; -// }; -// } - -// this.router = router; -// this.jsRuntime = jsRuntime; -// } - -// public void ApplyScrollPosition() -// { -// //Console.WriteLine($"Applying scroll position for {router.Uri}"); -// // check if we have a scroll position for this route -// if (scrollPositions.ContainsKey(router.Uri)) -// { -// // restore scroll position -// var scrollPosition = scrollPositions[router.Uri]; -// jsRuntime.InvokeVoidAsync("setScrollPosition", scrollPosition.X, scrollPosition.Y); -// } -// } - -// record ScrollPosition(float X, float Y); -// } -// } diff --git a/src/BlazeKit.Hydration/BKitData.razor b/src/BlazeKit.Hydration/BKitData.razor new file mode 100644 index 0000000..6256269 --- /dev/null +++ b/src/BlazeKit.Hydration/BKitData.razor @@ -0,0 +1,18 @@ +@code { + [Inject] + private DataHydrationContext HydrationContext { get; set; } + + protected override void OnInitialized() + { + // set update callback to render the component when data has been added to the data hydration context + HydrationContext.OnUpdate(() => + { + Console.WriteLine("Data has changed -> rerender"); + InvokeAsync(this.StateHasChanged); + }); + } +} +@if(!HydrationContext.IsEmpty()) +{ + @(new MarkupString($"")) +} diff --git a/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj b/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj index fa71b7a..8181b85 100644 --- a/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj +++ b/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj @@ -1,9 +1,13 @@ - + net8.0 enable enable + + + + diff --git a/src/BlazeKit.Hydration/DataHydrationContext.cs b/src/BlazeKit.Hydration/DataHydrationContext.cs index c9d813c..6d1cbb0 100644 --- a/src/BlazeKit.Hydration/DataHydrationContext.cs +++ b/src/BlazeKit.Hydration/DataHydrationContext.cs @@ -4,21 +4,29 @@ namespace BlazeKit.Hydration; public class DataHydrationContext { private bool initalized = false; - public DataHydrationContext(Func> loadData = null) + private Action? onUpdate = null; + private Dictionary hydrationData; + private readonly Func>? loadData; + + + public DataHydrationContext(Func>? loadData = null) { - _hydrationData = new Dictionary(); + hydrationData = new Dictionary(); this.loadData = loadData; } - private Dictionary _hydrationData; - private readonly Func> loadData; public void Add(string key, object value) { - if(_hydrationData.ContainsKey(key)) { - _hydrationData[key] = value; + if(hydrationData.ContainsKey(key)) { + hydrationData[key] = value; } else { - _hydrationData.Add(key, value); + hydrationData.Add(key, value); + } + + if(this.onUpdate != null) + { + this.onUpdate(); } } @@ -29,9 +37,19 @@ public async Task GetAsync(string key, T defaultValue) await LoadData(); } - if(_hydrationData.TryGetValue(key, out var tmpValue)) { - var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText()); - result = deserialzed; + if(OperatingSystem.IsBrowser()) + { + if (hydrationData.TryGetValue(key, out var tmpValue)) + { + var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText(),new JsonSerializerOptions() { IncludeFields = true}); + result = deserialzed; + } + } else + { + if(hydrationData.TryGetValue(key, out var tmpValue)) { + //var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText(),new JsonSerializerOptions() { IncludeFields = true}); + result = (T)tmpValue; + } } return result; } @@ -42,7 +60,7 @@ public bool TryGet(string key, out T value) LoadData(); } value = default; - if(_hydrationData.TryGetValue(key, out var tmpValue)) { + if(hydrationData.TryGetValue(key, out var tmpValue)) { // check if tmpValue is of type T if(tmpValue is T) { value = (T)tmpValue; @@ -57,28 +75,30 @@ public bool TryGet(string key, out T value) } public string Serialized() { - return System.Text.Json.JsonSerializer.Serialize(_hydrationData); + return System.Text.Json.JsonSerializer.Serialize(hydrationData); + } + + + internal void OnUpdate(Action onUpdate) + { + this.onUpdate = onUpdate; + } + + internal bool IsEmpty() + { + return this.hydrationData.Keys.Count == 0; } private async Task LoadData() { // check if initalized if not hydrate data from dom - Console.WriteLine("Loading page data"); var data = System.Text.Json.JsonSerializer.Deserialize>(await loadData()); foreach(var item in data) { - if(_hydrationData.ContainsKey(item.Key)) { - _hydrationData[item.Key] = item.Value; + if(hydrationData.ContainsKey(item.Key)) { + hydrationData[item.Key] = item.Value; } else { - _hydrationData.Add(item.Key, item.Value); + hydrationData.Add(item.Key, item.Value); } } - // if(!initalized) { - // var data = System.Text.Json.JsonSerializer.Deserialize>(await loadData()); - // foreach(var item in data) { - // _hydrationData.Add(item.Key, item.Value); - // } - // initalized = true; - // } - return true; } -} \ No newline at end of file +} diff --git a/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj b/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj index 591994c..35b094f 100644 --- a/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj +++ b/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj @@ -13,19 +13,23 @@ - + - + + + + + diff --git a/src/BlazeKit.Static/BlazeKit.Static.csproj b/src/BlazeKit.Static/BlazeKit.Static.csproj index 6bcba89..21f0444 100644 --- a/src/BlazeKit.Static/BlazeKit.Static.csproj +++ b/src/BlazeKit.Static/BlazeKit.Static.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,11 @@ + + + diff --git a/src/BlazeKit.Static/BlazorRenderer.cs b/src/BlazeKit.Static/BlazorRenderer.cs index cf5f66d..91e23e1 100644 --- a/src/BlazeKit.Static/BlazorRenderer.cs +++ b/src/BlazeKit.Static/BlazorRenderer.cs @@ -2,8 +2,10 @@ using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.HtmlRendering; +using Microsoft.AspNetCore.Html; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Text.Encodings.Web; namespace BlazeKit.Static; #pragma warning disable BL0006 // Do not use RenderTree types @@ -12,7 +14,7 @@ public sealed class BlazorRenderer : Renderer { private readonly Lazy htmlRenderer; - public BlazorRenderer(ServiceProvider serviceProvider) : this( + public BlazorRenderer(IServiceProvider serviceProvider) : this( () => new HtmlRenderer( serviceProvider, @@ -22,13 +24,13 @@ public BlazorRenderer(ServiceProvider serviceProvider) : this( ) { } - public BlazorRenderer(HtmlRenderer htmlRenderer,ServiceProvider serviceProvider) :this( + public BlazorRenderer(HtmlRenderer htmlRenderer,IServiceProvider serviceProvider) :this( () => htmlRenderer, serviceProvider ) { } - public BlazorRenderer(Func htmlRenderer,ServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService()) + public BlazorRenderer(Func htmlRenderer, IServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService()) { this.htmlRenderer = new Lazy(htmlRenderer); } @@ -84,21 +86,90 @@ private Task RenderComponent(ParameterView parameters) where T : ICom return htmlRenderer.Value.Dispatcher.InvokeAsync(async () => { HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(parameters); + await output.QuiescenceTask; return output.ToHtmlString(); }); } + //private Task RenderComponent(Type componentType, ParameterView parameters) + //{ + // // Use the default dispatcher to invoke actions in the context of the + // // static HTML renderer and return as a string + // return htmlRenderer.Value.Dispatcher.InvokeAsync(async () => + // { + // HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(componentType, parameters); + // await output.QuiescenceTask; + + // return output.ToHtmlString(); + // }); + //} + private Task RenderComponent(Type componentType, ParameterView parameters) { // Use the default dispatcher to invoke actions in the context of the // static HTML renderer and return as a string return htmlRenderer.Value.Dispatcher.InvokeAsync(async () => { - HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(componentType, parameters); + var output = this.htmlRenderer.Value.BeginRenderingComponent(componentType, parameters); + await output.QuiescenceTask; return output.ToHtmlString(); }); } + /// + /// HTML content which can be written asynchronously to a TextWriter. + /// + public interface IHtmlAsyncContent : IHtmlContent + { + /// + /// Writes the content to the specified . + /// + /// The to which the content is written. + ValueTask WriteToAsync(TextWriter writer); + } + + // An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response. + // We don't construct the actual HTML until we receive the call to WriteTo. + public class PrerenderedComponentHtmlContent : IHtmlAsyncContent + { + private readonly Dispatcher? _dispatcher; + private readonly HtmlRootComponent? _htmlToEmitOrNull; + + public static PrerenderedComponentHtmlContent Empty { get; } + = new PrerenderedComponentHtmlContent(null, default); + + public PrerenderedComponentHtmlContent( + Dispatcher? dispatcher, // If null, we're only emitting the markers + HtmlRootComponent? htmlToEmitOrNull) + { + _dispatcher = dispatcher; + _htmlToEmitOrNull = htmlToEmitOrNull; + } + + public async ValueTask WriteToAsync(TextWriter writer) + { + if (_dispatcher is null) + { + WriteTo(writer, HtmlEncoder.Default); + } + else + { + await _dispatcher.InvokeAsync(() => WriteTo(writer, HtmlEncoder.Default)); + } + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (_htmlToEmitOrNull is { } htmlToEmit) + { + htmlToEmit.WriteHtmlTo(writer); + } + } + + public Task QuiescenceTask => + _htmlToEmitOrNull.HasValue ? _htmlToEmitOrNull.Value.QuiescenceTask : Task.CompletedTask; + } + } #pragma warning restore BL0006 // Do not use RenderTree types diff --git a/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs b/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs new file mode 100644 index 0000000..6f137e3 --- /dev/null +++ b/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs @@ -0,0 +1,64 @@ +using System.Linq.Expressions; +using BlazeKit.Static.Utils; +namespace BlazeKit.Static.ContentCollections; + +/// +/// A collection of content items +/// +public abstract class ContentCollectionEnvelope : IContentCollection +{ + private readonly string name; + private readonly Lazy> list; + + /// + /// A collection of content items + /// + public ContentCollectionEnvelope(string collectionName, Func cast) : this( + collectionName, + Path.Combine("content", collectionName), + cast, + schema => true + ) + { } + + /// + /// A collection of content items + /// + public ContentCollectionEnvelope(string collectionName, Func cast, Func filter) : this( + collectionName, + Path.Combine("content", collectionName), + cast, + filter + ) + { } + + /// + /// A collection of content items + /// + public ContentCollectionEnvelope(string collectionName, string contentDirectory, Func cast, Func filter) + { + this.name = collectionName; + this.list = new Lazy>(() => { + + // read all files from the content directory + // and parse them into BlogSchema objects + // use Markdig and Frontmatter extension + var markdownFiles = Directory.GetFiles(contentDirectory, "*.md"); + var items = markdownFiles + .Select(File.ReadAllText) + .Select(md => { + var schema = cast(md); + schema.Content = md.Html(); + return schema; + }) + .Where(filter) + .ToList(); + return items.Cast().ToList(); + }); + } + public string Name => name; + + public IEnumerable Items => this.list.Value; + + public abstract string Route(ISchema item); +} diff --git a/src/BlazeKit.Static/ContentCollections/IContentCollection.cs b/src/BlazeKit.Static/ContentCollections/IContentCollection.cs new file mode 100644 index 0000000..b9a12f5 --- /dev/null +++ b/src/BlazeKit.Static/ContentCollections/IContentCollection.cs @@ -0,0 +1,9 @@ +namespace BlazeKit.Static.ContentCollections; + +public interface IContentCollection +{ + string Name { get;} + IEnumerable Items { get; } + + string Route(ISchema item); +} diff --git a/src/BlazeKit.Static/ContentCollections/ISchema.cs b/src/BlazeKit.Static/ContentCollections/ISchema.cs new file mode 100644 index 0000000..3eeffa2 --- /dev/null +++ b/src/BlazeKit.Static/ContentCollections/ISchema.cs @@ -0,0 +1,5 @@ +namespace BlazeKit.Static.ContentCollections; + +public interface ISchema { + string Content { get; set; } +} diff --git a/src/BlazeKit.Static/IStaticServiceCollection.cs b/src/BlazeKit.Static/IStaticServiceCollection.cs new file mode 100644 index 0000000..9fb82e9 --- /dev/null +++ b/src/BlazeKit.Static/IStaticServiceCollection.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace BlazeKit.Static; + +/// +/// A static service collection that is used to build a static site +/// +public interface IStaticServiceCollection +{ + /// + /// A static service collection that is used to build a static site + /// + IServiceCollection Services(); +} diff --git a/src/BlazeKit.Static/StaticSiteGenerator.cs b/src/BlazeKit.Static/StaticSiteGenerator.cs index f2f0504..30388e4 100644 --- a/src/BlazeKit.Static/StaticSiteGenerator.cs +++ b/src/BlazeKit.Static/StaticSiteGenerator.cs @@ -1,5 +1,6 @@ using System.Reflection; using BlazeKit.Hydration; +using BlazeKit.Static.ContentCollections; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; @@ -56,61 +57,176 @@ public StaticSiteGenerator(string outputDirectory, string wwwrootContent, Func>(routes); } - public void Build() + public Task Build() { var rootComponent = this.assembly.GetExportedTypes().FirstOrDefault(t => t.Name == "Index"); if(rootComponent == null) { - throw new Exception($"No root component found name 'Index' in assembly '{assembly.Location}' to build static site."); + throw new Exception($"No root component found with name 'Index' in assembly '{assembly.Location}' to build static site."); } - Build(rootComponent); + IServiceCollection serviceCollection = new ServiceCollection(); + // find a type which implements the BlazeKit.Static.IStaticServiceCollection interface from the assembly + var staticServiceCollectionType = this.assembly.GetExportedTypes().FirstOrDefault(t => t.GetInterfaces().Contains(typeof(IStaticServiceCollection))); + if(staticServiceCollectionType != null) + { + // create an instance of the type + Console.WriteLine($"Using static service collection: {staticServiceCollectionType.FullName}"); + var staticServiceCollection = Activator.CreateInstance(staticServiceCollectionType) as IStaticServiceCollection; + if(staticServiceCollection == null) + { + throw new Exception($"Failed to create instance of type '{staticServiceCollectionType.FullName}' from assembly '{this.assembly.Location}'"); + } else { + serviceCollection = staticServiceCollection.Services(); + } + } + return Build(rootComponent, serviceCollection); } - public async void Build(Type rootComponent) + public Task Build(Type rootComponent, IServiceCollection serviceCollection) { - // copy wwwroot - var source = Path.Combine(this.wwwrootContent); - var destination = Path.Combine(this.outputDirectory); - Console.WriteLine($"Copying wwwroot from {source} to {destination}"); - CopyDirectory(source, destination); - var serviceCollection = new ServiceCollection(); - - var routeManager = new StaticNavigationManager(); - serviceCollection.AddLogging(); - serviceCollection.AddSingleton(new BKitHostEnvironment("Production")); - serviceCollection.AddSingleton(routeManager); - serviceCollection.AddSingleton(new FkJsRuntime()); - serviceCollection.AddSingleton(new FkNavigationInterception()); - serviceCollection.AddSingleton(new FkScrollToLocationHash()); - serviceCollection.AddSingleton(new StaticErrorBoundaryLogger()); - serviceCollection.AddSingleton(new DataHydrationContext()); - foreach(var route in this.routes.Value) + return Task.Run(async () => { - try + // copy wwwroot + var source = Path.Combine(this.wwwrootContent); + var destination = Path.Combine(this.outputDirectory); + Console.WriteLine($"Copying wwwroot from {source} to {destination}"); + CopyDirectory(source, destination); + // var serviceCollection = new ServiceCollection(); + + var routeManager = new StaticNavigationManager(); + serviceCollection.AddLogging(); + serviceCollection.AddSingleton(new BKitHostEnvironment("Production")); + serviceCollection.AddSingleton(routeManager); + serviceCollection.AddSingleton(new FkJsRuntime()); + serviceCollection.AddSingleton(new FkNavigationInterception()); + serviceCollection.AddSingleton(new FkScrollToLocationHash()); + serviceCollection.AddSingleton(new StaticErrorBoundaryLogger()); + //serviceCollection.AddScoped(); + foreach (var route in this.routes.Value) { - Console.WriteLine($"Building route: {route}"); - routeManager.NavigateTo(route, forceLoad: true); - var html = - await + try + { + Console.WriteLine($"Building route: {route}"); + + // check if route has dynamic parameters + if(route.Contains("{")) + { + Console.WriteLine($"Route has dynamic parameters: {route}"); + // get the route template + var routeTemplate = route.Split('/')[0]; + // get all types which implememnt IContentCollection + var t = typeof(ISchema); + var contentCollectionTypes = + this.assembly.GetExportedTypes() + .Where(t => t.GetInterfaces().Contains(typeof(IContentCollection))); + + // create instaces of the types + var contentCollections = contentCollectionTypes.Select(t => Activator.CreateInstance(t) as IContentCollection); + // find the content collection which matches the route template + var contentCollection = contentCollections.FirstOrDefault(c => c.Name.Equals(routeTemplate, StringComparison.InvariantCultureIgnoreCase)); + // render each item of the content colltection + foreach(var item in contentCollection.Items) + { + var routeWithParams = contentCollection.Route(item); + Console.WriteLine($"Building route: {routeWithParams}"); + Prerender(routeWithParams, routeManager, rootComponent, serviceCollection); + } + + continue; + } + var spv = serviceCollection.BuildServiceProvider(); + var scoped = spv.CreateScope(); + var serviceProvider = scoped.ServiceProvider; + + // get the type which from this.assembly which has a route parameter which matches the route + var shouldPreRender = ShouldBePreRender(route); + if(!shouldPreRender) + { + Console.WriteLine($"Skipping '{route}' for pre-render due to 'prerender = {false}'"); + continue; + } + + //var route2 = "/loadtest"; + routeManager.NavigateTo(route, forceLoad: true); + //var html = + // await + // new BlazorRenderer( + // new HtmlRenderer( + // serviceCollection.BuildServiceProvider(), + // serviceCollection.BuildServiceProvider() + // .GetRequiredService() + // ), + // serviceCollection.BuildServiceProvider() + // ) + // .RenderComponent(rootComponent); + var renderer = new BlazorRenderer( - new HtmlRenderer( - serviceCollection.BuildServiceProvider(), - serviceCollection.BuildServiceProvider() - .GetRequiredService() - ), - serviceCollection.BuildServiceProvider() - ) - .RenderComponent(rootComponent); - var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower(); - GeneratePage(directory, html); + new HtmlRenderer( + serviceProvider, + serviceProvider + .GetRequiredService() + ), + serviceProvider + ); + var html = + await renderer.RenderComponent(rootComponent); + + + var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower(); + GeneratePage(directory, html); + } + catch (System.Exception ex) + { + Console.WriteLine($"Failed to build route: {route} {ex.ToString()}"); + throw; + } } - catch (System.Exception ex) + return Task.CompletedTask; + }); + } + + private async void Prerender(string route, NavigationManager routeManager, Type rootComponent, IServiceCollection serviceCollection) { + routeManager.NavigateTo(route, forceLoad: true); + var html = + await + new BlazorRenderer( + new HtmlRenderer( + serviceCollection.BuildServiceProvider(), + serviceCollection.BuildServiceProvider() + .GetRequiredService() + ), + serviceCollection.BuildServiceProvider() + ) + .RenderComponent(rootComponent); + var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower(); + GeneratePage(directory, html); + } + + private bool ShouldBePreRender(string route, bool fallback = true) + { + var result = fallback; + var page = + this.assembly + .GetExportedTypes() + .Where(t => t.GetCustomAttributes(typeof(RouteAttribute), true).Count() > 0) + .Where(t => + { + var routeValue = t.GetCustomAttributes(typeof(RouteAttribute), true).FirstOrDefault() as RouteAttribute; + + return routeValue.Template.Equals(route, StringComparison.InvariantCultureIgnoreCase); + }) + .FirstOrDefault(); + if (page != null) + { + var prerender = page.GetField("prerender", BindingFlags.Static | BindingFlags.NonPublic); + if(prerender != null) { - Console.WriteLine($"Failed to build route: {route} {ex.ToString()}"); - throw; + var shouldPreRender = (bool)prerender.GetValue(null); + result = shouldPreRender; } } + return result; } private void GeneratePage(string directory, string htmlContent) diff --git a/src/BlazeKit.Static/Utils/MarkdownExtensions.cs b/src/BlazeKit.Static/Utils/MarkdownExtensions.cs new file mode 100644 index 0000000..10efdea --- /dev/null +++ b/src/BlazeKit.Static/Utils/MarkdownExtensions.cs @@ -0,0 +1,55 @@ +using Markdig; +using Markdig.Extensions.Yaml; +using Markdig.Syntax; +using YamlDotNet.Serialization; + +namespace BlazeKit.Static.Utils; + +public static class MarkdownExtensions +{ + private static readonly IDeserializer YamlDeserializer = + new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + + private static readonly MarkdownPipeline Pipeline + = new MarkdownPipelineBuilder() + .UseYamlFrontMatter() + .Build(); + + public static T? GetFrontMatter(this string markdown) + { + var document = Markdown.Parse(markdown, Pipeline); + var block = document + .Descendants() + .FirstOrDefault(); + + if (block == null) + return default; + + var yaml = + block + // this is not a mistake + // we have to call .Lines 2x + .Lines // StringLineGroup[] + .Lines // StringLine[] + .OrderByDescending(x => x.Line) + .Select(x => $"{x}\n") + .ToList() + .Select(x => x.Replace("---", string.Empty)) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Aggregate((s, agg) => agg + s); + + // Console.WriteLine(yaml); + // var deserializer = new DeserializerBuilder().Build(); + // var result = deserializer.Deserialize>(yaml); + // Console.WriteLine(result["title"]); + // Console.WriteLine(result["author"]); + return YamlDeserializer.Deserialize(yaml); + } + + public static string Html(this string markdown) + { + return Markdown.ToHtml(markdown,Pipeline); + } +} diff --git a/src/BlazeKit.Web/BlazeKit.Web.csproj b/src/BlazeKit.Web/BlazeKit.Web.csproj new file mode 100644 index 0000000..db436c5 --- /dev/null +++ b/src/BlazeKit.Web/BlazeKit.Web.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/BlazeKit.Web/Component1.razor b/src/BlazeKit.Web/Component1.razor new file mode 100644 index 0000000..bb3e792 --- /dev/null +++ b/src/BlazeKit.Web/Component1.razor @@ -0,0 +1,3 @@ +
+ This component is defined in the BlazeKit.Web library. +
diff --git a/src/BlazeKit.Web/Component1.razor.css b/src/BlazeKit.Web/Component1.razor.css new file mode 100644 index 0000000..c6afca4 --- /dev/null +++ b/src/BlazeKit.Web/Component1.razor.css @@ -0,0 +1,6 @@ +.my-component { + border: 2px dashed red; + padding: 1em; + margin: 1em 0; + background-image: url('background.png'); +} diff --git a/src/BlazeKit.Web/Components/ClientHydrateMode.cs b/src/BlazeKit.Web/Components/ClientHydrateMode.cs new file mode 100644 index 0000000..58e051c --- /dev/null +++ b/src/BlazeKit.Web/Components/ClientHydrateMode.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace BlazeKit.Web.Components; + +/// +/// Determines when a interactive component should be hydrated on the client +/// +public enum ClientHydrateMode +{ + /// + /// Hydrates the component as soon as possible + /// + Load, + /// + /// Hydrates the component when the browser is idle + /// + Idle, + /// + /// Hydrates the component when the user hovers over it + /// + Hover, + /// + /// Hydrates the component when it is visible in the viewport + /// + Visible, + /// + /// Does not hydrate the component + /// The component will be rendered as a static HTML element + /// + None +} diff --git a/src/BlazeKit.Web/Components/IslandComponent.cs b/src/BlazeKit.Web/Components/IslandComponent.cs new file mode 100644 index 0000000..3fe7fde --- /dev/null +++ b/src/BlazeKit.Web/Components/IslandComponent.cs @@ -0,0 +1,205 @@ +using BlazeKit.Abstraction; +using BlazeKit.Hydration; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace BlazeKit.Web.Components; + + +public abstract class IslandComponent : IslandComponent +{ + +} + +public abstract class IslandComponent : IComponent, IHandleEvent, IReactiveComponent +{ + [Parameter] public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None; + + [Inject] required public DataHydrationContext HydrationContext { get; init; } + [Inject] private Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; init; } + + private RenderFragment renderFragment; + private RenderHandle renderHandle; + private TResult data; + private string dataKey; + private string prerenderId = Guid.NewGuid().ToString(); + private string locationhash = Guid.NewGuid().ToString(); + + + /// + /// The data which has been loaded with LoadAsync. + /// If the method is not overwritten by the derived class the + /// page data will be null. + /// + [CascadingParameter(Name = "PageData")] + public TResult PageData + { + get + { + return data; + } + set + { + data = value; + } + } + + public IslandComponent(/*string dataKey*/) + { + renderFragment = BuildRenderTree; + this.dataKey = "pagedata"; + } + + public void Attach(RenderHandle renderHandle) + { + this.renderHandle = renderHandle; + } + public async Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + if(IsServer()) + { + // render the component decorated with the wasm marker + renderHandle.Render(Render); + } else + { + // Render component without the wasm marker + var data = await LoadHydratedPageDataAsync(default(TResult)); + if (data == null) + { + throw new InvalidOperationException($"Could not load page data for '{this.dataKey}'"); + } + else + { + this.data = data; + } + renderHandle.Render(renderFragment); + } + + if(OperatingSystem.IsBrowser()) + { + OnMount(); + } + } + + /// + /// Renders the component to the supplied . + /// + /// A that will receive the render output. + protected virtual void BuildRenderTree(RenderTreeBuilder builder) + { + // Developers can either override this method in derived classes, or can use Razor + // syntax to define a derived class and have the compiler generate the method. + + // Other code within this class should *not* invoke BuildRenderTree directly, + // but instead should invoke the _renderFragment field. + } + + /// + /// This method is called slient-side when the component has been mounted/rendered into the DOM + /// + protected virtual void OnMount() + { } + + + void Render(RenderTreeBuilder builder) + { + var needsHydration = Client != ClientHydrateMode.None; + if (!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase)) + { + // render marker around the balzekit div. + // This will remove the div when the component is hydrated on the client + if (needsHydration) + { + builder.AddMarkupContent(0, OpenWasmComponent()); + } + + builder.OpenElement(1, "div"); + builder.AddAttribute(2, "blazekit-id", prerenderId); + builder.AddAttribute(3, "client", Client.ToString().ToLower()); + + builder.OpenComponent>(0); + builder.AddComponentParameter(1, "Value", this.PageData); + builder.AddComponentParameter(2, "ChildContent", renderFragment); + builder.CloseComponent(); + + //this.BuildRenderTree(builder); + builder.CloseElement(); + + if (needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + } + else + { + // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client + // we can use this div to visualize the component boundaries in the browser + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "blazekit-id", prerenderId); + builder.AddAttribute(2, "client", Client.ToString().ToLower()); + if (needsHydration) + builder.AddMarkupContent(0, OpenWasmComponent()); + + + builder.OpenComponent>(0); + builder.AddComponentParameter(1, "Value", this.PageData); + builder.AddComponentParameter(2, "ChildContent", (RenderFragment)((builder2) => BuildRenderTree(builder2))); + builder.CloseComponent(); + + + if (needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + builder.CloseElement(); + } + } + + protected Task LoadHydratedPageDataAsync(T fallback) + { + return HydrationContext.GetAsync(dataKey, fallback); + } + + private string OpenWasmComponent() + { + var typeName = this.GetType().FullName; + var assemblyName = $"{this.GetType().Assembly.GetName().Name}"; + return @$""; + } + + private string CloseWasmComponent() + { + return @$""; + } + private bool IsServer() + { + return !OperatingSystem.IsBrowser(); + } + + public Task HandleEventAsync(EventCallbackWorkItem callback, object? arg) + { + var task = callback.InvokeAsync(arg); + var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && + task.Status != TaskStatus.Canceled; + + // After each event, we synchronously re-render (unless !ShouldRender()) + // This just saves the developer the trouble of putting "StateHasChanged();" + // at the end of every event callback. + Update(); + + return Task.CompletedTask; + + //return shouldAwaitTask ? + // CallStateHasChangedOnAsyncCompletion(task) : + // Task.CompletedTask; + } + + public void Update() + { + renderHandle.Render(renderFragment); + } +} diff --git a/src/BlazeKit.Web/Components/IslandComponentBase.cs b/src/BlazeKit.Web/Components/IslandComponentBase.cs new file mode 100644 index 0000000..dcad829 --- /dev/null +++ b/src/BlazeKit.Web/Components/IslandComponentBase.cs @@ -0,0 +1,423 @@ +using BlazeKit.Abstraction; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace BlazeKit.Web.Components; + +// +// Summary: +// Optional base class for components. Alternatively, components may implement Microsoft.AspNetCore.Components.IComponent +// directly. +public abstract class IslandComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReactiveComponent +{ + [Parameter] + public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None; + + [Inject] + public Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; set; } + + private readonly RenderFragment _renderFragment; + + private RenderHandle _renderHandle; + + private bool _initialized; + + private bool _hasNeverRendered = true; + + private bool _hasPendingQueuedRender; + + private bool _hasCalledOnAfterRender; + + private string prerenderId = Guid.NewGuid().ToString(); + private string locationhash = Guid.NewGuid().ToString(); + + + + // + // Summary: + // Constructs an instance of Microsoft.AspNetCore.Components.ComponentBase. + public IslandComponentBase() + { + _renderFragment = delegate (RenderTreeBuilder builder) + { + _hasPendingQueuedRender = false; + _hasNeverRendered = false; + // BuildRenderTree(builder); + if(!OperatingSystem.IsBrowser()) { + // render the component decorated with the wasm marker + Render(builder); + } + else { + // Render component without the wasm marker + BuildRenderTree(builder); + } + + }; + } + + void Render(RenderTreeBuilder builder) + { + var needsHydration = Client != ClientHydrateMode.None; + if(!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase)) + { + // render marker around the balzekit div. + // This will remove the div when the component is hydrated on the client + if(needsHydration) + { + builder.AddMarkupContent(0, OpenWasmComponent()); + } + + builder.OpenElement(1, "div"); + builder.AddAttribute(2, "blazekit-id", prerenderId); + builder.AddAttribute(3, "client", Client.ToString().ToLower()); + this.BuildRenderTree(builder); + builder.CloseElement(); + + if(needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + } else { + // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client + // we can use this div to visualize the component boundaries in the browser + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "blazekit-id", prerenderId); + builder.AddAttribute(2, "client", Client.ToString().ToLower()); + if(needsHydration) + builder.AddMarkupContent(0, OpenWasmComponent()); + + this.BuildRenderTree(builder); + if(needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + builder.CloseElement(); + } + } + + + private string OpenWasmComponent() + { + var typeName = this.GetType().FullName; + var assemblyName = $"{this.GetType().Assembly.GetName().Name}"; + return @$""; + } + + private string CloseWasmComponent() + { + return @$""; + } + + // Summary: + // Renders the component to the supplied Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder. + // + // + // Parameters: + // builder: + // A Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder that will receive + // the render output. + protected virtual void BuildRenderTree(RenderTreeBuilder builder){} + + // + // Summary: + // Method invoked when the component is ready to start, having received its initial + // parameters from its parent in the render tree. + protected virtual void OnInitialized() + { + } + + // + // Summary: + // Method invoked when the component is ready to start, having received its initial + // parameters from its parent in the render tree. Override this method if you will + // perform an asynchronous operation and want the component to refresh when that + // operation is completed. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + protected virtual Task OnInitializedAsync() + { + return Task.CompletedTask; + } + + // + // Summary: + // Method invoked when the component has received parameters from its parent in + // the render tree, and the incoming values have been assigned to properties. + protected virtual void OnParametersSet() + { + } + + // + // Summary: + // Method invoked when the component has received parameters from its parent in + // the render tree, and the incoming values have been assigned to properties. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + protected virtual Task OnParametersSetAsync() + { + return Task.CompletedTask; + } + + // + // Summary: + // Notifies the component that its state has changed. When applicable, this will + // cause the component to be re-rendered. + protected void StateHasChanged() + { + if (_hasPendingQueuedRender || (!_hasNeverRendered && !ShouldRender() && !_renderHandle.IsRenderingOnMetadataUpdate)) + { + return; + } + + _hasPendingQueuedRender = true; + try + { + _renderHandle.Render(_renderFragment); + } + catch + { + _hasPendingQueuedRender = false; + throw; + } + } + + // + // Summary: + // Returns a flag to indicate whether the component should render. + protected virtual bool ShouldRender() + { + return true; + } + + // + // Summary: + // Method invoked after each time the component has rendered interactively and the + // UI has finished updating (for example, after elements have been added to the + // browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields will + // be populated by the time this runs. This method is not invoked during prerendering + // or server-side rendering, because those processes are not attached to any live + // browser DOM and are already complete before the DOM is updated. + // + // Parameters: + // firstRender: + // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // has been invoked on this component instance; otherwise false. + // + // Remarks: + // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean) + // lifecycle methods are useful for performing interop, or interacting with values + // received from @ref. Use the firstRender parameter to ensure that initialization + // work is only performed once. + protected virtual void OnAfterRender(bool firstRender) + { + } + + // + // Summary: + // Method invoked after each time the component has been rendered interactively + // and the UI has finished updating (for example, after elements have been added + // to the browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields + // will be populated by the time this runs. This method is not invoked during prerendering + // or server-side rendering, because those processes are not attached to any live + // browser DOM and are already complete before the DOM is updated. Note that the + // component does not automatically re-render after the completion of any returned + // System.Threading.Tasks.Task, because that would cause an infinite render loop. + // + // + // Parameters: + // firstRender: + // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // has been invoked on this component instance; otherwise false. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + // + // Remarks: + // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean) + // lifecycle methods are useful for performing interop, or interacting with values + // received from @ref. Use the firstRender parameter to ensure that initialization + // work is only performed once. + protected virtual Task OnAfterRenderAsync(bool firstRender) + { + return Task.CompletedTask; + } + + // + // Summary: + // Executes the supplied work item on the associated renderer's synchronization + // context. + // + // Parameters: + // workItem: + // The work item to execute. + protected Task InvokeAsync(Action workItem) + { + return _renderHandle.Dispatcher.InvokeAsync(workItem); + } + + // + // Summary: + // Executes the supplied work item on the associated renderer's synchronization + // context. + // + // Parameters: + // workItem: + // The work item to execute. + protected Task InvokeAsync(Func workItem) + { + return _renderHandle.Dispatcher.InvokeAsync(workItem); + } + + // + // Summary: + // Treats the supplied exception as being thrown by this component. This will cause + // the enclosing ErrorBoundary to transition into a failed state. If there is no + // enclosing ErrorBoundary, it will be regarded as an exception from the enclosing + // renderer. This is useful if an exception occurs outside the component lifecycle + // methods, but you wish to treat it the same as an exception from a component lifecycle + // method. + // + // Parameters: + // exception: + // The System.Exception that will be dispatched to the renderer. + // + // Returns: + // A System.Threading.Tasks.Task that will be completed when the exception has finished + // dispatching. + protected Task DispatchExceptionAsync(Exception exception) + { + return _renderHandle.DispatchExceptionAsync(exception); + } + + void IComponent.Attach(RenderHandle renderHandle) + { + if (_renderHandle.IsInitialized) + { + throw new InvalidOperationException("The render handle is already set. Cannot initialize a ComponentBase more than once."); + } + + _renderHandle = renderHandle; + } + + // + // Summary: + // Sets parameters supplied by the component's parent in the render tree. + // + // Parameters: + // parameters: + // The parameters. + // + // Returns: + // A System.Threading.Tasks.Task that completes when the component has finished + // updating and rendering itself. + // + // Remarks: + // Parameters are passed when Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView) + // is called. It is not required that the caller supply a parameter value for all + // of the parameters that are logically understood by the component. + // + // The default implementation of Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView) + // will set the value of each property decorated with Microsoft.AspNetCore.Components.ParameterAttribute + // or Microsoft.AspNetCore.Components.CascadingParameterAttribute that has a corresponding + // value in the Microsoft.AspNetCore.Components.ParameterView. Parameters that do + // not have a corresponding value will be unchanged. + public virtual Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + if (!_initialized) + { + _initialized = true; + return RunInitAndSetParametersAsync(); + } + + return CallOnParametersSetAsync(); + } + + private async Task RunInitAndSetParametersAsync() + { + OnInitialized(); + Task task = OnInitializedAsync(); + if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled) + { + StateHasChanged(); + try + { + await task; + } + catch + { + if (!task.IsCanceled) + { + throw; + } + } + } + + await CallOnParametersSetAsync(); + } + + private Task CallOnParametersSetAsync() + { + OnParametersSet(); + Task task = OnParametersSetAsync(); + bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; + StateHasChanged(); + if (!num) + { + return Task.CompletedTask; + } + + return CallStateHasChangedOnAsyncCompletion(task); + } + + private async Task CallStateHasChangedOnAsyncCompletion(Task task) + { + try + { + await task; + } + catch + { + if (task.IsCanceled) + { + return; + } + + throw; + } + + StateHasChanged(); + } + + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) + { + Task task = callback.InvokeAsync(arg); + bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; + StateHasChanged(); + if (!num) + { + return Task.CompletedTask; + } + + return CallStateHasChangedOnAsyncCompletion(task); + } + + Task IHandleAfterRender.OnAfterRenderAsync() + { + bool firstRender = !_hasCalledOnAfterRender; + _hasCalledOnAfterRender = true; + OnAfterRender(firstRender); + return OnAfterRenderAsync(firstRender); + } + + public void Update() + { + StateHasChanged(); + } +} diff --git a/src/BlazeKit.Web/Components/IslandComponentBase2.cs b/src/BlazeKit.Web/Components/IslandComponentBase2.cs new file mode 100644 index 0000000..9f87571 --- /dev/null +++ b/src/BlazeKit.Web/Components/IslandComponentBase2.cs @@ -0,0 +1,452 @@ +using BlazeKit.Abstraction; +using BlazeKit.Hydration; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace BlazeKit.Web.Components; + +// +// Summary: +// Optional base class for components. Alternatively, components may implement Microsoft.AspNetCore.Components.IComponent +// directly. +public abstract class IslandComponentBase2 : IComponent, IHandleEvent, IHandleAfterRender, IReactiveComponent +{ + private TResult data; + private string dataKey = "pagedata"; + + [Inject] required public DataHydrationContext HydrationContext { get; init; } + + [Parameter] + public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None; + + [Inject] + public Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; set; } + + private readonly RenderFragment _renderFragment; + + private RenderHandle _renderHandle; + + private bool _initialized; + + private bool _hasNeverRendered = true; + + private bool _hasPendingQueuedRender; + + private bool _hasCalledOnAfterRender; + + private string prerenderId = Guid.NewGuid().ToString(); + private string locationhash = Guid.NewGuid().ToString(); + + /// + /// The data which has been loaded with LoadAsync. + /// If the method is not overwritten by the derived class the + /// page data will be null. + /// + [CascadingParameter(Name = "PageData")] + public TResult PageData + { + get + { + return data; + } + set + { + data = value; + } + } + + // + // Summary: + // Constructs an instance of Microsoft.AspNetCore.Components.ComponentBase. + public IslandComponentBase2() + { + _renderFragment = delegate (RenderTreeBuilder builder) + { + _hasPendingQueuedRender = false; + _hasNeverRendered = false; + // BuildRenderTree(builder); + if(!OperatingSystem.IsBrowser()) { + // render the component decorated with the wasm marker + Render(builder); + } + else { + // Render component without the wasm marker + BuildRenderTree(builder); + } + + }; + } + + void Render(RenderTreeBuilder builder) + { + var needsHydration = Client != ClientHydrateMode.None; + if(!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase)) + { + // render marker around the balzekit div. + // This will remove the div when the component is hydrated on the client + if(needsHydration) + { + builder.AddMarkupContent(0, OpenWasmComponent()); + } + + builder.OpenElement(1, "div"); + builder.AddAttribute(2, "blazekit-id", prerenderId); + builder.AddAttribute(3, "client", Client.ToString().ToLower()); + this.BuildRenderTree(builder); + builder.CloseElement(); + + if(needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + } else { + // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client + // we can use this div to visualize the component boundaries in the browser + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "blazekit-id", prerenderId); + builder.AddAttribute(2, "client", Client.ToString().ToLower()); + if(needsHydration) + builder.AddMarkupContent(0, OpenWasmComponent()); + + this.BuildRenderTree(builder); + if(needsHydration) + { + builder.AddMarkupContent(4, CloseWasmComponent()); + } + + builder.CloseElement(); + } + } + + + private string OpenWasmComponent() + { + var typeName = this.GetType().FullName; + var assemblyName = $"{this.GetType().Assembly.GetName().Name}"; + return @$""; + } + + private string CloseWasmComponent() + { + return @$""; + } + + // Summary: + // Renders the component to the supplied Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder. + // + // + // Parameters: + // builder: + // A Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder that will receive + // the render output. + protected virtual void BuildRenderTree(RenderTreeBuilder builder){} + + // + // Summary: + // Method invoked when the component is ready to start, having received its initial + // parameters from its parent in the render tree. + protected virtual void OnInitialized() + { + } + + // + // Summary: + // Method invoked when the component is ready to start, having received its initial + // parameters from its parent in the render tree. Override this method if you will + // perform an asynchronous operation and want the component to refresh when that + // operation is completed. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + protected virtual Task OnInitializedAsync() + { + return Task.CompletedTask; + } + + // + // Summary: + // Method invoked when the component has received parameters from its parent in + // the render tree, and the incoming values have been assigned to properties. + protected virtual void OnParametersSet() + { + } + + // + // Summary: + // Method invoked when the component has received parameters from its parent in + // the render tree, and the incoming values have been assigned to properties. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + protected virtual Task OnParametersSetAsync() + { + return Task.CompletedTask; + } + + // + // Summary: + // Notifies the component that its state has changed. When applicable, this will + // cause the component to be re-rendered. + protected void StateHasChanged() + { + if (_hasPendingQueuedRender || (!_hasNeverRendered && !ShouldRender() && !_renderHandle.IsRenderingOnMetadataUpdate)) + { + return; + } + + _hasPendingQueuedRender = true; + try + { + _renderHandle.Render(_renderFragment); + } + catch + { + _hasPendingQueuedRender = false; + throw; + } + } + + // + // Summary: + // Returns a flag to indicate whether the component should render. + protected virtual bool ShouldRender() + { + return true; + } + + // + // Summary: + // Method invoked after each time the component has rendered interactively and the + // UI has finished updating (for example, after elements have been added to the + // browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields will + // be populated by the time this runs. This method is not invoked during prerendering + // or server-side rendering, because those processes are not attached to any live + // browser DOM and are already complete before the DOM is updated. + // + // Parameters: + // firstRender: + // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // has been invoked on this component instance; otherwise false. + // + // Remarks: + // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean) + // lifecycle methods are useful for performing interop, or interacting with values + // received from @ref. Use the firstRender parameter to ensure that initialization + // work is only performed once. + protected virtual void OnAfterRender(bool firstRender) + { + } + + // + // Summary: + // Method invoked after each time the component has been rendered interactively + // and the UI has finished updating (for example, after elements have been added + // to the browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields + // will be populated by the time this runs. This method is not invoked during prerendering + // or server-side rendering, because those processes are not attached to any live + // browser DOM and are already complete before the DOM is updated. Note that the + // component does not automatically re-render after the completion of any returned + // System.Threading.Tasks.Task, because that would cause an infinite render loop. + // + // + // Parameters: + // firstRender: + // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // has been invoked on this component instance; otherwise false. + // + // Returns: + // A System.Threading.Tasks.Task representing any asynchronous operation. + // + // Remarks: + // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean) + // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean) + // lifecycle methods are useful for performing interop, or interacting with values + // received from @ref. Use the firstRender parameter to ensure that initialization + // work is only performed once. + protected virtual Task OnAfterRenderAsync(bool firstRender) + { + return Task.CompletedTask; + } + + // + // Summary: + // Executes the supplied work item on the associated renderer's synchronization + // context. + // + // Parameters: + // workItem: + // The work item to execute. + protected Task InvokeAsync(Action workItem) + { + return _renderHandle.Dispatcher.InvokeAsync(workItem); + } + + // + // Summary: + // Executes the supplied work item on the associated renderer's synchronization + // context. + // + // Parameters: + // workItem: + // The work item to execute. + protected Task InvokeAsync(Func workItem) + { + return _renderHandle.Dispatcher.InvokeAsync(workItem); + } + + // + // Summary: + // Treats the supplied exception as being thrown by this component. This will cause + // the enclosing ErrorBoundary to transition into a failed state. If there is no + // enclosing ErrorBoundary, it will be regarded as an exception from the enclosing + // renderer. This is useful if an exception occurs outside the component lifecycle + // methods, but you wish to treat it the same as an exception from a component lifecycle + // method. + // + // Parameters: + // exception: + // The System.Exception that will be dispatched to the renderer. + // + // Returns: + // A System.Threading.Tasks.Task that will be completed when the exception has finished + // dispatching. + protected Task DispatchExceptionAsync(Exception exception) + { + return _renderHandle.DispatchExceptionAsync(exception); + } + + void IComponent.Attach(RenderHandle renderHandle) + { + if (_renderHandle.IsInitialized) + { + throw new InvalidOperationException("The render handle is already set. Cannot initialize a ComponentBase more than once."); + } + + _renderHandle = renderHandle; + } + + // + // Summary: + // Sets parameters supplied by the component's parent in the render tree. + // + // Parameters: + // parameters: + // The parameters. + // + // Returns: + // A System.Threading.Tasks.Task that completes when the component has finished + // updating and rendering itself. + // + // Remarks: + // Parameters are passed when Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView) + // is called. It is not required that the caller supply a parameter value for all + // of the parameters that are logically understood by the component. + // + // The default implementation of Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView) + // will set the value of each property decorated with Microsoft.AspNetCore.Components.ParameterAttribute + // or Microsoft.AspNetCore.Components.CascadingParameterAttribute that has a corresponding + // value in the Microsoft.AspNetCore.Components.ParameterView. Parameters that do + // not have a corresponding value will be unchanged. + public virtual Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + if (!_initialized) + { + _initialized = true; + return RunInitAndSetParametersAsync(); + } + + return CallOnParametersSetAsync(); + } + + private async Task RunInitAndSetParametersAsync() + { + this.data = await LoadPageDataAsync(default(TResult)); + OnInitialized(); + Task task = OnInitializedAsync(); + if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled) + { + StateHasChanged(); + try + { + await task; + } + catch + { + if (!task.IsCanceled) + { + throw; + } + } + } + + await CallOnParametersSetAsync(); + } + + private Task CallOnParametersSetAsync() + { + OnParametersSet(); + Task task = OnParametersSetAsync(); + bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; + StateHasChanged(); + if (!num) + { + return Task.CompletedTask; + } + + return CallStateHasChangedOnAsyncCompletion(task); + } + + private async Task CallStateHasChangedOnAsyncCompletion(Task task) + { + try + { + await task; + } + catch + { + if (task.IsCanceled) + { + return; + } + + throw; + } + + StateHasChanged(); + } + + protected Task LoadPageDataAsync(T fallback) + { + return HydrationContext.GetAsync(dataKey, fallback); + } + + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) + { + Task task = callback.InvokeAsync(arg); + bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; + StateHasChanged(); + if (!num) + { + return Task.CompletedTask; + } + + return CallStateHasChangedOnAsyncCompletion(task); + } + + Task IHandleAfterRender.OnAfterRenderAsync() + { + bool firstRender = !_hasCalledOnAfterRender; + _hasCalledOnAfterRender = true; + OnAfterRender(firstRender); + return OnAfterRenderAsync(firstRender); + } + + public void Update() + { + StateHasChanged(); + } +} diff --git a/src/BlazeKit.Web/Components/PageComponentBase.cs b/src/BlazeKit.Web/Components/PageComponentBase.cs new file mode 100644 index 0000000..0224367 --- /dev/null +++ b/src/BlazeKit.Web/Components/PageComponentBase.cs @@ -0,0 +1,102 @@ +using BlazeKit.Hydration; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using System.Diagnostics.CodeAnalysis; + +namespace BlazeKit.Web.Components; +public abstract class PageComponentBase : IComponent where TResult : PageDataBase +{ + [Inject] required public DataHydrationContext HydrationContext { get; init; } + + private RenderFragment renderFragment; + private readonly string dataKey; + private RenderHandle renderHandle; + private TResult data; + + + /// + /// The data which has been loaded with LoadAsync. + /// If the method is not overwritten by the derived class the + /// page data will be null. + /// + public TResult PageData + { + get + { + return data; + } + set + { + data = value; + } + } + public PageComponentBase() + { + renderFragment = BuildRenderTree; + this.data = default(TResult); + this.dataKey = "pagedata"; + } + + public void Attach(RenderHandle renderHandle) + { + this.renderHandle = renderHandle; + } + public async Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + if(IsServer()) + { + var data = await this.ServerLoadAsync(); + if(data != null) + { + this.data = data; + // we add the data to the data hydration context to access it on client side + HydrationContext.Add(this.dataKey, data); + } + } + + renderHandle.Render(Render); + } + + /// + /// Renders the component to the supplied . + /// + /// A that will receive the render output. + protected virtual void BuildRenderTree(RenderTreeBuilder builder) + { + // Developers can either override this method in derived classes, or can use Razor + // syntax to define a derived class and have the compiler generate the method. + + // Other code within this class should *not* invoke BuildRenderTree directly, + // but instead should invoke the _renderFragment field. + } + + protected virtual Task ServerLoadAsync() + { + return Task.FromResult(default(TResult)); + } + + + void Render(RenderTreeBuilder builder) + { + // decorate the ChildContent with a CascadingValue contianing the PageData + builder.OpenComponent>(0); + builder.AddComponentParameter(1, "Value", this.PageData); + builder.AddComponentParameter(1, "Name", "PageData"); + builder.AddComponentParameter(2, "ChildContent", renderFragment); + builder.CloseComponent(); + } + + protected Task LoadPageDataAsync(T fallback) + { + return HydrationContext.GetAsync(this.dataKey, fallback); + } + + private bool IsServer() + { + return !OperatingSystem.IsBrowser(); + } + + +} diff --git a/src/BlazeKit.Website/Components/WebAssemblyLoadProgress.razor b/src/BlazeKit.Web/Components/WebAssemblyLoadProgress.razor similarity index 100% rename from src/BlazeKit.Website/Components/WebAssemblyLoadProgress.razor rename to src/BlazeKit.Web/Components/WebAssemblyLoadProgress.razor diff --git a/src/BlazeKit.Web/ExampleJsInterop.cs b/src/BlazeKit.Web/ExampleJsInterop.cs new file mode 100644 index 0000000..00fdaa9 --- /dev/null +++ b/src/BlazeKit.Web/ExampleJsInterop.cs @@ -0,0 +1,36 @@ +using Microsoft.JSInterop; + +namespace BlazeKit.Web; + +// This class provides an example of how JavaScript functionality can be wrapped +// in a .NET class for easy consumption. The associated JavaScript module is +// loaded on demand when first needed. +// +// This class can be registered as scoped DI service and then injected into Blazor +// components for use. + +public class ExampleJsInterop : IAsyncDisposable +{ + private readonly Lazy> moduleTask; + + public ExampleJsInterop(IJSRuntime jsRuntime) + { + moduleTask = new (() => jsRuntime.InvokeAsync( + "import", "./_content/BlazeKit.Web/exampleJsInterop.js").AsTask()); + } + + public async ValueTask Prompt(string message) + { + var module = await moduleTask.Value; + return await module.InvokeAsync("showPrompt", message); + } + + public async ValueTask DisposeAsync() + { + if (moduleTask.IsValueCreated) + { + var module = await moduleTask.Value; + await module.DisposeAsync(); + } + } +} diff --git a/src/BlazeKit.Web/PageDataBase.cs b/src/BlazeKit.Web/PageDataBase.cs new file mode 100644 index 0000000..f2f1b90 --- /dev/null +++ b/src/BlazeKit.Web/PageDataBase.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace BlazeKit.Web; + +public class PageDataBase +{ + public PageDataBase() + { + + } + [JsonIgnore] + public string Key => "pagedata"; + + public static PageDataBase Empty() + { + return new PageDataBase(); + } + +} diff --git a/src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs b/src/BlazeKit.Web/Utils/CssClass.cs similarity index 72% rename from src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs rename to src/BlazeKit.Web/Utils/CssClass.cs index ed7b389..a1b6c7a 100644 --- a/src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs +++ b/src/BlazeKit.Web/Utils/CssClass.cs @@ -1,18 +1,18 @@ -namespace BlazeKit.Web; -public sealed class ConditionalClasses +namespace BlazeKit.Web.Utils; +public sealed class CssClass { private IDictionary> classes; - public ConditionalClasses() + public CssClass() { classes = new Dictionary>(); } - public ConditionalClasses AddIf(string className, bool condition) + public CssClass AddIf(string className, bool condition) { return this.AddIf(className,() => condition); } - public ConditionalClasses AddIf(string className, Func condition) + public CssClass AddIf(string className, Func condition) { if(this.classes.ContainsKey(className)) { diff --git a/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs b/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs new file mode 100644 index 0000000..1b13cd3 --- /dev/null +++ b/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs @@ -0,0 +1,10 @@ +namespace BlazeKit.Web.Utils +{ + public static class OperatingSystemExtensions + { + public static bool IsServer(this OperatingSystem operatingSystem) + { + return OperatingSystem.IsBrowser() == false; + } + } +} diff --git a/src/BlazeKit.Web/_Imports.razor b/src/BlazeKit.Web/_Imports.razor new file mode 100644 index 0000000..7728512 --- /dev/null +++ b/src/BlazeKit.Web/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/src/BlazeKit.Web/wwwroot/background.png b/src/BlazeKit.Web/wwwroot/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e15a3bde6e2bdb380df6a0b46d7ed00bdeb0aaa8 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^x**KK1SGdsl%54rjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwr2>%=KS^ie7oTIEF;HpS|GCbyPusHSqiXaCu3qf)82(9Gq&mZq2{Kq}M*X&MWtJ zSi1Jo7ZzfImg%g=t(qo=wsSR2lZoP(Rj#3wacN=q0?Br(rXzgZEGK2$ID{|A=5S{xJEuzSH>!M+7wSY6hB<=-E^*n0W7 S8wY^CX7F_Nb6Mw<&;$S{dxtsz literal 0 HcmV?d00001 diff --git a/src/BlazeKit.Web/wwwroot/exampleJsInterop.js b/src/BlazeKit.Web/wwwroot/exampleJsInterop.js new file mode 100644 index 0000000..ea8d76a --- /dev/null +++ b/src/BlazeKit.Web/wwwroot/exampleJsInterop.js @@ -0,0 +1,6 @@ +// This is a JavaScript module that is loaded on demand. It can export any number of +// functions, and may import other JavaScript modules if required. + +export function showPrompt(message) { + return prompt(message, 'Type anything here'); +} diff --git a/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs b/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs new file mode 100644 index 0000000..2f9fb71 --- /dev/null +++ b/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace BlazeKit.Web; + +public class BKitHostEnvironment : IHostEnvironment +{ + public BKitHostEnvironment(string environmentName) + { + EnvironmentName = environmentName; + } + public string ApplicationName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public string ContentRootPath { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public string EnvironmentName { get;set;} +} diff --git a/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj b/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj index 37027fa..b441af1 100644 --- a/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj +++ b/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj @@ -4,7 +4,6 @@ net8.0 enable enable - BlazeKit.Website.Client Default true .blazekit @@ -13,16 +12,11 @@ + - - - Components\%(RecursiveDir)/%(FileName)%(Extension) - - - Components\%(RecursiveDir)/%(FileName)%(Extension) - - + +
diff --git a/src/BlazeKit.Website.Islands/ClientLoadMode.cs b/src/BlazeKit.Website.Islands/ClientLoadMode.cs deleted file mode 100644 index 6788b3d..0000000 --- a/src/BlazeKit.Website.Islands/ClientLoadMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; - -namespace BlazeKit.Islands; - - public enum ClientLoadMode -{ - Idle, - Hover, - Visible, - None -} diff --git a/src/BlazeKit.Website/Islands/Components/Counter.razor b/src/BlazeKit.Website.Islands/Components/Counter.razor similarity index 96% rename from src/BlazeKit.Website/Islands/Components/Counter.razor rename to src/BlazeKit.Website.Islands/Components/Counter.razor index a60bc95..0e4c343 100644 --- a/src/BlazeKit.Website/Islands/Components/Counter.razor +++ b/src/BlazeKit.Website.Islands/Components/Counter.razor @@ -1,5 +1,6 @@ -@using BlazeKit.Web; -@inherits ReactiveComponentEnvelope + +@using BlazeKit.Web.Utils +@inherits BlazeKit.Web.Components.IslandComponentBase @code { @@ -102,7 +103,7 @@

Counter: @counter.Value

Doubled Counter: @doubled.Value

Triggered Is Even-Side Effects (counter.Value % 2 == 0): @booms

-
+
@@ -113,7 +114,7 @@

Conditional CSS Classes

FindRoutes() - { - var result = new List(); - var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(asm => !asm.FullName.StartsWith("Microsoft.") || !asm.FullName.StartsWith("System.")); - foreach(var asm in assemblies) { - result.AddRange( - asm.GetExportedTypes() - .Select(t => { - Console.WriteLine($"Looking for Routes in {t.Assembly.FullName}"); - return t; - }) - .Where(t => t.GetCustomAttributes(typeof(RouteAttribute), true).Count() > 0) - .Select(t => - { - var route = t.GetCustomAttributes(typeof(RouteAttribute), true).FirstOrDefault() as RouteAttribute; - return route!.Template; - }) - ); - } - - return result; - @* return AppDomain.CurrentDomain.GetAssemblies() - .GetExportedTypes() - .Select(t => { - Console.WriteLine($"Looking for Routes in {t.Assembly.FullName}"); - return t; - }) - .Where(t => t.GetCustomAttributes(typeof(RouteAttribute), true).Count() > 0) - .Select(t => - { - var route = t.GetCustomAttributes(typeof(RouteAttribute), true).FirstOrDefault() as RouteAttribute; - return route!.Template; - }).ToList(); *@ - - } - - private IList FindComponentsUsed() - { - var result = new List(); - @* var assemblies = AppDomain.CurrentDomain.GetAssemblies(); *@ - var assemblies = - new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).GetFiles("*.dll").ToList().Select(file => { - // load assembly for reflection only - var assembly = System.Reflection.Assembly.LoadFile(file.FullName); - return assembly; - }); - - foreach (var assembly in assemblies) - { - Console.WriteLine(assembly.FullName); - result.AddRange( - assembly - .GetExportedTypes() - .Where(t => t.IsClass) - @* .Where(t => t.BaseType.Name == "ComponentBase") *@ - .Where(t => t.BaseType == (typeof(ComponentBase)) || t.BaseType.Name == "ReactiveComponentEnvelope") - .Select(t => - { - return $"{t.FullName}"; - }) - ); - } - return result.Distinct().ToList(); - } }