Initial commit. Yes, I should use git better.

This commit is contained in:
osmarks 2018-08-12 17:26:57 +01:00
commit d05688b267
23 changed files with 11416 additions and 0 deletions

8
.editorconfig Normal file
View 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
View 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

View 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

Binary file not shown.

115
build.fsx Normal file
View 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
View File

@ -0,0 +1 @@
{ "sdk": { "version": "2.1.300" } }

7828
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

118
src/Client/Client.fs Normal file
View 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
View 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
View 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

View 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

View 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>

View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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