├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dotnet-unpkg.sln ├── global.json ├── src ├── RendleLabs.Unpkg.Build │ ├── Download.cs │ ├── RendleLabs.Unpkg.Build.csproj │ ├── RendleLabs.Unpkg.Build.targets │ ├── Restore.cs │ ├── RestoreTask.cs │ └── Settings.cs ├── RendleLabs.Unpkg │ ├── Add.cs │ ├── Annotations │ │ └── JetbrainsAnnotations.cs │ ├── Dist.cs │ ├── DistFile.cs │ ├── Download.cs │ ├── Help.cs │ ├── RendleLabs.Unpkg.csproj │ ├── Restore.cs │ ├── Settings.cs │ ├── TrimExtensions.cs │ ├── UnpkgJson.cs │ ├── UnpkgJsonEntry.cs │ ├── UnpkgJsonFile.cs │ ├── Upgrade.cs │ ├── ValueTupleLinq.cs │ └── VersionComparison.cs └── cli │ ├── Program.cs │ └── unpkg.csproj └── test ├── TestConsoleApp ├── Program.cs └── TestConsoleApp.csproj └── TestLibmanApp ├── Program.cs └── TestLibmanApp.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | build/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | .vscode/ 31 | .idea/ 32 | 33 | # Local test files 34 | wwwroot/ 35 | unpkg.json 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # .NET Core 51 | project.lock.json 52 | project.fragment.lock.json 53 | artifacts/ 54 | **/Properties/launchSettings.json 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # Visual Studio code coverage results 122 | *.coverage 123 | *.coveragexml 124 | 125 | # NCrunch 126 | _NCrunch_* 127 | .*crunch*.local.xml 128 | nCrunchTemp_* 129 | 130 | # MightyMoose 131 | *.mm.* 132 | AutoTest.Net/ 133 | 134 | # Web workbench (sass) 135 | .sass-cache/ 136 | 137 | # Installshield output folder 138 | [Ee]xpress/ 139 | 140 | # DocProject is a documentation generator add-in 141 | DocProject/buildhelp/ 142 | DocProject/Help/*.HxT 143 | DocProject/Help/*.HxC 144 | DocProject/Help/*.hhc 145 | DocProject/Help/*.hhk 146 | DocProject/Help/*.hhp 147 | DocProject/Help/Html2 148 | DocProject/Help/html 149 | 150 | # Click-Once directory 151 | publish/ 152 | 153 | # Publish Web Output 154 | *.[Pp]ublish.xml 155 | *.azurePubxml 156 | # TODO: Comment the next line if you want to checkin your web deploy settings 157 | # but database connection strings (with potential passwords) will be unencrypted 158 | *.pubxml 159 | *.publishproj 160 | 161 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 162 | # checkin your Azure Web App publish settings, but sensitive information contained 163 | # in these scripts will be unencrypted 164 | PublishScripts/ 165 | 166 | # NuGet Packages 167 | *.nupkg 168 | # The packages folder can be ignored because of Package Restore 169 | **/packages/* 170 | # except build/, which is used as an MSBuild target. 171 | !**/packages/build/ 172 | # Uncomment if necessary however generally it will be regenerated when needed 173 | #!**/packages/repositories.config 174 | # NuGet v3's project.json files produces more ignorable files 175 | *.nuget.props 176 | *.nuget.targets 177 | 178 | # Microsoft Azure Build Output 179 | csx/ 180 | *.build.csdef 181 | 182 | # Microsoft Azure Emulator 183 | ecf/ 184 | rcf/ 185 | 186 | # Windows Store app package directories and files 187 | AppPackages/ 188 | BundleArtifacts/ 189 | Package.StoreAssociation.xml 190 | _pkginfo.txt 191 | 192 | # Visual Studio cache files 193 | # files ending in .cache can be ignored 194 | *.[Cc]ache 195 | # but keep track of directories ending in .cache 196 | !*.[Cc]ache/ 197 | 198 | # Others 199 | ClientBin/ 200 | ~$* 201 | *~ 202 | *.dbmdl 203 | *.dbproj.schemaview 204 | *.jfm 205 | *.pfx 206 | *.publishsettings 207 | orleans.codegen.cs 208 | 209 | # Since there are multiple workflows, uncomment next line to ignore bower_components 210 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 211 | #bower_components/ 212 | 213 | # RIA/Silverlight projects 214 | Generated_Code/ 215 | 216 | # Backup & report files from converting an old project file 217 | # to a newer Visual Studio version. Backup files are not needed, 218 | # because we have git ;-) 219 | _UpgradeReport_Files/ 220 | Backup*/ 221 | UpgradeLog*.XML 222 | UpgradeLog*.htm 223 | 224 | # SQL Server files 225 | *.mdf 226 | *.ldf 227 | *.ndf 228 | 229 | # Business Intelligence projects 230 | *.rdl.data 231 | *.bim.layout 232 | *.bim_*.settings 233 | 234 | # Microsoft Fakes 235 | FakesAssemblies/ 236 | 237 | # GhostDoc plugin setting file 238 | *.GhostDoc.xml 239 | 240 | # Node.js Tools for Visual Studio 241 | .ntvs_analysis.dat 242 | node_modules/ 243 | 244 | # Typescript v1 declaration files 245 | typings/ 246 | 247 | # Visual Studio 6 build log 248 | *.plg 249 | 250 | # Visual Studio 6 workspace options file 251 | *.opt 252 | 253 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 254 | *.vbw 255 | 256 | # Visual Studio LightSwitch build output 257 | **/*.HTMLClient/GeneratedArtifacts 258 | **/*.DesktopClient/GeneratedArtifacts 259 | **/*.DesktopClient/ModelManifest.xml 260 | **/*.Server/GeneratedArtifacts 261 | **/*.Server/ModelManifest.xml 262 | _Pvt_Extensions 263 | 264 | # Paket dependency manager 265 | .paket/paket.exe 266 | paket-files/ 267 | 268 | # FAKE - F# Make 269 | .fake/ 270 | 271 | # JetBrains Rider 272 | .idea/ 273 | *.sln.iml 274 | 275 | # CodeRush 276 | .cr/ 277 | 278 | # Python Tools for Visual Studio (PTVS) 279 | __pycache__/ 280 | *.pyc 281 | 282 | # Cake - Uncomment if you are using it 283 | # tools/** 284 | # !tools/packages.config 285 | 286 | # Telerik's JustMock configuration file 287 | *.jmconfig 288 | 289 | # BizTalk build output 290 | *.btp.cs 291 | *.btm.cs 292 | *.odx.cs 293 | *.xsd.cs 294 | 295 | src/slideface/theme 296 | src/slideface/images -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 2 | 3 | - Added support for sub-directories in namespaced packages like `dotnet unpkg add @aspnet/signalr/browser` 4 | 5 | ## 1.1.0 6 | 7 | - Added support for namespaced packages like `@aspnet/signalr` 8 | - Simplified `restore` functionality 9 | 10 | ## 1.0.0 11 | 12 | - Initial release 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 RendleLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NuGet package: [nuget.org/packages/RendleLabs.UnpkgCli](https://www.nuget.org/packages/RendleLabs.UnpkgCli)** 2 | 3 | # dotnet unpkg 4 | I got fed up of needing to have Node.js and NPM installed just so I could install 5 | front-end packages like jQuery and Bootstrap. I'm not using Webpack or anything, 6 | and I don't want 100MB of `node_modules` in every project. 7 | 8 | So I made a `dotnet` command to do it instead. 9 | 10 | ## Why should I use it? 11 | 12 | Because you're building an ASP.NET Core application which just needs common front-end 13 | packages like Bootstrap, jQuery and Popper.js, and it's going to serve them from a 14 | CDN in Production but with fallback to local files. You're not compiling your 15 | front-end code with Webpack or anything, and you just want an easy way to acquire 16 | those libraries, without needing to install Node.js and NPM or Yarn or Bower, 17 | and without adding a Gulp or Grunt step just to copy the files you actually need 18 | out of `node_modules`. 19 | 20 | `unpkg` is written in C#, with no dependency on JavaScript runtimes, so it installs 21 | as a .NET Global Tool. It'll grab the files you need from the same public CDN you can 22 | use for Production — [unpkg.com](https://unpkg.com) — and puts them right 23 | into `wwwroot\lib`, where they belong. 24 | 25 | ## Why shouldn't I use it? 26 | 27 | If you are building a complex SPA, with Angular or TypeScript or Webpack or suchlike, 28 | and you've got code that loads packages from `node_modules` using `import` syntax, 29 | then this is not for those projects, and you should use [NPM](https://npmjs.com). 30 | (You could also use Yarn, but that's by Facebook so for all you know it might be 31 | sending copies of your dependency graphs to shady data-mining organisations; be 32 | careful out there.) 33 | 34 | ## How does it work? 35 | 36 | There's a magic CDN called [unpkg.com](https://unpkg.com) that delivers files from 37 | NPM packages. If those packages follow a simple rule, which is to put all their 38 | runtime files into a folder called `dist`, they can be served from **unpkg**. 39 | 40 | It also provides metadata about the packages in JSON format, including the `integrity` 41 | hash that you can use in your script tags to make sure you're getting the right data 42 | and the user's connection hasn't been compromised. 43 | 44 | `unpkg` uses that metadata to discover the files in the package and download 45 | them right into your `wwwroot/lib` folder. 46 | 47 | Sometimes the packages don't have a `dist` folder, in which case `unpkg` will 48 | download pretty much everything. 49 | 50 | ## Usage 51 | 52 | You'll need the .NET Core SDK 2.1 installed on your machine. 53 | 54 | Then you can install the package as a global tool like this: 55 | 56 | ```bash 57 | $ dotnet tool install -g --version 2.0.0 RendleLabs.UnpkgCli 58 | ``` 59 | 60 | Then, from the command line: 61 | 62 | ``` 63 | $ unpkg add vue 64 | ``` 65 | 66 | It supports NPM-namespaced packages: 67 | 68 | ``` 69 | $ unpkg add @aspnet/signalr 70 | ``` 71 | 72 | You can install multiple packages in a single command: 73 | 74 | ``` 75 | $ unpkg add jquery bootstrap popper.js 76 | ``` 77 | 78 | If you want a specific version, use the `@` notation: 79 | 80 | ``` 81 | $ unpkg add bootstrap@3.3.7 82 | ``` 83 | 84 | You can also specify a path within the package, which is a feature I added 85 | specifically for [Bootswatch](https://bootswatch.com) so I could do this: 86 | 87 | ``` 88 | $ unpkg add bootswatch/yeti 89 | ``` 90 | That just installs the **Yeti** theme within the larger Bootswatch package. If 91 | you just install Bootswatch by itself, you'll get all 20-odd themes. 92 | 93 | You can also specify paths with namespaced packages. This is incredibly useful if you 94 | want to install [Rx.js](http://reactivex.io/rxjs/) because it's *huge*, and all you 95 | want is the `global` folder: 96 | 97 | ``` 98 | $ unpkg add @reactivex/rxjs/global 99 | ``` 100 | And you'll just get the four ` 174 | ``` 175 | 176 | #### Example link tag 177 | 178 | ```html 179 | 187 | ``` 188 | 189 | **Note to self:** maybe generate these tags, either as an extra command or 190 | into another file somewhere? 191 | 192 | ## Open Source 193 | 194 | `unpkg` is open source, and incorporates the work of other open source projects, specifically: 195 | 196 | - [UNPKG](https://github.com/unpkg) by Michael Jackson 197 | - [semver](https://github.com/maxhauser/semver) by Max Hauser 198 | - [JSON.NET](https://www.newtonsoft.com/json) by James Newton-King 199 | 200 | Thank you to all these creators for their contributions to the open-source ecosystem. 201 | -------------------------------------------------------------------------------- /dotnet-unpkg.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F756BE52-940A-4D3D-BBC3-5F0D5F6EC560}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unpkg", "src\cli\unpkg.csproj", "{9CBAC885-EBF9-419A-9E74-EE56199491E7}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RendleLabs.Unpkg", "src\RendleLabs.Unpkg\RendleLabs.Unpkg.csproj", "{3C399570-7893-4D32-A7F7-4838905F5648}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RendleLabs.Unpkg.Build", "src\RendleLabs.Unpkg.Build\RendleLabs.Unpkg.Build.csproj", "{13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{81F49E61-16B8-47C2-AEDD-65ACC7A4F0B5}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{2D367013-A448-45A7-AF0C-967FE7E27118}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|x64.Build.0 = Debug|Any CPU 35 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Debug|x86.Build.0 = Debug|Any CPU 37 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|x64.ActiveCfg = Release|Any CPU 40 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|x64.Build.0 = Release|Any CPU 41 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|x86.ActiveCfg = Release|Any CPU 42 | {9CBAC885-EBF9-419A-9E74-EE56199491E7}.Release|x86.Build.0 = Release|Any CPU 43 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|x64.Build.0 = Debug|Any CPU 47 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {3C399570-7893-4D32-A7F7-4838905F5648}.Debug|x86.Build.0 = Debug|Any CPU 49 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|x64.ActiveCfg = Release|Any CPU 52 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|x64.Build.0 = Release|Any CPU 53 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|x86.ActiveCfg = Release|Any CPU 54 | {3C399570-7893-4D32-A7F7-4838905F5648}.Release|x86.Build.0 = Release|Any CPU 55 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|x64.Build.0 = Debug|Any CPU 59 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Debug|x86.Build.0 = Debug|Any CPU 61 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|x64.ActiveCfg = Release|Any CPU 64 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|x64.Build.0 = Release|Any CPU 65 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|x86.ActiveCfg = Release|Any CPU 66 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3}.Release|x86.Build.0 = Release|Any CPU 67 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|x64.Build.0 = Debug|Any CPU 71 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Debug|x86.Build.0 = Debug|Any CPU 73 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|x64.ActiveCfg = Release|Any CPU 76 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|x64.Build.0 = Release|Any CPU 77 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|x86.ActiveCfg = Release|Any CPU 78 | {2D367013-A448-45A7-AF0C-967FE7E27118}.Release|x86.Build.0 = Release|Any CPU 79 | EndGlobalSection 80 | GlobalSection(NestedProjects) = preSolution 81 | {9CBAC885-EBF9-419A-9E74-EE56199491E7} = {F756BE52-940A-4D3D-BBC3-5F0D5F6EC560} 82 | {3C399570-7893-4D32-A7F7-4838905F5648} = {F756BE52-940A-4D3D-BBC3-5F0D5F6EC560} 83 | {13C04E76-7C18-476C-ACBD-ECAB4FE3A2B3} = {F756BE52-940A-4D3D-BBC3-5F0D5F6EC560} 84 | {2D367013-A448-45A7-AF0C-967FE7E27118} = {81F49E61-16B8-47C2-AEDD-65ACC7A4F0B5} 85 | EndGlobalSection 86 | EndGlobal 87 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.300" 4 | } 5 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/Download.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace RendleLabs.Unpkg.Build 8 | { 9 | public static class Download 10 | { 11 | private static readonly char[] SplitChar = {'/', '\\'}; 12 | private static readonly HttpClient Client = new HttpClient 13 | { 14 | BaseAddress = new Uri("https://unpkg.com") 15 | }; 16 | private static readonly string BaseDirectory = Path.Combine(Settings.Wwwroot, "lib"); 17 | 18 | public static async Task<(string, string)> DistFile(string package, string path) 19 | { 20 | var packageSegments = package.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries); 21 | var target = TargetFile(package, path); 22 | 23 | if (packageSegments.Length > 1) 24 | { 25 | var targetSegments = target.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries); 26 | if (targetSegments.Length > 1) 27 | { 28 | if (packageSegments.Last() == targetSegments.First()) 29 | { 30 | target = string.Join(Path.DirectorySeparatorChar.ToString(), targetSegments.Skip(1)); 31 | } 32 | } 33 | } 34 | 35 | for (int i = 0; i < packageSegments.Length; i++) 36 | { 37 | if (packageSegments[i].Contains('@') && !packageSegments[i].StartsWith("@")) 38 | { 39 | packageSegments[i] = packageSegments[i].Split('@')[0]; 40 | } 41 | } 42 | 43 | package = string.Join("/", packageSegments); 44 | 45 | using (var response = await Client.GetAsync(path)) 46 | { 47 | if (response.IsSuccessStatusCode) 48 | { 49 | var file = Path.GetFileName(target); 50 | if (file == null) 51 | { 52 | return (default, default); 53 | } 54 | 55 | var targetDirectoryName = Path.GetDirectoryName(target); 56 | var directory = targetDirectoryName != null 57 | ? Path.Combine(BaseDirectory, package, targetDirectoryName) 58 | : Path.Combine(BaseDirectory, package); 59 | 60 | if (!Directory.Exists(directory)) 61 | { 62 | Directory.CreateDirectory(directory); 63 | } 64 | 65 | var localPath = Path.Combine(directory, file); 66 | using (var fileStream = File.Create(localPath)) 67 | { 68 | await response.Content.CopyToAsync(fileStream); 69 | } 70 | 71 | return (response.RequestMessage.RequestUri.ToString(), localPath); 72 | } 73 | else 74 | { 75 | return default; 76 | } 77 | } 78 | } 79 | 80 | public static async Task RestoreDistFile(string url, string path) 81 | { 82 | using (var response = await Client.GetAsync(url)) 83 | { 84 | if (response.IsSuccessStatusCode) 85 | { 86 | path = path.Replace('/', Path.DirectorySeparatorChar); 87 | var directory = Path.GetDirectoryName(path); 88 | if (directory != null && !Directory.Exists(directory)) 89 | { 90 | Directory.CreateDirectory(directory); 91 | } 92 | 93 | using (var fileStream = File.Create(path)) 94 | { 95 | await response.Content.CopyToAsync(fileStream); 96 | } 97 | } 98 | } 99 | } 100 | 101 | private static string TargetFile(string package, string path) 102 | { 103 | var packageEnd = package.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries).Last(); 104 | var pathParts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).ToArray(); 105 | 106 | if (pathParts.Contains("dist")) 107 | { 108 | pathParts = pathParts 109 | .SkipWhile(s => !s.Equals("dist", StringComparison.OrdinalIgnoreCase)) 110 | .Skip(1).ToArray(); 111 | 112 | switch (pathParts.Length) 113 | { 114 | case 0: 115 | return path; 116 | case 1: 117 | return pathParts[0]; 118 | default: 119 | if (pathParts[0].Equals(packageEnd, StringComparison.OrdinalIgnoreCase)) 120 | { 121 | pathParts = pathParts.Skip(1).ToArray(); 122 | } 123 | 124 | break; 125 | } 126 | } 127 | else 128 | { 129 | pathParts = pathParts.Skip(1).ToArray(); 130 | } 131 | 132 | return string.Join(Path.DirectorySeparatorChar.ToString(), pathParts); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/RendleLabs.Unpkg.Build.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net46 4 | true 5 | 2.0.0 6 | library, package, client-side, build 7 | Mark Rendle 8 | RendleLabs Ltd. 9 | MSBuild target for RendleLabs UNPKG utility 10 | Copyright © RendleLabs 11 | RendleLabs.Unpkg.Build 12 | RendleLabs.Unpkg.Build 13 | https://github.com/RendleLabs/dotnet-unpkg 14 | https://github.com/RendleLabs/dotnet-unpkg/blob/master/LICENSE 15 | https://github.com/aspnet/LibraryManager 16 | ..\..\artifacts\$(Configuration) 17 | true 18 | tools 19 | latest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | true 36 | build 37 | 38 | 39 | 40 | 41 | 45 | 46 | <_PackageFiles Include="bin\$(Configuration)\*\*.Unpkg.dll;bin\$(Configuration)\*\Newtonsoft.Json.dll"> 47 | tools\%(RecursiveDir) 48 | false 49 | Content 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/RendleLabs.Unpkg.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <_PackageTaskAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">..\tools\netstandard2.0\RendleLabs.Unpkg.Build.dll 6 | <_PackageTaskAssembly Condition="'$(MSBuildRuntimeType)' != 'Core'">..\tools\net46\RendleLabs.Unpkg.Build.dll 7 | 8 | 9 | 10 | 11 | UnpkgRestore; 12 | $(BuildDependsOn) 13 | 14 | 15 | 16 | UnpkgRestore; 17 | $(CopyAllFilesToSingleFolderForPackageDependsOn); 18 | 19 | 20 | 21 | UnpkgRestore; 22 | $(CopyAllFilesToSingleFolderForMsdeployDependsOn); 23 | 24 | 25 | 26 | UnpkgRestore; 27 | $(CopyFilesToOutputDirectory); 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | %(_FilesWritten.Identity) 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/Restore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace RendleLabs.Unpkg.Build 9 | { 10 | public static class Restore 11 | { 12 | public static Task Run() 13 | { 14 | return Run(Environment.CurrentDirectory, "unpkg.json"); 15 | } 16 | 17 | public static async Task Run(string directory, string unpkgFile) 18 | { 19 | if (directory == null) throw new ArgumentNullException(nameof(directory)); 20 | if (unpkgFile == null) throw new ArgumentNullException(nameof(unpkgFile)); 21 | 22 | var fullFilePath = Path.Combine(directory, unpkgFile); 23 | 24 | if (!File.Exists(fullFilePath)) 25 | { 26 | return new RestoreResults {Error = $"No {unpkgFile} file found in current directory."}; 27 | } 28 | 29 | string json; 30 | using (var reader = File.OpenText(fullFilePath)) 31 | { 32 | json = await reader.ReadToEndAsync(); 33 | } 34 | 35 | JObject file; 36 | try 37 | { 38 | file = JObject.Parse(json); 39 | } 40 | catch 41 | { 42 | return new RestoreResults {Error = $"Error parsing {unpkgFile}."}; 43 | } 44 | 45 | var allFiles = await Task.WhenAll(file.Properties().Select(p => DownloadFiles((JObject) p.Value))); 46 | 47 | return new RestoreResults {Results = allFiles.SelectMany(f => f).ToArray()}; 48 | 49 | } 50 | 51 | private static Task DownloadFiles(JObject entry) 52 | { 53 | var files = (JArray) entry["files"]; 54 | 55 | return Task.WhenAll(files 56 | .Select(f => DownloadFile((JObject)f))); 57 | } 58 | 59 | private static async Task DownloadFile(JObject file) 60 | { 61 | var local = file["local"]?.Value()?.Replace('/', Path.DirectorySeparatorChar); 62 | var cdn = file["cdn"]?.Value(); 63 | 64 | if (string.IsNullOrWhiteSpace(local) || string.IsNullOrWhiteSpace(cdn)) 65 | { 66 | return new RestoreResult {Error = $"Could not restore: {cdn}"}; 67 | } 68 | 69 | if (!File.Exists(local) 70 | || !TryGetHashAlgorithm(file["integrity"].Value(), out var hashAlgorithm, out var storedHash) 71 | || !storedHash.Equals(GetCurrentFileHash(local, hashAlgorithm))) 72 | { 73 | await Download.RestoreDistFile(cdn, local); 74 | return new RestoreResult {CdnUrl = cdn, LocalFile = local}; 75 | } 76 | 77 | return new RestoreResult {Message = $"{local} is up-to-date."}; 78 | 79 | } 80 | 81 | private static string GetCurrentFileHash(string local, HashAlgorithm hashAlgorithm) 82 | { 83 | using (var stream = File.OpenRead(local)) 84 | { 85 | return Convert.ToBase64String(hashAlgorithm.ComputeHash(stream)); 86 | } 87 | } 88 | 89 | private static bool TryGetHashAlgorithm(string integrity, out HashAlgorithm hashAlgorithm, out string storedHash) 90 | { 91 | if (!string.IsNullOrWhiteSpace(integrity)) 92 | { 93 | var integrityBits = integrity.Split(new[]{'-'}, 2); 94 | if (integrityBits.Length == 2) 95 | { 96 | hashAlgorithm = GetAlgorithm(integrityBits[0]); 97 | storedHash = integrityBits[1]; 98 | return hashAlgorithm != null; 99 | } 100 | } 101 | 102 | hashAlgorithm = default; 103 | storedHash = default; 104 | return false; 105 | } 106 | 107 | private static HashAlgorithm GetAlgorithm(string name) 108 | { 109 | switch (name.ToLowerInvariant()) 110 | { 111 | case "sha256": 112 | return SHA256.Create(); 113 | case "sha384": 114 | return SHA384.Create(); 115 | case "sha512": 116 | return SHA512.Create(); 117 | default: 118 | return null; 119 | } 120 | } 121 | } 122 | 123 | public class RestoreResults 124 | { 125 | public RestoreResult[] Results { get; set; } 126 | public string Message { get; set; } 127 | public string Error { get; set; } 128 | } 129 | 130 | public class RestoreResult 131 | { 132 | public string LocalFile { get; set; } 133 | public string Message { get; set; } 134 | public string Error { get; set; } 135 | public string CdnUrl { get; set; } 136 | } 137 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/RestoreTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Microsoft.Build.Framework; 6 | using Microsoft.Build.Utilities; 7 | 8 | namespace RendleLabs.Unpkg.Build 9 | { 10 | public class RestoreTask : Task 11 | { 12 | [Required] 13 | public string FileName { get; set; } 14 | 15 | [Required] 16 | public string ProjectDirectory { get; set; } 17 | 18 | [Output] 19 | public ITaskItem[] FilesWritten { get; set; } 20 | 21 | public override bool Execute() 22 | { 23 | Log.LogMessage(MessageImportance.High, $"{Environment.NewLine}Restoring UNPKG libraries"); 24 | 25 | var results = Restore.Run().GetAwaiter().GetResult(); 26 | if (!string.IsNullOrEmpty(results.Error)) 27 | { 28 | Log.LogError(results.Error); 29 | return false; 30 | } 31 | 32 | int errorCount = results.Results.Count(r => !string.IsNullOrWhiteSpace(r.Error)); 33 | if (errorCount > 0) 34 | { 35 | Log.LogWarning($"{Environment.NewLine}UNPKG restore completed with {errorCount} errors."); 36 | } 37 | else 38 | { 39 | Log.LogMessage(MessageImportance.High, $"{Environment.NewLine}UNPKG restore completed."); 40 | } 41 | 42 | var pattern = $"^{Regex.Escape(ProjectDirectory)}[/\\\\]"; 43 | var regex = new Regex(pattern); 44 | 45 | FilesWritten = results.Results 46 | .Where(r => r.LocalFile != null) 47 | .Select(r => regex.Replace(r.LocalFile, "")) 48 | .Select(s => new TaskItem(s)) 49 | .ToArray(); 50 | 51 | return true; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg.Build/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace RendleLabs.Unpkg.Build 6 | { 7 | public static class Settings 8 | { 9 | public static void Initialize(string[] args) 10 | { 11 | var localConfig = Path.Combine(Environment.CurrentDirectory, "unpkg.config"); 12 | var userConfig = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unpkg", "unpkg.config"); 13 | 14 | Wwwroot = TryCommandLineArgs(args, "wwwroot") 15 | ?? TryJsonFile(localConfig, "wwwroot") 16 | ?? TryEnvironmentVariable("wwwroot") 17 | ?? TryJsonFile(userConfig, Wwwroot) 18 | ?? "wwwroot"; 19 | } 20 | 21 | public static string Wwwroot { get; set; } = "wwwroot"; 22 | 23 | private static string TryJsonFile(string path, string key) 24 | { 25 | if (!File.Exists(path)) 26 | { 27 | return null; 28 | } 29 | 30 | try 31 | { 32 | using (var stream = File.OpenText(path)) 33 | { 34 | var json = stream.ReadToEnd(); 35 | var jobj = JObject.Parse(json); 36 | if (jobj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out var value)) 37 | { 38 | return value.ToString(); 39 | } 40 | } 41 | } 42 | catch 43 | { 44 | return null; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | private static string TryCommandLineArgs(string[] args, string key) 51 | { 52 | key = $"--{key}"; 53 | try 54 | { 55 | for (int i = 0; i < args.Length; i++) 56 | { 57 | if (args[i].StartsWith(key, StringComparison.OrdinalIgnoreCase)) 58 | { 59 | if (args[i].Contains("=")) 60 | { 61 | var parts = args[i].Split(new[] {'='}, 2); 62 | return parts[1]; 63 | } 64 | else 65 | { 66 | if (args.Length > i + 1) 67 | { 68 | return args[i + 1]; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | catch 75 | { 76 | return null; 77 | } 78 | 79 | return null; 80 | } 81 | 82 | private static string TryEnvironmentVariable(string key) 83 | { 84 | try 85 | { 86 | return Environment.GetEnvironmentVariable($"UNPKG_{key}"); 87 | } 88 | catch 89 | { 90 | return null; 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Add.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace RendleLabs.Unpkg 9 | { 10 | public static class Add 11 | { 12 | private static readonly Regex DistInPath = new Regex(@"\/dist\/.*"); 13 | private static readonly string BaseDirectory = Path.Combine(Settings.Wwwroot, "lib"); 14 | 15 | public static async Task Run(IEnumerable args) 16 | { 17 | var argList = args.ToList(); 18 | if (argList[0] == "--help" || argList[0] == "-h") 19 | { 20 | Help.Add(); 21 | return; 22 | } 23 | 24 | if (!Directory.Exists(BaseDirectory)) 25 | { 26 | Directory.CreateDirectory(BaseDirectory); 27 | } 28 | 29 | var results = await Task.WhenAll(argList.Select(AddPackage)); 30 | await UnpkgJson.Save(results); 31 | } 32 | 33 | private static async Task AddPackage(string package) 34 | { 35 | var distFile = await Dist.Get(package); 36 | if (distFile == null) 37 | { 38 | return null; 39 | } 40 | 41 | var distDirectory = distFile.Files.FirstOrDefault(f => 42 | f.Type.Equals("directory", StringComparison.OrdinalIgnoreCase) && f.Path.Equals("/dist", StringComparison.OrdinalIgnoreCase)); 43 | 44 | if (distDirectory != null) 45 | { 46 | distDirectory.BaseUrl = distFile.BaseUrl; 47 | distFile = distDirectory; 48 | } 49 | 50 | await DownloadPackage(package, distFile.BaseUrl, distFile.Files); 51 | return UnpkgJsonEntry.Create(package, distFile); 52 | } 53 | 54 | private static Task DownloadPackage(string package, string basePath, IEnumerable files) 55 | { 56 | basePath = DistInPath.Replace(basePath, string.Empty); 57 | var tasks = new List(); 58 | foreach (var file in files) 59 | { 60 | if (file.Type == "file") 61 | { 62 | tasks.Add(DownloadFile(package, basePath, file)); 63 | } 64 | else if (file.Files?.Count > 0) 65 | { 66 | tasks.Add(DownloadPackage(package, basePath, file.Files)); 67 | } 68 | } 69 | 70 | return Task.WhenAll(tasks); 71 | } 72 | 73 | private static async Task DownloadFile(string package, string basePath, DistFile file) 74 | { 75 | basePath = basePath.TrimSlashes(); 76 | var path = file.Path.TrimSlashes(); 77 | var pathSegments = path.Split('/'); 78 | if (basePath.Split('/').LastOrDefault() == pathSegments.FirstOrDefault()) 79 | { 80 | path = string.Join("/", pathSegments.Skip(1)); 81 | } 82 | var (cdn, localPath) = await Download.DistFile(package, $"{basePath}/{path}"); 83 | file.Url = cdn; 84 | file.LocalPath = localPath.Replace(Path.DirectorySeparatorChar, '/'); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Annotations/JetbrainsAnnotations.cs: -------------------------------------------------------------------------------- 1 | /* MIT License 2 | 3 | Copyright (c) 2016 JetBrains http://www.jetbrains.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | using System; 24 | 25 | #pragma warning disable 1591 26 | // ReSharper disable UnusedMember.Global 27 | // ReSharper disable MemberCanBePrivate.Global 28 | // ReSharper disable UnusedAutoPropertyAccessor.Global 29 | // ReSharper disable IntroduceOptionalParameters.Global 30 | // ReSharper disable MemberCanBeProtected.Global 31 | // ReSharper disable InconsistentNaming 32 | 33 | namespace RendleLabs.Unpkg.Annotations 34 | { 35 | /// 36 | /// Indicates that the value of the marked element could be null sometimes, 37 | /// so the check for null is necessary before its usage. 38 | /// 39 | /// 40 | /// [CanBeNull] object Test() => null; 41 | /// 42 | /// void UseTest() { 43 | /// var p = Test(); 44 | /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' 45 | /// } 46 | /// 47 | [AttributeUsage( 48 | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | 49 | AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | 50 | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] 51 | internal sealed class CanBeNullAttribute : Attribute { } 52 | 53 | /// 54 | /// Indicates that the value of the marked element could never be null. 55 | /// 56 | /// 57 | /// [NotNull] object Foo() { 58 | /// return null; // Warning: Possible 'null' assignment 59 | /// } 60 | /// 61 | [AttributeUsage( 62 | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | 63 | AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | 64 | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] 65 | internal sealed class NotNullAttribute : Attribute { } 66 | 67 | /// 68 | /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task 69 | /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property 70 | /// or of the Lazy.Value property can never be null. 71 | /// 72 | [AttributeUsage( 73 | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | 74 | AttributeTargets.Delegate | AttributeTargets.Field)] 75 | internal sealed class ItemNotNullAttribute : Attribute { } 76 | 77 | /// 78 | /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task 79 | /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property 80 | /// or of the Lazy.Value property can be null. 81 | /// 82 | [AttributeUsage( 83 | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | 84 | AttributeTargets.Delegate | AttributeTargets.Field)] 85 | internal sealed class ItemCanBeNullAttribute : Attribute { } 86 | 87 | /// 88 | /// Indicates that the marked method builds string by format pattern and (optional) arguments. 89 | /// Parameter, which contains format string, should be given in constructor. The format string 90 | /// should be in -like form. 91 | /// 92 | /// 93 | /// [StringFormatMethod("message")] 94 | /// void ShowError(string message, params object[] args) { /* do something */ } 95 | /// 96 | /// void Foo() { 97 | /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string 98 | /// } 99 | /// 100 | [AttributeUsage( 101 | AttributeTargets.Constructor | AttributeTargets.Method | 102 | AttributeTargets.Property | AttributeTargets.Delegate)] 103 | internal sealed class StringFormatMethodAttribute : Attribute 104 | { 105 | /// 106 | /// Specifies which parameter of an annotated method should be treated as format-string 107 | /// 108 | public StringFormatMethodAttribute([NotNull] string formatParameterName) 109 | { 110 | FormatParameterName = formatParameterName; 111 | } 112 | 113 | [NotNull] public string FormatParameterName { get; private set; } 114 | } 115 | 116 | /// 117 | /// For a parameter that is expected to be one of the limited set of values. 118 | /// Specify fields of which type should be used as values for this parameter. 119 | /// 120 | [AttributeUsage( 121 | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, 122 | AllowMultiple = true)] 123 | internal sealed class ValueProviderAttribute : Attribute 124 | { 125 | public ValueProviderAttribute([NotNull] string name) 126 | { 127 | Name = name; 128 | } 129 | 130 | [NotNull] public string Name { get; private set; } 131 | } 132 | 133 | /// 134 | /// Indicates that the function argument should be string literal and match one 135 | /// of the parameters of the caller function. For example, ReSharper annotates 136 | /// the parameter of . 137 | /// 138 | /// 139 | /// void Foo(string param) { 140 | /// if (param == null) 141 | /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol 142 | /// } 143 | /// 144 | [AttributeUsage(AttributeTargets.Parameter)] 145 | internal sealed class InvokerParameterNameAttribute : Attribute { } 146 | 147 | /// 148 | /// Indicates that the method is contained in a type that implements 149 | /// System.ComponentModel.INotifyPropertyChanged interface and this method 150 | /// is used to notify that some property value changed. 151 | /// 152 | /// 153 | /// The method should be non-static and conform to one of the supported signatures: 154 | /// 155 | /// NotifyChanged(string) 156 | /// NotifyChanged(params string[]) 157 | /// NotifyChanged{T}(Expression{Func{T}}) 158 | /// NotifyChanged{T,U}(Expression{Func{T,U}}) 159 | /// SetProperty{T}(ref T, T, string) 160 | /// 161 | /// 162 | /// 163 | /// public class Foo : INotifyPropertyChanged { 164 | /// public event PropertyChangedEventHandler PropertyChanged; 165 | /// 166 | /// [NotifyPropertyChangedInvocator] 167 | /// protected virtual void NotifyChanged(string propertyName) { ... } 168 | /// 169 | /// string _name; 170 | /// 171 | /// public string Name { 172 | /// get { return _name; } 173 | /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } 174 | /// } 175 | /// } 176 | /// 177 | /// Examples of generated notifications: 178 | /// 179 | /// NotifyChanged("Property") 180 | /// NotifyChanged(() => Property) 181 | /// NotifyChanged((VM x) => x.Property) 182 | /// SetProperty(ref myField, value, "Property") 183 | /// 184 | /// 185 | [AttributeUsage(AttributeTargets.Method)] 186 | internal sealed class NotifyPropertyChangedInvocatorAttribute : Attribute 187 | { 188 | public NotifyPropertyChangedInvocatorAttribute() { } 189 | public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) 190 | { 191 | ParameterName = parameterName; 192 | } 193 | 194 | [CanBeNull] public string ParameterName { get; private set; } 195 | } 196 | 197 | /// 198 | /// Describes dependency between method input and output. 199 | /// 200 | /// 201 | ///

