Initial commit. Yes, I should use git better.
This commit is contained in:
commit
d05688b267
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = false
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
.fake/
|
||||
.vs/
|
||||
obj/
|
||||
bin/
|
||||
packages/
|
||||
paket-files/
|
||||
node_modules/
|
||||
src/Client/public/js/
|
||||
release.cmd
|
||||
release.sh
|
||||
.idea/
|
||||
*.DotSettings.user
|
299
.paket/Paket.Restore.targets
Normal file
299
.paket/Paket.Restore.targets
Normal file
@ -0,0 +1,299 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Prevent dotnet template engine to parse this file -->
|
||||
<!--/-:cnd:noEmit-->
|
||||
<PropertyGroup>
|
||||
<!-- make MSBuild track this file for incremental builds. -->
|
||||
<!-- ref https://blogs.msdn.microsoft.com/msbuild/2005/09/26/how-to-ensure-changes-to-a-custom-target-file-prompt-a-rebuild/ -->
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
<!-- Mark that this target file has been loaded. -->
|
||||
<IsPaketRestoreTargetsFileLoaded>true</IsPaketRestoreTargetsFileLoaded>
|
||||
<PaketToolsPath>$(MSBuildThisFileDirectory)</PaketToolsPath>
|
||||
<PaketRootPath>$(MSBuildThisFileDirectory)..\</PaketRootPath>
|
||||
<PaketRestoreCacheFile>$(PaketRootPath)paket-files\paket.restore.cached</PaketRestoreCacheFile>
|
||||
<PaketLockFilePath>$(PaketRootPath)paket.lock</PaketLockFilePath>
|
||||
<MonoPath Condition="'$(MonoPath)' == '' And Exists('/Library/Frameworks/Mono.framework/Commands/mono')">/Library/Frameworks/Mono.framework/Commands/mono</MonoPath>
|
||||
<MonoPath Condition="'$(MonoPath)' == ''">mono</MonoPath>
|
||||
<!-- Paket command -->
|
||||
<PaketExePath Condition=" '$(PaketExePath)' == '' AND Exists('$(PaketRootPath)paket.exe')">$(PaketRootPath)paket.exe</PaketExePath>
|
||||
<PaketExePath Condition=" '$(PaketExePath)' == '' ">$(PaketToolsPath)paket.exe</PaketExePath>
|
||||
<PaketCommand Condition=" '$(OS)' == 'Windows_NT'">"$(PaketExePath)"</PaketCommand>
|
||||
<PaketCommand Condition=" '$(OS)' != 'Windows_NT' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"</PaketCommand>
|
||||
|
||||
<!-- .net core fdd -->
|
||||
<_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)"))</_PaketExeExtension>
|
||||
<PaketCommand Condition=" '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"</PaketCommand>
|
||||
|
||||
<!-- no extension is a shell script -->
|
||||
<PaketCommand Condition=" '$(_PaketExeExtension)' == '' ">"$(PaketExePath)"</PaketCommand>
|
||||
|
||||
<PaketBootStrapperExePath Condition=" '$(PaketBootStrapperExePath)' == '' AND Exists('$(PaketRootPath)paket.bootstrapper.exe')">$(PaketRootPath)paket.bootstrapper.exe</PaketBootStrapperExePath>
|
||||
<PaketBootStrapperExePath Condition=" '$(PaketBootStrapperExePath)' == '' ">$(PaketToolsPath)paket.bootstrapper.exe</PaketBootStrapperExePath>
|
||||
<PaketBootStrapperCommand Condition=" '$(OS)' == 'Windows_NT'">"$(PaketBootStrapperExePath)"</PaketBootStrapperCommand>
|
||||
<PaketBootStrapperCommand Condition=" '$(OS)' != 'Windows_NT' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)"</PaketBootStrapperCommand>
|
||||
|
||||
<!-- Disable automagic references for F# dotnet sdk -->
|
||||
<!-- This will not do anything for other project types -->
|
||||
<!-- see https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1032-fsharp-in-dotnet-sdk.md -->
|
||||
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
|
||||
<DisableImplicitSystemValueTupleReference>true</DisableImplicitSystemValueTupleReference>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PaketRestore" Condition="'$(PaketRestoreDisabled)' != 'True'" BeforeTargets="_GenerateDotnetCliToolReferenceSpecs;_GenerateProjectRestoreGraphPerFramework;_GenerateRestoreGraphWalkPerFramework;CollectPackageReferences" >
|
||||
|
||||
<!-- Step 1 Check if lockfile is properly restored -->
|
||||
<PropertyGroup>
|
||||
<PaketRestoreRequired>true</PaketRestoreRequired>
|
||||
<NoWarn>$(NoWarn);NU1603;NU1604;NU1605;NU1608</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Because ReadAllText is slow on osx/linux, try to find shasum and awk -->
|
||||
<PropertyGroup>
|
||||
<PaketRestoreCachedHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreCachedHasher)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum "$(PaketRestoreCacheFile)" | /usr/bin/awk '{ print $1 }'</PaketRestoreCachedHasher>
|
||||
<PaketRestoreLockFileHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreLockFileHash)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum "$(PaketLockFilePath)" | /usr/bin/awk '{ print $1 }'</PaketRestoreLockFileHasher>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- If shasum and awk exist get the hashes -->
|
||||
<Exec StandardOutputImportance="Low" Condition=" '$(PaketRestoreCachedHasher)' != '' " Command="$(PaketRestoreCachedHasher)" ConsoleToMSBuild='true'>
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreCachedHash" />
|
||||
</Exec>
|
||||
<Exec StandardOutputImportance="Low" Condition=" '$(PaketRestoreLockFileHasher)' != '' " Command="$(PaketRestoreLockFileHasher)" ConsoleToMSBuild='true'>
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreLockFileHash" />
|
||||
</Exec>
|
||||
|
||||
<!-- Debug whats going on -->
|
||||
<Message Importance="low" Text="calling paket restore with targetframework=$(TargetFramework) targetframeworks=$(TargetFrameworks)" />
|
||||
|
||||
<PropertyGroup Condition="Exists('$(PaketRestoreCacheFile)') ">
|
||||
<!-- if no hash has been done yet fall back to just reading in the files and comparing them -->
|
||||
<PaketRestoreCachedHash Condition=" '$(PaketRestoreCachedHash)' == '' ">$([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)'))</PaketRestoreCachedHash>
|
||||
<PaketRestoreLockFileHash Condition=" '$(PaketRestoreLockFileHash)' == '' ">$([System.IO.File]::ReadAllText('$(PaketLockFilePath)'))</PaketRestoreLockFileHash>
|
||||
<PaketRestoreRequired>true</PaketRestoreRequired>
|
||||
<PaketRestoreRequired Condition=" '$(PaketRestoreLockFileHash)' == '$(PaketRestoreCachedHash)' ">false</PaketRestoreRequired>
|
||||
<PaketRestoreRequired Condition=" '$(PaketRestoreLockFileHash)' == '' ">true</PaketRestoreRequired>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(PaketPropsVersion)' != '5.174.2' ">
|
||||
<PaketRestoreRequired>true</PaketRestoreRequired>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Do a global restore if required -->
|
||||
<Exec Command='$(PaketBootStrapperCommand)' Condition="Exists('$(PaketBootStrapperExePath)') AND !(Exists('$(PaketExePath)'))" ContinueOnError="false" />
|
||||
<Exec Command='$(PaketCommand) restore' Condition=" '$(PaketRestoreRequired)' == 'true' " ContinueOnError="false" />
|
||||
|
||||
<!-- Step 2 Detect project specific changes -->
|
||||
<ItemGroup>
|
||||
<MyTargetFrameworks Condition="'$(TargetFramework)' != '' " Include="$(TargetFramework)"></MyTargetFrameworks>
|
||||
<!-- Don't include all frameworks when msbuild explicitly asks for a single one -->
|
||||
<MyTargetFrameworks Condition="'$(TargetFrameworks)' != '' AND '$(TargetFramework)' == '' " Include="$(TargetFrameworks)"></MyTargetFrameworks>
|
||||
<PaketResolvedFilePaths Include="@(MyTargetFrameworks -> '$(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).%(Identity).paket.resolved')"></PaketResolvedFilePaths>
|
||||
</ItemGroup>
|
||||
<Message Importance="low" Text="MyTargetFrameworks=@(MyTargetFrameworks) PaketResolvedFilePaths=@(PaketResolvedFilePaths)" />
|
||||
<PropertyGroup>
|
||||
<PaketReferencesCachedFilePath>$(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached</PaketReferencesCachedFilePath>
|
||||
<!-- MyProject.fsproj.paket.references has the highest precedence -->
|
||||
<PaketOriginalReferencesFilePath>$(MSBuildProjectFullPath).paket.references</PaketOriginalReferencesFilePath>
|
||||
<!-- MyProject.paket.references -->
|
||||
<PaketOriginalReferencesFilePath Condition=" !Exists('$(PaketOriginalReferencesFilePath)')">$(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references</PaketOriginalReferencesFilePath>
|
||||
<!-- paket.references -->
|
||||
<PaketOriginalReferencesFilePath Condition=" !Exists('$(PaketOriginalReferencesFilePath)')">$(MSBuildProjectDirectory)\paket.references</PaketOriginalReferencesFilePath>
|
||||
|
||||
<DoAllResolvedFilesExist>false</DoAllResolvedFilesExist>
|
||||
<DoAllResolvedFilesExist Condition="Exists(%(PaketResolvedFilePaths.Identity))">true</DoAllResolvedFilesExist>
|
||||
<PaketRestoreRequired>true</PaketRestoreRequired>
|
||||
<PaketRestoreRequiredReason>references-file-or-cache-not-found</PaketRestoreRequiredReason>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Step 2 a Detect changes in references file -->
|
||||
<PropertyGroup Condition="Exists('$(PaketOriginalReferencesFilePath)') AND Exists('$(PaketReferencesCachedFilePath)') ">
|
||||
<PaketRestoreCachedHash>$([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)'))</PaketRestoreCachedHash>
|
||||
<PaketRestoreReferencesFileHash>$([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)'))</PaketRestoreReferencesFileHash>
|
||||
<PaketRestoreRequiredReason>references-file</PaketRestoreRequiredReason>
|
||||
<PaketRestoreRequired Condition=" '$(PaketRestoreReferencesFileHash)' == '$(PaketRestoreCachedHash)' ">false</PaketRestoreRequired>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="!Exists('$(PaketOriginalReferencesFilePath)') AND !Exists('$(PaketReferencesCachedFilePath)') ">
|
||||
<!-- If both don't exist there is nothing to do. -->
|
||||
<PaketRestoreRequired>false</PaketRestoreRequired>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Step 2 b detect relevant changes in project file (new targetframework) -->
|
||||
<PropertyGroup Condition=" '$(DoAllResolvedFilesExist)' != 'true' ">
|
||||
<PaketRestoreRequired>true</PaketRestoreRequired>
|
||||
<PaketRestoreRequiredReason>target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths)</PaketRestoreRequiredReason>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Step 3 Restore project specific stuff if required -->
|
||||
<Message Condition=" '$(PaketRestoreRequired)' == 'true' " Importance="low" Text="Detected a change ('$(PaketRestoreRequiredReason)') in the project file '$(MSBuildProjectFullPath)', calling paket restore" />
|
||||
<Exec Command='$(PaketCommand) restore --project "$(MSBuildProjectFullPath)" --target-framework "$(TargetFrameworks)"' Condition=" '$(PaketRestoreRequired)' == 'true' AND '$(TargetFramework)' == '' " ContinueOnError="false" />
|
||||
<Exec Command='$(PaketCommand) restore --project "$(MSBuildProjectFullPath)" --target-framework "$(TargetFramework)"' Condition=" '$(PaketRestoreRequired)' == 'true' AND '$(TargetFramework)' != '' " ContinueOnError="false" />
|
||||
|
||||
<!-- This shouldn't actually happen, but just to be sure. -->
|
||||
<PropertyGroup>
|
||||
<DoAllResolvedFilesExist>false</DoAllResolvedFilesExist>
|
||||
<DoAllResolvedFilesExist Condition="Exists(%(PaketResolvedFilePaths.Identity))">true</DoAllResolvedFilesExist>
|
||||
</PropertyGroup>
|
||||
<Error Condition=" '$(DoAllResolvedFilesExist)' != 'true' AND '$(ResolveNuGetPackages)' != 'False' " Text="One Paket file '@(PaketResolvedFilePaths)' is missing while restoring $(MSBuildProjectFile). Please delete 'paket-files/paket.restore.cached' and call 'paket restore'." />
|
||||
|
||||
<!-- Step 4 forward all msbuild properties (PackageReference, DotNetCliToolReference) to msbuild -->
|
||||
<ReadLinesFromFile Condition="($(DesignTimeBuild) != true OR '$(PaketPropsLoaded)' != 'true') AND '@(PaketResolvedFilePaths)' != ''" File="%(PaketResolvedFilePaths.Identity)" >
|
||||
<Output TaskParameter="Lines" ItemName="PaketReferencesFileLines"/>
|
||||
</ReadLinesFromFile>
|
||||
|
||||
<ItemGroup Condition="($(DesignTimeBuild) != true OR '$(PaketPropsLoaded)' != 'true') AND '@(PaketReferencesFileLines)' != '' " >
|
||||
<PaketReferencesFileLinesInfo Include="@(PaketReferencesFileLines)" >
|
||||
<PackageName>$([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0])</PackageName>
|
||||
<PackageVersion>$([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1])</PackageVersion>
|
||||
<AllPrivateAssets>$([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4])</AllPrivateAssets>
|
||||
</PaketReferencesFileLinesInfo>
|
||||
<PackageReference Include="%(PaketReferencesFileLinesInfo.PackageName)">
|
||||
<Version>%(PaketReferencesFileLinesInfo.PackageVersion)</Version>
|
||||
<PrivateAssets Condition=" ('%(PaketReferencesFileLinesInfo.AllPrivateAssets)' == 'true') Or ('$(PackAsTool)' == 'true') ">All</PrivateAssets>
|
||||
<ExcludeAssets Condition="%(PaketReferencesFileLinesInfo.AllPrivateAssets) == 'exclude'">runtime</ExcludeAssets>
|
||||
<Publish Condition=" '$(PackAsTool)' == 'true' ">true</Publish>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PaketCliToolFilePath>$(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools</PaketCliToolFilePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ReadLinesFromFile File="$(PaketCliToolFilePath)" >
|
||||
<Output TaskParameter="Lines" ItemName="PaketCliToolFileLines"/>
|
||||
</ReadLinesFromFile>
|
||||
|
||||
<ItemGroup Condition=" '@(PaketCliToolFileLines)' != '' " >
|
||||
<PaketCliToolFileLinesInfo Include="@(PaketCliToolFileLines)" >
|
||||
<PackageName>$([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0])</PackageName>
|
||||
<PackageVersion>$([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1])</PackageVersion>
|
||||
</PaketCliToolFileLinesInfo>
|
||||
<DotNetCliToolReference Include="%(PaketCliToolFileLinesInfo.PackageName)">
|
||||
<Version>%(PaketCliToolFileLinesInfo.PackageVersion)</Version>
|
||||
</DotNetCliToolReference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Disabled for now until we know what to do with runtime deps - https://github.com/fsprojects/Paket/issues/2964
|
||||
<PropertyGroup>
|
||||
<RestoreConfigFile>$(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).NuGet.Config</RestoreConfigFile>
|
||||
</PropertyGroup> -->
|
||||
|
||||
</Target>
|
||||
|
||||
<Target Name="PaketDisableDirectPack" AfterTargets="_IntermediatePack" BeforeTargets="GenerateNuspec" Condition="('$(IsPackable)' == '' Or '$(IsPackable)' == 'true') And Exists('$(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).references')" >
|
||||
<PropertyGroup>
|
||||
<ContinuePackingAfterGeneratingNuspec>false</ContinuePackingAfterGeneratingNuspec>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="PaketOverrideNuspec" AfterTargets="GenerateNuspec" Condition="('$(IsPackable)' == '' Or '$(IsPackable)' == 'true') And Exists('$(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).references')" >
|
||||
<ItemGroup>
|
||||
<_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PaketProjectFile>$(MSBuildProjectDirectory)/$(MSBuildProjectFile)</PaketProjectFile>
|
||||
<ContinuePackingAfterGeneratingNuspec>true</ContinuePackingAfterGeneratingNuspec>
|
||||
<UseNewPack>false</UseNewPack>
|
||||
<UseNewPack Condition=" '$(NuGetToolVersion)' != '4.0.0' ">true</UseNewPack>
|
||||
<AdjustedNuspecOutputPath>$(BaseIntermediateOutputPath)$(Configuration)</AdjustedNuspecOutputPath>
|
||||
<AdjustedNuspecOutputPath Condition="@(_NuspecFilesNewLocation) == ''">$(BaseIntermediateOutputPath)</AdjustedNuspecOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Exec Command='$(PaketCommand) fix-nuspecs files "@(_NuspecFiles)" project-file "$(PaketProjectFile)" ' Condition="@(_NuspecFiles) != ''" />
|
||||
|
||||
<ConvertToAbsolutePath Condition="@(_NuspecFiles) != ''" Paths="@(_NuspecFiles)">
|
||||
<Output TaskParameter="AbsolutePaths" PropertyName="NuspecFileAbsolutePath" />
|
||||
</ConvertToAbsolutePath>
|
||||
|
||||
|
||||
<!-- Call Pack -->
|
||||
<PackTask Condition="$(UseNewPack)"
|
||||
PackItem="$(PackProjectInputFile)"
|
||||
PackageFiles="@(_PackageFiles)"
|
||||
PackageFilesToExclude="@(_PackageFilesToExclude)"
|
||||
PackageVersion="$(PackageVersion)"
|
||||
PackageId="$(PackageId)"
|
||||
Title="$(Title)"
|
||||
Authors="$(Authors)"
|
||||
Description="$(Description)"
|
||||
Copyright="$(Copyright)"
|
||||
RequireLicenseAcceptance="$(PackageRequireLicenseAcceptance)"
|
||||
LicenseUrl="$(PackageLicenseUrl)"
|
||||
ProjectUrl="$(PackageProjectUrl)"
|
||||
IconUrl="$(PackageIconUrl)"
|
||||
ReleaseNotes="$(PackageReleaseNotes)"
|
||||
Tags="$(PackageTags)"
|
||||
DevelopmentDependency="$(DevelopmentDependency)"
|
||||
BuildOutputInPackage="@(_BuildOutputInPackage)"
|
||||
TargetPathsToSymbols="@(_TargetPathsToSymbols)"
|
||||
TargetFrameworks="@(_TargetFrameworks)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
PackageOutputPath="$(PackageOutputAbsolutePath)"
|
||||
IncludeSymbols="$(IncludeSymbols)"
|
||||
IncludeSource="$(IncludeSource)"
|
||||
PackageTypes="$(PackageType)"
|
||||
IsTool="$(IsTool)"
|
||||
RepositoryUrl="$(RepositoryUrl)"
|
||||
RepositoryType="$(RepositoryType)"
|
||||
SourceFiles="@(_SourceFiles->Distinct())"
|
||||
NoPackageAnalysis="$(NoPackageAnalysis)"
|
||||
MinClientVersion="$(MinClientVersion)"
|
||||
Serviceable="$(Serviceable)"
|
||||
FrameworkAssemblyReferences="@(_FrameworkAssemblyReferences)"
|
||||
ContinuePackingAfterGeneratingNuspec="$(ContinuePackingAfterGeneratingNuspec)"
|
||||
NuspecOutputPath="$(AdjustedNuspecOutputPath)"
|
||||
IncludeBuildOutput="$(IncludeBuildOutput)"
|
||||
BuildOutputFolder="$(BuildOutputTargetFolder)"
|
||||
ContentTargetFolders="$(ContentTargetFolders)"
|
||||
RestoreOutputPath="$(RestoreOutputAbsolutePath)"
|
||||
NuspecFile="$(NuspecFileAbsolutePath)"
|
||||
NuspecBasePath="$(NuspecBasePath)"
|
||||
NuspecProperties="$(NuspecProperties)"/>
|
||||
|
||||
<PackTask Condition="! $(UseNewPack)"
|
||||
PackItem="$(PackProjectInputFile)"
|
||||
PackageFiles="@(_PackageFiles)"
|
||||
PackageFilesToExclude="@(_PackageFilesToExclude)"
|
||||
PackageVersion="$(PackageVersion)"
|
||||
PackageId="$(PackageId)"
|
||||
Title="$(Title)"
|
||||
Authors="$(Authors)"
|
||||
Description="$(Description)"
|
||||
Copyright="$(Copyright)"
|
||||
RequireLicenseAcceptance="$(PackageRequireLicenseAcceptance)"
|
||||
LicenseUrl="$(PackageLicenseUrl)"
|
||||
ProjectUrl="$(PackageProjectUrl)"
|
||||
IconUrl="$(PackageIconUrl)"
|
||||
ReleaseNotes="$(PackageReleaseNotes)"
|
||||
Tags="$(PackageTags)"
|
||||
TargetPathsToAssemblies="@(_TargetPathsToAssemblies->'%(FinalOutputPath)')"
|
||||
TargetPathsToSymbols="@(_TargetPathsToSymbols)"
|
||||
TargetFrameworks="@(_TargetFrameworks)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
PackageOutputPath="$(PackageOutputAbsolutePath)"
|
||||
IncludeSymbols="$(IncludeSymbols)"
|
||||
IncludeSource="$(IncludeSource)"
|
||||
PackageTypes="$(PackageType)"
|
||||
IsTool="$(IsTool)"
|
||||
RepositoryUrl="$(RepositoryUrl)"
|
||||
RepositoryType="$(RepositoryType)"
|
||||
SourceFiles="@(_SourceFiles->Distinct())"
|
||||
NoPackageAnalysis="$(NoPackageAnalysis)"
|
||||
MinClientVersion="$(MinClientVersion)"
|
||||
Serviceable="$(Serviceable)"
|
||||
AssemblyReferences="@(_References)"
|
||||
ContinuePackingAfterGeneratingNuspec="$(ContinuePackingAfterGeneratingNuspec)"
|
||||
NuspecOutputPath="$(AdjustedNuspecOutputPath)"
|
||||
IncludeBuildOutput="$(IncludeBuildOutput)"
|
||||
BuildOutputFolder="$(BuildOutputTargetFolder)"
|
||||
ContentTargetFolders="$(ContentTargetFolders)"
|
||||
RestoreOutputPath="$(RestoreOutputAbsolutePath)"
|
||||
NuspecFile="$(NuspecFileAbsolutePath)"
|
||||
NuspecBasePath="$(NuspecBasePath)"
|
||||
NuspecProperties="$(NuspecProperties)"/>
|
||||
</Target>
|
||||
<!--/+:cnd:noEmit-->
|
||||
</Project>
|
BIN
.paket/paket.exe
Normal file
BIN
.paket/paket.exe
Normal file
Binary file not shown.
115
build.fsx
Normal file
115
build.fsx
Normal file
@ -0,0 +1,115 @@
|
||||
#r "paket: groupref build //"
|
||||
#load "./.fake/build.fsx/intellisense.fsx"
|
||||
|
||||
#if !FAKE
|
||||
#r "netstandard"
|
||||
#r "Facades/netstandard" // https://github.com/ionide/ionide-vscode-fsharp/issues/839#issuecomment-396296095
|
||||
#endif
|
||||
|
||||
open System
|
||||
|
||||
open Fake.Core
|
||||
open Fake.DotNet
|
||||
open Fake.IO
|
||||
|
||||
let serverPath = Path.getFullName "./src/Server"
|
||||
let clientPath = Path.getFullName "./src/Client"
|
||||
let deployDir = Path.getFullName "./deploy"
|
||||
|
||||
let platformTool tool winTool =
|
||||
let tool = if Environment.isUnix then tool else winTool
|
||||
match Process.tryFindFileOnPath tool with
|
||||
| Some t -> t
|
||||
| _ ->
|
||||
let errorMsg =
|
||||
tool + " was not found in path. " +
|
||||
"Please install it and make sure it's available from your path. " +
|
||||
"See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info"
|
||||
failwith errorMsg
|
||||
|
||||
let nodeTool = platformTool "node" "node.exe"
|
||||
let npmTool = platformTool "npm" "npm.cmd"
|
||||
|
||||
let install = lazy DotNet.install DotNet.Release_2_1_300
|
||||
|
||||
let inline withWorkDir wd =
|
||||
DotNet.Options.lift install.Value
|
||||
>> DotNet.Options.withWorkingDirectory wd
|
||||
|
||||
let runTool cmd args workingDir =
|
||||
let result =
|
||||
Process.execSimple (fun info ->
|
||||
{ info with
|
||||
FileName = cmd
|
||||
WorkingDirectory = workingDir
|
||||
Arguments = args })
|
||||
TimeSpan.MaxValue
|
||||
if result <> 0 then failwithf "'%s %s' failed" cmd args
|
||||
|
||||
let runDotNet cmd workingDir =
|
||||
let result =
|
||||
DotNet.exec (withWorkDir workingDir) cmd ""
|
||||
if result.ExitCode <> 0 then failwithf "'dotnet %s' failed in %s" cmd workingDir
|
||||
|
||||
let openBrowser url =
|
||||
let result =
|
||||
//https://github.com/dotnet/corefx/issues/10361
|
||||
Process.execSimple (fun info ->
|
||||
{ info with
|
||||
FileName = url
|
||||
UseShellExecute = true })
|
||||
TimeSpan.MaxValue
|
||||
if result <> 0 then failwithf "opening browser failed"
|
||||
|
||||
Target.create "Clean" (fun _ ->
|
||||
Shell.cleanDirs [deployDir]
|
||||
)
|
||||
|
||||
Target.create "InstallClient" (fun _ ->
|
||||
printfn "Node version:"
|
||||
runTool nodeTool "--version" __SOURCE_DIRECTORY__
|
||||
printfn "Npm version:"
|
||||
runTool npmTool "--version" __SOURCE_DIRECTORY__
|
||||
runTool npmTool "install" __SOURCE_DIRECTORY__
|
||||
runDotNet "restore" clientPath
|
||||
)
|
||||
|
||||
Target.create "RestoreServer" (fun _ ->
|
||||
runDotNet "restore" serverPath
|
||||
)
|
||||
|
||||
Target.create "Build" (fun _ ->
|
||||
runDotNet "build" serverPath
|
||||
runDotNet "fable webpack -- -p" clientPath
|
||||
)
|
||||
|
||||
Target.create "Run" (fun _ ->
|
||||
let server = async {
|
||||
runDotNet "watch run" serverPath
|
||||
}
|
||||
let client = async {
|
||||
runDotNet "fable webpack-dev-server" clientPath
|
||||
}
|
||||
let browser = async {
|
||||
Threading.Thread.Sleep 5000
|
||||
openBrowser "http://localhost:8080"
|
||||
}
|
||||
|
||||
[ server; client; browser ]
|
||||
|> Async.Parallel
|
||||
|> Async.RunSynchronously
|
||||
|> ignore
|
||||
)
|
||||
|
||||
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
"Clean"
|
||||
==> "InstallClient"
|
||||
==> "Build"
|
||||
|
||||
"InstallClient"
|
||||
==> "RestoreServer"
|
||||
==> "Run"
|
||||
|
||||
Target.runOrDefault "Build"
|
1
global.json
Normal file
1
global.json
Normal file
@ -0,0 +1 @@
|
||||
{ "sdk": { "version": "2.1.300" } }
|
7828
package-lock.json
generated
Normal file
7828
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"babel-polyfill": "6.26.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"funnies": "^2.0.0",
|
||||
"fuse.js": "^3.2.1",
|
||||
"react": "16.4.0",
|
||||
"react-bootstrap": "0.32.1",
|
||||
"react-dom": "16.4.0",
|
||||
"react-spinners": "^0.3.2",
|
||||
"remotedev": "0.2.7",
|
||||
"sanitize-html": "^1.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-transform-runtime": "6.23.0",
|
||||
"babel-preset-env": "1.7.0",
|
||||
"concurrently": "3.5.1",
|
||||
"fable-loader": "1.1.6",
|
||||
"fable-utils": "1.0.6",
|
||||
"stylus-loader": "^3.0.2",
|
||||
"webpack": "4.11.1",
|
||||
"webpack-cli": "^3.0.2",
|
||||
"webpack-dev-server": "3.1.4"
|
||||
}
|
||||
}
|
32
paket.dependencies
Normal file
32
paket.dependencies
Normal file
@ -0,0 +1,32 @@
|
||||
group Server
|
||||
storage: none
|
||||
source https://api.nuget.org/v3/index.json
|
||||
|
||||
nuget FSharp.Core
|
||||
nuget Saturn
|
||||
nuget Fable.Remoting.Server ~> 3.6
|
||||
nuget Fable.Remoting.Giraffe ~> 2.7
|
||||
nuget System.ServiceModel.Syndication
|
||||
nuget Newtonsoft.Json
|
||||
|
||||
group Client
|
||||
storage: none
|
||||
source https://api.nuget.org/v3/index.json
|
||||
|
||||
nuget Fable.Core
|
||||
nuget Fable.Elmish.Debugger
|
||||
nuget Fable.Elmish.React
|
||||
nuget Fable.Elmish.HMR
|
||||
nuget Fable.Remoting.Client ~> 2.5.1
|
||||
|
||||
clitool dotnet-fable
|
||||
|
||||
group Build
|
||||
storage: none
|
||||
source https://api.nuget.org/v3/index.json
|
||||
|
||||
nuget FSharp.Core 4.3.4 // https://github.com/fsharp/FAKE/issues/2001
|
||||
nuget Fake.Core.Target
|
||||
nuget Fake.DotNet.Cli
|
||||
nuget Fake.IO.FileSystem
|
||||
|
2564
paket.lock
Normal file
2564
paket.lock
Normal file
File diff suppressed because it is too large
Load Diff
118
src/Client/Client.fs
Normal file
118
src/Client/Client.fs
Normal file
@ -0,0 +1,118 @@
|
||||
module Client
|
||||
|
||||
open Elmish
|
||||
open Elmish.React
|
||||
|
||||
open Fable.Helpers.React
|
||||
open Fable.Helpers.React.Props
|
||||
open Fable.PowerPack.Fetch
|
||||
|
||||
open Shared
|
||||
open Shared.Feed
|
||||
|
||||
type LoadedModel = {
|
||||
items : Feed.Item array
|
||||
searchIndex : Feed.Item Imports.Fuse.Fuse
|
||||
searchResults : Feed.Item array }
|
||||
|
||||
type Model =
|
||||
| Loading
|
||||
| Loaded of LoadedModel
|
||||
| Errored of exn
|
||||
|
||||
type Msg =
|
||||
| NewItems of Feed.Item array
|
||||
| SearchUpdate of string
|
||||
| LoadError of exn
|
||||
| LoadMore
|
||||
| Refresh
|
||||
|
||||
module Server =
|
||||
open Shared
|
||||
open Fable.Remoting.Client
|
||||
|
||||
let api : FeedReaderProtocol =
|
||||
Proxy.remoting<FeedReaderProtocol> {
|
||||
use_route_builder Route.builder
|
||||
}
|
||||
|
||||
let loadData start number =
|
||||
Cmd.ofAsync
|
||||
Server.api.getItems
|
||||
(start, number)
|
||||
NewItems
|
||||
LoadError
|
||||
|
||||
let init () : Model * Cmd<Msg> =
|
||||
Loading, loadData 0 20
|
||||
let fuzzySearchOptions = [ Imports.Fuse.Keys [| "description"; "title"; "source" |] ]
|
||||
|
||||
// Takes the existing model and an array of items to add, and updates the model to include these items
|
||||
let addItems (model : Model) (newItems : Feed.Item array) =
|
||||
let originalItems =
|
||||
match model with
|
||||
| Loaded l -> l.items
|
||||
| _ -> [||]
|
||||
let updatedItems = Feed.merge originalItems newItems
|
||||
let index = Imports.Fuse.make updatedItems fuzzySearchOptions
|
||||
match model with
|
||||
| Loaded l -> Loaded { l with searchResults = [||]; items = updatedItems; searchIndex = index }
|
||||
| _ -> Loaded { searchResults = [||]; items = updatedItems; searchIndex = index }
|
||||
|
||||
let update (msg : Msg) (model : Model) : Model * Cmd<Msg> =
|
||||
match msg, model with
|
||||
| NewItems d, _ -> addItems model d, Cmd.none
|
||||
| SearchUpdate s, Loaded m -> Loaded { m with searchResults = Imports.Fuse.search m.searchIndex s }, Cmd.none
|
||||
| LoadError e, _ -> Errored e, Cmd.none
|
||||
| Refresh, _-> model, loadData 0 20
|
||||
| LoadMore, Loaded m -> model, loadData (Array.length m.items) 20
|
||||
| _, _ -> model, Cmd.none
|
||||
|
||||
let loader = div [ Class "overlay" ] [ Imports.loadingSpinner [ Imports.Color "#000000" ] []; str (Imports.Funny.message ()) ]
|
||||
|
||||
let inlineDivider = str " - "
|
||||
|
||||
// Displays a Feed.Item
|
||||
let viewItem { title = title; description = desc; time = time; link = url; source = source } =
|
||||
li [ ClassName "feed-item" ]
|
||||
[ div [ ClassName "item-title" ] [ str title ]
|
||||
span [ ClassName "item-source" ] [ str source ]
|
||||
inlineDivider
|
||||
span [ ClassName "item-date" ] [ str <| time.LocalDateTime.ToString("HH:mm:ss dd/MM/yyyyy") ] // display date/time of article using local time
|
||||
inlineDivider
|
||||
a [ Href url; ClassName "item-link" ] [str url]
|
||||
div [ ClassName "item-contents"; DangerouslySetInnerHTML { __html = Imports.sanitize desc } ] [] ]
|
||||
|
||||
let viewItems = Array.map viewItem >> Array.toList >> ul [ ClassName "feed-items" ]
|
||||
|
||||
let view (model : Model) (dispatch : Msg -> unit) =
|
||||
match model with
|
||||
| Loading -> loader
|
||||
| Errored e -> div [ ClassName "overlay" ] [
|
||||
div [] [ str <| sprintf "An error occured: %A" e ]
|
||||
div [] [ str "A team of monkeys will arrive shortly to correct this." ]
|
||||
button [ ClassName "retry-button"; OnClick (fun _ -> dispatch Refresh) ] [ str "Try Again" ] ]
|
||||
| Loaded model ->
|
||||
let items = if Array.length model.searchResults > 0 then model.searchResults else model.items
|
||||
div []
|
||||
[ div [ ClassName "controls-container" ] [
|
||||
input [ OnChange (fun fe -> dispatch <| SearchUpdate fe.Value); Type "search" ; Class "search-bar"; Placeholder "Search Feeds" ]
|
||||
button [ OnClick (fun _ -> dispatch Refresh); ClassName "refresh-button" ] [ str "Refresh" ] ]
|
||||
viewItems items
|
||||
button [ ClassName "load-more-button"; OnClick (fun _ -> dispatch LoadMore) ] [ str "Load More" ] ]
|
||||
|
||||
#if DEBUG
|
||||
open Elmish.Debug
|
||||
open Elmish.HMR
|
||||
#endif
|
||||
|
||||
Program.mkProgram init update view
|
||||
#if DEBUG
|
||||
|> Program.withConsoleTrace
|
||||
|> Program.withHMR
|
||||
#endif
|
||||
|> Program.withReact "app"
|
||||
#if DEBUG
|
||||
|> Program.withDebugger
|
||||
#endif
|
||||
|> Program.run
|
12
src/Client/Client.fsproj
Normal file
12
src/Client/Client.fsproj
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\Shared.fs" />
|
||||
<Compile Include="Imports.fs" />
|
||||
<Compile Include="Client.fs" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\.paket\Paket.Restore.targets" />
|
||||
</Project>
|
42
src/Client/Imports.fs
Normal file
42
src/Client/Imports.fs
Normal file
@ -0,0 +1,42 @@
|
||||
module Imports
|
||||
|
||||
open Fable.Core
|
||||
open Fable.Helpers.React
|
||||
open Fable.Import.React
|
||||
open Fable.Core.JsInterop
|
||||
|
||||
[<Import("default", from="sanitize-html")>]
|
||||
let sanitize (s : string) : string = jsNative
|
||||
|
||||
type SpinnerProps =
|
||||
| Color of string
|
||||
| Loading of bool
|
||||
|
||||
let inline loadingSpinner (props : SpinnerProps list) (elems : ReactElement list) : ReactElement =
|
||||
ofImport "ClimbingBoxLoader" "react-spinners" (keyValueList CaseRules.LowerFirst props) elems
|
||||
|
||||
module Funny =
|
||||
[<Import("default", from="funnies")>]
|
||||
let _funnies () = jsNative
|
||||
|
||||
let funny = createNew _funnies ()
|
||||
[<Emit("$0.message()")>]
|
||||
let _message (funny : obj) : string = jsNative
|
||||
let message () = funny |> _message
|
||||
|
||||
module Fuse =
|
||||
// Bindings for "Fuse.js" fuzzy search (see its docs) - very barebones
|
||||
type Option =
|
||||
| Keys of string array
|
||||
|
||||
type Fuse<'a> = CantDefineOpaqueTypesForJs of unit
|
||||
|
||||
[<Import("default", from="fuse.js")>]
|
||||
let _fuse (data : 'a array) (conf : obj) : 'a Fuse = jsNative
|
||||
|
||||
// Creates a new Fuse object
|
||||
let make (data : 'a array) (conf : Option list) : 'a Fuse = createNew _fuse (data, (keyValueList CaseRules.LowerFirst conf)) :?> 'a Fuse
|
||||
|
||||
// Allows you to search using a Fuse object
|
||||
[<Emit("$0.search($1)")>]
|
||||
let search (fuse : 'a Fuse) (query : string) = jsNative
|
8
src/Client/paket.references
Normal file
8
src/Client/paket.references
Normal file
@ -0,0 +1,8 @@
|
||||
group Client
|
||||
FSharp.Core
|
||||
Fable.Elmish.Debugger
|
||||
Fable.Elmish.React
|
||||
Fable.Elmish.HMR
|
||||
Fable.Core
|
||||
dotnet-fable
|
||||
Fable.Remoting.Client
|
86
src/Client/public/index.html
Normal file
86
src/Client/public/index.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Project #1008</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin: 1em;
|
||||
padding: 0.2em;
|
||||
width: 100%;
|
||||
border: solid 1px lightblue;
|
||||
}
|
||||
|
||||
button {
|
||||
border: solid 1px lightblue;
|
||||
margin: 1em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.load-more-button {
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feed-items {
|
||||
list-style: none;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding-left: 0; /* disable default list weirdness */
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
background: #CCC;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.feed-item:nth-child(odd) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.item-date {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.item-link {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.item-contents {
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./js/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
81
src/Client/webpack.config.js
Normal file
81
src/Client/webpack.config.js
Normal file
@ -0,0 +1,81 @@
|
||||
var path = require("path");
|
||||
var webpack = require("webpack");
|
||||
var fableUtils = require("fable-utils");
|
||||
|
||||
function resolve(filePath) {
|
||||
return path.join(__dirname, filePath)
|
||||
}
|
||||
|
||||
var babelOptions = fableUtils.resolveBabelOptions({
|
||||
presets: [
|
||||
["env", {
|
||||
"targets": {
|
||||
"browsers": ["last 2 versions"]
|
||||
},
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
plugins: ["transform-runtime"]
|
||||
});
|
||||
|
||||
|
||||
var isProduction = process.argv.indexOf("-p") >= 0;
|
||||
var port = process.env.SUAVE_FABLE_PORT || "8085";
|
||||
console.log("Bundling for " + (isProduction ? "production" : "development") + "...");
|
||||
|
||||
module.exports = {
|
||||
devtool: "source-map",
|
||||
entry: resolve('./Client.fsproj'),
|
||||
mode: isProduction ? "production" : "development",
|
||||
output: {
|
||||
path: resolve('./public/js'),
|
||||
publicPath: "/js",
|
||||
filename: "bundle.js"
|
||||
},
|
||||
resolve: {
|
||||
modules: [resolve("../../node_modules/")]
|
||||
},
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api/*': {
|
||||
target: 'http://localhost:' + port,
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
contentBase: "./public",
|
||||
hot: true,
|
||||
inline: true
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.fs(x|proj)?$/,
|
||||
use: {
|
||||
loader: "fable-loader",
|
||||
options: {
|
||||
babel: babelOptions,
|
||||
define: isProduction ? [] : ["DEBUG"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
use: {
|
||||
loader: "stylus-loader"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: isProduction ? [] : [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin()
|
||||
]
|
||||
};
|
15
src/Server/Config.fs
Normal file
15
src/Server/Config.fs
Normal file
@ -0,0 +1,15 @@
|
||||
module Config
|
||||
|
||||
open System
|
||||
open Newtonsoft.Json
|
||||
|
||||
type Config = {
|
||||
feeds : string array }
|
||||
|
||||
// JSON is not a great format for configuration files. I would use TOML, but there don't seem to be good parsers for it which support idiomatic F# and can be nugetted
|
||||
|
||||
let parseConfig = JsonConvert.DeserializeObject<Config>
|
||||
let readFile (filename : string) =
|
||||
use sr = new IO.StreamReader(filename)
|
||||
sr.ReadToEnd()
|
||||
let loadConfig = readFile >> parseConfig
|
46
src/Server/Feed.fs
Normal file
46
src/Server/Feed.fs
Normal file
@ -0,0 +1,46 @@
|
||||
module Feed
|
||||
|
||||
open System.Xml
|
||||
open System.ServiceModel.Syndication
|
||||
open Shared.Feed
|
||||
open System.Net
|
||||
open System
|
||||
open System.IO
|
||||
open Microsoft.FSharp.Control.CommonExtensions
|
||||
|
||||
// Parses a RSS feed from an XMLReader, into an array of items
|
||||
// The .NET Framework SyndicationFeed class does most of the work, except it has a non-F#y class-based model, annoyingly feed-type-specific stuff, and is generally inconvenient to actually use
|
||||
let parseFromReader (xmlReader : XmlReader) : Item array =
|
||||
let feed = SyndicationFeed.Load<SyndicationFeed>(xmlReader)
|
||||
feed.Items |> Seq.toArray |> Array.map (fun item ->
|
||||
// Try and find some kind of description/content/summary/whatever to display - what sort it is varies based on what type the feed we parse is
|
||||
let desc =
|
||||
match item.Content with
|
||||
| (:? TextSyndicationContent as txt) -> txt.Text
|
||||
| _ ->
|
||||
match item.Summary with
|
||||
| null -> "<No Description>"
|
||||
| txt -> txt.Text
|
||||
let date =
|
||||
match item.PublishDate.Ticks with // Due to format differences between Atom and RSS, we need to check whether PublishDate has an actual value and if not use something else
|
||||
| 0L -> item.LastUpdatedTime
|
||||
| _ -> item.PublishDate
|
||||
{ title = item.Title.Text; description = desc; link = item.Links.[0].Uri.ToString(); time = date; source = feed.Title.Text }
|
||||
)
|
||||
|
||||
let fetchAsync url = async {
|
||||
let req = WebRequest.Create(Uri(url))
|
||||
let! resp = req.AsyncGetResponse()
|
||||
return resp.GetResponseStream() }
|
||||
|
||||
// Asynchronously loads an RSS feed from a URL
|
||||
let loadFromUrl url = async {
|
||||
use! stream = fetchAsync url
|
||||
use xml = XmlReader.Create(stream)
|
||||
return parseFromReader xml
|
||||
}
|
||||
|
||||
// Given several feed URLs, loads them, concatenates them, and sorts them.
|
||||
let loadMany feeds = async {
|
||||
let! loadedFeeds = Seq.map loadFromUrl feeds |> Async.Parallel
|
||||
return Array.concat loadedFeeds |> sort }
|
44
src/Server/Server.fs
Normal file
44
src/Server/Server.fs
Normal file
@ -0,0 +1,44 @@
|
||||
module Server
|
||||
|
||||
open System.IO
|
||||
open System.Threading.Tasks
|
||||
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Giraffe
|
||||
open Saturn
|
||||
open Shared
|
||||
|
||||
open Fable.Remoting.Server
|
||||
open Fable.Remoting.Giraffe
|
||||
|
||||
let publicPath = Path.GetFullPath "../Client/public"
|
||||
let port = 8085us
|
||||
|
||||
let getItems (start, qty) = async {
|
||||
let! items =
|
||||
Feed.loadMany
|
||||
[ "http://feeds.bbci.co.uk/news/rss.xml"
|
||||
"https://www.theguardian.com/uk/rss"
|
||||
"https://www.theregister.co.uk/headlines.atom" ]
|
||||
return items.[start .. min (start + qty) (Array.length items - 1)] } // If there aren't enough items to provide "qty", send a smaller slice instead of failing
|
||||
|
||||
let webApp =
|
||||
let server =
|
||||
{ getItems = getItems }
|
||||
remoting server {
|
||||
use_route_builder Route.builder
|
||||
}
|
||||
|
||||
let app = application {
|
||||
url ("http://0.0.0.0:" + port.ToString() + "/")
|
||||
router webApp
|
||||
memory_cache
|
||||
use_static publicPath
|
||||
// sitemap diagnostic data cannot be inferred when using Fable.Remoting
|
||||
// Saturn issue at https://github.com/SaturnFramework/Saturn/issues/64
|
||||
disable_diagnostics
|
||||
use_gzip
|
||||
}
|
||||
|
||||
run app
|
14
src/Server/Server.fsproj
Normal file
14
src/Server/Server.fsproj
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\Shared.fs" />
|
||||
<Compile Include="Feed.fs" />
|
||||
<Compile Include="Config.fs" />
|
||||
<Compile Include="Server.fs" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\.paket\Paket.Restore.targets" />
|
||||
</Project>
|
6
src/Server/paket.references
Normal file
6
src/Server/paket.references
Normal file
@ -0,0 +1,6 @@
|
||||
group Server
|
||||
FSharp.Core
|
||||
Saturn
|
||||
Fable.Remoting.Giraffe
|
||||
System.ServiceModel.Syndication
|
||||
Newtonsoft.Json
|
24
src/Shared/Shared.fs
Normal file
24
src/Shared/Shared.fs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Shared
|
||||
|
||||
open System
|
||||
|
||||
type Counter = int
|
||||
|
||||
module Route =
|
||||
let builder typeName methodName =
|
||||
sprintf "/api/%s/%s" typeName methodName
|
||||
|
||||
module Feed =
|
||||
type Item =
|
||||
{ title : string
|
||||
description : string
|
||||
time : DateTimeOffset
|
||||
link : string
|
||||
source : string }
|
||||
|
||||
let sort (is : Item array) : Item array = Array.sortByDescending (fun (i : Item) -> i.time) is // newest articles first
|
||||
let deduplicate (is : Item array) : Item array = Array.distinctBy hash is
|
||||
let merge (is1 : Item array) (is2 : Item array) : Item array = Array.append is1 is2 |> sort |> deduplicate
|
||||
|
||||
type FeedReaderProtocol =
|
||||
{ getItems : (int * int) -> Feed.Item array Async }
|
33
src/safe.sln
Normal file
33
src/safe.sln
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2005
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Client", "Client\Client.fsproj", "{73E8E820-C8AA-47CC-BB2B-152CA4D0B855}"
|
||||
EndProject
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Server", "Server\Server.fsproj", "{73E8E820-C8AA-47CC-BB2B-152CA4D0B856}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B855}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B855}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B855}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B855}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B856}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B856}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{73E8E820-C8AA-47CC-BB2B-152CA4D0B856}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7A0AA30E-4CD9-4359-9513-BA68E2E85245}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
Loading…
Reference in New Issue
Block a user