diff --git a/local-cli/core/config/index.js b/local-cli/core/config/index.js index a013d17ee..ccc521c94 100644 --- a/local-cli/core/config/index.js +++ b/local-cli/core/config/index.js @@ -11,6 +11,7 @@ const android = require('./android'); const findAssets = require('./findAssets'); const ios = require('./ios'); +const windows = require('./windows'); const path = require('path'); const wrapCommands = require('./wrapCommands'); @@ -28,6 +29,7 @@ exports.getProjectConfig = function getProjectConfig() { return Object.assign({}, rnpm, { ios: ios.projectConfig(folder, rnpm.ios || {}), android: android.projectConfig(folder, rnpm.android || {}), + windows: windows.projectConfig(folder, rnpm.windows || {}), assets: findAssets(folder, rnpm.assets), }); }; @@ -46,6 +48,7 @@ exports.getDependencyConfig = function getDependencyConfig(packageName) { return Object.assign({}, rnpm, { ios: ios.dependencyConfig(folder, rnpm.ios || {}), android: android.dependencyConfig(folder, rnpm.android || {}), + windows: windows.dependencyConfig(folder, rnpm.windows || {}), assets: findAssets(folder, rnpm.assets), commands: wrapCommands(rnpm.commands), params: rnpm.params || [], diff --git a/local-cli/core/config/windows/findNamespace.js b/local-cli/core/config/windows/findNamespace.js new file mode 100644 index 000000000..4867368f1 --- /dev/null +++ b/local-cli/core/config/windows/findNamespace.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +/** + * Gets package's namespace + * by searching for its declaration in all C# files present in the folder + * + * @param {String} folder Folder to find C# files + */ +module.exports = function getNamespace(folder) { + const files = glob.sync('**/*.cs', { cwd: folder }); + + const packages = files + .map(filePath => fs.readFileSync(path.join(folder, filePath), 'utf8')) + .map(file => file.match(/namespace (.*)[\s\S]+IReactPackage/)) + .filter(match => match); + + return packages.length ? packages[0][1] : null; +}; diff --git a/local-cli/core/config/windows/findPackageClassName.js b/local-cli/core/config/windows/findPackageClassName.js new file mode 100644 index 000000000..279610eac --- /dev/null +++ b/local-cli/core/config/windows/findPackageClassName.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +/** + * Gets package's class name (class that implements IReactPackage) + * by searching for its declaration in all C# files present in the folder + * + * @param {String} folder Folder to find C# files + */ +module.exports = function getPackageClassName(folder) { + const files = glob.sync('**/*.cs', { cwd: folder }); + + const packages = files + .map(filePath => fs.readFileSync(path.join(folder, filePath), 'utf8')) + .map(file => file.match(/class (.*) : IReactPackage/)) + .filter(match => match); + + return packages.length ? packages[0][1] : null; +}; diff --git a/local-cli/core/config/windows/findProject.js b/local-cli/core/config/windows/findProject.js new file mode 100644 index 000000000..3b0121f74 --- /dev/null +++ b/local-cli/core/config/windows/findProject.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const glob = require('glob'); +const path = require('path'); + +/** + * Find an C# project file + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findManifest(folder) { + const csprojPath = glob.sync(path.join('**', '*.csproj'), { + cwd: folder, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**'], + })[0]; + + return csprojPath ? path.join(folder, csprojPath) : null; +}; diff --git a/local-cli/core/config/windows/findWindowsSolution.js b/local-cli/core/config/windows/findWindowsSolution.js new file mode 100644 index 000000000..fb6782633 --- /dev/null +++ b/local-cli/core/config/windows/findWindowsSolution.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const glob = require('glob'); +const path = require('path'); + +/** + * Glob pattern to look for solution file + */ +const GLOB_PATTERN = '**/*.sln'; + +/** + * Regexp matching all test projects + */ +const TEST_PROJECTS = /test|example|sample/i; + +/** + * Base windows folder + */ +const WINDOWS_BASE = 'windows'; + +/** + * These folders will be excluded from search to speed it up + */ +const GLOB_EXCLUDE_PATTERN = ['**/@(node_modules)/**']; + +/** + * Finds windows project by looking for all .sln files + * in given folder. + * + * Returns first match if files are found or null + * + * Note: `./windows/*.sln` are returned regardless of the name + */ +module.exports = function findSolution(folder) { + const projects = glob + .sync(GLOB_PATTERN, { + cwd: folder, + ignore: GLOB_EXCLUDE_PATTERN, + }) + .filter(project => { + return path.dirname(project) === WINDOWS_BASE || !TEST_PROJECTS.test(project); + }) + .sort((projectA, projectB) => { + return path.dirname(projectA) === WINDOWS_BASE ? -1 : 1; + }); + + if (projects.length === 0) { + return null; + } + + return projects[0]; +}; diff --git a/local-cli/core/config/windows/generateGUID.js b/local-cli/core/config/windows/generateGUID.js new file mode 100644 index 000000000..0bbdcdead --- /dev/null +++ b/local-cli/core/config/windows/generateGUID.js @@ -0,0 +1,10 @@ +const s4 = () => { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} + +module.exports = function generateGUID() { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); +} diff --git a/local-cli/core/config/windows/index.js b/local-cli/core/config/windows/index.js new file mode 100644 index 000000000..f7d81bb8e --- /dev/null +++ b/local-cli/core/config/windows/index.js @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const findWindowsSolution = require('./findWindowsSolution'); +const findNamespace = require('./findNamespace'); +const findProject = require('./findProject'); +const findPackageClassName = require('./findPackageClassName'); +const path = require('path'); +const generateGUID = require('./generateGUID'); + +const relativeProjectPath = (fullProjPath) => { + const windowsPath = fullProjPath + .substring(fullProjPath.lastIndexOf("node_modules") - 1, fullProjPath.length) + .replace(/\//g, '\\'); + + return '..' + windowsPath; +} + +const getProjectName = (fullProjPath) => { + return fullProjPath.split('/').slice(-1)[0].replace(/\.csproj/i, ''); +} + +/** + * Gets windows project config by analyzing given folder and taking some + * defaults specified by user into consideration + */ +exports.projectConfig = function projectConfigWindows(folder, userConfig) { + + const csSolution = userConfig.csSolution || findWindowsSolution(folder); + const solutionPath = path.join(folder, csSolution); + + if (!csSolution) { + return null; + } + + // expects solutions to be named the same as project folders + const windowsAppFolder = csSolution.substring(0, csSolution.lastIndexOf(".sln")); + const src = userConfig.sourceDir || windowsAppFolder; + const sourceDir = path.join(folder, src); + const mainPage = path.join(sourceDir, 'MainPage.cs'); + const projectPath = userConfig.projectPath || findProject(folder); + + return { + sourceDir, + solutionPath, + projectPath, + mainPage, + folder, + userConfig, + }; +}; + +/** + * Same as projectConfigWindows except it returns + * different config that applies to packages only + */ +exports.dependencyConfig = function dependencyConfigWindows(folder, userConfig) { + + const csSolution = userConfig.csSolution || findWindowsSolution(folder); + + if (!csSolution) { + return null; + } + + // expects solutions to be named the same as project folders + const windowsAppFolder = csSolution.substring(0, csSolution.lastIndexOf(".sln")); + const src = userConfig.sourceDir || windowsAppFolder; + + if (!src) { + return null; + } + + const sourceDir = path.join(folder, src); + const packageClassName = findPackageClassName(sourceDir); + const namespace = userConfig.namespace || findNamespace(sourceDir); + const csProj = userConfig.csProj || findProject(folder); + + /** + * This module has no package to export or no namespace + */ + if (!packageClassName || !namespace) { + return null; + } + + const packageUsingPath = userConfig.packageUsingPath || + `using ${namespace};`; + + const packageInstance = userConfig.packageInstance || + `new ${packageClassName}()`; + + const projectGUID = generateGUID(); + const pathGUID = generateGUID(); + const projectName = getProjectName(csProj); + const relativeProjPath = relativeProjectPath(csProj); + + return { + sourceDir, + packageUsingPath, + packageInstance, + projectName, + csProj, + folder, + projectGUID, + pathGUID, + relativeProjPath, + }; +}; diff --git a/local-cli/link/link.js b/local-cli/link/link.js index cb8c07443..770fa6444 100644 --- a/local-cli/link/link.js +++ b/local-cli/link/link.js @@ -16,8 +16,10 @@ const chalk = require('chalk'); const isEmpty = require('lodash').isEmpty; const promiseWaterfall = require('./promiseWaterfall'); const registerDependencyAndroid = require('./android/registerNativeModule'); +const registerDependencyWindows = require('./windows/registerNativeModule'); const registerDependencyIOS = require('./ios/registerNativeModule'); const isInstalledAndroid = require('./android/isInstalled'); +const isInstalledWindows = require('./windows/isInstalled'); const isInstalledIOS = require('./ios/isInstalled'); const copyAssetsAndroid = require('./android/copyAssets'); const copyAssetsIOS = require('./ios/copyAssets'); @@ -58,6 +60,33 @@ const linkDependencyAndroid = (androidProject, dependency) => { }); }; +const linkDependencyWindows = (windowsProject, dependency) => { + + if (!windowsProject || !dependency.config.windows) { + return null; + } + + const isInstalled = isInstalledWindows(windowsProject, dependency.config.windows); + + if (isInstalled) { + log.info(chalk.grey(`Windows module ${dependency.name} is already linked`)); + return null; + } + + return pollParams(dependency.config.params).then(params => { + log.info(`Linking ${dependency.name} windows dependency`); + + registerDependencyWindows( + dependency.name, + dependency.config.windows, + params, + windowsProject + ); + + log.info(`Windows module ${dependency.name} has been successfully linked`); + }); +}; + const linkDependencyIOS = (iOSProject, dependency) => { if (!iOSProject || !dependency.config.ios) { return; @@ -96,7 +125,7 @@ const linkAssets = (project, assets) => { }; /** - * Updates project and linkes all dependencies to it + * Updates project and links all dependencies to it * * If optional argument [packageName] is provided, it's the only one that's checked */ @@ -128,6 +157,7 @@ function link(args, config) { () => promisify(dependency.config.commands.prelink || commandStub), () => linkDependencyAndroid(project.android, dependency), () => linkDependencyIOS(project.ios, dependency), + () => linkDependencyWindows(project.windows, dependency), () => promisify(dependency.config.commands.postlink || commandStub), ])); diff --git a/local-cli/link/unlink.js b/local-cli/link/unlink.js index 8910bf63a..99b66f20d 100644 --- a/local-cli/link/unlink.js +++ b/local-cli/link/unlink.js @@ -2,8 +2,10 @@ const log = require('npmlog'); const getProjectDependencies = require('./getProjectDependencies'); const unregisterDependencyAndroid = require('./android/unregisterNativeModule'); +const unregisterDependencyWindows = require('./windows/unregisterNativeModule'); const unregisterDependencyIOS = require('./ios/unregisterNativeModule'); const isInstalledAndroid = require('./android/isInstalled'); +const isInstalledWindows = require('./windows/isInstalled'); const isInstalledIOS = require('./ios/isInstalled'); const unlinkAssetsAndroid = require('./android/unlinkAssets'); const unlinkAssetsIOS = require('./ios/unlinkAssets'); @@ -39,6 +41,25 @@ const unlinkDependencyAndroid = (androidProject, dependency, packageName) => { log.info(`Android module ${packageName} has been successfully unlinked`); }; +const unlinkDependencyWindows = (windowsProject, dependency, packageName) => { + if (!windowsProject || !dependency.windows) { + return; + } + + const isInstalled = isInstalledWindows(windowsProject, dependency.windows); + + if (!isInstalled) { + log.info(`Windows module ${packageName} is not installed`); + return; + } + + log.info(`Unlinking ${packageName} windows dependency`); + + unregisterDependencyWindows(packageName, dependency.windows, windowsProject); + + log.info(`Windows module ${packageName} has been successfully unlinked`); +}; + const unlinkDependencyIOS = (iOSProject, dependency, packageName, iOSDependencies) => { if (!iOSProject || !dependency.ios) { return; @@ -99,6 +120,7 @@ function unlink(args, config) { () => promisify(thisDependency.config.commands.preunlink || commandStub), () => unlinkDependencyAndroid(project.android, dependency, packageName), () => unlinkDependencyIOS(project.ios, dependency, packageName, iOSDependencies), + () => unlinkDependencyWindows(project.windows, dependency, packageName), () => promisify(thisDependency.config.commands.postunlink || commandStub) ]; diff --git a/local-cli/link/windows/isInstalled.js b/local-cli/link/windows/isInstalled.js new file mode 100644 index 000000000..b96edcb21 --- /dev/null +++ b/local-cli/link/windows/isInstalled.js @@ -0,0 +1,8 @@ +const fs = require('fs'); +const makeUsingPatch = require('./patches/makeUsingPatch'); + +module.exports = function isInstalled(config, dependencyConfig) { + return fs + .readFileSync(config.mainPage) + .indexOf(makeUsingPatch(dependencyConfig.packageUsingPath).patch) > -1; +}; diff --git a/local-cli/link/windows/patches/applyParams.js b/local-cli/link/windows/patches/applyParams.js new file mode 100644 index 000000000..21c1e9545 --- /dev/null +++ b/local-cli/link/windows/patches/applyParams.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +const toCamelCase = require('lodash').camelCase; + +module.exports = function applyParams(str, params, prefix) { + return str.replace( + /\$\{(\w+)\}/g, + (pattern, param) => { + const name = toCamelCase(prefix) + '_' + param; + + return params[param] + ? `getResources().getString(R.string.${name})` + : null; + } + ); +}; diff --git a/local-cli/link/windows/patches/applyPatch.js b/local-cli/link/windows/patches/applyPatch.js new file mode 100644 index 000000000..55f84edf6 --- /dev/null +++ b/local-cli/link/windows/patches/applyPatch.js @@ -0,0 +1,11 @@ +const fs = require('fs'); + +module.exports = function applyPatch(file, patch, flip = false) { + + fs.writeFileSync(file, fs + .readFileSync(file, 'utf8') + .replace(patch.pattern, match => { + return flip ? `${patch.patch}${match}` : `${match}${patch.patch}` + }) + ); +}; diff --git a/local-cli/link/windows/patches/makePackagePatch.js b/local-cli/link/windows/patches/makePackagePatch.js new file mode 100644 index 000000000..ef60e1e6a --- /dev/null +++ b/local-cli/link/windows/patches/makePackagePatch.js @@ -0,0 +1,10 @@ +const applyParams = require('./applyParams'); + +module.exports = function makePackagePatch(packageInstance, params, prefix) { + const processedInstance = applyParams(packageInstance, params, prefix); + + return { + pattern: 'new MainReactPackage()', + patch: ',\n ' + processedInstance, + }; +}; diff --git a/local-cli/link/windows/patches/makeProjectPatch.js b/local-cli/link/windows/patches/makeProjectPatch.js new file mode 100644 index 000000000..6b3d85855 --- /dev/null +++ b/local-cli/link/windows/patches/makeProjectPatch.js @@ -0,0 +1,14 @@ +module.exports = function makeProjectPatch(windowsConfig) { + + const projectInsert = ` + {${windowsConfig.pathGUID}} + ${windowsConfig.projectName} + + `; + + return { + pattern: '', + patch: projectInsert, + unpatch: new RegExp(`