├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── ElectronInstaller ├── .gitignore ├── ElectronInstaller.sln └── ElectronInstaller │ ├── App.config │ ├── ElectronInstaller.csproj │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── atom.ico │ └── packages.config ├── LICENSE ├── README.md ├── appveyor.yml ├── bin ├── ElectronInstaller.exe └── windowsstore.js ├── commitlint.config.js ├── lib ├── assets.js ├── convert.js ├── deploy.js ├── dotfile.js ├── finalsay.js ├── index.js ├── makeappx.js ├── makepri.js ├── manifest.js ├── params.js ├── setup.js ├── sign.js ├── utils.js ├── vendor │ └── tail.js └── zip.js ├── package-lock.json ├── package.json ├── ps1 ├── convert.ps1 └── zip.ps1 ├── release.config.js ├── template ├── appxmanifest.xml └── assets │ ├── SampleAppx.150x150.png │ ├── SampleAppx.310x150.png │ ├── SampleAppx.44x44.png │ └── SampleAppx.50x50.png └── test ├── fixtures ├── assets-scaled │ ├── AppNameMedTile.scale-100.png │ ├── AppNameMedTile.scale-200.png │ └── AppNameMedTile.scale-400.png ├── assets │ └── AppNameMedTile.png └── child_process.js ├── lib ├── assets.js ├── bogus-private-key.pvk ├── deploy.js ├── makeappx.js ├── makepri.js ├── manifest.js ├── sign.js ├── utils.js └── zip.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Vim 36 | *.swp 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "10" 3 | 4 | jobs: 5 | include: 6 | - stage: test 7 | script: npm run test 8 | 9 | cache: 10 | npm: true 11 | 12 | before_install: 13 | - npm config set no-progress 14 | 15 | install: 16 | - npm install 17 | 18 | script: 19 | - commitlint-travis -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/bin/windowsstore.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": true, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /ElectronInstaller/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronInstaller", "ElectronInstaller\ElectronInstaller.csproj", "{07C90BD6-F73F-44C4-B3F0-2CCEDE07E046}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {07C90BD6-F73F-44C4-B3F0-2CCEDE07E046}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {07C90BD6-F73F-44C4-B3F0-2CCEDE07E046}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {07C90BD6-F73F-44C4-B3F0-2CCEDE07E046}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {07C90BD6-F73F-44C4-B3F0-2CCEDE07E046}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/ElectronInstaller.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {07C90BD6-F73F-44C4-B3F0-2CCEDE07E046} 8 | WinExe 9 | Properties 10 | ElectronInstaller 11 | ElectronInstaller 12 | v4.6 13 | 512 14 | 15 | publish\ 16 | true 17 | Disk 18 | false 19 | Foreground 20 | 7 21 | Days 22 | false 23 | false 24 | true 25 | 0 26 | 1.0.0.%2a 27 | false 28 | false 29 | true 30 | 31 | 32 | AnyCPU 33 | true 34 | full 35 | false 36 | bin\Debug\ 37 | DEBUG;TRACE 38 | prompt 39 | 4 40 | 41 | 42 | AnyCPU 43 | pdbonly 44 | true 45 | bin\Release\ 46 | TRACE 47 | prompt 48 | 4 49 | 50 | 51 | atom.ico 52 | 53 | 54 | false 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | False 84 | Microsoft .NET Framework 4.5 %28x86 and x64%29 85 | true 86 | 87 | 88 | False 89 | .NET Framework 3.5 SP1 Client Profile 90 | false 91 | 92 | 93 | False 94 | .NET Framework 3.5 SP1 95 | false 96 | 97 | 98 | 99 | 106 | -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.IO; 7 | using System.IO.Compression; 8 | using System.Reflection; 9 | 10 | namespace ElectronInstaller 11 | { 12 | class Program 13 | { 14 | public static string AssemblyDirectory 15 | { 16 | get 17 | { 18 | string codeBase = Assembly.GetExecutingAssembly().CodeBase; 19 | UriBuilder uri = new UriBuilder(codeBase); 20 | string path = Uri.UnescapeDataString(uri.Path); 21 | return Path.GetDirectoryName(path); 22 | } 23 | } 24 | 25 | static void Main(string[] args) 26 | { 27 | Console.WriteLine("This tool functions as a basic installer for Electron Apps, to be used with the Desktop to UWP converter (also known as Project Centennial)."); 28 | Console.WriteLine(""); 29 | 30 | var input = Path.Combine(AssemblyDirectory, "app.zip"); 31 | var output = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "e"); 32 | 33 | if (!Directory.Exists(output)) 34 | { 35 | Directory.CreateDirectory(output); 36 | } 37 | 38 | Logger("Input:"); 39 | Logger(input); 40 | Logger("Output:"); 41 | Logger(output); 42 | 43 | Unzip(input, output); 44 | } 45 | 46 | static void Logger(String lines) 47 | { 48 | var log = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "e", "log.txt"); 49 | System.IO.StreamWriter file = new System.IO.StreamWriter(log, true); 50 | file.WriteLine(lines); 51 | 52 | file.Close(); 53 | } 54 | 55 | static void Unzip(string zip, string destination) 56 | { 57 | 58 | if (!File.Exists(zip)) 59 | { 60 | Logger("app.zip does not exist or could not be found"); 61 | Environment.Exit(1); 62 | return; 63 | } 64 | 65 | Logger("Unzipping " + zip + " to " + destination); 66 | ZipFile.ExtractToDirectory(zip, destination); 67 | Logger("Unzip operation successful"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Electron Installer")] 9 | [assembly: AssemblyDescription("Installs Electron apps into Project Centennial")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft Corporation")] 12 | [assembly: AssemblyProduct("ElectronInstaller")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft Corporation 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("fece1ea4-ea62-47bd-837f-bd0f8e312459")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/atom.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/ElectronInstaller/ElectronInstaller/atom.ico -------------------------------------------------------------------------------- /ElectronInstaller/ElectronInstaller/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Felix Rieseberg 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 | # Electron Apps in the Windows Store 2 | npm version 3 | 4 | Electron-Windows-Store: A CLI that takes the packaged output of your Electron app, then converts it into an AppX package. This allows you to submit your Electron app to the Windows Store :package:. You can also distribute your app as an `.appx` without using the Windows Store, allowing users to just double-click your `.appx` to automatically install it. 5 | 6 | ![](https://cloud.githubusercontent.com/assets/1426799/15042115/3471f6a0-12b9-11e6-91b4-80f25ec1d0b8.jpg) 7 | 8 | To install this command line tool, get it directly from npm: 9 | 10 | ``` 11 | npm install -g electron-windows-store 12 | ``` 13 | 14 | Then, configure your PowerShell: 15 | 16 | ``` 17 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned 18 | ``` 19 | 20 | To turn an Electron app into an AppX package, run: 21 | 22 | ``` 23 | electron-windows-store --input-directory C:\myelectronapp --output-directory C:\output\myelectronapp --package-version 1.0.0.0 --package-name myelectronapp 24 | ``` 25 | 26 | This tool supports two methods to create AppX packages: Either using manual file copy operations, or using Windows Containers. The first option requires only the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk), while the second option also requires the [Desktop App Converter](https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-run-desktop-app-converter). 27 | 28 | # Usage 29 | Before running the Electron-Windows-Store CLI, let's make sure we have all the prerequisites in place. You will need: 30 | 31 | * Windows 10 with at least the Anniversary Update (if your Windows has been updated before 2018, you're good). 32 | * Windows 10 SDK from [here](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk) 33 | * Node 8 or above (to check, run `node -v`) 34 | 35 | ## Package Your Electron Application 36 | Package the application using [electron-packager](https://github.com/electron-userland/electron-packager) (or something similar). Make sure to remove node_modules that you don't need in your final application. 37 | 38 | The output should look roughly like this: 39 | ``` 40 | ├── Ghost.exe 41 | ├── LICENSE 42 | ├── content_resources_200_percent.pak 43 | ├── node.dll 44 | ├── pdf.dll 45 | ├── resources 46 | │   ├── app 47 | │   └── atom.asar 48 | ├── snapshot_blob.bin 49 | ├── [... and more files] 50 | ``` 51 | 52 | ## Convert with File Copying 53 | **From an elevated PowerShell (run it "as Administrator")**, run `electron-windows-store` with the required parameters, passing both the input and output directories, the app's name and version. If you don't pass these parameters, we will simply ask you for them. 54 | 55 | ``` 56 | electron-windows-store --input-directory C:\myelectronapp --output-directory C:\output\myelectronapp --package-version 1.0.0.0 --package-name myelectronapp 57 | ``` 58 | 59 | These are all options for the CLI: 60 | 61 | ``` 62 | -h, --help output usage information 63 | -V, --version output the version number 64 | -c, --container-virtualization Create package using Windows Container virtualization 65 | -b, --windows-build Display Windows Build information 66 | -i, --input-directory Directory containing your application 67 | -o, --output-directory Output directory for the appx 68 | -p, --package-version Version of the app package 69 | -n, --package-name Name of the app package 70 | --package-display-name Display name of the package 71 | --package-description Description of the package 72 | --package-background-color Background color for the app icon (example: #464646) 73 | -e, --package-executable Path to the package executable 74 | -a, --assets Path to the visual assets for the appx 75 | -m, --manifest Path to a manifest, if you want to be overwritten 76 | -d, --deploy Should the app be deployed after creation? 77 | --identity-name Name for identity 78 | --publisher Publisher to use (example: CN=developmentca) 79 | --publisher-display-name Publisher display name to use 80 | --make-pri Use makepri.exe (you don't need to unless you know you do) 81 | --windows-kit Path to the Windows Kit bin folder 82 | --dev-cert Path to the developer certificate to use 83 | --cert-pass Password to use when signing the application (only necessary if a p12 certication is used) 84 | --desktop-converter Path to the desktop converter tools 85 | --expanded-base-image Path to the expanded base image 86 | --makeappx-params Additional parameters for Make-AppXPackage (example: --makeappx-params "/l","/d") 87 | --signtool-params Additional parameters for signtool.exe (example: --makeappx-params "/l","/d") 88 | --create-config-params Additional parameters for makepri.exe "createconfig" (example: --create-config-params "/l","/d")') 89 | --create-pri-params Additional parameters for makepri.exe "new" (example: --create-pri-params "/l","/d")') 90 | --verbose Enable debugging (similar to setting a DEBUG=electron-windows-store environment variable) 91 | ``` 92 | 93 | ## Programmatic Usage 94 | You can call this package directly. All options correspond to the CLI options and are equally optional. There is one exception: You can provide a `finalSay` function, which will be executed right before `makeappx.exe` is being called. This allows you to modify the output folder right before we turn it into a package. 95 | 96 | ```js 97 | const convertToWindowsStore = require('electron-windows-store') 98 | 99 | convertToWindowsStore({ 100 | containerVirtualization: false, 101 | inputDirectory: 'C:\\input\\', 102 | outputDirectory: 'C:\\output\\', 103 | packageVersion: '1.0.0.0', 104 | packageName: 'Ghost', 105 | packageDisplayName: 'Ghost Desktop', 106 | packageDescription: 'Ghost for Desktops', 107 | packageExecutable: 'app/Ghost.exe', 108 | assets: 'C:\\assets\\', 109 | manifest: 'C:\\AppXManifest.xml', 110 | deploy: false, 111 | publisher: 'CN=developmentca', 112 | windowsKit: 'C:\\windowskit', 113 | devCert: 'C:\\devcert.pfx', 114 | certPass: 'abcd', 115 | desktopConverter: 'C:\\desktop-converter-tools', 116 | expandedBaseImage: 'C:\\base-image.wim', 117 | makeappxParams: ['/l'], 118 | signtoolParams: ['/p'], 119 | makePri: true, 120 | createConfigParams: ['/a'], 121 | createPriParams: ['/b'], 122 | protocol: "ghost-app", 123 | finalSay: function () { 124 | return new Promise((resolve, reject) => resolve()) 125 | } 126 | }) 127 | ``` 128 | 129 | ## Convert with Container Virtualization 130 | The Desktop App Converter is capable of running an installer and your app during conversion inside a Windows Container. This requires installation of the Desktop App Converter and has more advanced requirements. 131 | 132 | :warning: The _vast majority_ of Electron apps should be packaged using "File Copying". Unless you know that you need your `appx` to be created using a Windows container, use the "File Copying" method described above. 133 | 134 | :computer: Ensure that your computer is capable of running containers: You'll need a 64 bit (x64) processor, hardware-assisted virtualization and second Level Address Translation (SLAT). You will also need Windows 10 Enterprise Edition. 135 | 136 | :bulb: Before running the CLI for the first time, you will have to setup the "Windows Desktop App Converter". This will take a few minutes, but don't worry - you only have to do this once. Download and the Desktop App Converter from [here](https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-run-desktop-app-converter). You will receive two files: `DesktopAppConverter.zip` and `BaseImage-14316.wim`. 137 | 138 | 1. Unzip `DesktopAppConverter.zip`. From an elevated PowerShell (opened with "run as Administrator"., ensure that your systems execution policy allows us to run everything we intended to run by calling `Set-ExecutionPolicy bypass`. 139 | 2. Then, run the installation of the Desktop App Converter, passing in the location of the Windows .ase Image (downloaded as `BaseImage-14316.wim`), by calling `.\DesktopAppConverter.ps1 -Setup -BaseImage .\BaseImage-14316.wim`. 140 | 3. If running the above command prompts you for a reboot, please restart your machine and run the above command again after a successful restart. 141 | 142 | Then, run `electron-windows-store` with the `--container-virtualization` flag! 143 | 144 | #### What is the CLI Doing? 145 | Once executed, the tool goes to work: It accepts your Electron app as an input. Then, it archives your application as `app.zip`. Using an installer and a Windows Container, the tool creates an "expanded" AppX package - including the Windows Application Manifest (`AppXManifest.xml`) as well as the virtual file system and the virtual registry inside your output folder. 146 | 147 | Once we have the expanded AppX files, the tool uses the Windows App Packager (`MakeAppx.exe`) to create a single-file AppX package from those files on disk. Finally, the tool can be used to create a trusted certificate on your computer to sign the new AppX pacakge. With the signed AppX package, the CLI can also automatically install the package on your machine. 148 | 149 | ## Configuration 150 | :bulb: The first time you run this tool, it needs to know some settings. It will ask you only once and store your answers in your profile folder in a `.electron-windows-store` file. You can also provide these values as a parameter when running the CLI. 151 | 152 | ```json 153 | { 154 | "publisher": "CN=developmentca", 155 | "windowsKit": "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64", 156 | "devCert": "C:\\Tools\\DesktopConverter\\Certs\\devcert.pfx", 157 | "desktopConverter": "C:\\Tools\\DesktopConverter", 158 | "expandedBaseImage": "C:\\ProgramData\\Microsoft\\Windows\\Images\\BaseImage-14316\\" 159 | } 160 | ``` 161 | 162 | ## Using all the fancy Windows APIs 163 | You can pair up your Electron app with a little invisible UWP side-kick, enabling your Electron app to call all WinRT APIs. Check out [an example over here](https://github.com/felixrieseberg/electron-uwp-background). 164 | 165 | ## Devices 166 | The compiled AppX package still contains a win32 executable - and will therefore not run on Xbox, HoloLens, or Phones. 167 | 168 | ## Development 169 | `electron-windows-store` uses [Semantic Release](https://github.com/semantic-release/semantic-release) to 170 | automate the whole release process. In order to have a PR merged, please ensure that your PR 171 | follows the commit guidelines so that our robots can understand your change. This repository uses 172 | the [default `conventional-changelog` rules](https://www.conventionalcommits.org/en/v1.0.0-beta.2/). 173 | 174 | ## License 175 | Licensed using the MIT License (MIT); Copyright (c) Felix Rieseberg and Microsoft Corporation. For more information, please see [LICENSE](LICENSE). 176 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | 4 | # Test against these versions of Node.js. 5 | environment: 6 | matrix: 7 | - nodejs_version: "8" 8 | 9 | deploy: 10 | on: 11 | branch: master 12 | provider: script 13 | 14 | # Install scripts. (runs after repo cloning) 15 | install: 16 | - ps: Install-Product node $env:nodejs_version 17 | - npm install 18 | 19 | skip_tags: true 20 | skip_branch_with_pr: true 21 | max_jobs: 1 22 | 23 | # Post-install test scripts. 24 | test_script: 25 | # Output useful info for debugging. 26 | - node --version 27 | - npm --version 28 | # run tests 29 | - npm test 30 | 31 | deploy_script: 32 | - npm run semantic-release 33 | 34 | # Don't actually build. 35 | build: off 36 | 37 | # Set build version format here instead of in the admin panel. 38 | version: "{build}" 39 | 40 | cache: 41 | - node_modules # local npm modules 42 | - '%APPDATA%\npm-cache' # npm cache 43 | -------------------------------------------------------------------------------- /bin/ElectronInstaller.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/bin/ElectronInstaller.exe -------------------------------------------------------------------------------- /bin/windowsstore.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var program = require('commander') 8 | var os = require('os') 9 | var chalk = require('chalk') 10 | var pack = require('../package.json') 11 | 12 | // Ensure Node 4 13 | if (parseInt(process.versions.node[0], 10) < 4) { 14 | console.log('You need at least Node 4.x to run this script') 15 | } 16 | 17 | // Little helper function turning string input into an array 18 | function list (val) { 19 | return val.split(',') 20 | } 21 | 22 | program 23 | .version(pack.version) 24 | .option('-c, --container-virtualization', 'Create package using Windows Container virtualization') 25 | .option('-b, --windows-build', 'Display Windows Build information') 26 | .option('-i, --input-directory ', 'Directory containing your application') 27 | .option('-o, --output-directory ', 'Output directory for the appx') 28 | .option('-p, --package-version ', 'Version of the app package') 29 | .option('-n, --package-name ', 'Name of the app package') 30 | .option('--identity-name ', 'Name for identity') 31 | .option('--package-display-name ', 'Display name of the package') 32 | .option('--package-description ', 'Description of the package') 33 | .option('--package-background-color ', 'Background color for the app icon (example: #464646)') 34 | .option('-e, --package-executable ', 'Path to the package executable') 35 | .option('-a, --assets ', 'Path to the visual assets for the appx') 36 | .option('-m, --manifest ', 'Path to a manifest, if you want to overwrite the default one') 37 | .option('-d, --deploy ', 'Should the app be deployed after creation?') 38 | .option('--publisher ', 'Publisher to use (example: CN=developmentca)') 39 | .option('--publisher-display-name ', 'Publisher display name to use') 40 | .option('--windows-kit ', 'Path to the Windows Kit bin folder') 41 | .option('--dev-cert ', 'Path to the developer certificate to use') 42 | .option('--cert-pass ', 'Certification password') 43 | .option('--desktop-converter ', 'Path to the desktop converter tools') 44 | .option('--expanded-base-image ', 'Path to the expanded base image') 45 | .option('--make-pri ', 'Use makepri.exe (you don\'t need to unless you know you do)', (i) => (i === 'true')) 46 | .option('--makeappx-params ', 'Additional parameters for Make-AppXPackage (example: --makeappx-params "/l","/d")', list) 47 | .option('--signtool-params ', 'Additional parameters for signtool.exe (example: --makeappx-params "/l","/d")', list) 48 | .option('--create-config-params ', 'Additional parameters for makepri.exe "createconfig" (example: --create-config-params "/l","/d")', list) 49 | .option('--create-pri-params ', 'Additional parameters for makepri.exe "new" (example: --create-pri-params "/l","/d")', list) 50 | .option('--protocol ', 'Protocol scheme to start the application with.') 51 | .option('--verbose ', 'Enable debug mode') 52 | .parse(process.argv) 53 | 54 | if (program.windowsBuild) { 55 | console.log(os.release()) 56 | } 57 | 58 | if (program.verbose) { 59 | var debug = process.env.DEBUG || '' 60 | process.env.DEBUG = 'electron-windows-store,' + debug 61 | } 62 | 63 | var ensureParams = require('../lib/params') 64 | var zip = require('../lib/zip') 65 | var setup = require('../lib/setup') 66 | var sign = require('../lib/sign') 67 | var assets = require('../lib/assets') 68 | var convert = require('../lib/convert') 69 | var makeappx = require('../lib/makeappx') 70 | var manifest = require('../lib/manifest') 71 | var deploy = require('../lib/deploy') 72 | var makepri = require('../lib/makepri') 73 | 74 | setup(program) 75 | .then(() => ensureParams(program)) 76 | .then(() => zip(program)) 77 | .then(() => convert(program)) 78 | .then(() => assets(program)) 79 | .then(() => manifest(program)) 80 | .then(() => makepri(program)) 81 | .then(() => makeappx(program)) 82 | .then(() => sign.signAppx(program)) 83 | .then(() => deploy(program)) 84 | .then(() => console.log(chalk.bold.green('All done!'))) 85 | .catch(e => { 86 | console.log(e) 87 | console.log(e.stack) 88 | }) 89 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } 4 | -------------------------------------------------------------------------------- /lib/assets.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | const chalk = require('chalk') 6 | const utils = require('./utils') 7 | 8 | module.exports = function (program) { 9 | if (!program.assets) { 10 | return Promise.resolve() 11 | } 12 | 13 | // Let's copy in the assets 14 | utils.log(chalk.bold.green('Copying visual assets into pre-appx folder...')) 15 | 16 | const source = path.normalize(program.assets) 17 | const destination = path.join(program.outputDirectory, 'pre-appx', 'Assets') 18 | 19 | utils.debug(`Copying visual assets from ${source} to ${destination}`) 20 | 21 | return fs.copy(source, destination) 22 | .catch(error => { 23 | utils.debug(`Copying visual assets failed: ${JSON.stringify(error)}`) 24 | throw error 25 | }).then(() => utils.debug('Copying visual assets succeeded')) 26 | } 27 | -------------------------------------------------------------------------------- /lib/convert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const spawn = require('child_process').spawn 5 | const chalk = require('chalk') 6 | const fs = require('fs-extra') 7 | 8 | const Tail = require('./vendor/tail').Tail 9 | const utils = require('./utils') 10 | 11 | /** 12 | * Converts the given Electron app using Project Centennial 13 | * Container Virtualization. 14 | * 15 | * @param program - Program object containing the user's instructions 16 | * @returns - Promise 17 | */ 18 | function convertWithContainer (program) { 19 | return new Promise((resolve, reject) => { 20 | if (!program.desktopConverter) { 21 | utils.log('Could not find the Project Centennial Desktop App Converter, which is required to') 22 | utils.log('run the conversion to appx using a Windows Container.\n') 23 | utils.log('Consult the documentation at https://aka.ms/electron-windows-store for a tutorial.\n') 24 | utils.log('You can find the Desktop App Converter at https://www.microsoft.com/en-us/download/details.aspx?id=51691\n') 25 | utils.log('Exiting now - restart when you downloaded and unpacked the Desktop App Converter!') 26 | 27 | process.exit(0) 28 | } 29 | 30 | let preAppx = path.join(program.outputDirectory, 'pre-appx') 31 | let installer = path.join(program.outputDirectory, 'ElectronInstaller.exe') 32 | let logfile = path.join(program.outputDirectory, 'logs', 'conversion.log') 33 | let converterArgs = [ 34 | `-LogFile ${logfile}`, 35 | `-Installer '${installer}'`, 36 | `-Converter '${path.join(program.desktopConverter, 'DesktopAppConverter.ps1')}'`, 37 | `-ExpandedBaseImage ${program.expandedBaseImage}`, 38 | `-Destination '${preAppx}'`, 39 | `-PackageName "${program.packageName}"`, 40 | `-Version ${program.packageVersion}`, 41 | `-Publisher "${program.publisher}"`, 42 | `-AppExecutable '${program.packageExecutable}'` 43 | ] 44 | let args = `& {& '${path.resolve(__dirname, '..', 'ps1', 'convert.ps1')}' ${converterArgs.join(' ')}}` 45 | let child, tail 46 | 47 | utils.log(chalk.green.bold('Starting Conversion...')) 48 | utils.debug(`Conversion parameters used: ${JSON.stringify(converterArgs)}`) 49 | 50 | try { 51 | child = spawn('powershell.exe', ['-NoProfile', '-NoLogo', args]) 52 | } catch (error) { 53 | reject(error) 54 | } 55 | 56 | child.stdout.on('data', (data) => utils.debug(data.toString())) 57 | child.stderr.on('data', (data) => utils.debug(data.toString())) 58 | child.on('exit', () => { 59 | // The conversion process exited, let's look for a log file 60 | // However, give the PS process a 3s headstart, since we'll 61 | // crash if the logfile does not exist yet 62 | setTimeout(() => { 63 | tail = new Tail(logfile, { 64 | fromBeginning: true 65 | }) 66 | 67 | tail.on('line', (data) => { 68 | utils.log(data) 69 | 70 | if (data.indexOf('Conversion complete') > -1) { 71 | utils.log('') 72 | tail.unwatch() 73 | resolve() 74 | } else if (data.indexOf('An error occurred') > -1) { 75 | tail.unwatch() 76 | reject(new Error('Detected error in conversion log')) 77 | } 78 | }) 79 | 80 | tail.on('error', (err) => utils.log(err)) 81 | }, 3000) 82 | }) 83 | 84 | child.stdin.end() 85 | }) 86 | } 87 | 88 | /** 89 | * Converts the given Electron app using simple file copy 90 | * mechanisms. 91 | * 92 | * @param program - Program object containing the user's instructions 93 | * @returns - Promise 94 | */ 95 | function convertWithFileCopy (program) { 96 | return new Promise((resolve, reject) => { 97 | let preAppx = path.join(program.outputDirectory, 'pre-appx') 98 | let app = path.join(preAppx, 'app') 99 | let manifest = path.join(preAppx, 'AppXManifest.xml') 100 | let manifestTemplate = path.join(__dirname, '..', 'template', 'AppXManifest.xml') 101 | let assets = path.join(preAppx, 'assets') 102 | let assetsTemplate = path.join(__dirname, '..', 'template', 'assets') 103 | 104 | utils.log(chalk.green.bold('Starting Conversion...')) 105 | utils.debug(`Using pre-appx folder: ${preAppx}`) 106 | utils.debug(`Using app from: ${app}`) 107 | utils.debug(`Using manifest template from: ${manifestTemplate}`) 108 | utils.debug(`Using asset template from ${assetsTemplate}`) 109 | 110 | // Clean output folder 111 | utils.log(chalk.green.bold('Cleaning pre-appx output folder...')) 112 | fs.emptyDirSync(preAppx) 113 | 114 | // Copy in the new manifest, app, assets 115 | utils.log(chalk.green.bold('Copying data...')) 116 | fs.copySync(manifestTemplate, manifest) 117 | utils.debug('Copied manifest template to destination') 118 | fs.copySync(assetsTemplate, assets) 119 | utils.debug('Copied asset template to destination') 120 | fs.copySync(program.inputDirectory, app) 121 | utils.debug('Copied input app files to destination') 122 | 123 | // Then, overwrite the manifest 124 | fs.readFile(manifest, 'utf8', (err, data) => { 125 | utils.log(chalk.green.bold('Creating manifest..')) 126 | let result = data 127 | let executable = program.packageExecutable || `app\\${program.packageName}.exe` 128 | 129 | if (err) { 130 | utils.debug(`Could not read manifest template. Error: ${JSON.stringify(err)}`) 131 | return utils.log(err) 132 | } 133 | 134 | result = result.replace(/\${publisherName}/g, program.publisher) 135 | result = result.replace(/\${publisherDisplayName}/g, program.publisherDisplayName || 'Reserved') 136 | result = result.replace(/\${identityName}/g, program.identityName || program.packageName) 137 | result = result.replace(/\${packageVersion}/g, program.packageVersion) 138 | result = result.replace(/\${packageName}/g, program.packageName) 139 | result = result.replace(/\${packageExecutable}/g, executable) 140 | result = result.replace(/\${packageDisplayName}/g, program.packageDisplayName || program.packageName) 141 | result = result.replace(/\${packageDescription}/g, program.packageDescription || program.packageName) 142 | result = result.replace(/\${packageBackgroundColor}/g, program.packageBackgroundColor || '#464646') 143 | result = result.replace(/\${protocol}/g, program.protocol ? ` 144 | 145 | 146 | ` : '') 147 | 148 | fs.writeFile(manifest, result, 'utf8', (err) => { 149 | if (err) { 150 | const errorMessage = `Could not write manifest file in pre-appx location. Error: ${JSON.stringify(err)}` 151 | utils.debug(errorMessage) 152 | return reject(new Error(errorMessage)) 153 | } 154 | 155 | resolve() 156 | }) 157 | }) 158 | }) 159 | } 160 | 161 | module.exports = function (program) { 162 | if (program.containerVirtualization) { 163 | return convertWithContainer(program) 164 | } else { 165 | return convertWithFileCopy(program) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | 5 | const utils = require('./utils') 6 | 7 | module.exports = function (program) { 8 | return new Promise((resolve, reject) => { 9 | if (!program.deploy) { 10 | return resolve() 11 | } 12 | 13 | utils.log(chalk.bold.green('Deploying package to system...')) 14 | 15 | let args = `& {& Add-AppxPackage '${program.outputDirectory}/${program.packageName}.appx'}` 16 | 17 | return utils.executeChildProcess('powershell.exe', ['-NoProfile', '-NoLogo', args]) 18 | .then(() => resolve()) 19 | .catch((err) => reject(err)) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /lib/dotfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('lodash.merge') 5 | const fs = require('fs') 6 | const e = process.env 7 | 8 | let root = e.USERPROFILE || e.APPDATA || e.TMP || e.TEMP || e.HOME || e.PWD || '/tmp' 9 | 10 | function Config () { 11 | if (!(this instanceof Config)) { 12 | return new Config('.electron-windows-store') 13 | } 14 | 15 | this.path = path.join(root, '.electron-windows-store') 16 | } 17 | 18 | Config.prototype.get = function () { 19 | try { 20 | return JSON.parse(fs.readFileSync(this.path, 'utf8')) 21 | } catch (err) { 22 | return {} 23 | } 24 | } 25 | 26 | Config.prototype.set = function (config) { 27 | if (e.USER === 'root') { 28 | return 29 | } 30 | var properties = this.get() 31 | properties = merge(properties, config) 32 | try { 33 | fs.writeFileSync(this.path, JSON.stringify(properties, null, 2) + '\n') 34 | } catch (err) { 35 | } 36 | } 37 | 38 | Config.prototype.append = function (property, value) { 39 | if (e.USER === 'root') { 40 | return 41 | } 42 | 43 | var data = this.get() 44 | data[property] = value 45 | 46 | try { 47 | fs.writeFileSync(this.path, JSON.stringify(data, null, 2) + '\n') 48 | } catch (err) { 49 | } 50 | } 51 | 52 | module.exports = Config 53 | -------------------------------------------------------------------------------- /lib/finalsay.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (program) { 4 | if (program.finalSay) { 5 | return Promise.resolve(program.finalSay()) 6 | } else { 7 | return Promise.resolve() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const zip = require('./zip') 4 | const setup = require('./setup') 5 | const sign = require('./sign') 6 | const assets = require('./assets') 7 | const convert = require('./convert') 8 | const finalSay = require('./finalsay') 9 | const makeappx = require('./makeappx') 10 | const manifest = require('./manifest') 11 | const deploy = require('./deploy') 12 | const makepri = require('./makepri') 13 | 14 | /** 15 | * Transforms a given input directory into a Windows Store package. 16 | * 17 | * @param {WindowsStoreOptions} program 18 | * 19 | * @typedef WindowsStoreOptions 20 | * @type {Object} 21 | * @property {boolean} containerVirtualization - Create package using Windows Container virtualization 22 | * @property {string} inputDirectory - Directory containing your application 23 | * @property {string} outputDirectory - Output directory for the appx 24 | * @property {string} packageVersion - Version of the app package 25 | * @property {string} packageName - Name of the app package 26 | * @property {string} packageDisplayName - Dispay name of the package 27 | * @property {string} packageDescription - Description of the package 28 | * @property {string} packageBackgroundColor - Background color for the app icon (example: #464646) 29 | * @property {string} packageExecutable - Path to the package executable 30 | * @property {string} assets - Path to the visual assets for the appx 31 | * @property {string} manifest - Path to a manifest, if you want to overwrite the default one 32 | * @property {boolean} deploy - Should the app be deployed after creation? 33 | * @property {string} publisher - Publisher to use (example: CN=developmentca) 34 | * @property {string} windowsKit - Path to the Windows Kit bin folder 35 | * @property {string} devCert - Path to the developer certificate to use 36 | * @property {string} desktopConverter - Path to the desktop converter tools 37 | * @property {string} expandedBaseImage - Path to the expanded base image 38 | * @property {[string]} makeappxParams - Additional parameters for Make-AppXPackage 39 | * @property {[string]} signtoolParams - Additional parameters for signtool.exe 40 | * @property {[string]} createConfigParams - Additional parameters for makepri.exe createconfig 41 | * @property {[string]} createPriParams - Additional parameters for makepri.exe new 42 | * @property {function} finalSay - A function that is called before makeappx.exe executes. Accepts a promise. 43 | * @property {string} protocol - Protocol scheme to start the application with. 44 | * 45 | * @returns {Promise} - A promise that completes once the appx has been created 46 | */ 47 | module.exports = function windowsStore (program) { 48 | program.isModuleUse = true 49 | 50 | return setup(program) 51 | .then(() => zip(program)) 52 | .then(() => convert(program)) 53 | .then(() => assets(program)) 54 | .then(() => manifest(program)) 55 | .then(() => makepri(program)) 56 | .then(() => finalSay(program)) 57 | .then(() => makeappx(program)) 58 | .then(() => sign.signAppx(program)) 59 | .then(() => deploy(program)) 60 | } 61 | -------------------------------------------------------------------------------- /lib/makeappx.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const chalk = require('chalk') 6 | 7 | module.exports = function (program) { 8 | return new Promise((resolve, reject) => { 9 | if (!program.windowsKit) { 10 | return reject(new Error('Path to Windows Kit not specified')) 11 | } 12 | 13 | utils.log(chalk.bold.green('Creating appx package...')) 14 | 15 | let makeappx = path.join(program.windowsKit, 'makeappx.exe') 16 | let source = path.join(program.outputDirectory, 'pre-appx') 17 | let destination = path.join(program.outputDirectory, `${program.packageName}.appx`) 18 | let params = ['pack', '/d', source, '/p', destination, '/o'].concat(program.makeappxParams || []) 19 | 20 | utils.debug(`Using makeappx.exe in: ${makeappx}`) 21 | utils.debug(`Using pre-appx folder in: ${source}`) 22 | utils.debug(`Using following destination: ${destination}`) 23 | utils.debug(`Using parameters: ${JSON.stringify(params)}`) 24 | 25 | if (program.assets) { 26 | let assetPath = path.normalize(program.assets) 27 | 28 | if (utils.hasVariableResources(assetPath)) { 29 | utils.debug(`Determined that package has variable resources, calling makeappx.exe with /l`) 30 | params.push('/l') 31 | } 32 | } 33 | 34 | return utils.executeChildProcess(makeappx, params) 35 | .then(() => resolve()) 36 | .catch((err) => reject(err)) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /lib/makepri.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const chalk = require('chalk') 6 | 7 | function createConfig (program) { 8 | return new Promise((resolve, reject) => { 9 | if (!program.windowsKit) { 10 | return reject(new Error('Path to Windows Kit not specified')) 11 | } 12 | 13 | if (!program.makePri) { 14 | return resolve() 15 | } 16 | 17 | utils.log(chalk.bold.green('Creating priconfig...')) 18 | 19 | const makepri = path.join(program.windowsKit, 'makepri.exe') 20 | const source = path.join('pre-appx', 'priconfig.xml') 21 | const params = ['createconfig', '/cf', source, '/dq', 'en-US'].concat(program.createConfigParams || []) 22 | const options = { cwd: program.outputDirectory } 23 | 24 | utils.debug(`Using makepri.exe in: ${makepri}`) 25 | utils.debug(`Using pre-appx folder in: ${source}`) 26 | utils.debug(`Using parameters: ${JSON.stringify(params)}`) 27 | 28 | return utils.executeChildProcess(makepri, params, options) 29 | .then(() => resolve()) 30 | .catch((err) => reject(err)) 31 | }) 32 | } 33 | 34 | function createPri (program) { 35 | return new Promise((resolve, reject) => { 36 | if (!program.windowsKit) { 37 | return reject(new Error('Path to Windows Kit not specified')) 38 | } 39 | 40 | if (!program.makePri) { 41 | return resolve() 42 | } 43 | 44 | utils.log(chalk.bold.green('Creating pri file...')) 45 | 46 | const makepri = path.join(program.windowsKit, 'makepri.exe') 47 | const source = path.join('pre-appx', 'priconfig.xml') 48 | const projectFolder = 'pre-appx' 49 | const outFile = path.join('pre-appx', 'resources.pri') 50 | const params = ['new', '/pr', projectFolder, '/cf', source, '/of', outFile].concat(program.createPriParams || []) 51 | const options = { cwd: program.outputDirectory } 52 | 53 | utils.debug(`Using makepri.exe in: ${makepri}`) 54 | utils.debug(`Using pre-appx folder in: ${source}`) 55 | utils.debug(`Using parameters: ${JSON.stringify(params)}`) 56 | 57 | return utils.executeChildProcess(makepri, params, options) 58 | .then(() => resolve()) 59 | .catch((err) => reject(err)) 60 | }) 61 | } 62 | 63 | module.exports = function (program) { 64 | return createConfig(program).then(() => createPri(program)) 65 | } 66 | -------------------------------------------------------------------------------- /lib/manifest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | const chalk = require('chalk') 6 | 7 | const utils = require('./utils') 8 | 9 | module.exports = function (program) { 10 | if (!program.manifest) { 11 | return Promise.resolve() 12 | } 13 | 14 | // Let's copy in the new manifest 15 | utils.log(chalk.bold.green('Overwriting manifest...')) 16 | 17 | const source = path.normalize(program.manifest) 18 | const destination = path.join(program.outputDirectory, 'pre-appx', 'AppXManifest.xml') 19 | 20 | utils.debug(`Copying manifest from ${source} to ${destination}`) 21 | 22 | return fs.copy(source, destination) 23 | .catch(error => { 24 | utils.debug(`Could not overwrite manifest. Error: ${JSON.stringify(error)}`) 25 | 26 | throw error 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/params.js: -------------------------------------------------------------------------------- 1 | // Ensures correct parameters 2 | 'use strict' 3 | 4 | const inquirer = require('inquirer') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const utils = require('./utils') 9 | const cwd = process.cwd() 10 | 11 | module.exports = function (program) { 12 | return new Promise((resolve, reject) => { 13 | const questions = [ 14 | { 15 | name: 'inputDirectory', 16 | type: 'input', 17 | message: 'Please enter the path to your built Electron app: ', 18 | validate: (input) => { 19 | if (!utils.isDirectory(input)) { 20 | // Not found, let's try the subdir 21 | return (utils.isDirectory(path.join(cwd, input))) 22 | } 23 | 24 | return true 25 | }, 26 | filter: (input) => { 27 | if (!utils.isDirectory(input)) { 28 | return path.join(cwd, input) 29 | } else { 30 | return input 31 | } 32 | }, 33 | when: () => (!program.inputDirectory) 34 | }, 35 | { 36 | name: 'outputDirectory', 37 | type: 'input', 38 | message: 'Please enter the path to your output directory: ', 39 | default: path.join(cwd, 'windows-store'), 40 | validate: (input) => { 41 | utils.log(input) 42 | if (!utils.isDirectory(input) && !utils.isDirectory(path.join(cwd, input))) { 43 | try { 44 | fs.mkdirSync(input) 45 | return true 46 | } catch (err) { 47 | utils.log(err) 48 | return false 49 | } 50 | } 51 | 52 | return true 53 | }, 54 | when: () => (!program.outputDirectory) 55 | }, 56 | { 57 | name: 'packageName', 58 | type: 'input', 59 | message: "Please enter your app's package name (name of your exe - without '.exe'): ", 60 | when: () => (!program.packageName) 61 | }, 62 | { 63 | name: 'packageVersion', 64 | type: 'input', 65 | default: '1.0.0.0', 66 | message: "Please enter your app's package version: ", 67 | when: () => (!program.packageVersion) 68 | } 69 | ] 70 | 71 | // First, ensure that we're even running Windows 72 | // (and the right version of it) 73 | utils.ensureWindows() 74 | 75 | // Then, let's ensure our parameters 76 | inquirer.prompt(questions) 77 | .then((answers) => { 78 | if (!program.packageExecutable && program.containerVirtualization) { 79 | program.packageExecutable = `C:\\Users\\ContainerAdministrator\\AppData\\Roaming\\e\\${program.packageName}.exe` 80 | } 81 | 82 | Object.assign(program, answers) 83 | }) 84 | .then(() => { 85 | // Verify optional parameters 86 | if (program.devCert.match(/.p12$/i)) { 87 | if (!program.certPass) { 88 | utils.debug(`Error: Using a P12 certification file but the password is missing`) 89 | return reject(new Error('No certificate password specified!')) 90 | } 91 | } 92 | resolve() 93 | }) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /lib/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * For setup, we need a number of params: 5 | * - DesktopConverter "C:\Tools\DesktopConverter" 6 | * - ExpandedBaseImage "C:\ProgramData\Microsoft\Windows\Images\BaseImage-14316\" 7 | * - Publisher "CN=testca" 8 | * - DevCert "C:\Tools\DesktopConverter\Certs\devcert.pfx" 9 | */ 10 | 11 | const path = require('path') 12 | const inquirer = require('inquirer') 13 | const pathExists = require('path-exists') 14 | const defaults = require('lodash.defaults') 15 | const multiline = require('multiline') 16 | const chalk = require('chalk') 17 | 18 | const utils = require('./utils') 19 | const sign = require('./sign') 20 | const dotfile = require('./dotfile')() 21 | 22 | /** 23 | * Determines whether all setup settings are okay. 24 | * 25 | * @returns {boolean} - Whether everything is setup correctly. 26 | */ 27 | function isSetupRequired (program) { 28 | const config = dotfile.get() || {} 29 | const hasPublisher = (config.publisher || program.publisher) 30 | const hasDevCert = (config.devCert || program.devCert) 31 | const hasWindowsKit = (config.windowsKit || program.windowsKit) 32 | const hasBaseImage = (config.expandedBaseImage || program.expandedBaseImage) 33 | const hasConverterTools = (config.desktopConverter || program.desktopConverter) 34 | 35 | if (!program.containerVirtualization) { 36 | return (hasPublisher && hasDevCert && hasWindowsKit) 37 | } else { 38 | return (hasPublisher && hasDevCert && hasWindowsKit && hasBaseImage && hasConverterTools) 39 | } 40 | } 41 | 42 | /** 43 | * Asks the user if dependencies are installed. If she/he declines, we exit the process. 44 | * 45 | * @param program - Commander program object 46 | * @returns {Promise} - Promsise that returns once user responded 47 | */ 48 | function askForDependencies (program) { 49 | if (program.isModuleUse) { 50 | return Promise.resolve(program) 51 | } 52 | 53 | const questions = [ 54 | { 55 | name: 'didInstallDesktopAppConverter', 56 | type: 'confirm', 57 | message: 'Did you download and install the Desktop App Converter? It is *not* required to run this tool. ' 58 | }, 59 | { 60 | name: 'makeCertificate', 61 | type: 'confirm', 62 | message: 'You need to install a development certificate in order to run your app. Would you like us to create one? ' 63 | } 64 | ] 65 | 66 | return inquirer.prompt(questions) 67 | .then((answers) => { 68 | program.didInstallDesktopAppConverter = answers.didInstallDesktopAppConverter 69 | program.makeCertificate = answers.makeCertificate 70 | }) 71 | } 72 | 73 | /** 74 | * Runs a wizard, helping the user setup configuration 75 | * 76 | * @param program - Commander program object 77 | * @returns {Promise} - Promsise that returns once wizard completed 78 | */ 79 | function wizardSetup (program) { 80 | const welcome = multiline.stripIndent(function () { /* 81 | Welcome to the Electron-Windows-Store tool! 82 | 83 | This tool will assist you with turning your Electron app into 84 | a swanky Windows Store app. 85 | 86 | We need to know some settings. We will ask you only once and store 87 | your answers in your profile folder in a .electron-windows-store 88 | file. 89 | 90 | */ 91 | }) 92 | const complete = multiline.stripIndent(function () { /* 93 | 94 | Setup complete, moving on to package your app! 95 | 96 | */ 97 | }) 98 | 99 | let questions = [ 100 | { 101 | name: 'desktopConverter', 102 | type: 'input', 103 | message: 'Please enter the path to your Desktop App Converter (DesktopAppConverter.ps1): ', 104 | validate: (input) => pathExists.sync(input), 105 | when: () => (!program.desktopConverter) 106 | }, 107 | { 108 | name: 'expandedBaseImage', 109 | type: 'input', 110 | message: 'Please enter the path to your Expanded Base Image: ', 111 | default: 'C:\\ProgramData\\Microsoft\\Windows\\Images\\BaseImage-14316\\', 112 | validate: (input) => pathExists.sync(input), 113 | when: () => (!program.expandedBaseImage) 114 | }, 115 | { 116 | name: 'devCert', 117 | type: 'input', 118 | message: 'Please enter the path to your development PFX certficate: ', 119 | default: null, 120 | when: () => (!dotfile.get().makeCertificate || !program.devCert) 121 | }, 122 | { 123 | name: 'publisher', 124 | type: 'input', 125 | message: 'Please enter your publisher identity: ', 126 | default: 'CN=developmentca', 127 | when: () => (!program.publisher) 128 | }, 129 | { 130 | name: 'windowsKit', 131 | type: 'input', 132 | message: "Please enter the location of your Windows Kit's bin folder: ", 133 | default: utils.getDefaultWindowsKitLocation(), 134 | when: () => (!program.windowsKit) 135 | } 136 | ] 137 | 138 | if (!program.isModuleUse) { 139 | utils.log(welcome) 140 | } 141 | 142 | // Remove the Desktop Converter Questions if not installed 143 | if (program.didInstallDesktopAppConverter === false) { 144 | questions = questions.slice(3) 145 | } 146 | 147 | if (program.isModuleUse) { 148 | program.windowsKit = program.windowsKit || utils.getDefaultWindowsKitLocation() 149 | 150 | return Promise.resolve(program) 151 | } 152 | 153 | return inquirer.prompt(questions) 154 | .then((answers) => { 155 | dotfile.set({ 156 | desktopConverter: answers.desktopConverter || false, 157 | expandedBaseImage: answers.expandedBaseImage || false, 158 | devCert: answers.devCert, 159 | publisher: answers.publisher, 160 | windowsKit: answers.windowsKit, 161 | makeCertificate: dotfile.get().makeCertificate 162 | }) 163 | 164 | program.desktopConverter = answers.desktopConverter 165 | program.expandedBaseImage = answers.expandedBaseImage 166 | program.devCert = answers.devCert 167 | program.publisher = answers.publisher 168 | program.windowsKit = answers.windowsKit 169 | 170 | if (program.makeCertificate) { 171 | utils.log(chalk.bold.green('Creating Certficate')) 172 | let publisher = dotfile.get().publisher.split('=')[1] 173 | let certFolder = path.join(process.env.APPDATA, 'electron-windows-store', publisher) 174 | 175 | return sign.makeCert({ publisherName: publisher, certFilePath: certFolder, program: program }) 176 | .then(pfxFile => { 177 | utils.log('Created and installed certificate:') 178 | utils.log(pfxFile) 179 | dotfile.set({ devCert: pfxFile }) 180 | }) 181 | } 182 | 183 | utils.log(complete) 184 | }) 185 | } 186 | 187 | /** 188 | * Logs the current configuration to utils 189 | * 190 | * @param program - Commander program object 191 | */ 192 | function logConfiguration (program) { 193 | utils.log(chalk.bold.green.underline('\nConfiguration: ')) 194 | utils.log(`Desktop Converter Location: ${program.desktopConverter}`) 195 | utils.log(`Expanded Base Image: ${program.expandedBaseImage}`) 196 | utils.log(`Publisher: ${program.publisher}`) 197 | utils.log(`Dev Certificate: ${program.devCert}`) 198 | utils.log(`Windows Kit Location: ${program.windowsKit} 199 | `) 200 | } 201 | 202 | /** 203 | * Runs setup, checking if all configuration is existent, 204 | * and merging the dotfile with the program object 205 | * 206 | * @param program - Commander program object 207 | * @returns {Promise} - Promsise that returns once setup completed 208 | */ 209 | function setup (program) { 210 | return new Promise((resolve, reject) => { 211 | if (isSetupRequired(program)) { 212 | // If we're setup, merge the dotfile configuration into the program 213 | defaults(program, dotfile.get()) 214 | logConfiguration(program) 215 | resolve() 216 | } else { 217 | // We're not setup, let's do that now 218 | askForDependencies(program) 219 | .then(() => wizardSetup(program)) 220 | .then(() => logConfiguration(program)) 221 | .then(() => resolve()) 222 | .catch((e) => reject(e)) 223 | } 224 | }) 225 | } 226 | 227 | module.exports = setup 228 | -------------------------------------------------------------------------------- /lib/sign.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | const chalk = require('chalk') 6 | 7 | const utils = require('./utils') 8 | 9 | const isValidPublisherName = (function () { 10 | // MakeCert looks like it accepts RFC1779 / X.500 distinguished names 11 | // See https://msdn.microsoft.com/en-us/library/windows/apps/br211441.aspx 12 | // https://msdn.microsoft.com/en-us/library/aa366101 13 | // http://www.itu.int/rec/T-REC-X.520-198811-S/en 14 | // 15 | // However, in practice there seem to be some discepencies, such as not supported comma/space escaping, 16 | // so we adapt this to match the observed behavior of makecert.exe. 17 | const validKeyPatterns = [ 18 | 'CN', // commonName 19 | 'OU', // organizationalUnitName 20 | 'O', // organizationName 21 | 'STREET', // streetAddress 22 | 'L', // localityName 23 | 'ST', // stateOrProvinceName 24 | 'C', // countryName 25 | 'DC', // domainComponent 26 | 'SN', // surname 27 | 'GN', // given name 28 | 'E', // email 29 | 'S', // (non-standard) "State" used by MS Identity objects 30 | 'T', // ?? Title / telephone 31 | 'G', // ?? generationQualifier 32 | 'I', // ?? IP Address 33 | 'SERIALNUMBER', // serialNumber 34 | '(?:OID\\.(0|[1-9][0-9]*)(?:\\.(0|[1-9][0-9]*))+)' // Object IDentifier by explicit numeric code 35 | ] 36 | const validKeyPattern = validKeyPatterns.join('|') 37 | const doubleQuotedPatternWithoutEmbeddedDoubleQuote = '"[^"\\\\]*(?:[^"][^"\\\\]*)*"' 38 | const validKeyValuePairPattern = `(${validKeyPattern})=((?:${doubleQuotedPatternWithoutEmbeddedDoubleQuote})|[^,"]*)` 39 | const validSequencePattern = `${validKeyValuePairPattern}(:?\\s*[,;]\\s*${validKeyValuePairPattern})*,?` 40 | const validDNRegex = new RegExp(`^${validSequencePattern}$`, 'i') 41 | 42 | return function isValidPublisherName (publisherName) { 43 | return typeof publisherName === 'string' && 44 | (publisherName.length === 0 || validDNRegex.test(publisherName)) 45 | } 46 | }()) 47 | 48 | function makeCert (parametersOrPublisherName, certFilePath, program) { 49 | let publisherName 50 | let certFileName 51 | let install = true 52 | 53 | // We accept both an object and a string here - a string was used 54 | // in the first release of electron-windows-store, while the object 55 | // was added later to allow additional flexibility for consuming apps. 56 | if (typeof parametersOrPublisherName === 'string') { 57 | publisherName = parametersOrPublisherName 58 | } else { 59 | publisherName = parametersOrPublisherName.publisherName 60 | certFilePath = parametersOrPublisherName.certFilePath 61 | certFileName = parametersOrPublisherName.certFileName || publisherName 62 | program = parametersOrPublisherName.program 63 | if (typeof parametersOrPublisherName.install === 'boolean') { 64 | install = parametersOrPublisherName.install 65 | } 66 | } 67 | 68 | if (typeof publisherName !== 'string') { 69 | throw new Error('publisherName must be a string') 70 | } 71 | 72 | if (!isValidPublisherName(publisherName)) { 73 | publisherName = `CN=${publisherName}` 74 | } 75 | 76 | certFilePath = certFilePath || '' 77 | const cer = path.join(certFilePath, `${certFileName}.cer`) 78 | const pvk = path.join(certFilePath, `${certFileName}.pvk`) 79 | const pfx = path.join(certFilePath, `${certFileName}.pfx`) 80 | 81 | const makecertExe = path.join(program.windowsKit, 'makecert.exe') 82 | const makecertArgs = ['-r', '-h', '0', '-n', publisherName, '-eku', '1.3.6.1.5.5.7.3.3', '-pe', '-sv', pvk, cer] 83 | 84 | const pk2pfx = path.join(program.windowsKit, 'pvk2pfx.exe') 85 | const pk2pfxArgs = ['-pvk', pvk, '-spc', cer, '-pfx', pfx] 86 | const installPfxArgs = ['Import-PfxCertificate', '-FilePath', pfx, '-CertStoreLocation', '"Cert:\\LocalMachine\\TrustedPeople"'] 87 | 88 | // Ensure the target directory exists 89 | fs.ensureDirSync(certFilePath) 90 | 91 | // If the private key file doesn't exist, makecert.exe will generate one and prompt user to set password 92 | if (!fs.existsSync(pvk)) { 93 | utils.log(chalk.green.bold('When asked to enter a password, please select "None".')) 94 | } 95 | 96 | return utils.executeChildProcess(makecertExe, makecertArgs) 97 | .then(() => utils.executeChildProcess(pk2pfx, pk2pfxArgs)) 98 | .then(() => { 99 | if (install) { 100 | utils.executeChildProcess('powershell.exe', installPfxArgs) 101 | } 102 | }) 103 | .then(() => { 104 | program.devCert = pfx 105 | return pfx 106 | }) 107 | } 108 | 109 | function signAppx (program) { 110 | return new Promise((resolve, reject) => { 111 | if (!program.devCert) { 112 | utils.debug(`Error: Tried to call signAppx, but program.devCert was undefined`) 113 | return reject(new Error('No developer certificate specified!')) 114 | } 115 | 116 | const pfxFile = program.devCert 117 | const appxFile = path.join(program.outputDirectory, `${program.packageName}.appx`) 118 | let params = ['sign', '-f', pfxFile, '-fd', 'SHA256', '-v'] 119 | if (program.certPass) { 120 | params.push('-p', program.certPass) 121 | } 122 | 123 | params = params.concat(program.signtoolParams || []) 124 | 125 | utils.debug(`Using PFX certificate from: ${pfxFile}`) 126 | utils.debug(`Signing appx package: ${appxFile}`) 127 | utils.debug(`Using the following parameters for signtool.exe: ${JSON.stringify(params)}`) 128 | 129 | params.push(appxFile) 130 | 131 | utils.executeChildProcess(path.join(program.windowsKit, 'signtool.exe'), params) 132 | .then(() => resolve()) 133 | .catch((err) => reject(err)) 134 | }) 135 | } 136 | 137 | module.exports = { 138 | isValidPublisherName, 139 | makeCert, 140 | signAppx 141 | } 142 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let _debug = null 4 | 5 | /** 6 | * Ensures that the currently running platform is Windows, 7 | * exiting the process if it is not 8 | */ 9 | function ensureWindows () { 10 | if (process.platform !== 'win32') { 11 | log('This tool requires Windows 10.\n') 12 | log('You can run a virtual machine using the free VirtualBox and') 13 | log('the free Windows Virtual Machines found at http://modern.ie.\n') 14 | log('For more information, please see the readme.') 15 | process.exit(1) 16 | } 17 | 18 | let release = require('os').release() 19 | let major = parseInt(release.slice(0, 2), 10) 20 | let minor = parseInt(release.slice(3, 4), 10) 21 | let build = parseInt(release.slice(5), 10) 22 | 23 | if (major < 10 || (minor === 0 && build < 14316)) { 24 | log(`You are running Windows ${release}. You need at least Windows 10.0.14316.`) 25 | log('We can\'t confirm that you\'re running the right version, but we won\'t stop') 26 | log('this process - should things fail though, you might have to update your') 27 | log('Windows.') 28 | } 29 | } 30 | 31 | /** 32 | * Makes an educated guess whether or not resources have 33 | * multiple variations or resource versions for 34 | * language, scale, contrast, etc 35 | * 36 | * @param assetsDirectory - Path to a the assets directory 37 | * @returns {boolean} - Are the assets variable? 38 | */ 39 | function hasVariableResources (assetsDirectory) { 40 | const files = require('fs-extra').readdirSync(assetsDirectory) 41 | const hasScale = files.find(file => /\.scale-...\./g.test(file)) 42 | 43 | return (!!hasScale) 44 | } 45 | 46 | /** 47 | * Tests a given directory for existence 48 | * 49 | * @param directory - Path to a directory 50 | * @returns {boolean} - Does the dir exist? 51 | */ 52 | function isDirectory (directory) { 53 | return require('path-exists').sync(directory) 54 | } 55 | 56 | /** 57 | * Starts a child process using the provided executable 58 | * 59 | * @param fileName - Path to the executable to start 60 | * @param args - Arguments for spawn 61 | * @param options - Options passed to spawn 62 | * @returns {Promise} - A promise that resolves when the 63 | * process exits 64 | */ 65 | function executeChildProcess (fileName, args, options) { 66 | return new Promise((resolve, reject) => { 67 | const child = require('child_process').spawn(fileName, args, options) 68 | 69 | child.stdout.on('data', (data) => log(data.toString())) 70 | child.stderr.on('data', (data) => log(data.toString())) 71 | 72 | child.on('exit', (code) => { 73 | if (code !== 0) { 74 | return reject(new Error(fileName + ' exited with code: ' + code)) 75 | } 76 | return resolve() 77 | }) 78 | 79 | child.stdin.end() 80 | }) 81 | } 82 | 83 | /** 84 | * Logs to console, unless tests are running (or is used as module) 85 | * 86 | * @param message - Message to log 87 | */ 88 | function log (message) { 89 | if (!global.testing && !global.isModuleUse) { 90 | console.log(message) 91 | } else { 92 | debug(message) 93 | } 94 | } 95 | 96 | /** 97 | * Logs debug message to console, unless tests are running 98 | * 99 | * @param message - Message to log 100 | */ 101 | function debug (message) { 102 | _debug = _debug || require('debug')('electron-windows-store') 103 | _debug(message) 104 | } 105 | 106 | /** 107 | * Returns the default location of the Windows kit. 108 | * 109 | * @param {string} Architecture - either ia32 or x64 110 | * @returns {string} Windows Kit location 111 | */ 112 | function getDefaultWindowsKitLocation (arch) { 113 | arch = arch || process.arch 114 | 115 | return 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\' + (arch === 'ia32' ? 'x86' : 'x64') 116 | } 117 | 118 | module.exports = { 119 | isDirectory, 120 | ensureWindows, 121 | executeChildProcess, 122 | hasVariableResources, 123 | log, 124 | debug, 125 | getDefaultWindowsKitLocation 126 | } 127 | -------------------------------------------------------------------------------- /lib/vendor/tail.js: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/lucagrulla/node-tail 2 | // and hacked to accept UTF16LE 3 | 4 | var Tail, environment, events, fs, 5 | bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }, 6 | extend = function (child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 7 | hasProp = {}.hasOwnProperty; 8 | 9 | events = require("events"); 10 | 11 | fs = require('fs'); 12 | 13 | environment = process.env['NODE_ENV'] || 'development'; 14 | 15 | Tail = (function (superClass) { 16 | extend(Tail, superClass); 17 | 18 | Tail.prototype.readBlock = function () { 19 | var block, stream; 20 | if (this.queue.length >= 1) { 21 | block = this.queue.shift(); 22 | if (block.end > block.start) { 23 | stream = fs.createReadStream(this.filename, { 24 | start: block.start, 25 | end: block.end - 1, 26 | encoding: "utf16le" 27 | }); 28 | stream.on('error', (function (_this) { 29 | return function (error) { 30 | console.error("Tail error:" + error); 31 | return _this.emit('error', error); 32 | }; 33 | })(this)); 34 | stream.on('end', (function (_this) { 35 | return function () { 36 | if (_this.queue.length >= 1) { 37 | return _this.internalDispatcher.emit("next"); 38 | } 39 | }; 40 | })(this)); 41 | return stream.on('data', (function (_this) { 42 | return function (data) { 43 | var chunk, i, len, parts, results; 44 | _this.buffer += data; 45 | parts = _this.buffer.split(_this.separator); 46 | _this.buffer = parts.pop(); 47 | results = []; 48 | for (i = 0, len = parts.length; i < len; i++) { 49 | chunk = parts[i]; 50 | results.push(_this.emit("line", chunk)); 51 | } 52 | return results; 53 | }; 54 | })(this)); 55 | } 56 | } 57 | }; 58 | 59 | function Tail(filename, options) { 60 | var pos, ref, ref1, ref2, ref3; 61 | this.filename = filename; 62 | if (options == null) { 63 | options = {}; 64 | } 65 | this.readBlock = bind(this.readBlock, this); 66 | this.separator = (ref = options.separator) != null ? ref : /[\r]{0,1}\n/, this.fsWatchOptions = (ref1 = options.fsWatchOptions) != null ? ref1 : {}, this.fromBeginning = (ref2 = options.fromBeginning) != null ? ref2 : false, this.follow = (ref3 = options.follow) != null ? ref3 : true; 67 | this.buffer = ''; 68 | this.internalDispatcher = new events.EventEmitter(); 69 | this.queue = []; 70 | this.isWatching = false; 71 | this.internalDispatcher.on('next', (function (_this) { 72 | return function () { 73 | return _this.readBlock(); 74 | }; 75 | })(this)); 76 | if (this.fromBeginning) { 77 | pos = 0; 78 | } 79 | this.watch(pos); 80 | } 81 | 82 | Tail.prototype.watch = function (pos) { 83 | var stats; 84 | if (this.isWatching) { 85 | return; 86 | } 87 | this.isWatching = true; 88 | stats = fs.statSync(this.filename); 89 | this.pos = pos != null ? pos : stats.size; 90 | if (fs.watch) { 91 | return this.watcher = fs.watch(this.filename, this.fsWatchOptions, (function (_this) { 92 | return function (e) { 93 | return _this.watchEvent(e); 94 | }; 95 | })(this)); 96 | } else { 97 | return fs.watchFile(this.filename, this.fsWatchOptions, (function (_this) { 98 | return function (curr, prev) { 99 | return _this.watchFileEvent(curr, prev); 100 | }; 101 | })(this)); 102 | } 103 | }; 104 | 105 | Tail.prototype.watchEvent = function (e) { 106 | var stats; 107 | if (e === 'change') { 108 | stats = fs.statSync(this.filename); 109 | if (stats.size < this.pos) { 110 | this.pos = stats.size; 111 | } 112 | if (stats.size > this.pos) { 113 | this.queue.push({ 114 | start: this.pos, 115 | end: stats.size 116 | }); 117 | this.pos = stats.size; 118 | if (this.queue.length === 1) { 119 | return this.internalDispatcher.emit("next"); 120 | } 121 | } 122 | } else if (e === 'rename') { 123 | this.unwatch(); 124 | if (this.follow) { 125 | return setTimeout(((function (_this) { 126 | return function () { 127 | return _this.watch(); 128 | }; 129 | })(this)), 1000); 130 | } else { 131 | console.error("'rename' event for " + this.filename + ". File not available."); 132 | return this.emit("error", "'rename' event for " + this.filename + ". File not available."); 133 | } 134 | } 135 | }; 136 | 137 | Tail.prototype.watchFileEvent = function (curr, prev) { 138 | if (curr.size > prev.size) { 139 | this.queue.push({ 140 | start: prev.size, 141 | end: curr.size 142 | }); 143 | if (this.queue.length === 1) { 144 | return this.internalDispatcher.emit("next"); 145 | } 146 | } 147 | }; 148 | 149 | Tail.prototype.unwatch = function () { 150 | if (fs.watch && this.watcher) { 151 | this.watcher.close(); 152 | } else { 153 | fs.unwatchFile(this.filename); 154 | } 155 | this.isWatching = false; 156 | return this.queue = []; 157 | }; 158 | 159 | return Tail; 160 | 161 | })(events.EventEmitter); 162 | 163 | exports.Tail = Tail; 164 | -------------------------------------------------------------------------------- /lib/zip.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | 6 | const utils = require('./utils') 7 | 8 | module.exports = function (program) { 9 | return new Promise((resolve, reject) => { 10 | // If we do a simple conversion, we don't need to zip. 11 | if (!program.containerVirtualization) { 12 | return resolve() 13 | } 14 | 15 | let input = program.inputDirectory 16 | let output = program.outputDirectory 17 | let args = `& {& '${path.resolve(__dirname, '..', 'ps1', 'zip.ps1')}' -source '${input}' -destination '${output}'}` 18 | 19 | utils.log(chalk.green('Zipping up built Electron application...')) 20 | 21 | return utils.executeChildProcess('powershell.exe', ['-NoProfile', '-NoLogo', args]) 22 | .then(() => resolve()) 23 | .catch((err) => reject(err)) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-windows-store", 3 | "version": "2.0.1", 4 | "description": "Compile Electron Apps into Windows Store AppX packages", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "electron-windows-store": "bin/windowsstore.js" 8 | }, 9 | "scripts": { 10 | "test": "standard \"lib/*.js\" \"bin/**/*.js\" && mocha", 11 | "semantic-release": "semantic-release", 12 | "commitlint": "commitlint" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/felixrieseberg/electron-windows-store.git" 17 | }, 18 | "keywords": [ 19 | "Electron", 20 | "App", 21 | "Windows", 22 | "Store", 23 | "Appx", 24 | "UWP" 25 | ], 26 | "engineStrict": true, 27 | "engines": { 28 | "node": ">=6.0.0" 29 | }, 30 | "author": "", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/felixrieseberg/electron-windows-store/issues" 34 | }, 35 | "homepage": "https://github.com/felixrieseberg/electron-windows-store#readme", 36 | "dependencies": { 37 | "chalk": "^2.4.1", 38 | "commander": "^2.19.0", 39 | "debug": "^4.1.0", 40 | "fs-extra": "^7.0.0", 41 | "inquirer": "^6.2.0", 42 | "lodash.defaults": "^4.2.0", 43 | "lodash.merge": "^4.6.1", 44 | "multiline": "^2.0.0", 45 | "path-exists": "^3.0.0" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^7.2.1", 49 | "@commitlint/config-conventional": "^7.1.2", 50 | "@commitlint/travis-cli": "^7.2.1", 51 | "chai": "^4.2.0", 52 | "chai-as-promised": "^7.1.1", 53 | "mocha": "^5.2.0", 54 | "mockery": "^2.1.0", 55 | "semantic-release": "^15.10.7", 56 | "standard": "^12.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ps1/convert.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName="Convert")] 2 | Param( 3 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 4 | [string] 5 | [ValidateNotNullOrEmpty()] 6 | $LogFile, 7 | 8 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 9 | [string] 10 | [ValidateNotNullOrEmpty()] 11 | $Installer, 12 | 13 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 14 | [string] 15 | [ValidateNotNullOrEmpty()] 16 | $Converter, 17 | 18 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 19 | [string] 20 | [ValidateNotNullOrEmpty()] 21 | $ExpandedBaseImage, 22 | 23 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 24 | [string] 25 | [ValidateNotNullOrEmpty()] 26 | $Destination, 27 | 28 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 29 | [string] 30 | [ValidateNotNullOrEmpty()] 31 | $PackageName, 32 | 33 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 34 | [string] 35 | [ValidateNotNullOrEmpty()] 36 | $Version, 37 | 38 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 39 | [string] 40 | [ValidateNotNullOrEmpty()] 41 | $Publisher, 42 | 43 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 44 | [string] 45 | [ValidateNotNullOrEmpty()] 46 | $AppExecutable 47 | ) 48 | 49 | Start-Process powershell -WindowStyle Hidden -ArgumentList "-noprofile -nologo -file $Converter -LogFile $LogFile -Installer $Installer -ExpandedBaseImage $ExpandedBaseImage -Destination $Destination -PackageName $PackageName -Version $Version -Publisher $Publisher -AppExecutable $AppExecutable -Verbose" -verb RunAs -------------------------------------------------------------------------------- /ps1/zip.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Takes a folder, turns it into a zip file. 3 | # 4 | 5 | [CmdletBinding(DefaultParameterSetName="Convert")] 6 | Param( 7 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 8 | [string] 9 | [ValidateNotNullOrEmpty()] 10 | $Source, 11 | 12 | [Parameter(Mandatory=$True, ParameterSetName="Convert")] 13 | [string] 14 | [ValidateNotNullOrEmpty()] 15 | $Destination 16 | ) 17 | 18 | If (-Not (Test-path $Source)) { 19 | return "Source directory cannot be found" 20 | } 21 | 22 | If (-Not (Test-path $Destination)) { 23 | return "Destination directory cannot be found" 24 | } 25 | 26 | # We're zipping using the sytem's compressor 27 | Add-Type -assembly "system.io.compression.filesystem" 28 | $Zip = Join-Path -Path $Destination -ChildPath app.zip 29 | 30 | If (Test-Path $Zip) { 31 | Remove-Item $Zip 32 | } 33 | 34 | [io.compression.zipfile]::CreateFromDirectory($Source, $Zip) 35 | 36 | # Copy over installer 37 | $Installer = (Get-Item $PSScriptRoot).parent.FullName + '\bin\ElectronInstaller.exe' 38 | Copy-item $Installer -Destination $Destination -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branch: 'master', 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/npm', 7 | '@semantic-release/github', 8 | ], 9 | } -------------------------------------------------------------------------------- /template/appxmanifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | ${packageDisplayName} 12 | ${publisherDisplayName} 13 | No description entered 14 | assets\SampleAppx.50x50.png 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | ${protocol} 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /template/assets/SampleAppx.150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/template/assets/SampleAppx.150x150.png -------------------------------------------------------------------------------- /template/assets/SampleAppx.310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/template/assets/SampleAppx.310x150.png -------------------------------------------------------------------------------- /template/assets/SampleAppx.44x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/template/assets/SampleAppx.44x44.png -------------------------------------------------------------------------------- /template/assets/SampleAppx.50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/template/assets/SampleAppx.50x50.png -------------------------------------------------------------------------------- /test/fixtures/assets-scaled/AppNameMedTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/test/fixtures/assets-scaled/AppNameMedTile.scale-100.png -------------------------------------------------------------------------------- /test/fixtures/assets-scaled/AppNameMedTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/test/fixtures/assets-scaled/AppNameMedTile.scale-200.png -------------------------------------------------------------------------------- /test/fixtures/assets-scaled/AppNameMedTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/test/fixtures/assets-scaled/AppNameMedTile.scale-400.png -------------------------------------------------------------------------------- /test/fixtures/assets/AppNameMedTile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/test/fixtures/assets/AppNameMedTile.png -------------------------------------------------------------------------------- /test/fixtures/child_process.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events') 4 | 5 | module.exports = class ChildProcessMock extends EventEmitter { 6 | constructor() { 7 | super() 8 | 9 | this.stdout = { 10 | on() {} 11 | } 12 | this.stderr = { 13 | on() {} 14 | } 15 | this.stdin = { 16 | end: () => { 17 | this.emit('exit', 0) 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test/lib/assets.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const mockery = require('mockery') 3 | 4 | describe('Assets', () => { 5 | afterEach(() => mockery.deregisterAll()) 6 | 7 | describe('assets()', () => { 8 | it('should attempt to copy assets if they have been passed', (done) => { 9 | const programMock = { 10 | assets: '/fakepath/to/assets', 11 | outputDirectory: '/fakepath/to/output' 12 | } 13 | const fsMock = { 14 | copy: function (source, destination, cb) { 15 | source.should.equal(path.normalize('/fakepath/to/assets')) 16 | destination.should.equal(path.normalize('/fakepath/to/output/pre-appx/Assets')) 17 | 18 | if (cb) { 19 | cb() 20 | } else { 21 | return Promise.resolve() 22 | } 23 | } 24 | } 25 | 26 | mockery.registerMock('fs-extra', fsMock) 27 | require('../../lib/assets')(programMock).should.be.fulfilled.and.notify(done) 28 | mockery.deregisterMock('fs-extra'); 29 | }) 30 | 31 | it('should resolve right away if no assets have been passed', (done) => { 32 | const programMock = {} 33 | require('../../lib/assets')(programMock).should.be.fulfilled.and.notify(done) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/lib/bogus-private-key.pvk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-windows-store/7b45c54574596863b8c2f790cea09aabc03b6883/test/lib/bogus-private-key.pvk -------------------------------------------------------------------------------- /test/lib/deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const mockery = require('mockery') 5 | const ChildProcessMock = require('../fixtures/child_process') 6 | 7 | describe('Deploy', () => { 8 | afterEach(() => mockery.deregisterAll()) 9 | 10 | describe('deploy()', () => { 11 | it('should attempt to deploy the app if requested', function (done) { 12 | let passedProcess 13 | let passedArgs 14 | 15 | const programMock = { 16 | deploy: true, 17 | outputDirectory: '/fakepath/to/output', 18 | packageName: 'testApp' 19 | } 20 | const cpMock = { 21 | spawn(_process, _args) { 22 | passedProcess = _process 23 | passedArgs = _args 24 | 25 | return new ChildProcessMock() 26 | } 27 | } 28 | 29 | mockery.registerMock('child_process', cpMock) 30 | 31 | require('../../lib/deploy')(programMock) 32 | .then(() => { 33 | passedProcess.should.equal('powershell.exe') 34 | passedArgs[2].should.equal(`& {& Add-AppxPackage '${programMock.outputDirectory}/${programMock.packageName}.appx'}`) 35 | done() 36 | }) 37 | }) 38 | 39 | it('should resolve right away if no deployment was requested', (done) => { 40 | const programMock = {} 41 | require('../../lib/deploy')(programMock).should.be.fulfilled.notify(done) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/lib/makeappx.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const mockery = require('mockery') 5 | 6 | const ChildProcessMock = require('../fixtures/child_process') 7 | 8 | describe('MakeAppX', () => { 9 | const cpMock = { 10 | spawn(_process, _args) { 11 | passedProcess = _process 12 | passedArgs = _args 13 | 14 | return new ChildProcessMock() 15 | } 16 | } 17 | 18 | let passedArgs 19 | let passedProcess 20 | 21 | afterEach(() => { 22 | mockery.deregisterAll() 23 | passedArgs = undefined 24 | passedProcess = undefined 25 | }) 26 | 27 | describe('makeappx()', () => { 28 | it('should attempt to call makeappx.exe for a pre-appx folder', function (done) { 29 | const programMock = { 30 | deploy: true, 31 | inputDirectory: '/fakepath/to/input', 32 | outputDirectory: '/fakepath/to/output', 33 | windowsKit: '/fakepath/to/windows/kit/bin', 34 | packageName: 'testapp' 35 | } 36 | 37 | mockery.registerMock('child_process', cpMock) 38 | 39 | require('../../lib/makeappx')(programMock) 40 | .then(() => { 41 | const expectedScript = path.join(programMock.windowsKit, 'makeappx.exe') 42 | const expectedOutput = path.join(programMock.outputDirectory, 'testapp.appx') 43 | const expectedInput = path.join(programMock.outputDirectory, 'pre-appx') 44 | const expectedParams = ['pack', '/d', expectedInput, '/p', expectedOutput, '/o'] 45 | 46 | passedProcess.should.equal(expectedScript) 47 | passedArgs.should.deep.equal(expectedParams) 48 | done() 49 | }) 50 | }) 51 | 52 | it('should pass the /l flag if the pre-appx folder contains variable assets', function (done) { 53 | const programMock = { 54 | deploy: true, 55 | assets: path.join(__dirname, '..', 'fixtures', 'assets-scaled'), 56 | inputDirectory: '/fakepath/to/input', 57 | outputDirectory: '/fakepath/to/output', 58 | windowsKit: '/fakepath/to/windows/kit/bin', 59 | packageName: 'testapp' 60 | } 61 | 62 | mockery.registerMock('child_process', cpMock) 63 | 64 | require('../../lib/makeappx')(programMock) 65 | .then(() => { 66 | const expectedScript = path.join(programMock.windowsKit, 'makeappx.exe') 67 | const expectedOutput = path.join(programMock.outputDirectory, 'testapp.appx') 68 | const expectedInput = path.join(programMock.outputDirectory, 'pre-appx') 69 | const expectedParams = ['pack', '/d', expectedInput, '/p', expectedOutput, '/o', '/l'] 70 | 71 | passedProcess.should.equal(expectedScript) 72 | passedArgs.should.deep.equal(expectedParams) 73 | done() 74 | }) 75 | }) 76 | 77 | it('should not pass the /l flag if the pre-appx folder does not contain variable assets', function (done) { 78 | const programMock = { 79 | deploy: true, 80 | assets: path.join(__dirname, '..', 'fixtures', 'assets'), 81 | inputDirectory: '/fakepath/to/input', 82 | outputDirectory: '/fakepath/to/output', 83 | windowsKit: '/fakepath/to/windows/kit/bin', 84 | packageName: 'testapp' 85 | } 86 | 87 | mockery.registerMock('child_process', cpMock) 88 | 89 | require('../../lib/makeappx')(programMock) 90 | .then(() => { 91 | const expectedScript = path.join(programMock.windowsKit, 'makeappx.exe') 92 | const expectedOutput = path.join(programMock.outputDirectory, 'testapp.appx') 93 | const expectedInput = path.join(programMock.outputDirectory, 'pre-appx') 94 | const expectedParams = ['pack', '/d', expectedInput, '/p', expectedOutput, '/o'] 95 | 96 | passedProcess.should.equal(expectedScript) 97 | passedArgs.should.deep.equal(expectedParams) 98 | done() 99 | }) 100 | }) 101 | 102 | it('should reject right away if no Windows Kit is available', (done) => { 103 | const programMock = {} 104 | require('../../lib/makeappx')(programMock).should.be.rejected.notify(done) 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/lib/makepri.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const mockery = require('mockery') 5 | 6 | const ChildProcessMock = require('../fixtures/child_process') 7 | 8 | describe('Makepri', () => { 9 | let spawnedProcesses = [] 10 | 11 | const cpMock = { 12 | spawn(_process, _args) { 13 | spawnedProcesses.push({ 14 | passedProcess: _process, 15 | passedArgs: _args 16 | }) 17 | 18 | return new ChildProcessMock() 19 | } 20 | } 21 | 22 | afterEach(() => { 23 | mockery.deregisterAll() 24 | spawnedProcesses = [] 25 | }) 26 | 27 | describe('makepri()', () => { 28 | it('should attempt to call makepri.exe with createconfig as parameter', function (done) { 29 | const programMock = { 30 | deploy: true, 31 | inputDirectory: '/fakepath/to/input', 32 | outputDirectory: '/fakepath/to/output', 33 | windowsKit: '/fakepath/to/windows/kit/bin', 34 | packageName: 'testapp', 35 | makePri: true 36 | } 37 | 38 | mockery.registerMock('child_process', cpMock) 39 | 40 | require('../../lib/makepri')(programMock) 41 | .then(() => { 42 | const exptectedTarget = path.join('pre-appx', 'priconfig.xml') 43 | const expectedScript = path.join(programMock.windowsKit, 'makepri.exe') 44 | const expectedParams = ['createconfig', '/cf', exptectedTarget, '/dq', 'en-US'] 45 | 46 | spawnedProcesses[0].passedProcess.should.equal(expectedScript) 47 | spawnedProcesses[0].passedArgs.should.deep.equal(expectedParams) 48 | done() 49 | }) 50 | }) 51 | 52 | it('should attempt to call makepri.exe with new as parameter', function (done) { 53 | const programMock = { 54 | deploy: true, 55 | inputDirectory: '/fakepath/to/input', 56 | outputDirectory: '/fakepath/to/output', 57 | windowsKit: '/fakepath/to/windows/kit/bin', 58 | packageName: 'testapp', 59 | makePri: true 60 | } 61 | 62 | mockery.registerMock('child_process', cpMock) 63 | 64 | require('../../lib/makepri')(programMock) 65 | .then(() => { 66 | const expectedProject = 'pre-appx' 67 | const exptectedTarget = path.join('pre-appx', 'priconfig.xml') 68 | const expectedOutput = path.join('pre-appx', 'resources.pri') 69 | const expectedScript = path.join(programMock.windowsKit, 'makepri.exe') 70 | const expectedParams = ['new', '/pr', expectedProject, '/cf', exptectedTarget, '/of', expectedOutput] 71 | 72 | spawnedProcesses[1].passedProcess.should.equal(expectedScript) 73 | spawnedProcesses[1].passedArgs.should.deep.equal(expectedParams) 74 | done() 75 | }) 76 | }) 77 | 78 | it('should reject right away if no Windows Kit is available', (done) => { 79 | const programMock = {} 80 | require('../../lib/makepri')(programMock).should.be.rejected.notify(done) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/lib/manifest.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const mockery = require('mockery') 3 | 4 | describe('Manifest', () => { 5 | afterEach(() => mockery.deregisterAll()) 6 | 7 | describe('manifest()', () => { 8 | it('should attempt to copy a manifest if it has been passed', () => { 9 | const programMock = { 10 | outputDirectory: '/fakepath/to/output', 11 | manifest: '/fakepath/to/manifest' 12 | } 13 | const fsMock = { 14 | copy: function (source, destination, cb) { 15 | const expectedSource = path.normalize(programMock.manifest) 16 | const expectedDestination = path.join(programMock.outputDirectory, 'pre-appx', 'AppXManifest.xml') 17 | 18 | source.should.equal(expectedSource) 19 | destination.should.equal(expectedDestination) 20 | 21 | if (cb) { 22 | cb() 23 | } else { 24 | return Promise.resolve() 25 | } 26 | } 27 | } 28 | 29 | mockery.registerMock('fs-extra', fsMock) 30 | return require('../../lib/manifest')(programMock).should.be.fulfilled 31 | }) 32 | 33 | it('should resolve right away if no manifest was passed', () => { 34 | const programMock = {} 35 | return require('../../lib/manifest')(programMock).should.be.fulfilled 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/lib/sign.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs-extra') 4 | const path = require('path') 5 | const mockery = require('mockery') 6 | 7 | const ChildProcessMock = require('../fixtures/child_process') 8 | const sign = require('../../lib/sign') 9 | const utils = require('../../lib/utils') 10 | 11 | describe('Sign', () => { 12 | const tmpDir = path.join(require('os').tmpdir(), 'electron-windows-store-cert-test') 13 | 14 | let passedArgs = [] 15 | let passedProcess = [] 16 | 17 | const cpMock = { 18 | spawn(_process, _args) { 19 | passedProcess.push(_process) 20 | passedArgs.push(_args) 21 | 22 | return new ChildProcessMock() 23 | } 24 | } 25 | 26 | before(() => { 27 | fs.ensureDirSync(tmpDir) 28 | }) 29 | 30 | after(() => { 31 | fs.removeSync(tmpDir) 32 | }) 33 | 34 | afterEach(() => { 35 | mockery.deregisterAll() 36 | passedArgs = [] 37 | passedProcess = [] 38 | }) 39 | 40 | describe('signappx()', () => { 41 | it('should attempt to sign the current app', function (done) { 42 | const programMock = { 43 | inputDirectory: '/fakepath/to/input', 44 | outputDirectory: '/fakepath/to/output', 45 | windowsKit: '/fakepath/to/windows/kit/bin', 46 | packageName: 'testapp', 47 | devCert: 'fakepath/to/devcert.pfx' 48 | } 49 | 50 | mockery.registerMock('child_process', cpMock) 51 | 52 | sign.signAppx(programMock) 53 | .then(() => { 54 | const expectedScript = path.join(programMock.windowsKit, 'signtool.exe') 55 | const expectedPfxFile = programMock.devCert 56 | const expectedAppx = path.join(programMock.outputDirectory, `${programMock.packageName}.appx`) 57 | const expectedParams = ['sign', '-f', expectedPfxFile, '-fd', 'SHA256', '-v', expectedAppx] 58 | 59 | passedProcess.length.should.equal(1) 60 | passedProcess[0].should.equal(expectedScript) 61 | passedArgs[0].should.deep.equal(expectedParams) 62 | done() 63 | }) 64 | }) 65 | 66 | it('should pass along the certificate password', function () { 67 | const programMock = { 68 | inputDirectory: '/fakepath/to/input', 69 | outputDirectory: '/fakepath/to/output', 70 | windowsKit: '/fakepath/to/windows/kit/bin', 71 | packageName: 'testapp', 72 | devCert: 'fakepath/to/devcert.p12', 73 | certPass: '12345' 74 | } 75 | 76 | mockery.registerMock('child_process', cpMock) 77 | 78 | return sign.signAppx(programMock) 79 | .then(() => { 80 | const expectedScript = path.join(programMock.windowsKit, 'signtool.exe') 81 | const expectedPfxFile = programMock.devCert 82 | const expectedAppx = path.join(programMock.outputDirectory, `${programMock.packageName}.appx`) 83 | const expectedParams = ['sign', '-f', expectedPfxFile, '-fd', 'SHA256', '-v', '-p', '12345', expectedAppx] 84 | 85 | passedProcess.length.should.equal(1) 86 | passedProcess[0].should.equal(expectedScript) 87 | passedArgs[0].should.deep.equal(expectedParams) 88 | }) 89 | }) 90 | 91 | it('should reject if no certificate is present', function () { 92 | const programMock = {} 93 | return sign.signAppx(programMock).should.be.rejected 94 | }) 95 | }) 96 | 97 | describe('makeCert()', () => { 98 | it('should not attempt to import certificate when install === false', function (done) { 99 | mockery.registerMock('child_process', cpMock) 100 | 101 | sign.makeCert({ 102 | publisherName: 'CN=Test', 103 | certFilePath: tmpDir, 104 | install: false, 105 | program: { 106 | windowsKit: '/fake/kit' 107 | } 108 | }) 109 | .then(() => { 110 | passedArgs.every((args) => { 111 | return args.every(arg => arg.indexOf('Import-PfxCertificate') === -1) 112 | }).should.equal(true) 113 | done() 114 | }) 115 | .catch(() => {}) 116 | }) 117 | }) 118 | 119 | 120 | describe('isValidPublisherName()', () => { 121 | const windowsSdkPath = process.arch === 'x64' ? 122 | 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64' : 123 | 'C:\\Program Files\\Windows Kits\\10\\bin\\x64'; 124 | const makecertExe = path.join(windowsSdkPath, 'makecert.exe') 125 | const pvkFileName = path.resolve(__dirname, 'bogus-private-key.pvk'); 126 | const skipMakeCertExecution = !fs.existsSync(makecertExe) || !fs.existsSync(pvkFileName) 127 | 128 | const scenarios = [{ 129 | publisherName: '' 130 | }, { 131 | publisherName: 'CN=' 132 | }, { 133 | publisherName: 'CN=-' 134 | }, { 135 | publisherName: 'cn=lower, ou=case' 136 | }, { 137 | publisherName: 'CN=first.last' 138 | }, { 139 | publisherName: 'CN="Pointlessly quoted"' 140 | }, { 141 | publisherName: 'CN=no,o=spaces' 142 | }, { 143 | publisherName: 'CN=" Leading and Trailing Spaces "' 144 | }, { 145 | publisherName: 'CN=Common Name,O=Some organization' 146 | }, { 147 | publisherName: 'O="Quoted comma, Inc."' 148 | }, { 149 | publisherName: 'CN=!@#$^&*()[]{}<>|\/.~\'-=,O="Symbols are Cool, LLC"' 150 | }, { 151 | publisherName: 'OU=Sales+CN=J. Smith,O=Multi-valued' 152 | }, { 153 | publisherName: 'CN=Duplicate+CN=Attribute' 154 | }, { 155 | publisherName: 'OU=Trailing plus+', 156 | }, { 157 | publisherName: 'CN=trailing comma,', 158 | }, { 159 | publisherName: 'CN="Escaped\\ and\\ quoted\\ spaces"' 160 | }, { 161 | publisherName: 'CN=First M Last, O="Acme, Inc."' 162 | }, { 163 | publisherName: 'CN=Marshall T. Rose, O=Dover Beach Consulting, L=Santa Clara,ST=California, C=US' 164 | }, { 165 | publisherName: 'CN=FTAM Service, CN=Bells, OU=Computer Science,\nO=University College London, C=GB' 166 | }, { 167 | publisherName: 'CN=Markus Kuhn, O=University of Erlangen, T=Mr., C=DE' 168 | }, { 169 | publisherName: 'CN=Steve Kille,\n O=ISODE Consortium,\n C=GB' 170 | }, { 171 | publisherName: 'CN=Christian Huitema; O=INRIA; C=FR' 172 | }, { 173 | publisherName: 'CN=This is a really long distinguished name: 2048 chars!........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' 174 | }, { 175 | publisherName: 'CN=Even longer: 4096 chars 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678' 176 | }, { 177 | publisherName: 'CN=Long enough for anyone: 8192 chars 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123' 178 | }, { 179 | publisherName: 'CN=L. Eagle, O="Sue, Grabbit and Runn", C=GB' 180 | }, { 181 | publisherName: 'O=No CN' 182 | }, { 183 | publisherName: 'SERIALNUMBER=1' 184 | }, { 185 | publisherName: 'DC=A,CN=B,OU=C,O=D,STREET=123 Main St.,L=Big City,ST=Nowhere,C=XX,SN=Surname,GN=Given name,E=nobody@example.com,S=E,T=F,G=G,I=1.2.3.4' 186 | }, { 187 | publisherName: 'CN="+"', 188 | }, { 189 | publisherName: 'CN=X+' 190 | }, { 191 | publisherName: 'X', 192 | expectInvalid: true 193 | }, { 194 | publisherName: 'CN=X,UID=userId', 195 | expectInvalid: true 196 | }, { 197 | publisherName: 'CN=\ Escaped leading space"', 198 | expectInvalid: true 199 | }, { 200 | publisherName: 'CN="Quotation \\" Mark"', 201 | expectInvalid: true 202 | }, { 203 | publisherName: 'CN=X,DNQ=qualifier', 204 | expectInvalid: true 205 | }, { 206 | // According to RFC1779 and RFC2243 this should be legal but MakeCert.exe does not seem to accept it 207 | publisherName: 'CN=Sue\\, Grabbit and Runn', 208 | expectInvalid: true 209 | }] 210 | 211 | scenarios.forEach((scenario) => { 212 | const actualResult = sign.isValidPublisherName(scenario.publisherName) 213 | let nameToPrint = scenario.publisherName.replace(/\n/g, '\\n') 214 | if (nameToPrint.length > 60) 215 | nameToPrint = nameToPrint.slice(0,61) + '...' 216 | 217 | // Compare pre-determined checks (previously confirmed with makecert.exe) 218 | it(`return ${scenario.expectInvalid ? 'false' : 'true'} for ${nameToPrint}`, () => { 219 | actualResult.should.equal(!scenario.expectInvalid, scenario.publisherName) 220 | }) 221 | 222 | // Run makecert.exe and check whether or not it fails with this publisherName 223 | if (!skipMakeCertExecution) { 224 | const rnd = Date.now() + '-' + Math.floor(Math.random()*10000) 225 | const crtFileName = path.join(tmpDir, `makecert-${rnd}.crt`) 226 | const makecertArgs = ['-r', '-h', '0', '-n', scenario.publisherName, '-eku', '1.3.6.1.5.5.7.3.3', '-pe', '-sv', pvkFileName, crtFileName] 227 | 228 | it(`makecert.exe should ${scenario.expectInvalid ? 'fail' : 'succeed'} for ${nameToPrint}`, () => { 229 | return utils.executeChildProcess(makecertExe, makecertArgs) 230 | .then(() => { return true }) 231 | .catch(() => { return false }) 232 | .should.eventually.equal(!scenario.expectInvalid) 233 | }) 234 | } 235 | }) 236 | 237 | if (skipMakeCertExecution) { 238 | it.skip('should be re-tested against actual MakeCert.exe from SDK') 239 | } 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /test/lib/utils.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../lib/utils') 2 | const path = require('path') 3 | 4 | describe('Utilities', () => { 5 | describe('hasVariableResources()', () => { 6 | it('should return true if files contain scale- indication', () => { 7 | return utils.hasVariableResources(path.join(__dirname, '..', 'fixtures', 'assets-scaled')).should.equal(true) 8 | }) 9 | 10 | it('should return false if files do not contain scale- indication', () => { 11 | return utils.hasVariableResources(path.join(__dirname, '..', 'fixtures', 'assets')).should.equal(false) 12 | }) 13 | 14 | it('should return the correct location for the default windows kit locataion (x86)', () => { 15 | return utils.getDefaultWindowsKitLocation('ia32').should.equal('C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86') 16 | }) 17 | 18 | it('should return the correct location for the default windows kit locataion (x64)', () => { 19 | return utils.getDefaultWindowsKitLocation('x64').should.equal('C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64') 20 | }) 21 | 22 | it('should return the correct location for the default windows kit locataion (default)', () => { 23 | const x86 = 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86' 24 | const x64 = 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64' 25 | 26 | if (process.arch === 'ia32') { 27 | return utils.getDefaultWindowsKitLocation().should.equal(x86) 28 | } else { 29 | return utils.getDefaultWindowsKitLocation().should.equal(x64) 30 | } 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/lib/zip.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const mockery = require('mockery') 5 | const should = require('chai').should() 6 | 7 | const ChildProcessMock = require('../fixtures/child_process') 8 | 9 | describe('Zip', () => { 10 | const cpMock = { 11 | spawn(_process, _args) { 12 | passedProcess = _process 13 | passedArgs = _args 14 | 15 | return new ChildProcessMock() 16 | } 17 | } 18 | 19 | let passedArgs 20 | let passedProcess 21 | 22 | afterEach(() => { 23 | mockery.deregisterAll() 24 | passedArgs = undefined 25 | passedProcess = undefined 26 | }) 27 | 28 | describe('signappx()', () => { 29 | it('should attempt to sign the current app', (done) => { 30 | const programMock = { 31 | inputDirectory: '/fakepath/to/input', 32 | outputDirectory: '/fakepath/to/output', 33 | containerVirtualization: true 34 | } 35 | 36 | mockery.registerMock('child_process', cpMock) 37 | 38 | require('../../lib/zip')(programMock).should.be.fulfilled 39 | .then(() => { 40 | const expectedInput = programMock.inputDirectory 41 | const expectedOutput = programMock.outputDirectory 42 | const expectedScript = path.resolve(__dirname, '..', '..', 'ps1', 'zip.ps1') 43 | const expectedPsArgs = `& {& '${expectedScript}' -source '${expectedInput}' -destination '${expectedOutput}'}` 44 | const expectedArgs = ['-NoProfile', '-NoLogo', expectedPsArgs] 45 | 46 | passedProcess.should.equal('powershell.exe') 47 | passedArgs.should.deep.equal(expectedArgs) 48 | }) 49 | .should.notify(done) 50 | }) 51 | 52 | it('does not zip if using simple (non-container) conversion', done => { 53 | const programMock = {} 54 | require('../../lib/zip')(programMock).should.be.fulfilled 55 | .then(() => { 56 | should.not.exist(passedArgs) 57 | should.not.exist(passedProcess) 58 | }) 59 | .should.notify(done) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiAsPromised = require('chai-as-promised') 3 | const mockery = require('mockery') 4 | 5 | chai.should() 6 | chai.use(chaiAsPromised) 7 | mockery.enable({ warnOnUnregistered: false }) 8 | 9 | global.testing = true 10 | 11 | // Run tests 12 | require('./lib/assets'); 13 | require('./lib/deploy'); 14 | require('./lib/makeappx'); 15 | require('./lib/makepri'); 16 | require('./lib/manifest'); 17 | require('./lib/sign'); 18 | require('./lib/zip'); 19 | require('./lib/utils'); 20 | --------------------------------------------------------------------------------