├── index.js ├── .gitattributes ├── .editorconfig ├── package.json ├── .gitignore ├── LICENSE ├── bin └── sync.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Rotorz Limited. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root. 3 | 4 | throw new Error("This package is not supposed to be used directly."); 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | 4 | *.cs text 5 | *.css text 6 | *.html text 7 | *.ini text 8 | *.js text 9 | *.jsdoc text 10 | *.json text 11 | *.md text 12 | *.txt text 13 | *.yaml text 14 | 15 | *.pdf binary 16 | *.png binary 17 | *.rar binary 18 | *.jpg binary 19 | *.zip binary 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; Grab the EditorConfig extension for Visual Studio: 2 | ; https://visualstudiogallery.msdn.microsoft.com/c8bccfe2-650c-4b42-bc5c-845e21f96328 3 | 4 | ; Top-most EditorConfig file 5 | root = true 6 | 7 | ; Unix-style newlines with a newline ending every file 8 | [*] 9 | end_of_line = LF 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | trim_trailing_whitespace = true 14 | 15 | [*.{md,yaml}] 16 | trim_trailing_whitespace = false 17 | 18 | [*.{js,json,yaml,html,css,styl}] 19 | indent_size = 2 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity3d-package-syncer", 3 | "version": "1.0.8", 4 | "description": "A command line utility that synchronizes asset files from npm packages into an appropriate directory of a game project that is made using the Unity game engine.", 5 | "main": "index.js", 6 | "bin": { 7 | "unity3d--sync": "./bin/sync.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/rotorz/unity3d-package-syncer" 15 | }, 16 | "author": "Rotorz Limited", 17 | "license": "MIT", 18 | "keywords": [ 19 | "unity3d" 20 | ], 21 | "dependencies": { 22 | "colors": "^1.1.2", 23 | "extfs": "0.0.7", 24 | "fs-extra": "^6.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Private files 2 | _private 3 | 4 | # Build output 5 | lib 6 | 7 | # Vim 8 | *.swp 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directory 36 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 37 | node_modules 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # OS X 46 | .DS_Store 47 | 48 | # Windows 49 | Thumbs.db 50 | ehthumbs.db 51 | Desktop.ini 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Rotorz Limited 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 | -------------------------------------------------------------------------------- /bin/sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright (c) Rotorz Limited. All rights reserved. 4 | // Licensed under the MIT license. See LICENSE file in the project root. 5 | 6 | "use strict" 7 | 8 | const colors = require('colors/safe'); 9 | const extfs = require('extfs'); 10 | const fs = require("fs-extra"); 11 | const path = require("path"); 12 | 13 | 14 | // 15 | // DO NOT MODIFY: The following relative path should be hard coded because it is used as 16 | // a safety guard when removing redundant packages (directories are only 17 | // removed if they contain this path). 18 | // 19 | const PROJECT_RELATIVE_PACKAGES_PATH = path.join("Assets", "Plugins", "Packages"); 20 | 21 | 22 | const projectRootDir = process.cwd(); 23 | const projectConfigPath = path.join(projectRootDir, "package.json"); 24 | const projectConfig = fs.readJsonSync(projectConfigPath); 25 | const projectPackagesDir = path.join(projectRootDir, PROJECT_RELATIVE_PACKAGES_PATH); 26 | 27 | 28 | const specialKeyword = "unity3d-package"; 29 | const extraFiles = [ "package.json", "README.md", "LICENSE" ]; 30 | 31 | 32 | console.log("Syncing packages in Unity project..."); 33 | 34 | 35 | // Phase 1 - Get flat list of package dependencies. 36 | 37 | //const projectDependencies = (projectConfig.dependencies || { }); 38 | //const projectDependencyNames = new Set(Object.keys(projectDependencies)); 39 | 40 | const projectDependencies = getPackageListing(path.resolve(projectRootDir, "node_modules")); 41 | const projectDependencyNames = new Set(projectDependencies); 42 | 43 | 44 | // Phase 2 - Copy packages from node packages into project's packages directory. 45 | 46 | for (let dependencyName of projectDependencyNames) { 47 | const dependencyDir = path.join(projectRootDir, "node_modules", dependencyName); 48 | const dependencyConfigPath = path.join(dependencyDir, "package.json"); 49 | const dependencyConfig = fs.readJsonSync(dependencyConfigPath); 50 | const dependencyKeywords = dependencyConfig.keywords || [ ]; 51 | 52 | // Skip packages that do not need to be installed using this mechanism. 53 | if (!dependencyKeywords.includes(specialKeyword)) { 54 | continue; 55 | } 56 | 57 | console.log(""); 58 | console.log(colors.cyan(" " + dependencyName)); 59 | 60 | // Get information about the package that has already been installed into 61 | // the Unity project. 62 | const assetsTargetPath = path.resolve(projectPackagesDir, dependencyName); 63 | validateProjectPackageDirectory(assetsTargetPath); 64 | 65 | const assetsTargetConfigPath = path.join(assetsTargetPath, "package.json"); 66 | if (fs.existsSync(assetsTargetConfigPath)) { 67 | const assetsTargetConfig = fs.readJsonSync(assetsTargetConfigPath); 68 | 69 | // Skip installation of this package if the one that has already been 70 | // installed into the Unity project has the same version number. 71 | if (dependencyConfig.version === assetsTargetConfig.version) { 72 | console.log(" Already latest version."); 73 | continue; 74 | } 75 | } 76 | 77 | 78 | // Okay, we need to install the package into the Unity project. 79 | console.log(" Preparing package directory..."); 80 | fs.emptyDirSync(assetsTargetPath); 81 | console.log(" Copying asset files..."); 82 | copyIfExistsSync("assets", ""); 83 | console.log(" Copying extra files..."); 84 | extraFiles.forEach(copyIfExistsSync); 85 | 86 | 87 | function copyIfExistsSync(sourceRelativeFileName, targetRelativeFileName) { 88 | const sourcePath = path.join(dependencyDir, sourceRelativeFileName); 89 | if (fs.existsSync(sourcePath)) { 90 | if (typeof targetRelativeFileName !== "string") { 91 | targetRelativeFileName = sourceRelativeFileName; 92 | } 93 | const targetPath = path.join(assetsTargetPath, targetRelativeFileName); 94 | fs.copySync(sourcePath, targetPath); 95 | } 96 | } 97 | } 98 | 99 | console.log(""); 100 | 101 | 102 | // Phase 3 - Remove redundant packages from the project's packages directory. 103 | 104 | const projectPackageListing = getPackageListing(projectPackagesDir); 105 | const projectRedundantPackageNames = projectPackageListing.filter(packageName => !projectDependencyNames.has(packageName)); 106 | 107 | for (let redundantPackageName of projectRedundantPackageNames) { 108 | const redundantPackageDir = path.resolve(projectPackagesDir, redundantPackageName); 109 | const redundantPackageMetaPath = path.resolve(projectPackagesDir, redundantPackageName + ".meta"); 110 | validateProjectPackageDirectory(redundantPackageDir); 111 | 112 | console.log(colors.red(" Removing " + redundantPackageName)); 113 | 114 | if (fs.existsSync(redundantPackageDir)) { 115 | fs.removeSync(redundantPackageDir); 116 | } 117 | 118 | if (fs.existsSync(redundantPackageMetaPath)) { 119 | fs.unlinkSync(redundantPackageMetaPath); 120 | } 121 | } 122 | 123 | console.log(""); 124 | 125 | 126 | // Phase 4 - Remove empty scope directories from project's packages directory. 127 | 128 | for (let projectPackageScope of getProjectPackageScopes()) { 129 | const projectPackageScopeDir = path.resolve(projectPackagesDir, projectPackageScope); 130 | const projectPackageScopeMeta = path.resolve(projectPackagesDir, projectPackageScope + ".meta"); 131 | validateProjectPackageDirectory(projectPackageScopeDir); 132 | 133 | if (extfs.isEmptySync(projectPackageScopeDir)) { 134 | fs.removeSync(projectPackageScopeDir); 135 | 136 | if (fs.existsSync(projectPackageScopeMeta)) { 137 | fs.unlinkSync(projectPackageScopeMeta); 138 | } 139 | } 140 | } 141 | 142 | 143 | // Helper functions: 144 | 145 | function validateProjectPackageDirectory(packageDir) { 146 | if (!packageDir.includes(PROJECT_RELATIVE_PACKAGES_PATH)) { 147 | throw new Error("Project package has an unexpected path: " + packageDir); 148 | } 149 | } 150 | 151 | function getPackageListing(dir) { 152 | return flatMap(getDirectories(dir), packageDirectory => 153 | packageDirectory.startsWith("@") 154 | ? getDirectories(path.join(dir, packageDirectory)) 155 | .map(scopedPackageName => packageDirectory + "/" + scopedPackageName) 156 | : packageDirectory 157 | ) 158 | .filter(dir => !path.basename(dir).startsWith(".")); 159 | } 160 | 161 | function getProjectPackageScopes() { 162 | return getDirectories(projectPackagesDir) 163 | .filter(packageDirectory => packageDirectory.startsWith("@")); 164 | } 165 | 166 | // Copied from: http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript 167 | function flatMap(a, cb) { 168 | return [].concat(...a.map(cb)); 169 | } 170 | 171 | // Copied from: http://stackoverflow.com/a/24594123/656172 172 | function getDirectories(srcpath) { 173 | return fs.readdirSync(srcpath) 174 | .filter(file => fs.statSync(path.join(srcpath, file)).isDirectory()) 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unity3d-package-syncer 2 | 3 | [![npm version](https://badge.fury.io/js/unity3d-package-syncer.svg)](https://badge.fury.io/js/unity3d-package-syncer) 4 | [![Dependency Status](https://david-dm.org/rotorz/unity3d-package-syncer.svg)](https://david-dm.org/rotorz/unity3d-package-syncer) 5 | [![devDependency Status](https://david-dm.org/rotorz/unity3d-package-syncer/dev-status.svg)](https://david-dm.org/rotorz/unity3d-package-syncer#info=devDependencies) 6 | 7 | A command line utility that synchronizes asset files from yarn / npm packages into an 8 | appropriate directory of a game project that is made using the Unity game engine. 9 | 10 | 11 | ## Usage 12 | 13 | The package can be executed using the npx tool that ships with Node.js. 14 | 15 | Use the following command at the root of your project to synchronize the Unity packages 16 | into your project's "Assets" directory: 17 | 18 | ```sh 19 | $ npx unity3d-package-syncer 20 | ``` 21 | 22 | For example, if you were to add a new package with yarn: 23 | ```bash 24 | yarn add unity3d-package-example 25 | npx unity3d-package-syncer 26 | ``` 27 | 28 | Or, with npm: 29 | ```bash 30 | npm install --save unity3d-package-example 31 | npx unity3d-package-syncer 32 | ``` 33 | 34 | If your package has an editor script that needs to generate data assets (such as presets, 35 | project settings, etc) then it should store those in the `Assets/Plugins/PackageData/{package-name}` 36 | path. You may find the [unity3d-package-utils](https://github.com/rotorz/unity3d-package-utils) 37 | package useful in assiting with this. 38 | 39 | 40 | ## yarn vs npm 41 | 42 | From my experience the [yarn](https://yarnpkg.com/en/) tool seems to work better with the 43 | Unity development workflow since packages are installed deterministically. The minimum 44 | required version of yarn is v1.9.0. 45 | 46 | 47 | ## Creating Unity packages 48 | 49 | The **unity3d-package-syncer** only considers packages that have the keyword 50 | `unity3d-package` declared in their `package.json` file when checking and copying files 51 | from the package into a Unity project. 52 | 53 | The following is an example of how the `package.json` of such a package might look: 54 | 55 | ```json 56 | { 57 | "name": "some-cool-package", 58 | "version": "1.0.0", 59 | "description": "A cool package for Unity projects which ...", 60 | "keywords": [ 61 | "unity3d", 62 | "unity3d-package" 63 | ] 64 | } 65 | ``` 66 | 67 | For an example checkout the [unity3d-package-example](https://github.com/rotorz/unity3d-package-example) repository. 68 | 69 | 70 | ## Which files are copied from the package? 71 | 72 | All files in the package's `assets/` directory are copied into the root of the package 73 | inside the Unity project. 74 | 75 | Some *extra files* are also copied from the package into the Unity project: 76 | 77 | - `package.json` (required) 78 | 79 | - `LICENSE` 80 | 81 | - `README.md` 82 | 83 | For example, the file structure of the following package: 84 | 85 | ``` 86 | /some-cool-package/ 87 | |-- assets/ 88 | | |-- Logo.png 89 | | |-- Logo.png.meta 90 | |-- docs/ 91 | | |-- getting-started.md 92 | |-- index.js 93 | |-- LICENSE 94 | |-- package.json 95 | |-- README.md 96 | ``` 97 | 98 | Results in the following directory structure in a Unity project: 99 | 100 | ``` 101 | /MyUnityProject/ 102 | |-- Assets/ 103 | | |-- Plugins/ 104 | | | |-- Packages/ 105 | | | | |-- some-cool-package/ 106 | | | | | |-- Logo.png 107 | | | | | |-- Logo.png.meta (Copied from package) 108 | | | | | |-- LICENSE 109 | | | | | |-- LICENSE.meta (Unity generates this automatically) 110 | | | | | |-- package.json 111 | | | | | |-- package.json.meta (Unity generates this automatically) 112 | | | | | |-- README.md 113 | | | | | |-- README.md.meta (Unity generates this automatically) 114 | | | | |-- some-cool-package.meta (Unity generates this automatically) 115 | |-- Library/ 116 | |-- package.json 117 | ``` 118 | 119 | The `*.meta` files that are automatically generated by Unity should be included for each 120 | file that is included in the `assets/` directory of your package. 121 | 122 | Since nothing in the Unity project should be referencing the *extra files*; there 123 | shouldn't be any issues with these being automatically generated by Unity each time the 124 | package is copied into your project. 125 | 126 | 127 | ## How does the **unity3d-package-syncer** know when a package needs to be copied? 128 | 129 | - If the package has not yet been copied into the Unity project. 130 | 131 | - If the version of the package already in the Unity project differs from the version of 132 | the currently installed package. 133 | 134 | 135 | ## How does the **unity3d-package-syncer** know when a package needs to be removed? 136 | 137 | - If the package exists in the Unity project but is not declared in `package.json`. 138 | 139 | 140 | ## What happens if I modify the package files that were copied into the Unity project? 141 | 142 | **IMPORTANT: Any modifications will be lost when the package is next updated or uninstalled.** 143 | 144 | If you need to modify the files of the package then you will need to update the package 145 | with the modifications and adjust the version number of the package. 146 | 147 | If you want to modify the files of a package that you don't currently maintain, then you 148 | will need to create a fork of the original package so that you can maintain that. 149 | 150 | Packages with editor scripts that create asset files **SHOULD NOT** be storing these 151 | inside the `{unity-project}/Assets/Plugins/Packages/` directory structure because these 152 | files will be lost if the package is updated or uninstalled. 153 | 154 | If your package needs to generate configuration and/or data files then they should be 155 | stored in a different location in the Unity project. I would suggest that packages adopt 156 | the following directory structure for generated data files for consistency with one 157 | another: 158 | 159 | ``` 160 | {unity-project}/Assets/PackageData/{name-of-package}/{generated-data-files} 161 | ``` 162 | 163 | 164 | ## Contribution Agreement 165 | 166 | This project is licensed under the MIT license (see LICENSE). To be in the best 167 | position to enforce these licenses the copyright status of this project needs to 168 | be as simple as possible. To achieve this the following terms and conditions 169 | must be met: 170 | 171 | - All contributed content (including but not limited to source code, text, 172 | image, videos, bug reports, suggestions, ideas, etc.) must be the 173 | contributors own work. 174 | 175 | - The contributor disclaims all copyright and accepts that their contributed 176 | content will be released to the public domain. 177 | 178 | - The act of submitting a contribution indicates that the contributor agrees 179 | with this agreement. This includes (but is not limited to) pull requests, issues, 180 | tickets, e-mails, newsgroups, blogs, forums, etc. 181 | --------------------------------------------------------------------------------