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(`