Function Definition Table syntax:

202 | /// 203 | /// FDT ::= FDTRow [;FDTRow]* 204 | /// FDTRow ::= Input => Output | Output <= Input 205 | /// Input ::= ParameterName: Value [, Input]* 206 | /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} 207 | /// Value ::= true | false | null | notnull | canbenull 208 | /// 209 | /// If method has single input parameter, it's name could be omitted.
210 | /// Using halt (or void/nothing, which is the same) for method output 211 | /// means that the methos doesn't return normally (throws or terminates the process).
212 | /// Value canbenull is only applicable for output parameters.
213 | /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute 214 | /// with rows separated by semicolon. There is no notion of order rows, all rows are checked 215 | /// for applicability and applied per each program state tracked by R# analysis.
216 | ///
217 | /// 218 | /// 219 | /// [ContractAnnotation("=> halt")] 220 | /// public void TerminationMethod() 221 | /// 222 | /// 223 | /// [ContractAnnotation("halt <= condition: false")] 224 | /// public void Assert(bool condition, string text) // regular assertion method 225 | /// 226 | /// 227 | /// [ContractAnnotation("s:null => true")] 228 | /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() 229 | /// 230 | /// 231 | /// // A method that returns null if the parameter is null, 232 | /// // and not null if the parameter is not null 233 | /// [ContractAnnotation("null => null; notnull => notnull")] 234 | /// public object Transform(object data) 235 | /// 236 | /// 237 | /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] 238 | /// public bool TryParse(string s, out Person result) 239 | /// 240 | /// 241 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 242 | internal sealed class ContractAnnotationAttribute : Attribute 243 | { 244 | public ContractAnnotationAttribute([NotNull] string contract) 245 | : this(contract, false) { } 246 | 247 | public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) 248 | { 249 | Contract = contract; 250 | ForceFullStates = forceFullStates; 251 | } 252 | 253 | [NotNull] public string Contract { get; private set; } 254 | 255 | public bool ForceFullStates { get; private set; } 256 | } 257 | 258 | /// 259 | /// Indicates that marked element should be localized or not. 260 | /// 261 | /// 262 | /// [LocalizationRequiredAttribute(true)] 263 | /// class Foo { 264 | /// string str = "my string"; // Warning: Localizable string 265 | /// } 266 | /// 267 | [AttributeUsage(AttributeTargets.All)] 268 | internal sealed class LocalizationRequiredAttribute : Attribute 269 | { 270 | public LocalizationRequiredAttribute() : this(true) { } 271 | 272 | public LocalizationRequiredAttribute(bool required) 273 | { 274 | Required = required; 275 | } 276 | 277 | public bool Required { get; private set; } 278 | } 279 | 280 | /// 281 | /// Indicates that the value of the marked type (or its derivatives) 282 | /// cannot be compared using '==' or '!=' operators and Equals() 283 | /// should be used instead. However, using '==' or '!=' for comparison 284 | /// with null is always permitted. 285 | /// 286 | /// 287 | /// [CannotApplyEqualityOperator] 288 | /// class NoEquality { } 289 | /// 290 | /// class UsesNoEquality { 291 | /// void Test() { 292 | /// var ca1 = new NoEquality(); 293 | /// var ca2 = new NoEquality(); 294 | /// if (ca1 != null) { // OK 295 | /// bool condition = ca1 == ca2; // Warning 296 | /// } 297 | /// } 298 | /// } 299 | /// 300 | [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] 301 | internal sealed class CannotApplyEqualityOperatorAttribute : Attribute { } 302 | 303 | /// 304 | /// When applied to a target attribute, specifies a requirement for any type marked 305 | /// with the target attribute to implement or inherit specific type or types. 306 | /// 307 | /// 308 | /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement 309 | /// class ComponentAttribute : Attribute { } 310 | /// 311 | /// [Component] // ComponentAttribute requires implementing IComponent interface 312 | /// class MyComponent : IComponent { } 313 | /// 314 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 315 | [BaseTypeRequired(typeof(Attribute))] 316 | internal sealed class BaseTypeRequiredAttribute : Attribute 317 | { 318 | public BaseTypeRequiredAttribute([NotNull] Type baseType) 319 | { 320 | BaseType = baseType; 321 | } 322 | 323 | [NotNull] public Type BaseType { get; private set; } 324 | } 325 | 326 | /// 327 | /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), 328 | /// so this symbol will not be marked as unused (as well as by other usage inspections). 329 | /// 330 | [AttributeUsage(AttributeTargets.All)] 331 | internal sealed class UsedImplicitlyAttribute : Attribute 332 | { 333 | public UsedImplicitlyAttribute() 334 | : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } 335 | 336 | public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) 337 | : this(useKindFlags, ImplicitUseTargetFlags.Default) { } 338 | 339 | public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) 340 | : this(ImplicitUseKindFlags.Default, targetFlags) { } 341 | 342 | public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) 343 | { 344 | UseKindFlags = useKindFlags; 345 | TargetFlags = targetFlags; 346 | } 347 | 348 | public ImplicitUseKindFlags UseKindFlags { get; private set; } 349 | 350 | public ImplicitUseTargetFlags TargetFlags { get; private set; } 351 | } 352 | 353 | /// 354 | /// Should be used on attributes and causes ReSharper to not mark symbols marked with such attributes 355 | /// as unused (as well as by other usage inspections) 356 | /// 357 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter)] 358 | internal sealed class MeansImplicitUseAttribute : Attribute 359 | { 360 | public MeansImplicitUseAttribute() 361 | : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } 362 | 363 | public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) 364 | : this(useKindFlags, ImplicitUseTargetFlags.Default) { } 365 | 366 | public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) 367 | : this(ImplicitUseKindFlags.Default, targetFlags) { } 368 | 369 | public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) 370 | { 371 | UseKindFlags = useKindFlags; 372 | TargetFlags = targetFlags; 373 | } 374 | 375 | [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } 376 | 377 | [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } 378 | } 379 | 380 | [Flags] 381 | internal enum ImplicitUseKindFlags 382 | { 383 | Default = Access | Assign | InstantiatedWithFixedConstructorSignature, 384 | /// Only entity marked with attribute considered used. 385 | Access = 1, 386 | /// Indicates implicit assignment to a member. 387 | Assign = 2, 388 | /// 389 | /// Indicates implicit instantiation of a type with fixed constructor signature. 390 | /// That means any unused constructor parameters won't be reported as such. 391 | /// 392 | InstantiatedWithFixedConstructorSignature = 4, 393 | /// Indicates implicit instantiation of a type. 394 | InstantiatedNoFixedConstructorSignature = 8, 395 | } 396 | 397 | /// 398 | /// Specify what is considered used implicitly when marked 399 | /// with or . 400 | /// 401 | [Flags] 402 | internal enum ImplicitUseTargetFlags 403 | { 404 | Default = Itself, 405 | Itself = 1, 406 | /// Members of entity marked with attribute are considered used. 407 | Members = 2, 408 | /// Entity marked with attribute and all its members considered used. 409 | WithMembers = Itself | Members 410 | } 411 | 412 | /// 413 | /// This attribute is intended to mark publicly available API 414 | /// which should not be removed and so is treated as used. 415 | /// 416 | [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] 417 | internal sealed class PublicAPIAttribute : Attribute 418 | { 419 | public PublicAPIAttribute() { } 420 | 421 | public PublicAPIAttribute([NotNull] string comment) 422 | { 423 | Comment = comment; 424 | } 425 | 426 | [CanBeNull] public string Comment { get; private set; } 427 | } 428 | 429 | /// 430 | /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. 431 | /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. 432 | /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. 433 | /// 434 | [AttributeUsage(AttributeTargets.Parameter)] 435 | internal sealed class InstantHandleAttribute : Attribute { } 436 | 437 | /// 438 | /// Indicates that a method does not make any observable state changes. 439 | /// The same as System.Diagnostics.Contracts.PureAttribute. 440 | /// 441 | /// 442 | /// [Pure] int Multiply(int x, int y) => x * y; 443 | /// 444 | /// void M() { 445 | /// Multiply(123, 42); // Waring: Return value of pure method is not used 446 | /// } 447 | /// 448 | [AttributeUsage(AttributeTargets.Method)] 449 | internal sealed class PureAttribute : Attribute { } 450 | 451 | /// 452 | /// Indicates that the return value of method invocation must be used. 453 | /// 454 | [AttributeUsage(AttributeTargets.Method)] 455 | internal sealed class MustUseReturnValueAttribute : Attribute 456 | { 457 | public MustUseReturnValueAttribute() { } 458 | 459 | public MustUseReturnValueAttribute([NotNull] string justification) 460 | { 461 | Justification = justification; 462 | } 463 | 464 | [CanBeNull] public string Justification { get; private set; } 465 | } 466 | 467 | /// 468 | /// Indicates the type member or parameter of some type, that should be used instead of all other ways 469 | /// to get the value that type. This annotation is useful when you have some "context" value evaluated 470 | /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. 471 | /// 472 | /// 473 | /// class Foo { 474 | /// [ProvidesContext] IBarService _barService = ...; 475 | /// 476 | /// void ProcessNode(INode node) { 477 | /// DoSomething(node, node.GetGlobalServices().Bar); 478 | /// // ^ Warning: use value of '_barService' field 479 | /// } 480 | /// } 481 | /// 482 | [AttributeUsage( 483 | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | 484 | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] 485 | internal sealed class ProvidesContextAttribute : Attribute { } 486 | 487 | /// 488 | /// Indicates that a parameter is a path to a file or a folder within a web project. 489 | /// Path can be relative or absolute, starting from web root (~). 490 | /// 491 | [AttributeUsage(AttributeTargets.Parameter)] 492 | internal sealed class PathReferenceAttribute : Attribute 493 | { 494 | public PathReferenceAttribute() { } 495 | 496 | public PathReferenceAttribute([NotNull, PathReference] string basePath) 497 | { 498 | BasePath = basePath; 499 | } 500 | 501 | [CanBeNull] public string BasePath { get; private set; } 502 | } 503 | 504 | /// 505 | /// An extension method marked with this attribute is processed by ReSharper code completion 506 | /// as a 'Source Template'. When extension method is completed over some expression, it's source code 507 | /// is automatically expanded like a template at call site. 508 | /// 509 | /// 510 | /// Template method body can contain valid source code and/or special comments starting with '$'. 511 | /// Text inside these comments is added as source code when the template is applied. Template parameters 512 | /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. 513 | /// Use the attribute to specify macros for parameters. 514 | /// 515 | /// 516 | /// In this example, the 'forEach' method is a source template available over all values 517 | /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: 518 | /// 519 | /// [SourceTemplate] 520 | /// public static void forEach<T>(this IEnumerable<T> xs) { 521 | /// foreach (var x in xs) { 522 | /// //$ $END$ 523 | /// } 524 | /// } 525 | /// 526 | /// 527 | [AttributeUsage(AttributeTargets.Method)] 528 | internal sealed class SourceTemplateAttribute : Attribute { } 529 | 530 | /// 531 | /// Allows specifying a macro for a parameter of a source template. 532 | /// 533 | /// 534 | /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression 535 | /// is defined in the property. When applied on a method, the target 536 | /// template parameter is defined in the property. To apply the macro silently 537 | /// for the parameter, set the property value = -1. 538 | /// 539 | /// 540 | /// Applying the attribute on a source template method: 541 | /// 542 | /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] 543 | /// public static void forEach<T>(this IEnumerable<T> collection) { 544 | /// foreach (var item in collection) { 545 | /// //$ $END$ 546 | /// } 547 | /// } 548 | /// 549 | /// Applying the attribute on a template method parameter: 550 | /// 551 | /// [SourceTemplate] 552 | /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { 553 | /// /*$ var $x$Id = "$newguid$" + x.ToString(); 554 | /// x.DoSomething($x$Id); */ 555 | /// } 556 | /// 557 | /// 558 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] 559 | internal sealed class MacroAttribute : Attribute 560 | { 561 | /// 562 | /// Allows specifying a macro that will be executed for a source template 563 | /// parameter when the template is expanded. 564 | /// 565 | [CanBeNull] public string Expression { get; set; } 566 | 567 | /// 568 | /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. 569 | /// 570 | /// 571 | /// If the target parameter is used several times in the template, only one occurrence becomes editable; 572 | /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, 573 | /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. 574 | /// > 575 | public int Editable { get; set; } 576 | 577 | /// 578 | /// Identifies the target parameter of a source template if the 579 | /// is applied on a template method. 580 | /// 581 | [CanBeNull] public string Target { get; set; } 582 | } 583 | 584 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 585 | internal sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute 586 | { 587 | public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) 588 | { 589 | Format = format; 590 | } 591 | 592 | [NotNull] public string Format { get; private set; } 593 | } 594 | 595 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 596 | internal sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute 597 | { 598 | public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) 599 | { 600 | Format = format; 601 | } 602 | 603 | [NotNull] public string Format { get; private set; } 604 | } 605 | 606 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 607 | internal sealed class AspMvcAreaViewLocationFormatAttribute : Attribute 608 | { 609 | public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) 610 | { 611 | Format = format; 612 | } 613 | 614 | [NotNull] public string Format { get; private set; } 615 | } 616 | 617 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 618 | internal sealed class AspMvcMasterLocationFormatAttribute : Attribute 619 | { 620 | public AspMvcMasterLocationFormatAttribute([NotNull] string format) 621 | { 622 | Format = format; 623 | } 624 | 625 | [NotNull] public string Format { get; private set; } 626 | } 627 | 628 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 629 | internal sealed class AspMvcPartialViewLocationFormatAttribute : Attribute 630 | { 631 | public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) 632 | { 633 | Format = format; 634 | } 635 | 636 | [NotNull] public string Format { get; private set; } 637 | } 638 | 639 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 640 | internal sealed class AspMvcViewLocationFormatAttribute : Attribute 641 | { 642 | public AspMvcViewLocationFormatAttribute([NotNull] string format) 643 | { 644 | Format = format; 645 | } 646 | 647 | [NotNull] public string Format { get; private set; } 648 | } 649 | 650 | /// 651 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter 652 | /// is an MVC action. If applied to a method, the MVC action name is calculated 653 | /// implicitly from the context. Use this attribute for custom wrappers similar to 654 | /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). 655 | /// 656 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 657 | internal sealed class AspMvcActionAttribute : Attribute 658 | { 659 | public AspMvcActionAttribute() { } 660 | 661 | public AspMvcActionAttribute([NotNull] string anonymousProperty) 662 | { 663 | AnonymousProperty = anonymousProperty; 664 | } 665 | 666 | [CanBeNull] public string AnonymousProperty { get; private set; } 667 | } 668 | 669 | /// 670 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. 671 | /// Use this attribute for custom wrappers similar to 672 | /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). 673 | /// 674 | [AttributeUsage(AttributeTargets.Parameter)] 675 | internal sealed class AspMvcAreaAttribute : Attribute 676 | { 677 | public AspMvcAreaAttribute() { } 678 | 679 | public AspMvcAreaAttribute([NotNull] string anonymousProperty) 680 | { 681 | AnonymousProperty = anonymousProperty; 682 | } 683 | 684 | [CanBeNull] public string AnonymousProperty { get; private set; } 685 | } 686 | 687 | /// 688 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is 689 | /// an MVC controller. If applied to a method, the MVC controller name is calculated 690 | /// implicitly from the context. Use this attribute for custom wrappers similar to 691 | /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). 692 | /// 693 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 694 | internal sealed class AspMvcControllerAttribute : Attribute 695 | { 696 | public AspMvcControllerAttribute() { } 697 | 698 | public AspMvcControllerAttribute([NotNull] string anonymousProperty) 699 | { 700 | AnonymousProperty = anonymousProperty; 701 | } 702 | 703 | [CanBeNull] public string AnonymousProperty { get; private set; } 704 | } 705 | 706 | /// 707 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. Use this attribute 708 | /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). 709 | /// 710 | [AttributeUsage(AttributeTargets.Parameter)] 711 | internal sealed class AspMvcMasterAttribute : Attribute { } 712 | 713 | /// 714 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. Use this attribute 715 | /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). 716 | /// 717 | [AttributeUsage(AttributeTargets.Parameter)] 718 | internal sealed class AspMvcModelTypeAttribute : Attribute { } 719 | 720 | /// 721 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC 722 | /// partial view. If applied to a method, the MVC partial view name is calculated implicitly 723 | /// from the context. Use this attribute for custom wrappers similar to 724 | /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). 725 | /// 726 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 727 | internal sealed class AspMvcPartialViewAttribute : Attribute { } 728 | 729 | /// 730 | /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. 731 | /// 732 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 733 | internal sealed class AspMvcSuppressViewErrorAttribute : Attribute { } 734 | 735 | /// 736 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. 737 | /// Use this attribute for custom wrappers similar to 738 | /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). 739 | /// 740 | [AttributeUsage(AttributeTargets.Parameter)] 741 | internal sealed class AspMvcDisplayTemplateAttribute : Attribute { } 742 | 743 | /// 744 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. 745 | /// Use this attribute for custom wrappers similar to 746 | /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). 747 | /// 748 | [AttributeUsage(AttributeTargets.Parameter)] 749 | internal sealed class AspMvcEditorTemplateAttribute : Attribute { } 750 | 751 | /// 752 | /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. 753 | /// Use this attribute for custom wrappers similar to 754 | /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). 755 | /// 756 | [AttributeUsage(AttributeTargets.Parameter)] 757 | internal sealed class AspMvcTemplateAttribute : Attribute { } 758 | 759 | /// 760 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter 761 | /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly 762 | /// from the context. Use this attribute for custom wrappers similar to 763 | /// System.Web.Mvc.Controller.View(Object). 764 | /// 765 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 766 | internal sealed class AspMvcViewAttribute : Attribute { } 767 | 768 | /// 769 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter 770 | /// is an MVC view component name. 771 | /// 772 | [AttributeUsage(AttributeTargets.Parameter)] 773 | internal sealed class AspMvcViewComponentAttribute : Attribute { } 774 | 775 | /// 776 | /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter 777 | /// is an MVC view component view. If applied to a method, the MVC view component view name is default. 778 | /// 779 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 780 | internal sealed class AspMvcViewComponentViewAttribute : Attribute { } 781 | 782 | /// 783 | /// ASP.NET MVC attribute. When applied to a parameter of an attribute, 784 | /// indicates that this parameter is an MVC action name. 785 | /// 786 | /// 787 | /// [ActionName("Foo")] 788 | /// public ActionResult Login(string returnUrl) { 789 | /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK 790 | /// return RedirectToAction("Bar"); // Error: Cannot resolve action 791 | /// } 792 | /// 793 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] 794 | internal sealed class AspMvcActionSelectorAttribute : Attribute { } 795 | 796 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] 797 | internal sealed class HtmlElementAttributesAttribute : Attribute 798 | { 799 | public HtmlElementAttributesAttribute() { } 800 | 801 | public HtmlElementAttributesAttribute([NotNull] string name) 802 | { 803 | Name = name; 804 | } 805 | 806 | [CanBeNull] public string Name { get; private set; } 807 | } 808 | 809 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] 810 | internal sealed class HtmlAttributeValueAttribute : Attribute 811 | { 812 | public HtmlAttributeValueAttribute([NotNull] string name) 813 | { 814 | Name = name; 815 | } 816 | 817 | [NotNull] public string Name { get; private set; } 818 | } 819 | 820 | /// 821 | /// Razor attribute. Indicates that a parameter or a method is a Razor section. 822 | /// Use this attribute for custom wrappers similar to 823 | /// System.Web.WebPages.WebPageBase.RenderSection(String). 824 | /// 825 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] 826 | internal sealed class RazorSectionAttribute : Attribute { } 827 | 828 | /// 829 | /// Indicates how method, constructor invocation or property access 830 | /// over collection type affects content of the collection. 831 | /// 832 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] 833 | internal sealed class CollectionAccessAttribute : Attribute 834 | { 835 | public CollectionAccessAttribute(CollectionAccessType collectionAccessType) 836 | { 837 | CollectionAccessType = collectionAccessType; 838 | } 839 | 840 | public CollectionAccessType CollectionAccessType { get; private set; } 841 | } 842 | 843 | [Flags] 844 | internal enum CollectionAccessType 845 | { 846 | /// Method does not use or modify content of the collection. 847 | None = 0, 848 | /// Method only reads content of the collection but does not modify it. 849 | Read = 1, 850 | /// Method can change content of the collection but does not add new elements. 851 | ModifyExistingContent = 2, 852 | /// Method can add new elements to the collection. 853 | UpdatedContent = ModifyExistingContent | 4 854 | } 855 | 856 | /// 857 | /// Indicates that the marked method is assertion method, i.e. it halts control flow if 858 | /// one of the conditions is satisfied. To set the condition, mark one of the parameters with 859 | /// attribute. 860 | /// 861 | [AttributeUsage(AttributeTargets.Method)] 862 | internal sealed class AssertionMethodAttribute : Attribute { } 863 | 864 | /// 865 | /// Indicates the condition parameter of the assertion method. The method itself should be 866 | /// marked by attribute. The mandatory argument of 867 | /// the attribute is the assertion type. 868 | /// 869 | [AttributeUsage(AttributeTargets.Parameter)] 870 | internal sealed class AssertionConditionAttribute : Attribute 871 | { 872 | public AssertionConditionAttribute(AssertionConditionType conditionType) 873 | { 874 | ConditionType = conditionType; 875 | } 876 | 877 | public AssertionConditionType ConditionType { get; private set; } 878 | } 879 | 880 | /// 881 | /// Specifies assertion type. If the assertion method argument satisfies the condition, 882 | /// then the execution continues. Otherwise, execution is assumed to be halted. 883 | /// 884 | internal enum AssertionConditionType 885 | { 886 | /// Marked parameter should be evaluated to true. 887 | IS_TRUE = 0, 888 | /// Marked parameter should be evaluated to false. 889 | IS_FALSE = 1, 890 | /// Marked parameter should be evaluated to null value. 891 | IS_NULL = 2, 892 | /// Marked parameter should be evaluated to not null value. 893 | IS_NOT_NULL = 3, 894 | } 895 | 896 | /// 897 | /// Indicates that the marked method unconditionally terminates control flow execution. 898 | /// For example, it could unconditionally throw exception. 899 | /// 900 | [Obsolete("Use [ContractAnnotation('=> halt')] instead")] 901 | [AttributeUsage(AttributeTargets.Method)] 902 | internal sealed class TerminatesProgramAttribute : Attribute { } 903 | 904 | /// 905 | /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, 906 | /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters 907 | /// of delegate type by analyzing LINQ method chains. 908 | /// 909 | [AttributeUsage(AttributeTargets.Method)] 910 | internal sealed class LinqTunnelAttribute : Attribute { } 911 | 912 | /// 913 | /// Indicates that IEnumerable, passed as parameter, is not enumerated. 914 | /// 915 | [AttributeUsage(AttributeTargets.Parameter)] 916 | internal sealed class NoEnumerationAttribute : Attribute { } 917 | 918 | /// 919 | /// Indicates that parameter is regular expression pattern. 920 | /// 921 | [AttributeUsage(AttributeTargets.Parameter)] 922 | internal sealed class RegexPatternAttribute : Attribute { } 923 | 924 | /// 925 | /// Prevents the Member Reordering feature from tossing members of the marked class. 926 | /// 927 | /// 928 | /// The attribute must be mentioned in your member reordering patterns 929 | /// 930 | [AttributeUsage( 931 | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] 932 | internal sealed class NoReorderAttribute : Attribute { } 933 | 934 | /// 935 | /// XAML attribute. Indicates the type that has ItemsSource property and should be treated 936 | /// as ItemsControl-derived type, to enable inner items DataContext type resolve. 937 | /// 938 | [AttributeUsage(AttributeTargets.Class)] 939 | internal sealed class XamlItemsControlAttribute : Attribute { } 940 | 941 | /// 942 | /// XAML attribute. Indicates the property of some BindingBase-derived type, that 943 | /// is used to bind some item of ItemsControl-derived type. This annotation will 944 | /// enable the DataContext type resolve for XAML bindings for such properties. 945 | /// 946 | /// 947 | /// Property should have the tree ancestor of the ItemsControl type or 948 | /// marked with the attribute. 949 | /// 950 | [AttributeUsage(AttributeTargets.Property)] 951 | internal sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } 952 | 953 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 954 | internal sealed class AspChildControlTypeAttribute : Attribute 955 | { 956 | public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) 957 | { 958 | TagName = tagName; 959 | ControlType = controlType; 960 | } 961 | 962 | [NotNull] public string TagName { get; private set; } 963 | 964 | [NotNull] public Type ControlType { get; private set; } 965 | } 966 | 967 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] 968 | internal sealed class AspDataFieldAttribute : Attribute { } 969 | 970 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] 971 | internal sealed class AspDataFieldsAttribute : Attribute { } 972 | 973 | [AttributeUsage(AttributeTargets.Property)] 974 | internal sealed class AspMethodPropertyAttribute : Attribute { } 975 | 976 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 977 | internal sealed class AspRequiredAttributeAttribute : Attribute 978 | { 979 | public AspRequiredAttributeAttribute([NotNull] string attribute) 980 | { 981 | Attribute = attribute; 982 | } 983 | 984 | [NotNull] public string Attribute { get; private set; } 985 | } 986 | 987 | [AttributeUsage(AttributeTargets.Property)] 988 | internal sealed class AspTypePropertyAttribute : Attribute 989 | { 990 | public bool CreateConstructorReferences { get; private set; } 991 | 992 | public AspTypePropertyAttribute(bool createConstructorReferences) 993 | { 994 | CreateConstructorReferences = createConstructorReferences; 995 | } 996 | } 997 | 998 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 999 | internal sealed class RazorImportNamespaceAttribute : Attribute 1000 | { 1001 | public RazorImportNamespaceAttribute([NotNull] string name) 1002 | { 1003 | Name = name; 1004 | } 1005 | 1006 | [NotNull] public string Name { get; private set; } 1007 | } 1008 | 1009 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 1010 | internal sealed class RazorInjectionAttribute : Attribute 1011 | { 1012 | public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) 1013 | { 1014 | Type = type; 1015 | FieldName = fieldName; 1016 | } 1017 | 1018 | [NotNull] public string Type { get; private set; } 1019 | 1020 | [NotNull] public string FieldName { get; private set; } 1021 | } 1022 | 1023 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 1024 | internal sealed class RazorDirectiveAttribute : Attribute 1025 | { 1026 | public RazorDirectiveAttribute([NotNull] string directive) 1027 | { 1028 | Directive = directive; 1029 | } 1030 | 1031 | [NotNull] public string Directive { get; private set; } 1032 | } 1033 | 1034 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 1035 | internal sealed class RazorPageBaseTypeAttribute : Attribute 1036 | { 1037 | public RazorPageBaseTypeAttribute([NotNull] string baseType) 1038 | { 1039 | BaseType = baseType; 1040 | } 1041 | public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) 1042 | { 1043 | BaseType = baseType; 1044 | PageName = pageName; 1045 | } 1046 | 1047 | [NotNull] public string BaseType { get; private set; } 1048 | [CanBeNull] public string PageName { get; private set; } 1049 | } 1050 | 1051 | [AttributeUsage(AttributeTargets.Method)] 1052 | internal sealed class RazorHelperCommonAttribute : Attribute { } 1053 | 1054 | [AttributeUsage(AttributeTargets.Property)] 1055 | internal sealed class RazorLayoutAttribute : Attribute { } 1056 | 1057 | [AttributeUsage(AttributeTargets.Method)] 1058 | internal sealed class RazorWriteLiteralMethodAttribute : Attribute { } 1059 | 1060 | [AttributeUsage(AttributeTargets.Method)] 1061 | internal sealed class RazorWriteMethodAttribute : Attribute { } 1062 | 1063 | [AttributeUsage(AttributeTargets.Parameter)] 1064 | internal sealed class RazorWriteMethodParameterAttribute : Attribute { } 1065 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Dist.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | 10 | namespace RendleLabs.Unpkg 11 | { 12 | public static class Dist 13 | { 14 | private static readonly Regex EndPart = new Regex(@"\/dist\/\?meta$"); 15 | private static readonly HttpClient Client = CreateClient(); 16 | 17 | public static async Task Get(string package) 18 | { 19 | var response = await Find(package); 20 | var url = response.RequestMessage.RequestUri.AbsolutePath; 21 | if (response.IsSuccessStatusCode) 22 | { 23 | var version = UnpkgJson.ExtractVersion(url); 24 | var json = await response.Content.ReadAsStringAsync(); 25 | var distFile = JsonConvert.DeserializeObject(json); 26 | distFile.BaseUrl = EndPart.Replace(url, string.Empty); 27 | distFile.Version = version; 28 | return distFile; 29 | } 30 | 31 | Console.Error.WriteLine($"{url} returned status {(int)response.StatusCode}."); 32 | return null; 33 | } 34 | 35 | private static async Task Find(string package) 36 | { 37 | string url; 38 | var parts = package.Split('/'); 39 | string sub = null; 40 | 41 | if (package.StartsWith("@") && parts.Length > 1) 42 | { 43 | package = $"{parts[0]}/{parts[1]}"; 44 | if (parts.Length > 2) 45 | { 46 | sub = string.Join("/", parts.Skip(2)); 47 | } 48 | } 49 | else if (parts.Length > 1) 50 | { 51 | package = parts[0]; 52 | sub = string.Join("/", parts.Skip(1)); 53 | } 54 | 55 | HttpResponseMessage response; 56 | 57 | if (sub != null) 58 | { 59 | url = $"{package}/dist/{sub}/?meta"; 60 | response = await FollowRedirects(url); 61 | if (response.IsSuccessStatusCode) 62 | { 63 | return response; 64 | } 65 | else 66 | { 67 | response.Dispose(); 68 | url = $"{package}/{sub}/?meta"; 69 | } 70 | } 71 | else 72 | { 73 | url = $"{package}/dist/?meta"; 74 | } 75 | 76 | response = await FollowRedirects(url); 77 | 78 | if (response.IsSuccessStatusCode) 79 | { 80 | return response; 81 | } 82 | 83 | response.Dispose(); 84 | 85 | return await FollowRedirects($"{package}/?meta"); 86 | } 87 | 88 | private static async Task FollowRedirects(string url) 89 | { 90 | var response = await Client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); 91 | 92 | while (response.StatusCode == HttpStatusCode.Redirect) 93 | { 94 | url = response.Headers.Location.ToString(); 95 | response.Dispose(); 96 | response = await Client.GetAsync(url); 97 | } 98 | 99 | return response; 100 | } 101 | 102 | private static HttpClient CreateClient() 103 | { 104 | var httpMessageHandler = new HttpClientHandler 105 | { 106 | AllowAutoRedirect = false, 107 | }; 108 | var client = new HttpClient(httpMessageHandler) 109 | { 110 | BaseAddress = new Uri("https://unpkg.com") 111 | }; 112 | client.DefaultRequestHeaders.Accept.Clear(); 113 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 114 | return client; 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/DistFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using RendleLabs.Unpkg.Annotations; 4 | 5 | namespace RendleLabs.Unpkg 6 | { 7 | [PublicAPI] 8 | public class DistFile 9 | { 10 | public string BaseUrl { get; set; } 11 | public DateTimeOffset LastModified { get; set; } 12 | public string ContentType { get; set; } 13 | public string Path { get; set; } 14 | public string LocalPath { get; set; } 15 | public int Size { get; set; } 16 | public string Type { get; set; } 17 | public string Integrity { get; set; } 18 | public List Files { get; set; } 19 | public string Url { get; set; } 20 | public string Version { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Download.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace RendleLabs.Unpkg 8 | { 9 | public static class Download 10 | { 11 | private static readonly char[] SplitChar = {'/', '\\'}; 12 | private static readonly HttpClient Client = new HttpClient 13 | { 14 | BaseAddress = new Uri("https://unpkg.com") 15 | }; 16 | private static readonly string BaseDirectory = Path.Combine(Settings.Wwwroot, "lib"); 17 | 18 | public static async Task<(string, string)> DistFile(string package, string path) 19 | { 20 | var packageSegments = package.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries); 21 | var target = TargetFile(package, path); 22 | 23 | if (packageSegments.Length > 1) 24 | { 25 | var targetSegments = target.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries); 26 | if (targetSegments.Length > 1) 27 | { 28 | if (packageSegments.Last() == targetSegments.First()) 29 | { 30 | target = string.Join(Path.DirectorySeparatorChar.ToString(), targetSegments.Skip(1)); 31 | } 32 | } 33 | } 34 | 35 | for (int i = 0; i < packageSegments.Length; i++) 36 | { 37 | if (packageSegments[i].Contains('@') && !packageSegments[i].StartsWith("@")) 38 | { 39 | packageSegments[i] = packageSegments[i].Split('@')[0]; 40 | } 41 | } 42 | 43 | package = string.Join("/", packageSegments); 44 | 45 | using (var response = await Client.GetAsync(path)) 46 | { 47 | if (response.IsSuccessStatusCode) 48 | { 49 | var file = Path.GetFileName(target); 50 | if (file == null) 51 | { 52 | return (default, default); 53 | } 54 | 55 | var targetDirectoryName = Path.GetDirectoryName(target); 56 | var directory = targetDirectoryName != null 57 | ? Path.Combine(BaseDirectory, package, targetDirectoryName) 58 | : Path.Combine(BaseDirectory, package); 59 | 60 | if (!Directory.Exists(directory)) 61 | { 62 | Directory.CreateDirectory(directory); 63 | } 64 | 65 | var localPath = Path.Combine(directory, file); 66 | using (var fileStream = File.Create(localPath)) 67 | { 68 | await response.Content.CopyToAsync(fileStream); 69 | } 70 | 71 | Console.WriteLine($"{response.RequestMessage.RequestUri}... OK"); 72 | return (response.RequestMessage.RequestUri.ToString(), localPath); 73 | } 74 | else 75 | { 76 | Console.WriteLine($"{response.RequestMessage.RequestUri}... failed ({(int)response.StatusCode})"); 77 | return default; 78 | } 79 | } 80 | } 81 | 82 | public static async Task RestoreDistFile(string url, string path) 83 | { 84 | using (var response = await Client.GetAsync(url)) 85 | { 86 | if (response.IsSuccessStatusCode) 87 | { 88 | path = path.Replace('/', Path.DirectorySeparatorChar); 89 | var directory = Path.GetDirectoryName(path); 90 | if (directory != null && !Directory.Exists(directory)) 91 | { 92 | Directory.CreateDirectory(directory); 93 | } 94 | 95 | using (var fileStream = File.Create(path)) 96 | { 97 | await response.Content.CopyToAsync(fileStream); 98 | } 99 | 100 | Console.WriteLine($"{response.RequestMessage.RequestUri}... OK"); 101 | } 102 | else 103 | { 104 | Console.WriteLine($"{response.RequestMessage.RequestUri}... failed ({(int)response.StatusCode})"); 105 | } 106 | } 107 | } 108 | 109 | private static string TargetFile(string package, string path) 110 | { 111 | var packageEnd = package.Split(SplitChar, StringSplitOptions.RemoveEmptyEntries).Last(); 112 | var pathParts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).ToArray(); 113 | 114 | if (pathParts.Contains("dist")) 115 | { 116 | pathParts = pathParts 117 | .SkipWhile(s => !s.Equals("dist", StringComparison.OrdinalIgnoreCase)) 118 | .Skip(1).ToArray(); 119 | 120 | switch (pathParts.Length) 121 | { 122 | case 0: 123 | return path; 124 | case 1: 125 | return pathParts[0]; 126 | default: 127 | if (pathParts[0].Equals(packageEnd, StringComparison.OrdinalIgnoreCase)) 128 | { 129 | pathParts = pathParts.Skip(1).ToArray(); 130 | } 131 | 132 | break; 133 | } 134 | } 135 | else 136 | { 137 | pathParts = pathParts.Skip(1).ToArray(); 138 | } 139 | 140 | return string.Join(Path.DirectorySeparatorChar.ToString(), pathParts); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Help.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RendleLabs.Unpkg 4 | { 5 | public static class Help 6 | { 7 | public static void Empty() 8 | { 9 | Console.WriteLine("Usage: unpkg [command] [options] [arguments]"); 10 | Console.WriteLine(); 11 | Console.WriteLine("Commands:"); 12 | Console.WriteLine(" add Add a package"); 13 | Console.WriteLine(" restore Restore packages"); 14 | Console.WriteLine(" upgrade Upgrade packages"); 15 | Console.WriteLine(" --add-task Install the RendleLabs.Unpkg.Build package to restore as part of build"); 16 | } 17 | 18 | public static void Add() 19 | { 20 | Console.WriteLine("Usage: unpkg add [OPTIONS] [...]"); 21 | Console.WriteLine(); 22 | Console.WriteLine("Arguments:"); 23 | Console.WriteLine(" The name of a package on unpkg.com."); 24 | Console.WriteLine(); 25 | Console.WriteLine("Options:"); 26 | Console.WriteLine(" --wwwroot Name of your static files directory"); 27 | Console.WriteLine(); 28 | Console.WriteLine("Examples:"); 29 | Console.WriteLine(" unpkg add --wwwroot=public jquery bootstrap popper.js"); 30 | Console.WriteLine(" unpkg add bootswatch/yeti"); 31 | Console.WriteLine(" unpkg add @aspnet/signalr/browser"); 32 | Console.WriteLine(); 33 | } 34 | 35 | public static void Restore() 36 | { 37 | Console.WriteLine("Usage: unpkg restore"); 38 | Console.WriteLine(); 39 | } 40 | 41 | public static void Upgrade() 42 | { 43 | Console.WriteLine("Usage: unpkg upgrade [ [...]]"); 44 | Console.WriteLine(); 45 | Console.WriteLine("Arguments:"); 46 | Console.WriteLine(" (Optional) The name of an installed package."); 47 | Console.WriteLine(" If omitted, all packages will be upgraded."); 48 | Console.WriteLine(); 49 | Console.WriteLine("Examples:"); 50 | Console.WriteLine(" unpkg upgrade"); 51 | Console.WriteLine(" unpkg upgrade jquery bootstrap popper.js"); 52 | Console.WriteLine(" unpkg upgrade @aspnet/signalr/browser"); 53 | Console.WriteLine(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/RendleLabs.Unpkg.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net46 4 | latest 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Restore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Threading.Tasks; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace RendleLabs.Unpkg 10 | { 11 | public static class Restore 12 | { 13 | public static Task Run(IEnumerable args) 14 | { 15 | var argList = args.ToList(); 16 | if (argList.Count > 0 && (argList[0] == "--help" || argList[0] == "-h")) 17 | { 18 | Help.Restore(); 19 | return Task.FromResult(new RestoreResults()); 20 | } 21 | 22 | return Run(Environment.CurrentDirectory, "unpkg.json"); 23 | } 24 | 25 | public static async Task Run(string directory, string unpkgFile) 26 | { 27 | if (directory == null) throw new ArgumentNullException(nameof(directory)); 28 | if (unpkgFile == null) throw new ArgumentNullException(nameof(unpkgFile)); 29 | 30 | var fullFilePath = Path.Combine(directory, unpkgFile); 31 | 32 | if (!File.Exists(fullFilePath)) 33 | { 34 | return new RestoreResults {Error = $"No {unpkgFile} file found in current directory."}; 35 | } 36 | 37 | string json; 38 | using (var reader = File.OpenText(fullFilePath)) 39 | { 40 | json = await reader.ReadToEndAsync(); 41 | } 42 | 43 | JObject file; 44 | try 45 | { 46 | file = JObject.Parse(json); 47 | } 48 | catch 49 | { 50 | return new RestoreResults {Error = $"Error parsing {unpkgFile}."}; 51 | } 52 | 53 | var allFiles = await Task.WhenAll(file.Properties().Select(p => DownloadFiles((JObject) p.Value))); 54 | 55 | return new RestoreResults {Results = allFiles.SelectMany(f => f).ToArray()}; 56 | 57 | } 58 | 59 | private static Task DownloadFiles(JObject entry) 60 | { 61 | var files = (JArray) entry["files"]; 62 | 63 | return Task.WhenAll(files 64 | .Select(f => DownloadFile((JObject)f))); 65 | } 66 | 67 | private static async Task DownloadFile(JObject file) 68 | { 69 | var local = file["local"]?.Value()?.Replace('/', Path.DirectorySeparatorChar); 70 | var cdn = file["cdn"]?.Value(); 71 | 72 | if (string.IsNullOrWhiteSpace(local) || string.IsNullOrWhiteSpace(cdn)) 73 | { 74 | return new RestoreResult {Error = $"Could not restore: {cdn}"}; 75 | } 76 | 77 | if (!File.Exists(local) 78 | || !TryGetHashAlgorithm(file["integrity"].Value(), out var hashAlgorithm, out var storedHash) 79 | || !storedHash.Equals(GetCurrentFileHash(local, hashAlgorithm))) 80 | { 81 | await Download.RestoreDistFile(cdn, local); 82 | return new RestoreResult {CdnUrl = cdn, LocalFile = local}; 83 | } 84 | 85 | Console.WriteLine($"{local} is up-to-date."); 86 | return new RestoreResult {Message = $"{local} is up-to-date."}; 87 | 88 | } 89 | 90 | private static string GetCurrentFileHash(string local, HashAlgorithm hashAlgorithm) 91 | { 92 | using (var stream = File.OpenRead(local)) 93 | { 94 | return Convert.ToBase64String(hashAlgorithm.ComputeHash(stream)); 95 | } 96 | } 97 | 98 | private static bool TryGetHashAlgorithm(string integrity, out HashAlgorithm hashAlgorithm, out string storedHash) 99 | { 100 | if (!string.IsNullOrWhiteSpace(integrity)) 101 | { 102 | var integrityBits = integrity.Split(new[]{'-'}, 2); 103 | if (integrityBits.Length == 2) 104 | { 105 | hashAlgorithm = GetAlgorithm(integrityBits[0]); 106 | storedHash = integrityBits[1]; 107 | return hashAlgorithm != null; 108 | } 109 | } 110 | 111 | hashAlgorithm = default; 112 | storedHash = default; 113 | return false; 114 | } 115 | 116 | private static HashAlgorithm GetAlgorithm(string name) 117 | { 118 | switch (name.ToLowerInvariant()) 119 | { 120 | case "sha256": 121 | return SHA256.Create(); 122 | case "sha384": 123 | return SHA384.Create(); 124 | case "sha512": 125 | return SHA512.Create(); 126 | default: 127 | return null; 128 | } 129 | } 130 | } 131 | 132 | public class RestoreResults 133 | { 134 | public RestoreResult[] Results { get; set; } 135 | public string Message { get; set; } 136 | public string Error { get; set; } 137 | } 138 | 139 | public class RestoreResult 140 | { 141 | public string LocalFile { get; set; } 142 | public string Message { get; set; } 143 | public string Error { get; set; } 144 | public string CdnUrl { get; set; } 145 | } 146 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace RendleLabs.Unpkg 6 | { 7 | public static class Settings 8 | { 9 | public static void Initialize(string[] args) 10 | { 11 | var localConfig = Path.Combine(Environment.CurrentDirectory, "unpkg.config"); 12 | var userConfig = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unpkg", "unpkg.config"); 13 | 14 | Wwwroot = TryCommandLineArgs(args, "wwwroot") 15 | ?? TryJsonFile(localConfig, "wwwroot") 16 | ?? TryEnvironmentVariable("wwwroot") 17 | ?? TryJsonFile(userConfig, Wwwroot) 18 | ?? "wwwroot"; 19 | } 20 | 21 | public static string Wwwroot { get; set; } 22 | 23 | private static string TryJsonFile(string path, string key) 24 | { 25 | if (!File.Exists(path)) 26 | { 27 | return null; 28 | } 29 | 30 | try 31 | { 32 | using (var stream = File.OpenText(path)) 33 | { 34 | var json = stream.ReadToEnd(); 35 | var jobj = JObject.Parse(json); 36 | if (jobj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out var value)) 37 | { 38 | return value.ToString(); 39 | } 40 | } 41 | } 42 | catch 43 | { 44 | return null; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | private static string TryCommandLineArgs(string[] args, string key) 51 | { 52 | key = $"--{key}"; 53 | try 54 | { 55 | for (int i = 0; i < args.Length; i++) 56 | { 57 | if (args[i].StartsWith(key, StringComparison.OrdinalIgnoreCase)) 58 | { 59 | if (args[i].Contains("=")) 60 | { 61 | var parts = args[i].Split(new[] {'='}, 2); 62 | return parts[1]; 63 | } 64 | else 65 | { 66 | if (args.Length > i + 1) 67 | { 68 | return args[i + 1]; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | catch 75 | { 76 | return null; 77 | } 78 | 79 | return null; 80 | } 81 | 82 | private static string TryEnvironmentVariable(string key) 83 | { 84 | try 85 | { 86 | return Environment.GetEnvironmentVariable($"UNPKG_{key}"); 87 | } 88 | catch 89 | { 90 | return null; 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/TrimExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace RendleLabs.Unpkg 2 | { 3 | public static class TrimExtensions 4 | { 5 | public static string TrimSlashes(this string path) => path?.Trim().Trim('/'); 6 | } 7 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/UnpkgJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace RendleLabs.Unpkg 10 | { 11 | public static class UnpkgJson 12 | { 13 | public static async Task Save(IEnumerable entries) 14 | { 15 | JObject file; 16 | if (File.Exists("unpkg.json")) 17 | { 18 | using (var reader = File.OpenText("unpkg.json")) 19 | { 20 | var content = await reader.ReadToEndAsync(); 21 | file = JObject.Parse(content); 22 | } 23 | } 24 | else 25 | { 26 | file = new JObject(); 27 | } 28 | 29 | foreach (var entry in entries) 30 | { 31 | var jEntry = CreateSaveObject(entry); 32 | file[CleanPackageName(entry.PackageName)] = jEntry; 33 | } 34 | 35 | using (var writer = File.CreateText("unpkg.json")) 36 | { 37 | await writer.WriteAsync(file.ToString(Formatting.Indented)); 38 | } 39 | } 40 | 41 | private static JObject CreateSaveObject(UnpkgJsonEntry entry) 42 | { 43 | var jEntry = new JObject 44 | { 45 | ["version"] = ExtractVersion(entry.Version), 46 | ["files"] = JArray.FromObject(entry.Files.Select(f => 47 | new {file = f.Path, cdn = f.CdnUrl, local = f.LocalPath, integrity = f.Integrity})) 48 | }; 49 | return jEntry; 50 | } 51 | 52 | public static async Task Load() 53 | { 54 | var file = await LoadJson(); 55 | return file == null ? Array.Empty() : Parse(file.Properties()).ToArray(); 56 | } 57 | 58 | private static IEnumerable Parse(IEnumerable properties) 59 | { 60 | return properties.Select(property => new UnpkgJsonEntry 61 | { 62 | Version = property.Value["version"].Value(), 63 | PackageName = property.Name, 64 | Files = property.Value["files"].Values() 65 | .Select(ParseUnpkgJsonFile).ToList() 66 | }); 67 | } 68 | 69 | private static UnpkgJsonFile ParseUnpkgJsonFile(JObject f) 70 | { 71 | return new UnpkgJsonFile 72 | { 73 | Path = f["file"].Value(), 74 | CdnUrl = f["cdn"].Value(), 75 | LocalPath = f["local"].Value(), 76 | Integrity = f["integrity"].Value() 77 | }; 78 | } 79 | 80 | private static async Task LoadJson() 81 | { 82 | if (!File.Exists("unpkg.json")) return default; 83 | using (var reader = File.OpenText("unpkg.json")) 84 | { 85 | var content = await reader.ReadToEndAsync(); 86 | return JObject.Parse(content); 87 | } 88 | } 89 | 90 | private static string CleanPackageName(string full) 91 | { 92 | var segments = full.Split('/'); 93 | 94 | for (int i = 0, l = segments.Length; i < l; i++) 95 | { 96 | if (segments[i].IndexOf('@') > 0) 97 | { 98 | segments[i] = segments[i].Split('@')[0]; 99 | } 100 | } 101 | 102 | return string.Join("/", segments); 103 | } 104 | 105 | public static string ExtractVersion(string full) 106 | { 107 | var parts = full.Split(new[]{'/'}, StringSplitOptions.RemoveEmptyEntries); 108 | var fileAtVersion = parts 109 | .FirstOrDefault(s => s[0] != '@' && s.Contains('@')); 110 | if (fileAtVersion == null) return parts[0]; 111 | parts = fileAtVersion.Split(new[]{'@'}, 2, StringSplitOptions.RemoveEmptyEntries); 112 | return parts[1]; 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/UnpkgJsonEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RendleLabs.Unpkg.Annotations; 3 | 4 | namespace RendleLabs.Unpkg 5 | { 6 | [PublicAPI] 7 | public class UnpkgJsonEntry 8 | { 9 | public string PackageName { get; set; } 10 | public string Version { get; set; } 11 | public List Files { get; set; } 12 | 13 | public static UnpkgJsonEntry Create(string packageName, DistFile file) 14 | { 15 | var entry = new UnpkgJsonEntry 16 | { 17 | PackageName = packageName, 18 | Version = file.BaseUrl, 19 | Files = new List() 20 | }; 21 | AddFiles(entry.Files, file.Files); 22 | return entry; 23 | } 24 | 25 | private static void AddFiles(List files, IEnumerable distFiles) 26 | { 27 | foreach (var distFile in distFiles) 28 | { 29 | if (distFile.Type == "file") 30 | { 31 | files.Add(new UnpkgJsonFile 32 | { 33 | Path = distFile.Path, 34 | LocalPath = distFile.LocalPath, 35 | CdnUrl = distFile.Url, 36 | Integrity = distFile.Integrity 37 | }); 38 | } 39 | else if (distFile.Files?.Count > 0) 40 | { 41 | AddFiles(files, distFile.Files); 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/UnpkgJsonFile.cs: -------------------------------------------------------------------------------- 1 | namespace RendleLabs.Unpkg 2 | { 3 | public class UnpkgJsonFile 4 | { 5 | public string Path { get; set; } 6 | public string Integrity { get; set; } 7 | public string CdnUrl { get; set; } 8 | public string LocalPath { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/Upgrade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace RendleLabs.Unpkg 8 | { 9 | public static class Upgrade 10 | { 11 | private static readonly string BaseDirectory = Path.Combine("wwwroot", "lib"); 12 | 13 | public static async Task Run(IEnumerable args) 14 | { 15 | var argList = args.ToList(); 16 | HashSet packages = null; 17 | if (argList.Count > 0) 18 | { 19 | if (argList[0] == "--help" || argList[0] == "-h") 20 | { 21 | Help.Upgrade(); 22 | return; 23 | } 24 | else 25 | { 26 | packages = new HashSet(argList, StringComparer.OrdinalIgnoreCase); 27 | } 28 | } 29 | 30 | if (!Directory.Exists(BaseDirectory)) 31 | { 32 | Directory.CreateDirectory(BaseDirectory); 33 | } 34 | 35 | var entries = await UnpkgJson.Load(); 36 | 37 | if (packages != null) 38 | { 39 | entries = entries.Where(e => packages.Contains(e.PackageName)).ToArray(); 40 | } 41 | 42 | entries = (await Task.WhenAll(entries.Select(GetDistFile))) 43 | .Where((e, d) => VersionComparison.IsGreater(d.Version, e.Version)) 44 | .Select((e, _) => e) 45 | .ToArray(); 46 | 47 | foreach (var entry in entries) 48 | { 49 | foreach (var file in entry.Files) 50 | { 51 | if (File.Exists(file.LocalPath)) 52 | { 53 | File.Delete(file.LocalPath); 54 | } 55 | } 56 | } 57 | 58 | await Add.Run(entries.Select(e => e.PackageName)); 59 | } 60 | 61 | private static async Task<(UnpkgJsonEntry, DistFile)> GetDistFile(UnpkgJsonEntry entry) 62 | { 63 | return (entry, await Dist.Get(entry.PackageName)); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/ValueTupleLinq.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RendleLabs.Unpkg 6 | { 7 | public static class ValueTupleLinq 8 | { 9 | public static IEnumerable> Where(this IEnumerable> source, Func predicate) 10 | { 11 | return source.Where(t => predicate(t.Item1, t.Item2)); 12 | } 13 | 14 | public static IEnumerable Select(this IEnumerable> source, Func selector) 15 | { 16 | return source.Select(t => selector(t.Item1, t.Item2)); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/RendleLabs.Unpkg/VersionComparison.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Semver; 3 | 4 | namespace RendleLabs.Unpkg 5 | { 6 | public static class VersionComparison 7 | { 8 | public static bool IsGreater(string left, string right) 9 | { 10 | if (left.Equals(right, StringComparison.OrdinalIgnoreCase)) return false; 11 | 12 | if (SemVersion.TryParse(left, out var lv)) 13 | { 14 | if (SemVersion.TryParse(right, out var rv)) 15 | { 16 | return lv > rv; 17 | } 18 | else 19 | { 20 | Console.WriteLine($"'{right}' makes no sense as a version number."); 21 | } 22 | } 23 | else 24 | { 25 | Console.WriteLine($"'{left}' makes no sense as a version number."); 26 | } 27 | 28 | return false; // Don't know, let's be pessimistic 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using RendleLabs.Unpkg; 8 | 9 | namespace dotnet_unpkg 10 | { 11 | public static class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | if (args.Length == 0) 16 | { 17 | Help.Empty(); 18 | return; 19 | } 20 | 21 | Settings.Initialize(Flags(args).ToArray()); 22 | 23 | try 24 | { 25 | switch (args[0]) 26 | { 27 | case "--help": 28 | case "-h": 29 | Help.Empty(); 30 | return; 31 | case "--add-task": 32 | AddBuildTask(); 33 | return; 34 | case "a": 35 | case "add": 36 | await Add.Run(CommandArguments(args)); 37 | break; 38 | case "u": 39 | case "update": 40 | case "upgrade": 41 | case "up": 42 | await Upgrade.Run(CommandArguments(args)); 43 | break; 44 | case "r": 45 | case "restore": 46 | var results = await Restore.Run(CommandArguments(args)); 47 | WriteRestoreResults(results); 48 | break; 49 | default: 50 | Help.Empty(); 51 | break; 52 | } 53 | } 54 | catch (Exception exception) 55 | { 56 | Console.WriteLine(exception); 57 | } 58 | } 59 | 60 | private static void AddBuildTask() 61 | { 62 | var psi = new ProcessStartInfo 63 | { 64 | FileName = "dotnet", 65 | Arguments = "add package RendleLabs.Unpkg.Build", 66 | UseShellExecute = false, 67 | LoadUserProfile = true 68 | }; 69 | Process.Start(psi)?.WaitForExit(); 70 | } 71 | 72 | private static void WriteRestoreResults(RestoreResults results) 73 | { 74 | if (!string.IsNullOrEmpty(results.Message)) 75 | { 76 | Console.WriteLine(results.Message); 77 | } 78 | else 79 | { 80 | foreach (var result in results.Results) 81 | { 82 | if (!string.IsNullOrEmpty(result.Message)) 83 | { 84 | Console.WriteLine(result.Message); 85 | } 86 | else 87 | { 88 | Console.WriteLine($"{result.CdnUrl} -> {result.LocalFile}"); 89 | } 90 | } 91 | } 92 | } 93 | 94 | private static IEnumerable Flags(string[] args) 95 | { 96 | for (int i = 1, l = args.Length; i < l; i++) 97 | { 98 | if (args[i].StartsWith('-') || args[i].StartsWith('/')) 99 | { 100 | yield return args[i]; 101 | 102 | if (!args[i].Contains('=')) 103 | { 104 | if (++i >= args.Length) 105 | { 106 | break; 107 | } 108 | 109 | yield return args[i]; 110 | } 111 | } 112 | } 113 | } 114 | 115 | private static IEnumerable CommandArguments(string[] args) 116 | { 117 | for (int i = 1, l = args.Length; i < l; i++) 118 | { 119 | if (args[i].StartsWith('-')) 120 | { 121 | // --wwwroot=public 122 | if (args[i].Contains('=')) continue; 123 | 124 | // --wwwroot public 125 | i++; 126 | continue; 127 | } 128 | 129 | yield return args[i]; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/cli/unpkg.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 2.0.0 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | unpkg 8 | true 9 | ..\..\artifacts\$(Configuration) 10 | RendleLabs.UnpkgCli 11 | dotnet cli extension for installing front-end packages from unpkg.com 12 | false 13 | Supports namespaces packages 14 | Copyright (C) 2018 RendleLabs 15 | frontend package-management 16 | https://github.com/rendlelabs/dotnet-unpkg 17 | https://github.com/RendleLabs/dotnet-unpkg/blob/master/LICENSE 18 | https://github.com/rendlelabs/dotnet-unpkg 19 | True 20 | unpkg 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/TestConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TestConsoleApp 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | Console.WriteLine("Hello World!"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestConsoleApp/TestConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/TestLibmanApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TestLibmanApp 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | Console.WriteLine("Hello World!"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestLibmanApp/TestLibmanApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------